Skip to content

Instantly share code, notes, and snippets.

@epinna
Last active April 7, 2018 16:50
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save epinna/6065043 to your computer and use it in GitHub Desktop.
Save epinna/6065043 to your computer and use it in GitHub Desktop.
Popfinder is a simple script that helps PHP code analysis of PHP in case of object injection via unserialize function.
<?
/*
popfinder.php - Emilio Pinna 2013
Popfinder is a simple script that can helps code analysis of PHP in case of possible object
injection[1][2] in unserialize function.
Wrapping the serialized object string in a probe object, popfinder does:
1. Collect all the methods called on the object, both magic and normal methods
2. Starting from collected methods, enumerates all the visible object methods in scope
to find what is callable and potentially exploitable
## USAGE
Get the line of the injectable unserialize function
```
unserialize($_POST['serialized_object']);
```
And wrap the string that contains the serialized object with wrap_serialized_object() function
```
include('popfinder.php');
unserialize(wrap_serialized_object($_POST['serialized_object']));
```
Then, run the script to collect the methods called on function during the object life cycle, and
to find the potentially reusable methods visible in the scope. The output is saved in
'/tmp/popfinder_FILENAME_FILELINE.txt'.
```
$ cat /tmp/popfinder_test.php_16.txt
# POPfinder v 0.1 - Emilio Pinna 2013
# METHODS CALLED ON THE UNSERIALIZED OBJECT
__wakeup()
#0 test.php:16 unserialize()
#1 called_pops_obj::__wakeup()
method_called_on_object() Arguments: Array( [0] => string ) called_pops_obj::method_called_on_object()
#0 test.php:22 called_pops_obj::method_called_on_object()
#1 test.php:22 called_pops_obj::__call()
__sleep()
#0 /var/www/wordpress-no8/popfinder/test.php:19 serialize()
#1 called_pops_obj::__sleep()
# AVAILABLE METHODS IN SCOPE
# SELECTION: __wakeup() method_called_on_object() __destruct()
DateTime::__wakeup()
DateInterval::__wakeup()
Phar::__destruct()
PharData::__destruct()
PharFileInfo::__destruct()
PDO::__wakeup()
PDOStatement::__wakeup()
VulnerableObject::__destruct()
```
The popfinder analysis shows that three methods are called on the unserialized object:
"__wakeup()" "method_called_on_object()" and "__destruct()" and that a possible exploitable
method object is exposed in current scope, "VulnerableObject::__destruct()".
References:
[1] OWASP - PHP Object Injection - https://www.owasp.org/index.php/PHP_Object_Injection
[2] Utilizing Code Reuse/ROP in PHP Application Exploits - http://media.blackhat.com/bh-us-10/presentations/Esser/BlackHat-USA-2010-Esser-Utilizing-Code-Reuse-Or-Return-Oriented-Programming-In-PHP-Application-Exploits-slides.pdf
*/
$banner = "POPfinder v 0.1 - Emilio Pinna 2013";
function _get_fileline($step=2) {
$bt = debug_backtrace();
$bt_step = $bt[$step];
$repr = '';
if (array_key_exists('file', $bt_step))
$repr .= '_' . basename($bt_step['file']);
if (array_key_exists('line', $bt_step))
$repr .= '_' . $bt_step['line'];
return $repr;
}
function _get_stackpoint($step=1, $all=True) {
$bt = debug_backtrace();
if(count($bt)<=$step)
$step = count($bt)-1;
if($all)
$bt_steps = array_reverse($bt);
else
$bt_steps = array($bt[$step]);
$repr = '';
foreach($bt_steps as $bt_step_num => $bt_step) {
$repr .= PHP_EOL . '#' . $bt_step_num . ' ';
if (array_key_exists('file', $bt_step))
$repr .= $bt_step['file'];
if (array_key_exists('line', $bt_step))
$repr .= ':' . $bt_step['line'];
$repr .= ' ';
if (array_key_exists('class', $bt_step))
$repr .= $bt_step['class'] . '::';
$repr .= $bt_step['function'] . '()';
}
return $repr . PHP_EOL;
}
function _get_available_pops() {
$pops = array();
$classes = get_declared_classes();
foreach( $classes as $c) {
if($c == 'called_pops_obj')
continue;
$methods = get_class_methods($c);
foreach($methods as $m) {
array_push($pops, array($c, $m));
}
}
return $pops;
}
function _parse_arguments($output, $mode, $methods_included, $methods_excluded) {
// Get mode argument from REQUEST if available
if(!$mode) {
if(in_array('popfinder_mode',$_REQUEST) and $_REQUEST['popfinder_mode'])
$GLOBALS['mode'] = $_REQUEST['popfinder_mode'];
else
$GLOBALS['mode'] = 'find';
}
else {
$GLOBALS['mode'] = $mode;
}
// Set mode as default with wrong value
if(!in_array($GLOBALS['mode'], array('all', 'find', 'set'))) {
$GLOBALS['mode'] = 'find';
}
// Set included methods according to set mode
if($GLOBALS['mode'] == 'all') {
$GLOBALS["methods_inc"]=array( '__construct',
'__destruct',
'__call',
'__callStatic',
'__get',
'__set',
'__isset',
'__unset',
'__sleep',
'__wakeup',
'__toString',
'__invoke',
'__set_state',
'__clone');
}
else {
$GLOBALS["methods_inc"] = $methods_included;
}
$GLOBALS["methods_exc"] = $methods_excluded;
// Get output argument from REQUEST if available
if(!$output) {
if(in_array('popfinder_output',$_REQUEST) and $_REQUEST['popfinder_output'])
$GLOBALS["output"] = $_REQUEST['popfinder_output'];
else
$GLOBALS["output"] = '/tmp/popfinder' . _get_fileline() . '.txt';
}
else {
$GLOBALS["output"] = $output;
}
}
function wrap_serialized_object(
$serialized_object,
$output='',
$mode='',
$methods_included=array(),
$methods_excluded=array( '__construct' ))
{
$GLOBALS["called"] = array();
$GLOBALS["pops"] = _get_available_pops();
_parse_arguments($output, $mode, $methods_included, $methods_excluded);
return 'O:15:"called_pops_obj":1:{s:6:"object";' . $serialized_object . '}';
}
function _add_called($method, $formatted_params = '', $stackpoint = 3) {
array_push($GLOBALS["called"], array($method, $formatted_params, _get_stackpoint($stackpoint)));
}
function _dump_all() {
if(file_exists($GLOBALS["output"])) unlink($GLOBALS["output"]);
$called_methods = array();
$outdata = '# ' . $GLOBALS["banner"] . PHP_EOL . PHP_EOL . '# METHODS CALLED ON THE UNSERIALIZED OBJECT' . PHP_EOL . PHP_EOL;
foreach (array_values($GLOBALS["called"]) as $value) {
// Save it only if $GLOBALS["mode"] != 'set', where only methods_included and excluded are used
if($GLOBALS["mode"] != 'set')
array_push($called_methods,$value[0]);
$outdata .= $value[0] . "() " . $value[1] . " " . $value[2] . PHP_EOL;
}
$selected_methods = array_diff(array_merge($called_methods,$GLOBALS["methods_inc"]),$GLOBALS["methods_exc"]);
$outdata .= PHP_EOL . '# AVAILABLE METHODS IN SCOPE' . PHP_EOL;
$outdata .= '# SELECTION: ' . join("() ", $selected_methods) . "()" . PHP_EOL;
foreach (array_values($GLOBALS["pops"]) as $value) {
if(in_array($value[1],$selected_methods))
$outdata .= $value[0] . "::" . $value[1] . "()" . PHP_EOL;
}
file_put_contents($GLOBALS["output"], $outdata);
}
class called_pops_obj {
public $object;
public function __construct() {
_add_called("__construct");
}
public function __destruct( ) {
_add_called("__destruct");
_dump_all();
if(method_exists($this->object,'__destruct'))
$this->object->__destruct();
}
public function __call( $name, $arguments ) {
_add_called($name, 'Arguments ' . str_replace(PHP_EOL, ' ', print_r($arguments,True)));
call_user_func_array(array($this->object,$name), $arguments);
}
public static function __callStatic( $name, $arguments ) {
_add_called($name, '(static) Arguments ' . str_replace(PHP_EOL, ' ', print_r($arguments,True)));
}
public function __set($name, $value) {
_add_called('__set', $name . ' = ' . $value);
$this->object->$name = $value;
}
public function __get($name) {
_add_called('__get', $name);
return $this->object->$name;
}
public function __isset($name) {
_add_called('__isset', $name);
return isset($this->object->$name);
}
public function __unset($name) {
_add_called('__unset', $name);
unset($this->object->$name);
}
public function __sleep() {
_add_called('__sleep');
if(method_exists($this->object,'__sleep'))
return $this->object->__sleep();
return array();
}
public function __wakeup() {
_add_called('__wakeup', '');
}
public function __toString() {
_add_called('__toString');
return strval($this->object);
}
public function __invoke($arguments = array()) {
_add_called("__invoke", 'Arguments ' . str_replace(PHP_EOL, ' ', print_r($arguments,True)));
return $this->object($arguments);
}
public static function __set_state($properties) {
_add_called("__set_state", '(static) Arguments ' . str_replace(PHP_EOL, ' ', print_r($properties,True)));
}
public function __clone() {
_add_called('__clone');
return clone $this->object;
}
}
?>
<?php
$log_file = "/tmp/poptracker.txt";
$tracked_class_list = array();
/*
* Logs to debug file.
*/
//function log_call() {
// print get_backtrace();
//}
function log_call($method, $formatted_params = '', $stackpoint = 3) {
print $method . PHP_EOL;
print get_backtrace();
}
/*
* Returns pretty printed backtrace if at least one of the
* classes in the stack is tracked.
*/
function get_backtrace()
{
$result = '';
$trace = end(debug_backtrace());
$parameters = '';
if (array_key_exists('args', $trace)) {
foreach ($trace['args'] as $parameter)
$parameters .= $parameter . ', ';
if (substr($parameters, -2) == ', ')
$parameters = substr($parameters, 0, -2);
}
if (array_key_exists('class', $trace)) {
$result .= sprintf("> %s:%s %s::%s(%s)".PHP_EOL, $trace['file'], $trace['line'], $trace['class'], $trace['function'], $parameters);
}
else {
$result .= sprintf("> %s:%s %s(%s)".PHP_EOL, $trace['file'], $trace['line'], $trace['function'], $parameters);
}
return $result;
}
/*
* Extends the tracked classes with the name "TrackedClass"
* to facilitate the tracking.
*/
function register_tracking_classes($serialized_string, $class_list) {
global $tracked_class_list;
foreach($class_list as $orig_class) {
$orig_class_len = strlen($orig_class);
$tracked_class = "Tracked$orig_class";
$tracked_class_len = strlen($tracked_class);
eval("class $tracked_class extends $orig_class" . ' {
public function __construct() {
log_call("__construct");
parent::__construct();
}
public function __destruct( ) {
log_call("__destruct");
//_dump_all();
if(method_exists(parent,"__destruct"))
return parent::__destruct();
}
public function __call( $name, $arguments ) {
log_call($name . " Arguments " . str_replace(PHP_EOL, " ", print_r($arguments, True)));
if(method_exists(parent, $name))
return call_user_func_array(array(parent, $name), $arguments);
}
public static function __callStatic( $name, $arguments ) {
log_call($name, "(static) Arguments " . str_replace(PHP_EOL, " ", print_r($arguments,True)));
return call_user_func_array(array(parent, $name), $arguments);
}
public function __set($name, $value) {
log_call("__set", $name . " = " . $value);
$this->$name = $value;
}
public function __get($name) {
log_call("__get", $name);
return $this->$name;
}
public function __isset($name) {
log_call("__isset", $name);
return isset($this->$name);
}
public function __unset($name) {
log_call("__unset", $name);
unset($this->$name);
}
public function __sleep() {
// log_call("__sleep");
if(method_exists(parent,"__sleep"))
return parent::__sleep();
}
public function __wakeup() {
log_call("__wakeup ");
}
public function __toString() {
log_call("__toString");
return parent::__toString();
}
public function __invoke($arguments = array()) {
log_call("__invoke Arguments " . str_replace(PHP_EOL, " ", print_r($arguments,True)));
if(method_exists(parent,"__invoke"))
return parent::__invoke();
}
public static function __set_state($properties) {
log_call("__set_state (static) Arguments " . str_replace(PHP_EOL, " ", print_r($properties,True)));
}
public function __clone() {
log_call("__clone");
//return clone $this;
}
}
');
array_push($tracked_class_list, $tracked_class);
$serialized_string = str_replace(
"O:$orig_class_len:\"$orig_class\":",
"O:$tracked_class_len:\"$tracked_class\":",
$serialized_string
);
return $serialized_string;
}
}
function wrap_serialized_string($serialized_string, $class_list) {
global $tracked_class_list;
$serialized_string = register_tracking_classes($serialized_string, $class_list);
return $serialized_string;
}
?>
<?php
declare(ticks=1);
// A function called on each tick event
function tick_handler()
{
$trace = debug_backtrace();
if (count($trace) <= 1) {
return;
}
//var_dump($trace);
$frame = $trace[1];
unset($trace);
$function = $frame['function'];
echo __FUNCTION__ . ' :: ' . $function . PHP_EOL;
}
register_tick_function('tick_handler');
function bar() {
echo('');
}
function foo() {
echo('');
}
class Cane {
function baz() {
echo('');
}
}
function do_more() { echo(''); }
function do_something() { echo(''); do_more(); }
do_something();
$c = new Cane();
$c->baz();
<?php
include('poptracker.php');
class TestClass {
function __toString() {
print('');
return '';
}
function fun() {
print('');
}
}
$serialized = serialize(new TestClass());
$wrapped_string = wrap_serialized_string($serialized, array("TestClass"));
$obj = unserialize($wrapped_string);
serialize($obj);
//
// # Trigger fun()
$obj->fun();
// # Trigger __call()
$obj->nonexistent();
//
//
# Trigger __set()
$obj->variable = 4;
# Trigger __get()
$obj->variable;
# Trigger __toString()
(string)$obj;
//
# Trigger __invoke()
$obj();
//
// # Trigger __setState()
$obj->__set_state(array());
//
// # Trigger __staticCall()
$obj::fun();
// # Trigger __clone()
clone $obj;
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment