Exceptional Laravel and IPv6 bypass - Linux Examples (web) from m0leCon Teaser 2023

The m0leCon CTF Teaser took place this weekend. We played only very lightly, as most of the team was busy organizing the HackTM CTF 2023 Finals in Timișoara. I solved this challenge only, while taking a break from exam preparation :P


I wrote a simple website with some linux command examples, I hope you’ll like it!

Author: @Giotino



When first looking at a web challenge, I like to first explore its functionality without looking at the source code. Just to see what it does. And get some ideas what may be interesting to dig in further.

We first of all see that the website is not doing much.

On the main page we are presented with a couple of hrefs with linux commands and our IP address.

Linux Examples main page with commands

When clicking on one of the links it seems the command gets executed.

ls -al /
ls gets executed

We can tell that the commands are really executed, because their outputs are changing, ie in ps aux.

By the look of the URL we see that the commands have incremental IDs. If we try some higher number than the predefined ?cmd=0-5 we get this nice informative error page:

Command not allowed

With the Exception trigger highlighted:

Command not allowed

This finally brings us to the source code, so lets go.

Source code

Source tree
source file tree

Observations, in an approximate order of their making:

  • wow, that’s a lot of files
  • is that laravel?
  • it’s still heavy, for the little it does, even for laravel
  • oh most of it is just example, never used garbage
  • is the only interesting stuff in routes/web.php?

Apart from the code already leaked in the exception, there seemingly aren’t any more functions reachable.

So where the heck is the flag? And how are we supposed to get it?

We find the flag in a weird FlagSolution class in the app/Solutions/FlagSolution.php file:


namespace App\Solutions;

use Spatie\Ignition\Contracts\RunnableSolution;

class FlagSolution implements RunnableSolution
  public function getSolutionTitle(): string
    return 'Flag';

  public function getSolutionDescription(): string
    return 'Get the flag';

  public function getDocumentationLinks(): array
    return [];

  public function getSolutionActionDescription(): string
    return 'Get the flag';

  public function getRunButtonText(): string
    return 'Press here to get the flag';

  public function run(array $parameters = []): void
    throw 'PTM{THIS_IS_THE_FLAG}';

  public function getRunParameters(): array
    return ['url'];

  public function isRunnable(): bool
    return true;

So we somehow need to either trigger the run() method of this class, or get access to this file in some other way. But if it was the latter, why would they bother with this intricate solution class, right?

When googling for the base Ignition\Contracts\RunnableSolution class, we discover its documentation. It is part of the library Ignition responsible for the nice error pages we saw before. These RunnableSolutions allow for simple predefined one-click solutions for common Exceptions, such as missing API keys etc.

This immediately raises the idea to examine how these solutions are triggered. The documentation mentions getSolutions() method on a given Exception needs to be present for the solution to be visible on the error page. But we don’t need the solution to show up, we just need to trigger it.

After a while I found this writeup in Chinese talking about a similar challenge. Even though Google Translate refused to translate it, we can understand from the pictures, that it is indeed possible to trigger arbitrary solution using the /_ignition/execute-solution endpoint. We can confirm this by looking at the library source code.

So we won? Sending a request like this should do it:

curl -v --data '{"solution":"App\\Solutions\\FlagSolution"}' \
    --header 'Content-Type: application/json' \

Aaand no, we get an error:

Solutions can only be executed by requests from a local IP address. Please also make sure APP_DEBUG is set to false on ANY production environment.

Looking at the respective library source code it makes sense. Here is Ignition’s src/Http/Controllers/ExecuteSolutionController.php:

class ExecuteSolutionController
    use ValidatesRequests;

    public function __invoke(
        ExecuteSolutionRequest $request,
        SolutionProviderRepository $solutionProviderRepository
    ) {


        $solution->run($request->get('parameters', []));
    public function ensureLocalRequest()
        $ipIsPublic = filter_var(

        if ($ipIsPublic) {
            abort(403, "Solutions can only be executed by requests from a local IP address. Please also make sure `APP_DEBUG` is set to false on ANY production environment.");

It checks if the calling IP is from a public range!

Local IP check bypass

What’s interesting, is that the check is done using a deny method. The library checks if the IP meets some conditions, and if it does, it blocks it. filter_var is a standard PHP function. Here it checks whether the FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE conditions are met.

Notice the FILTER_FLAG_IPV4 condition, what about IPv6? Here I recalled one of the commands at the beginning was ip a. Does the server have a IPv6? It does! It’s just not in the DNS. Is the server listening on IPv6 though? With the ss -l command we can check. It does!

So if we now use IPv6, the IPv4 validator won’t match and the $ipIsPublic variable will be empty, meaning we get to execute our solution:

curl -v \
    --data '{"solution":"App\\Solutions\\FlagSolution"}' \
    --header 'Content-Type: application/json' \
    --header "Host: examples.challs.m0lecon.it" \

And get the flag:

A flag
An exceptional flag * badum tss *

Flag: ptm{IPv6_1s_th3_futur3}

Published on
ctf writeup web

Latest Posts