Code was the first box that made me consider getting Hack The Box’s VIP+ subscription, to peacefully work on it without other players’ interference. Stability is one thing, but it’s just too easy to get spoiled. Somehow, though, being partially spoiled ended up making this box more fun. You’ll see why.
RCE via restriction bypass
Obtained reverse shell connection by bypassing keyword restrictions in Python interpreter
Enumerate file system
Discovered MD5 hashes of credentials in application database
Foothold
Cracked weak passwords and logged into SSH as user martin
Enumerate permissions
Discovered that martin
can run backup script as root via sudo
Exploit vulnerability
Abused backup script’s path traversal vulnerability to gain access to files in /root
Privilege escalation
Logged into SSH as root using private key
Lo-fi hip hop music on
Our initial nmap
scan found a minimalist machine, just the way I like them:
Pointing the browser at this “Gunicorn” HTTP server found a Python interpreter: write code on the left, see the result on the right. It sounds too easy to pop a shell, but it’s just the sound of it - try a common one-liner such as __import__('os').popen('bash ...').read()
, and you’re immediately greeted with a try-harder message: “Use of restricted keywords is not allowed”.
Before I started trying too hard, I searched for known vulnerabilities on Gunicorn 20.0.4. Turns out it is possible to perform request smuggling, which I had to read up on, but it didn’t seem useful until I knew what to target in the first place. Nothing else popped up, so it looked like we were stuck trying to break free from this restricted keyword jail.
Why this write-up exists
I told you I wouldn’t write these if I didn’t learn anything. This part is where the learning happened. While the Command Injections module on HTB Academy had given me plenty of ideas for bypassing exclusion lists in general, I’d never had to do this with the Python language, with which I still had a hack-something-together kind of relationship.
Slowly going through just that one-liner above, we can find out that import
, os
, (p)open
, and even read
are restricted. The only other thing I knew was that anything that I could turn into a string to be interpreted would be easy to bypass with concatenation - for example, print("os")
would be restricted, but print("o"+"s")
wouldn’t.
This helped with scanning through Hacktricks’ immense list of things to try, looking for what could work under these limitations. system
, exec
and eval
adding to the list of restrictions didn’t leave that many options open, especially when trying to get a reverse shell. One thing that stood out was that despite not being able to use import
, I was able to use sys.modules["<some module>"]
. Together with the string concatenation trick, that got us very close to a reverse shell.
I started from the following reverse shell example from Revshells:
We can’t import
, so the interpreter doesn’t know what socket
, os
and pty
are. But with sys.modules[]
, I could do this:
And I got a connection on my netcat
listener! Only… it didn’t respond to my commands:
I noticed that the Python interpreter had printed 'pty'
, unexpectedly. Debugging, I wrote the following print statements, and got the responses shown directly underneath:
It turns out that sys.modules
doesn’t do what I thought it did. It doesn’t import, why would it? It maps module names to modules that have already been loaded. And obviously pty
had not been loaded by anything in this application yet. I started to think this was getting too complicated for an easy box. Luckily, I had been spoiled earlier on. Allow me to explain.
Cheese
Earlier, somewhere between giving up on the request smuggling avenue and starting to get deeper into the Python jail-breaking odyssey, I checked the result of the full TCP nmap
scan I’d left running as usual. And oh! There’s a couple of hidden ports!
I load them up on the browser, weirdly they show the same content but hey, I’m not complaining. There’s a tar.bz2
archive that I can get, and it seems to contain the app’s source code! And the sqlite
database that manages the users, so I can get user’s password hashes, and they crack! I use them to SSH in and this guy has sudo rights to a backup script, and…
… wait, what? What was the point of that whole Gunicorn app then? It dawned on me that I had probably just cheesed the box using some other player’s work, and sure enough, at the next box reset those HTTP servers weren’t there anymore. So I stopped and went back to figuring out how to get a shell out of this.
Flashback over, and we’re back to realising that we’d need to import pty
to get our reverse shell to work, and thinking that this was getting too complicated. The thought came to mind that spawning an HTTP server would probably be a whole lot easier, and indeed it was, again thanks to the necessary modules already being loaded:
But I felt spoiled. Would I have thought of this if I hadn’t seen someone else do it? And also, the reverse shell felt so close now… so I kept looking, and I’m glad I did.
Why this write-up exists, continued
Ok, so, we need to figure out a way to import pty
. I mean, there’s got to be one, right? Scrolling down that immense list of Hacktricks, most everything needs an eval
or an exec
, until hope is briefly restored in the Builtins section, with the sentence “If you can access the __builtins__
object you can import libraries”. I say briefly because, sure enough, __builtins__
is part of the restricted keyword list.
But this section is helpfully followed by “No builtins”, and it offers many ways of overcoming having no access to this object. Two options stand out:
So… we can access __globals__
through something like help.__call__
… and we can access __builtins__
, as a string, through it… which allows us to access __import__
, also as a string… So I guess we can do help.__call__.__globals__['__buil'+'tins__']['__imp'+'ort__']("pty")
… right?
Hahahahah right!
It doesn’t last for long due to the cleanup scripts (I think), so you should establish a stable shell from here, but it sure does the job. It definitely doesn’t feel like an intended path, so I most likely missed something easier, or the HTTP server is really what comes to mind to most people. I look forward to reading other write-ups on this one! Let’s move on.
The rest
The rest was straightforward. We get our user flag, and find the database.db
file that holds the saved python snippets for each user, but most importantly also each user’s password hash:
By the looks of it it’s simply MD5, and the app’s code confirms it:
Hashcat cracks both easily, but only Martin’s is useful to us:
We try the credentials with SSH, and [hacker voice] we’re in. What are the two commands we always run when we land on a Linux box? That’s right:
backy.sh
is a long script for what it does, which is setting up a backup job in the shape of a json file, for another backy
binary to run. These are the important bits:
In English: only allow paths that start with /var/
or /home/
, and use gsub
to remove any ../
from the path, trying to prevent path traversal.
A quick test showed that this was not a secure way to prevent path traversal. Since the substitutions won’t happen recursively, you just need to prepare a string that will still result in path traversal after the substitution:
So all we have to do is prepare our task.json
accordingly to get a backup of the /root/
directory. The important bit is the directories_to_archive
field, but you can also see my cute little attempt to both hide and prevent spoiling other people, and also make them feel guilty for looking:
Now we just call the script with sudo
, unpack the archive and get that root flag! Luckily, in case we wanted a shell, we also get an SSH private key:
I mean, why not? Back in our machine:
Roll the credits
- I still don’t know Python like I know Java, but now I know ways of accessing modules I might not be intended to have access to.
- In all seriousness, that was a useful, albeit off-the-deep-end dive into Python’s module system.
- As always, for reading this far, you’re a champ.