Snow Den

Hack The Box - Waldo

Published December 15, 2018

Box Info

Box profile: Waldo
OS: Windows
Maker: strawman & capnspacehook
Release date: August 4, 2018
Retire date: December 15, 2018
Own date: October 1, 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.

Initial Enumeration

After our initial full-range scan of the box, we're given a difficult choice between SSH and HTTP.

att$ nmap -p- -A -T4
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 list.js JavaScript file included in it, we can see that the functionality of the page is done via AJAX calls to back-end PHP scripts hosted on the server. These calls are sending parameters that appear to be relative paths. Let's use these JavaScript calls to see if we can get the contents of other files and directories. To do this right from our browser, we can simply open up Firefox developer tools with Ctrl+I and make sure we're on the "Console" tab.

 js> readFile('index.php')

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")
 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 ../ and .." 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")

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

Server Enumeration

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$
wal$ ip route
default via dev ens33 onlink dev ens33  src dev ens33  metric 1000 dev docker0  src
wal$ cat /etc/hosts   localhost   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
wal$ ls bin
ls  most  red  rnano
wal$ rnano /etc/passwd
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 tac (reverse 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.