HTB: Nexus Writeup
Nexus - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Nexus |
| OS | Linux |
| Difficulty | Easy |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.129.234.54 |
| Author | d3vn0mi |
Machine Rating
⭐⭐☆☆☆ (2/5)
Difficulty Assessment:
- Enumeration: ⭐⭐☆☆☆
- Real-world: ⭐⭐⭐⭐☆
- CVE: ⭐⭐⭐☆☆
- CTF-like: ⭐⭐☆☆☆
Summary
Nexus is an easy Linux box built around a leaky Gitea instance sitting in front of a Krayin CRM deployment. A password committed to git history in a Docker setup repo gives CRM login access, and the CRM itself is vulnerable to an authenticated file-upload RCE (CVE-2026-38526) once the CSRF protection is bypassed. From there, credential reuse inside the CRM’s own .env file hands over SSH access as a low-privileged user, and a root-owned systemd timer that syncs Gitea “template” repositories with an unsanitized os.path.join() becomes a directory-traversal write primitive straight into /root/.ssh/authorized_keys.
TL;DR: Gitea commit history leak → Krayin CRM login → CVE-2026-38526 authenticated upload RCE (www-data) → .env password reuse over SSH (jones) → Gitea template-sync path traversal → root.
Reconnaissance
Port Scanning
Initial scan against 10.129.234.54 turned up two open ports:
- 22/tcp — OpenSSH
- 80/tcp — nginx, redirecting to
nexus.htb
nexus.htb was added to /etc/hosts to follow the redirect and reach the site.
Service Enumeration
Virtual-host enumeration off the base domain surfaced two additional hosts:
git.nexus.htb— a Gitea instancebilling.nexus.htb— a Krayin CRM instance
Both were added to /etc/hosts alongside nexus.htb.
Vulnerability Assessment
Poking at the Gitea instance found a public repository, admin/krayin-docker-setup, used to stand up the CRM’s Docker environment. Its commit history was the weak point:
# reviewed commit history on the krayin-docker-setup repo via the Gitea web UI / API# commit 1615c465 predates a later cleanup and still carries a real secret in its diffCommit 1615c465 leaked:
DB_PASSWORD=N27xh!!2ucY04This is the classic Gitea/Git anti-pattern: rotating a secret in a later commit doesn’t erase it — the old blob is still reachable through history unless the repo is rewritten or the object is purged. With database credentials in hand and billing.nexus.htb already identified as the CRM, the next step was authentication.
Initial Foothold
Krayin CRM login
The leaked DB_PASSWORD was tried against the CRM login at billing.nexus.htb using the hiring-manager address discovered on the site, j.matthew@nexus.htb, and it authenticated successfully.
CVE-2026-38526 — Authenticated File Upload RCE
Krayin CRM (this instance) is vulnerable to CVE-2026-38526, an authenticated arbitrary file upload via the TinyMCE upload endpoint at /admin/tinymce/upload. The endpoint accepts a file upload but doesn’t properly validate the extension server-side, so a .php file disguised as an image is stored under the web root and becomes directly executable.
The catch is CSRF protection on the endpoint — normally this would block a scripted/CSRF-style upload. The bypass: Laravel-based apps (Krayin is Laravel-based) validate the CSRF token against the XSRF-TOKEN cookie value, so replaying that cookie’s value as the X-XSRF-TOKEN request header satisfies the check without needing a page-rendered token.
# Upload request (conceptually):# POST /admin/tinymce/upload# Cookie: XSRF-TOKEN=<value>; laravel_session=...# X-XSRF-TOKEN: <same value as XSRF-TOKEN cookie># multipart body: PHP webshell renamed with an image-passing extensionOnce uploaded, the shell was reached directly and returned execution as www-data.
uid=33(www-data) gid=33(www-data) groups=33(www-data)Privilege Escalation
www-data → jones
Inside the Krayin application directory, the app’s own .env file was read and disclosed a second, distinct credential:
DB_PASSWORD=y27xb3ha!!74GbRPassword reuse between the application-layer database credential and the local Unix account jones allowed direct SSH login:
ssh jones@10.129.234.54# authenticated with the .env-leaked password: y27xb3ha!!74GbRuser.txt was retrieved from jones’s home directory.
jones → root
Enumeration on the box turned up a root-owned systemd timer, gitea-template-sync.timer, firing a sync job against Gitea “template” repositories. The backing script processes file paths returned by git ls-tree and joins them onto a staging directory using Python’s os.path.join():
target = os.path.join(stage_path, filepath)os.makedirs(os.path.dirname(target), exist_ok=True)The bug: os.path.join() happily resolves .. segments, and nothing in the sync script validates filepath before that join. If a Gitea repository marked as a template contains tree entries with ../../../.. components in their names, the root-run sync job will write files outside its intended staging directory — anywhere the git/root context has permission to write, including /root/.ssh/.
Git’s own working-tree checkout (verify_path()) normally refuses to create paths containing .., which is why this can’t just be done with a normal git add/commit. The way around that check is to construct the malicious tree and blob objects directly as raw git objects (writing compressed objects straight into .git/objects/) rather than going through git’s path-validated commit machinery — the sync script’s own git ls-tree walk doesn’t apply the same guard when it later reads those objects back out.
Using this primitive:
- A Gitea repository (
jones/rce) was created and marked as a template, so it gets picked up bygitea-template-sync.timer. - A malicious tree was constructed containing a traversal path resolving to
../../../../../root/.ssh/authorized_keys, with an attacker-controlled SSH public key as the blob content. - Once the timer fired, the sync script’s unsanitized
os.path.join()wrote that key straight into/root/.ssh/authorized_keys. - SSH with the corresponding private key granted a root shell, and
root.txtwas retrieved.
uid=0(root) gid=0(root) groups=0(root)Attack Chain Summary
Gitea repo (admin/krayin-docker-setup) commit history leak (DB_PASSWORD) → Krayin CRM login (j.matthew@nexus.htb) → CVE-2026-38526 authenticated upload RCE (CSRF bypass via XSRF-TOKEN → X-XSRF-TOKEN) → shell as www-data → Krayin .env password reuse → SSH as jones (user.txt) → gitea-template-sync.timer os.path.join() traversal (raw git object bypass of verify_path()) → SSH key planted in /root/.ssh/authorized_keys → root (root.txt)Tools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning / service discovery |
| Gitea web UI | Repository and commit-history review, template repo creation |
| Krayin CRM (web) | Authenticated login, TinyMCE upload RCE |
| PHP webshell | Initial code execution as www-data |
ssh | Credential-reuse foothold as jones, key-based root access |
git / raw git object crafting | Directory-traversal write primitive against the template-sync timer |
| Python | Understanding/exploiting the os.path.join() flaw in template-sync.py |
Key Learnings
Techniques Practiced
- Virtual-host discovery and enumeration of Gitea/CRM subdomains
- Mining git commit history for secrets that survived a later “rotation”
- Exploiting a Laravel CSRF check by replaying the
XSRF-TOKENcookie as theX-XSRF-TOKENheader - CVE-2026-38526 authenticated file-upload RCE in Krayin CRM
- Credential reuse across an application’s
.envand OS-level accounts - Abusing
os.path.join()’s traversal-resolving behavior in a root-run automation job - Bypassing git’s
verify_path()protections by writing raw objects directly into.git/objects/
Lessons Learned
- Committing a secret and later “removing” it in a subsequent commit does not delete it from history — Gitea/GitHub repos remain exploitable via old commits unless the object is actually purged.
- CSRF double-submit-cookie schemes (token in cookie, mirrored in a header) are only as strong as the assumption that an attacker can’t read the cookie — once any other bug (or, here, direct cookie access) is available, the “protection” adds nothing.
- Reusing the same password across an application’s database config and a real OS account turns a web-app leak into a full SSH foothold.
os.path.join()in Python is not a sanitizer — it will honor..and absolute-path segments from its second argument, so any code trusting external input (like paths fromgit ls-treein an automated sync) needs explicit path-containment checks before writing to disk.- Automation that runs as root against attacker-influenceable input (like a Gitea template repository a normal user can create) needs the same trust boundary scrutiny as any other privileged service.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- TheCyberGeek & k1ph4ru, Nexus — official HackTheBox writeup (Machine Authors: TheCyberGeek & k1ph4ru), used here for the CVE-2026-38526 identification and the conceptual explanation of the
os.path.join()/ git raw-object traversal technique.