Code Breaking Puzzles

The challenges created by @Phith0n can always surprise me and help me learn a lot.

easy - function

Here comes the source code:

$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';

if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
} else {
    $action('', $arg);

Very short and simple php code with regular expression, the function name cannot start and end with alphanumeric characters. Following are what I have tried for this challenge:

  1. SQL syntax comes to my mind
    It comes to my mind that in mysql, there can be a space or /**/ between function name and left bracket. However, it is not out of my expectation that php can not identify the function name xxx () or xxx/**/().
  2. PHP7 new evaluation of indirect expressions: ('phpinfo')()
    In before, one of the challenge by @Phith0n taught me that I can use ($a)(); to dynamically execute function in version 7( However, when I tried it in this challenge, it still showed with the error: undefined function ('phpinfo'). I guess the reason is that I cannot pass arguments in this expression.

At the end, @Phith0n published the answer: function name can be \function_name($arg). It is based on the concept of global namespace. In the documentation of Using namespaces: Basics, it is vert helpful to understand the work of namespace. Directory in the file system is same as the concept of namespace, file.txt in relative path would be parsed as currentdirectory/file.txt. Therefore, the function_name would also be parsed as currentnamespace\function_name, and it becomes \function_name because the global namespace of php is default to \. Now, we bypass the regular expression, next challenge is to find the useful function.

What’s important. This challenge has limited many dangerous functions such like shell_exec() to run in the program. In other words, we cannot use such like system(ls) not only in $action but also in $arg. After google for a long time, I found one function which should already be deprecated but still working in this challenge: create_function.
In fact, this is my first time to try code injection in create_function because it was already deprecated.

//create_function('$arg', 'echo $arg');
function cf($arg){
    echo $arg;

Therefore, to complete code injection, I need to close the bracket. Following is my payload:

I avoid using system(ls) but use scandir() to list all the files in the parent directory. The end, get flag{03fdc0ee2fc464aac3c40ef0e20dcb5a} with payload of print_r(file_get_contents("../flag_h0w2execute_arb1trary_c0de"));.

easy - pcrewaf

Here comes the source code again:

function is_php($data){
    return preg_match('/<\?.*[(`;?>].*/is', $data);

$user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);
$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
    echo "bad request";
} else {
    @mkdir($user_dir, 0755);
    $path = $user_dir . '/' . random_int(0, 10) . '.php';
    move_uploaded_file($_FILES['file']['tmp_name'], $path);

    header("Location: $path", true, 303);

After I read the source code, there was no any ideas comes to my mind. Even my emoji shell or XOR shell needs semicolon to close the PHP sentence. This time, @Phith0n published a more astonishing answer: PCRE reDoS.

Some people said that same concept used in LCTF 2017.

Therefore, what I need to do now is not bypass the regular expression, but to crash the regular expression by over limited times of backtrack. Now, let’s take a look at the regex debugger:

Just like the picture showed above, the use of .* matches to the end of the string at the fourth time. However, when the regular expression expected to match the character such likes (;? after the end of the string, there is no match character anymore. According to the NFA engine used in PCRE library, when there is not matched character, it will start to backtrack. Therefore, at the fifth time, the regex started to backtrack, which would put back the character one by one until it is matched (I have explained it thoroughly by comments in the picture).

Finally, I come to the exploit part. Write a multipart/form-data with content of <?php eval($_GET['hi']);//A*100000. flag{216728a834fb4c1e0bc6893e135f436e} gotcha with payload of


One more interesting thing, how about applying this trick for sql injection to bypass waf ? :)

Same concept to crash the preg_match even though the waf limit your keyword!

easy - phpmagic

The source code of phpmagic:

define('DATA_DIR', dirname(__FILE__) . '/data/' . md5($_SERVER['REMOTE_ADDR']));

if(!is_dir(DATA_DIR)) {
    mkdir(DATA_DIR, 0755, true);
$domain = isset($_POST['domain']) ? $_POST['domain'] : '';
$log_name = isset($_POST['log']) ? $_POST['log'] : date('-Y-m-d');
if(!empty($_POST) && $domain):
    $command = sprintf("dig -t A -q %s", escapeshellarg($domain));
    $output = shell_exec($command);

    $output = htmlspecialchars($output, ENT_HTML401 | ENT_QUOTES);

    $log_name = $_SERVER['SERVER_NAME'] . $log_name;
    if(!in_array(pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true)) {
        file_put_contents($log_name, $output);

    echo $output;

The functionality of this php website is very simple that it receive our input of domain name then output the result. It was a shame that I spent so much time on trying command injection on the dig -t A -q %s and bypass the escapeshellarg.
There were many stories about bypassing escapeshellarg and I also learned something details in the process:
About escapeshellarg:

  1. only one argument can be given.
  2. user can only run the first command.

About escapeshellcmd:

  1. no limited numbers of the arguments given.
  2. user can only run the first command.

For the second point: user can only run the first command. We can always use cmd1;cmd2 to run two commands at the same time. Therefore, escapeshell* use this way to stop the problem. Generally speaking, these two functions all will convert argument into string by wrapping argument with single quote.
What I messed up all the way at the start time was to realize the attack likes GitList 0.6 - Remote Code Execution. The vulnerability in this attack is that you can inject command to the position of options. It is important to distinguish between options and values.

$ cmd -options = value

What’s more important, quotation can not be used to distinguish the options and values. Therefore, if a developer gives a sentence likes shell_exec("cmd -options ".escapeshellarg($input));. We can try to make our payload at the position of options. Then the command will become $ cmd -options '-options2=id;'. The part of -options2=id; is viewd as option and cause to code execution.
However, this time the challenge is different. dig -t A -q %s, escapeshellarg($domain) even uses fiormat string to fix our input at the position of value, and I spent a long time to figure out this point. Therefore, this might be kind of webshell writing challenge. Following are several challenges I went through at the end:

  • How to control our file name?
    It seems that $log_name would have a prefix of $_SERVER['SERVER_NAME'], and I was struggle for how to get the value. According to the Manual, I found that this variable should not be reliable because it can be spoofed with HOST header from the client. Therefore, here is the way to control our file name with host header.

Now, I can give my code to $domain and write webshell by function of file_put_contents. But the characters such like <, >, and quote would be damnly converted into htmlentities by htmlspecialchars(). Therefore, I can encode my webshell with base64 and use php://filter/write=convert.base64-decode/resource= in my file name to bypass the challenge.

  • Base64 decode
    I had never thought of the way base64 decode the string until this challenge.
    // base64 rules
    First, legal characters of base64 are [a-zA-Z0-9+/].
    Second, base64 encode the original string from 3 bytes to 4 bytes printable string. Therefore, the encoded string without '=' must be mutiple of four. 
    Third, If the original string is not multiple of 3 bytes, base64 will pad it with \0. So, The number of \0 bytes can only be 0, 1, 2, which must be same to the '=' numbers at the end of encoded string.

    I also spent a lot of time on this and found that if my webshell wasn’t a legal format of base64, then base64 decode would print out a blank page on my log file. Therfore, here I use my webshell with <?php eval($_POST["h1111"]), get PD9waHAgZXZhbCgkX1BPU1RbImgxMTExIl0pOz8+(length is 40) after encoding. (What needs to be care of is that ‘=’ can only be on the end of encoded string. But our payload is inserted at the middle, so my encoded payload should not have ‘=’). Nothing else is difficult now, here comes my final payload.

Flag is flag{8fd9046cde2d53d1ceea8970286fd38c} 👍

If you are look for how to bypass the file extension, you can see it at The Magic from 0CTF ezDoor

easy - phplimit

if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    // the function cannot take any parameters
} else {

The regex has appeared in RCTF2018 before. In the official writeup of rctf2018, the function getallheaders for apache was used to execute command injection in http headers. However, the server in this challenge is nginx, so the same writeup cannot be used again. But the concept is same!

This time, the function which would be useful is get_defined_vars(). The function can print out all the defined variables. Therefore, we can use this to bypass the limit which stops us from using parameters. Following screenshot is my payload:

First, I use reset function to initialize all the defined variables because they might interrupt code execution. Therefore, after reset there would only be $_GET left in the request. Another problem is that get_defined_vars return with the array data, so I use implode to concate them into a string. The end, eval and pass the code you want to execute in another $_GET variable.

easy - nodechr

// initial libraries
const Koa = require('koa')
const sqlite = require('sqlite')
const fs = require('fs')
const views = require('koa-views')
const Router = require('koa-router')
const send = require('koa-send')
const bodyParser = require('koa-bodyparser')
const session = require('koa-session')
const isString = require('underscore').isString
const basename = require('path').basename

const config = JSON.parse(fs.readFileSync('../config.json', {encoding: 'utf-8', flag: 'r'}))

async function main() {
    const app = new Koa()
    const router = new Router()
    const db = await':memory:')

    await db.exec(`CREATE TABLE "main"."users" (
        "username" TEXT NOT NULL,
        "password" TEXT,
        CONSTRAINT "unique_username" UNIQUE ("username")
    await db.exec(`CREATE TABLE "main"."flags" (
        "flag" TEXT NOT NULL
    for (let user of config.users) {
        await`INSERT INTO "users"("username", "password") VALUES ('${user.username}', '${user.password}')`)
    await`INSERT INTO "flags"("flag") VALUES ('${config.flag}')`)

    router.all('login', '/login/', login).get('admin', '/', admin).get('static', '/static/:path(.+)', static).get('/source', source)

    app.use(views(__dirname + '/views', {
        map: {
            html: 'underscore'
        extension: 'html'
    app.keys = config.signed
    app.context.db = db
    app.context.router = router

function safeKeyword(keyword) {
    if(isString(keyword) && !keyword.match(/(union|select|;|\-\-)/is)) {
        return keyword

    return undefined

async function login(ctx, next) {
    if(ctx.method == 'POST') {
        let username = safeKeyword(ctx.request.body['username'])
        let password = safeKeyword(ctx.request.body['password'])

        let jump = ctx.router.url('login')
        if (username && password) {
            let user = await ctx.db.get(`SELECT * FROM "users" WHERE "username" = '${username.toUpperCase()}' AND "password" = '${password.toUpperCase()}'`)

            if (user) {
                ctx.session.user = user

                jump = ctx.router.url('admin')


        ctx.status = 303
    } else {
        await ctx.render('index')

async function static(ctx, next) {
    await send(ctx, ctx.path)

async function admin(ctx, next) {
    if(!ctx.session.user) {
        ctx.status = 303
        return ctx.redirect(ctx.router.url('login'))

    await ctx.render('admin', {
        'user': ctx.session.user

async function source(ctx, next) {
    await send(ctx, basename(__filename))


After I read about the source code, what came to my mind was only SSTI or SQL injection. For SSTI, I can use config.flag to get the value, but there was no any way to upload the template. However, it could be realized in SQL injection. The regex /(union|select|;|\-\-)/is stopped us from using the keyword like union, select, but it could be bypassed with the use of javascript function toUpperCase(). There were two special characters would be useful:

1. ı (%c4%b1).toUpperCase() => I
2. ſ (%c5%bf) .toUpperCase() => S

Awesome! Now I know the how the payload should be.

password: hello' unıon ſelect 1,flag,3 where '0'='0
// the result would be showed at the username column

The writeup is still not completed