phpcms v9.6.0 会员注册getshell漏洞分析

前言

作者:血魂

强调

1.漏洞是远程文件下载保存在本地导致的getshell
2.因为我个人习惯把关键代码贴到notepad++分析,为了方便阅读,提到的行数都是notepad++里的,所以和源码行数不匹配很正常
3.本篇漏洞分析借鉴了http://www.myhack58.com/Article/html/3/62/2017/85219.htm的审计思路
4.这篇文章是学习代码审计总结笔记,如果有错误和改正之处还望不吝赐教(这点必须放第一,强烈欢迎批评指正)
5.如转载(或其它),请标注作者,不胜感激

漏洞涉及版本

phpcms v9.6.0(可能并不只涉及这一单一版本,我没试╮(╯_╰)╭)

漏洞触发涉及文件

Register函数:phpcms/phpcms/modules/member/index.php
Get函数:phpcms/caches/caches_model/caches_data/member_input.class.php
member_input类:phpcms/caches/caches_model/caches_data/content_form.class.php
Getcache函数:phpcms/phpcms/libs/functions/global.func.php
Editor函数:phpcms/caches/caches_model/caches_data/member_input.class.php
Attachment类:phpcms/phpcms/libs/classes/attachment.class.php
缓存配置文件:phpcmscachescaches_modelcaches_data

整体思路

这个漏洞发生在会员注册页面,是因为远程下载文件保存在本地导致的getshell,一般都会进行严格的过滤,而这里过滤不严或代码的一些问题,导致可以绕过对于后缀名的限制和对保存文件的后缀名可控。

漏洞分析

我们先看会员注册页面的register函数,直接看如下代码:
info.PNG
可以看到在13行以post方式接收了info,其中new_html_special_chars函数代码如下:
new_html_special_chars.PNG
这个函数只是对字符串做了一些转换,并不造成什么影响,回到开始那段代码继续往下走,可以看见info数组被传递进了get函数,跟进这个函数看看:
get.PNG
直接从第52行代码开始看,这里用到了$this->fields,而它的定义在member_input类的构造函数里面,代码如下:
construct.PNG
在第7行,可以看见是对拼接后的字符串传递进了getcache函数,跟进这个函数:
getcache.PNG
这个函数用于读取缓存,而我们知道传递进来的字符串是'model_field_'.$modelid,其中$modelid是我们传递进来的参数是可控的,我们看看符合条件的文件有哪些:
model_field.PNG
总共有5个文件,先回到get函数的52行,可以看到$this->fields[$field]['formtype'],
我们先假设传递进来的$modelid值是1,那么我们读取缓存配置的文件是model_field_1.cache.php,其中关于formtype的配置是:
formtype.PNG
所以get函数的53行代码后段部分等同于$value = $this->editor($field, $value);也就是调用了editor函数,这里需要强调的是,触发漏洞需要调用editor函数,而我们读取缓存配置的文件有5个,然而其中符合'formtype' => 'editor',的文件只有4个,model_field_10.cache.php不在其中,所以我们传递进来的$modelid值就必须是1,2,3,11中的一个。然后我们现在继续跟进editor函数,代码如下:
editor.PNG
其中$this->site_config 的定义在content_input类的构造函数,就不贴全代码了,贴关键的一行代码:
site_config.PNG
所以读取的缓存配置文件是phpcms/caches/caches_commons/caches_data/sitelist.cache.php,那么代码$this->site_config['setting']读取的setting的配置是:
setting.PNG
好了,然后我们接着往下,可以看到在editor函数的9行代码调用了download函数,这个函数在attachment类下,这里需要格外注意的是,可以看见传递进去的第一个参数是规定为content,那么我们利用的时候就得提交info[content]=xxx,download函数代码如下:
download.PNG
download2.PNG
我们看download函数的第63行代码,需要强调的是这里查了url后缀是否以gif|jpg|jpeg|bmp|png为后缀,不是就会直接结束。继续往下读,读到第73行代码,可以看见调用了fillurl函数,我们跟进这个函数:
fillurl.PNG
fillurl2.PNG
看代码的159行,这是最关键的一行代码$pos = strpos($surl,'#');这行代码将url里#前面的切割出来保存为url,而前面在download函数是查过后缀的,那么我们可以利用这一行代码达到绕过的目的,比如我们提交http://xxx/test.php#.jpg会通过对文件后缀的检查,然后经过切割,留下来的会是http://xxx/test.php成功绕过,然后fillurl函数剩下的代码就不是很重要了,主要做了先去掉url里的http://然后将在一起的多个单引号替换成了单个,最后返回的时候在加上http://。然后我们继续看download函数没看完的代码,看到第83行代码,看到这里调用了fileext函数取文件扩展名,fileext函数代码如下:
fileext.PNG
读过代码后我们可以发现取得是最后一个点之后的内容当后缀名,这里需要强调是最后一个点,接着往下读,看第89行代码,其中$this->upload_func的定义在attachment类的构造函数,代码如下:
upload_func.PNG
那么回到download函数,看第90行代码$upload_func($file, $newfile)就等同于copy($file, $newfile),将文件复制到了本地,那么利用刚才的‘最后一个点’,我们可以控制文件后缀名为php,到此漏洞触发的全过程已经展现,我们已经可以导致getshell,那么最后一个问题来了,我们怎么找到shell,有两种方法:
第一种方法:
它生成的文件名是:uploadfile/年份/月日/时间具体到秒+3位100-999之间的随机字符+文件后缀
所以我们可以直接爆破,至于为什么知道这个文件命名规则,将漏洞分析的代码读后,里面有,具体就不复述了。。。
第二种方法:
我们回到最开始的register函数,找到如下代码:
insert.PNG
代码将userid变量加到数组,再通过insert插入到数据库,插入的表是v9_member_detail数据表,我们看看这个数据表:
v9_member_detail.PNG
我们知道我们利用的时候传递进来的是info[content],而这个表里就两个字段,并没有content字段,就会导致报错,报错会爆出shell的地址

Poc

poc.PNG

Poc执行结果

phpinfo.PNG

添加新评论