2 minute read

Introduction

theRFC is a very nice web challenge in CyCTF 2023 finals. The web application is written in flask and has a very obvious host header injection vulnerability that you need to convert to Server-Side Template Injection.

Application Analysis

Starting by looking into the source code you’ll notice that it is a single page app. Looking at line 12, you’ll notice that the only place that could accept user-input is the Host header. On line 41 the app uses template.render() to render the response back to you which shows a very obvious SSTI vulnerability, if you can manage to bypass the simple filter on line 7, but is a bit tricky because the injection point is the Host header

0

If you tried to directly inject a simple SSTI payload into the Host header, It’ll fail with a 400 Bad Request response but, that’s normal because according to RFC 2616, you can’t use characters like { and } in the Host header.

1

The vulnerability here arises from the fact that the application is using cherrypy v18.8.0 to parse the Host header. cherrypy decodes headers following RFC 2047 and lucky for us, the application is using that exact same version to parse and decode the Host header

https://github.com/cherrypy/cherrypy/blob/v18.8.0/cherrypy/_cprequest.py#L727
https://github.com/cherrypy/cherrypy/blob/v18.8.0/cherrypy/lib/httputil.py#L251

2

Accodring to RFC 2047, you can use a much wider character set for the host header when encoded in a special way.

3

4

so something like =?iso-8859-1?q?this is some text?= will work but if you started adding characters like {, It’ll give you the 400 Response once again.

To Byass that, RFC 2045 has the answer as it states that you can send your payload as hex characters but the format has to be an = followed by a 2 hex digits so, i’ll have to repeat this process for every character I want to encode.

5

The final step is to bypass the simple SSTI filter shown below:

6

I’ve tested a lot of variations and this one from PayloadsAllTheThings ended up working.

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}

Now, the final step is to bypass the _, import, and os and Chivato’s Jinja2 SSTI Research is a great resource to do this. You can see that characters like a _ can be bypassed by hex-encoding it. It turns out that the same works for import and os

7

So, the final payload I end up with is

{{self['\x5f\x5finit\x5f\x5f']['\x5f\x5fglobals\x5f\x5f']['\x5f\x5fbuiltins\x5f\x5f']['\x5f\x5f\x69\x6D\x70\x6F\x72\x74\x5f\x5f']('\x6f\x73')['popen']('cat /flag.txt')['read']()}}
  • Where:
    • every _ is encoded to \x5f
    • __import__ is encoded to \x5f\x5f\x69\x6D\x70\x6F\x72\x74\x5f\x5f
    • os is encoded to \x6f\x73

I then hex-encoded that entire payload and added a = between every 2 hex digits and I got the flag.

7

Categories:

Updated: