
PoC and write-up for the HackTheBox "Nexus" privilege escalation: root-run Gitea template-sync service vulnerable to path traversal via forged Git objects. Educational use only.
Gitea template-sync path traversal via forged Git objects → root
A proof-of-concept privilege-escalation exploit for the HackTheBox machine Nexus.
A root-owned systemd service synchronizes Gitea template repositories to disk
without sanitizing entry names, allowing an authenticated low-privileged user to
write arbitrary files as root through directory traversal — used here to implant
an SSH key into /root/.ssh/authorized_keys.
This code is provided for educational purposes and authorized security testing only — i.e. lab environments such as HackTheBox, or systems you own or have explicit written permission to test. Unauthorized use against systems you do not control is illegal. The author assumes no liability for misuse.
If the Nexus machine is still active on HackTheBox, do not publish or share this exploit — HTB rules prohibit distributing solutions to active machines.
| Class | Path Traversal (CWE-22) → Local Privilege Escalation |
| Component | gitea-template-sync.service (custom /etc/gitea/template-sync.py) |
| Runs as | root, triggered every ~60s by gitea-template-sync.timer |
| Root cause | os.path.join() on unsanitized names taken from Git tree objects |
| Impact | Authenticated low-priv user → root |
The synchronization script reads files from repositories flagged as templates in Gitea and writes them to the local filesystem. The destination path is built roughly as:
dest = os.path.join(base_dir, name) # name comes straight from the Git tree
open(dest, "w").write(content)
os.path.join() does not neutralize .. sequences. A tree entry whose name
is a traversal path (e.g. ../../../../../root/.ssh/authorized_keys) therefore
makes the privileged process write outside the intended directory.
Git stores content and names separately:
name → object (this is where the filename lives);The filename in a tree entry is just text, so it can be .. — but the standard
Git client refuses to create such an entry, precisely to prevent this attack.
This PoC bypasses that client-side check by writing raw Git objects directly
into .git/objects/ (computing the SHA-1 ids by hand) and crafting refs/heads/main
manually. The pushed repository then contains a tree entry named .., which the
root sync process resolves and follows.
www-data..env, reused as a
system password → SSH as a low-priv user.root.The recurring theme across all three stages: a privileged process trusting attacker-controlled data without validation (a spoofed MIME type, a reused password, an unsanitized filename).
http://localhost:3000.git, python3, curl, and ssh available (default on the box).chmod +x nexus_privesc.sh
./nexus_privesc.sh -t <TARGET_IP> -u <GITEA_USER> -p <GITEA_PASS> [-d DEPTH]
Example:
./nexus_privesc.sh -t 10.129.21.192 -u jones -p 'REDACTED' -d 5
/tmp/.exploit_key)... traversal tree that maps to
/root/.ssh/authorized_keys, with the attacker public key as content.root with the implanted key.Monitor the sync on the target with:
tail -f /var/log/template-sync.log
A successful run logs a line like:
synced: ../../../../../root/.ssh/authorized_keys
DEPTHDEPTH is the number of .. levels needed to climb from the sync/staging
directory up to filesystem root (/). If the sync succeeds in the log but the
root SSH fails, the traversal landed at the wrong level — retry with -d 4 or
-d 6. The exact value can be confirmed by reading the staging path in
/etc/gitea/template-sync.py.
Never build filesystem paths from untrusted names with os.path.join() alone.
Normalize and confine paths, e.g.:
base = os.path.realpath(base_dir)
dest = os.path.realpath(os.path.join(base, name))
if not dest.startswith(base + os.sep):
raise ValueError("path traversal blocked")
Reject tree entry names containing .., absolute paths, or path separators.
Run the sync service under a dedicated low-privileged account, not root.
Enable receive.fsckObjects = true in Gitea to reject malformed pushed objects.
<your name / handle> — for educational use on HackTheBox.
| Flag | Description | Default |
|---|
-t | Target IP (for the final root SSH) | — (required) |
-u | Gitea username | — (required) |
-p | Gitea password | — (required) |
-d | Number of .. levels to reach / from the sync directory | 5 |