在 CMS
中难免会调用到 SQL
执行(废话),百密总有一疏,今天跟师傅们分享一些平时审计时遇到的注入,可能不全面,希望和师傅们一起学习。
这绝对是能拿来探讨探讨的,因为我看到的版本就有两三种了,这里给出两个例子。
首先我们举个最简单的例子作为开场:
public function login(){
$User = D('User');
$map = array('username' => $_POST['username']);
$user = $User->where($map)->find();
}
首先调用了 where
函数:
public function where($where,$parse=null){
....
if(isset($this->options['where'])){
$this->options['where'] = array_merge($this->options['where'],$where);
}else{
$this->options['where'] = $where;
}
return $this;
}
这里简单的将 where
放进了 options[where]
里。
然后第二步是调用 find
函数,find
的内部又调用了 ->db->select
,这是具体的实现,可以进去看看(查询时可能会查找三个,通常是 Driver.class.php
):
public function select($options=array()) {
$this->model = $options['model'];
$this->parseBind(!empty($options['bind'])?$options['bind']:array());
$sql = $this->buildSelectSql($options); // 生成 sql 语句
$result = $this->query($sql,!empty($options['fetch_sql']) ? true : false);
return $result;
}
继续跟入 buildSelectSql
函数,会发现里面调用了 parseSql
函数,继续跟:
public function parseSql($sql,$options=array()){
$sql = str_replace(
array('%TABLE%','%DISTINCT%','%FIELD%','%JOIN%','%WHERE%','%GROUP%','%HAVING%','%ORDER%','%LIMIT%','%UNION%','%LOCK%','%COMMENT%','%FORCE%'),
array(
$this->parseTable($options['table']),
$this->parseDistinct(isset($options['distinct'])?$options['distinct']:false),
$this->parseField(!empty($options['field'])?$options['field']:'*'),
$this->parseJoin(!empty($options['join'])?$options['join']:''),
$this->parseWhere(!empty($options['where'])?$options['where']:''),
$this->parseGroup(!empty($options['group'])?$options['group']:''),
$this->parseHaving(!empty($options['having'])?$options['having']:''),
$this->parseOrder(!empty($options['order'])?$options['order']:''),
$this->parseLimit(!empty($options['limit'])?$options['limit']:''),
$this->parseUnion(!empty($options['union'])?$options['union']:''),
$this->parseLock(isset($options['lock'])?$options['lock']:false),
$this->parseComment(!empty($options['comment'])?$options['comment']:''),
$this->parseForce(!empty($options['force'])?$options['force']:'')
),$sql);
return $sql;
}
这里替换了一些信息后成了最终的 sql
语句,我们可控的是 where
,所以我们跟入 parseWhere
:
protected function parseWhere($where) {
$whereStr = '';
if(is_string($where)) {
....
}else{ // 使用数组表达式
$operate = isset($where['_logic'])?strtoupper($where['_logic']):'';
if(in_array($operate,array('AND','OR','XOR'))){
....
}else{
....
}
foreach ($where as $key=>$val){
....
if(0===strpos($key,'_')) {
....
}else{
$multi = is_array($val) && isset($val['_multi']);
$key = trim($key);
if(strpos($key,'|')) {
...
}elseif(strpos($key,'&')){
.....
}else{
$whereStr .= $this->parseWhereItem($this->parseKey($key),$val);
}
}
$whereStr .= $operate;
}
$whereStr = substr($whereStr,0,-strlen($operate));
}
return empty($whereStr)?'':' WHERE '.$whereStr;
}
如果传入一个数组,最终会进入到,最后我留下的这个 parseWhereItem
。
这个函数是审计 TP
注入时的关键函数。
这里的 $val
即使是一个数组也是可以的,所以我们可以传入一个数组进 parseWhereItem
函数,继续跟进:
protected function parseWhereItem($key,$val) {
$whereStr = '';
if(is_array($val)) {
if(is_string($val[0])) {
$exp = strtolower($val[0]);
if(...) {
....
}elseif(....){
.....
}elseif('bind' == $exp ){
...
}elseif('exp' == $exp ){ //看这里~~~~
$whereStr .= $key.' '.$val[1];
}elseif(preg_match('/^(notin|not in|in)$/',$exp)){
.....
}elseif(preg_match('/^(notbetween|not between|between)$/',$exp)){
.....
}else{
E(L('_EXPRESS_ERROR_').':'.$val[0]);
}
}else {
...
}
}else {
....
}
return $whereStr;
}
这里省略了大部分代码,在留下的一个逻辑中可以看到,当 $val
为数组且第一位是 exp
时,直接将 $val
的第二位拼接进去了,没有过滤。。。我们可以传入个 payload
试试。
我们可以在 parseSql
函数 return
前加入一行 print_r($sql);
然后传入:
成功注入了,至于利用就不过多叙述了。
在 TP5
中 parseWhereItem
在 Db.class.php
,但其实也有一段和 3.2 中差不多的代码:
if(isset($val[2]) && 'exp'==$val[2]) {
$whereStr .= $key.' '.strtoupper($val[0]).' '.$val[1];
}
但是在 TP5.0
却加入了一个全局过滤:
他在我们的 EXP
后面给多加了一个空格,导致这里不能用了,贴一下这个在 5.0
中这个函数的代码:
protected function parseWhereItem($key,$val) {
$whereStr = '';
if(is_array($val)) {
if(is_string($val[0])) {
if(preg_match('/^(EQ|NEQ|GT|EGT|LT|ELT|NOTLIKE|LIKE)$/i',$val[0])) {
...
}elseif('exp'==strtolower($val[0])){
...
}elseif(preg_match('/^(NOTIN|NOT IN|IN)$/i',$val[0])){
...
}elseif(preg_match('/(NOTBETWEEN|NOT BETWEEN|BETWEEN)/i',$val[0])){ // 看这里~~~
$data = is_string($val[1])? explode(',',$val[1]):$val[1];
$whereStr .= ' ('.$key.' '.strtoupper($val[0]).' '.$this->parseValue($data[0]).' AND '.$this->parseValue($data[1]).' )';
}else{
throw_exception(L('_EXPRESS_ERROR_').':'.$val[0]);
}
}else {
...
}
}else {
...
}
return $whereStr;
}
可以前面两个正则都使用了 ^
和 $
来限定正则表达式的开始和结束。但是 BETWEEN
这里却没有,只要 $val[0]
里带有 between
即可,在下面中也没有对 val[0]
带入 parseValue
进行过滤,那么这里我们能就可以插入单引号。
举个例子:
我们传入:id[0]=BETWEEN '1234&id[1]=abcd
调试图:
可以看到 abcd
被我们逃逸出来了。到这里注入就算完成了。具体的利用得看实际情况中。
在 TP5.1
中全部参数都被绑定了,弟弟没有 0day
在手,所以无法分享这个版本。。233
其实其他函数可能还有注入,但是这里就不再去深入了,只分析了三个不同版本的 parseWhereItem
函数。其中可能有什么有误或者不清楚的地方,欢迎师傅们提出问题一起探讨。
这里就不得不提一手 红日团队 的:https://github.com/hongriSec/PHP-Audit-Labs
里面也有一些关于 TP
的