Analysis of the Safer eval RCE, aka “the WAHCKON bug”
This is an analysis of a bug found by @0d4rk30 and myself. The safer eval class is an old PHP library written as an attempt to allow restricted evaluation of user supplied data. The class is no longer maintained and was never promoted as production ready. The author does not intend to fix this issue. He suggested I write about more modern ways of safely evaluation user supplied data, which I may do at some stage, but not today. I first came across the class when looking at phpReAdmin (https://github.com/billbarsch/phpReAdmin), which is one of the few projects that have adopted the class. You can grab a copy of the safer eval class and example sandbox from http://evileval.sourceforge.net/ and have a play for yourself.
The example.php provided effectively boils down to the following code:
$se = new SaferEval();
$errors = $se->checkScript($_POST['code'], 1);
To get a better understanding of how this class attempts to limit eval() lets start by looking at the checkScript()
function:
function checkScript($code, $execute) {
$this->execute = $execute;
$this->code = $code;
$this->tokens = token_get_all('<?php '.$this->code.' ?>');
$this->errors = array();
$this->braces = 0;
// STEP 1: SYNTAX - Check if braces are balanced
foreach ($this->tokens as $token) {
if ($token == '{') $this->braces = $this->braces + 1;
else if ($token == '}') $this->braces = $this->braces - 1;
if ($this->braces < 0) { // Closing brace before one is open
$this->errors[0]['name'] = 'Syntax error.';
break;
}
}
if (empty($this->errors)) {
if ($this->braces) $this->errors[0]['name'] = 'Unbalanced braces.';
}
The first part of the function splits the user supplied code into parseable tokens using the token_get_all()
function. It then proceeds to iterate over these tokens and modify the braces value by a positive or negative amount. The goal of this is to ensure there is a closing brace for every opening brace. In the event that a closing brace is found before an opening brace is found the code sets an error message and breaks out of the iteration leaving the braces value as -1. Once the iteration completes or is broken out of it checks if an error has already been set. If no error has been set it checks if braces contains a positive value (more opening than closing braces), and sets an error message if true. Otherwise (an error had already been set) the function then continues execution into STEP 2 before proceeding to STEP 3:
// STEP 2: SYNTAX - Check if syntax is valid
else if (!$this->evalSyntax($this->code)) {
$this->errors[0]['name'] = 'Syntax error.';
}
// STEP 3: EXPRESSIONS - Check against various insecure elements
if (empty($this->errors)) foreach ($this->disallowedExpressions as $disallowedExpression) {
unset($matches);
preg_match($disallowedExpression, $this->code, $matches);
if($matches) {
$this->errors[0]['name'] = 'Execution operator / variable function name / variable variable name detected.';
break;
}
}
STEP 2 tries to validate the syntax of the code if an error was set from the token parsing. Keen readers will have noticed this can only occur if there is a closing bracket before an opening bracket. Ok, lets detour into the evalSyntax functionto see what’s going on:
function evalSyntax($code) { // Separate function for checking syntax without breaking the script
ob_start(); // Catch potential parse error messages
$code = eval('if(0){' . "\n" . $code . "\n" . '}'); // Put $code in a dead code sandbox to prevent its execution
ob_end_clean();
return $code !== false;
}
This function supresses output by using ob_start()
and ob_clean()
before and after an eval call which evaluates user supplied code. It attempts to prevent the user supplied code by wrapping it inside a if(0)
condition that will never be true and should not execute the code within it’s braces. Except, this code concatinates the user supplied code within the opening and closing brace in a function call that only gets called on a code that contains a closing brace before an opening brace…
Lets visualise this with some examples:
$v = rand(1,10);
This example has a braces value of 0 and does not enter step2.
{}}}
This example has a braces value of 2, but also skips step2 as there was no early error message.
} abc
This code has a braces value of -1 and ends of being concatinated into the if statement in step2 like this:
eval('if(0){
} abc
}');
Resulting in a silent syntax error trying to parse the abc bareword and an unclosed brace. We can now leverage this to execute code:
} else { phpinfo();
It gives no output and states syntax error. Not what you expected? The reason for this is that evalSyntax supresses output and the syntax error code occurs in STEP4 as this eval call does not contain any braces before our code.
This leaves us with a few options, it is possible to blindly setup a netcat connect back shell by calling system()
, or we can explicitly flush the buffers before ob_clean()
is called. Which can easily be done by including ob_flush()
or exit()
at the end of the injected code. For example:
} echo WAHCKON; exit; {
I chose to call this the WAHCKON bug as the annual WAHCKON conference starts in Perth WA tomorrow. This marks the third year the mismatched braces in their logo has been annoying the OCD out of me. So when this bug came along with its mismatched braces at a suitable time it seemed like a good fit. I hope everyone has a great time at WAHCKON this year!
Posted by Eldar Marcussen | Permanent link | File under: security, exploit, bug, advisory, vulnerability, hacking, disclosure