内容简介:CI框架下column注入
TCTF Web有大量的审计题,其中有很多有趣又get到新姿势的。其中有一道关于CI框架下的注入的问题。
具体注入点在CI的select()中,查看官方文档,我们知道,select()函数选中一个列名,然后经过处理拼接到 sql 语句中,而这个处理本身来说就存在着问题。
而关于列名的注入,本身来说有一定的鸡肋性质,开发者一般都不会让用户可控列名。
但是有些时候,开发者会偷懒定义一些奇怪的路由设定个API,将其中的关键词提取出来当作列名,比如user表中有id字段,对应的路由规则就是 http://xxxxxx/user/id/1.xxx 如果没有对user进行处理直接放入select就会导致注入。
首先看一下select函数/system/database/DB_query_builder.php
public function select($select = '*', $escape = NULL) { if (is_string($select)) { $select = explode(',', $select); } //var_dump($select); // If the escape value was not set, we will base it on the global setting is_bool($escape) OR $escape = $this->_protect_identifiers; foreach ($select as $val) { $val = trim($val); if ($val !== '') { $this->qb_select[] = $val; $this->qb_no_escape[] = $escape; if ($this->qb_caching === TRUE) { $this->qb_cache_select[] = $val; $this->qb_cache_exists[] = 'select'; $this->qb_cache_no_escape[] = $escape; } } } //var_dump($this->qb_select); return $this; }
可以看到直接把结果存在qb_select数组中,所有这样的变量在最后语句执行时,被拼接起来形成sql语句。
所以,这里没有做任何处理,接下来看下(最终执行sql语句的函数)get()
/system/database/DB_query_builder.php
public function get($table = '', $limit = NULL, $offset = NULL) { if ($table !== '') { $this->_track_aliases($table); $this->from($table); } if ( ! empty($limit)) { $this->limit($limit, $offset); } $result = $this->query($this->_compile_select()); $this->_reset_select(); return $result; }
这里也没有对刚才所说的数组进行处理,还要继续跟踪,看下_compile_select
贴一下关键位置
foreach ($this->qb_select as $key => $val) { $no_escape = isset($this->qb_no_escape[$key]) ? $this->qb_no_escape[$key] : NULL; $this->qb_select[$key] = $this->protect_identifiers($val, FALSE, $no_escape); } $sql .= implode(', ', $this->qb_select);
escape默认是1,紧接着把提取出来的非sql关键词带入到protect_identifiers进行转义简化
public function protect_identifiers($item, $prefix_single = FALSE, $protect_identifiers = NULL, $field_exists = TRUE) { if ( ! is_bool($protect_identifiers)) { $protect_identifiers = $this->_protect_identifiers; } if (is_array($item)) { $escaped_array = array(); foreach ($item as $k => $v) { $escaped_array[$this->protect_identifiers($k)] = $this->protect_identifiers($v, $prefix_single, $protect_identifiers, $field_exists); } //var_dump($escaped_array); return $escaped_array; } // This is basically a bug fix for queries that use MAX, MIN, etc. // If a parenthesis is found we know that we do not need to // escape the data or add a prefix. There's probably a more graceful // way to deal with this, but I'm not thinking of it -- Rick // // Added exception for single quotes as well, we don't want to alter // literal strings. -- Narf if (strcspn($item, "()'") !== strlen($item)) { return $item; } // Convert tabs or multiple spaces into single spaces $item = preg_replace('/\s+/', ' ', trim($item)); // If the item has an alias declaration we remove it and set it aside. // Note: strripos() is used in order to support spaces in table names if ($offset = strripos($item, ' AS ')) { $alias = ($protect_identifiers) ? substr($item, $offset, 4).$this->escape_identifiers(substr($item, $offset + 4)) : substr($item, $offset); $item = substr($item, 0, $offset); } elseif ($offset = strrpos($item, ' ')) { $alias = ($protect_identifiers) ? ' '.$this->escape_identifiers(substr($item, $offset + 1)) : substr($item, $offset); $item = substr($item, 0, $offset); } else { $alias = ''; } // Break the string apart if it contains periods, then insert the table prefix // in the correct location, assuming the period doesn't indicate that we're dealing // with an alias. While we're at it, we will escape the components if (strpos($item, '.') !== FALSE) { $parts = explode('.', $item); // Does the first segment of the exploded item match // one of the aliases previously identified? If so, // we have nothing more to do other than escape the item // // NOTE: The ! empty() condition prevents this method // from breaking when QB isn't enabled. if ( ! empty($this->qb_aliased_tables) && in_array($parts[0], $this->qb_aliased_tables)) { if ($protect_identifiers === TRUE) { foreach ($parts as $key => $val) { if ( ! in_array($val, $this->_reserved_identifiers)) { $parts[$key] = $this->escape_identifiers($val); } } $item = implode('.', $parts); } return $item.$alias; } // Is there a table prefix defined in the config file? If not, no need to do anything if ($this->dbprefix !== '') { // We now add the table prefix based on some logic. // Do we have 4 segments (hostname.database.table.column)? // If so, we add the table prefix to the column name in the 3rd segment. if (isset($parts[3])) { $i = 2; } // Do we have 3 segments (database.table.column)? // If so, we add the table prefix to the column name in 2nd position elseif (isset($parts[2])) { $i = 1; } // Do we have 2 segments (table.column)? // If so, we add the table prefix to the column name in 1st segment else { $i = 0; } // This flag is set when the supplied $item does not contain a field name. // This can happen when this function is being called from a JOIN. if ($field_exists === FALSE) { $i++; } // Verify table prefix and replace if necessary if ($this->swap_pre !== '' && strpos($parts[$i], $this->swap_pre) === 0) { $parts[$i] = preg_replace('/^'.$this->swap_pre.'(\S+?)/', $this->dbprefix.'\\1', $parts[$i]); } // We only add the table prefix if it does not already exist elseif (strpos($parts[$i], $this->dbprefix) !== 0) { $parts[$i] = $this->dbprefix.$parts[$i]; } // Put the parts back together $item = implode('.', $parts); } if ($protect_identifiers === TRUE) { $item = $this->escape_identifiers($item); } return $item.$alias; } // Is there a table prefix? If not, no need to insert it if ($this->dbprefix !== '') { // Verify table prefix and replace if necessary if ($this->swap_pre !== '' && strpos($item, $this->swap_pre) === 0) { $item = preg_replace('/^'.$this->swap_pre.'(\S+?)/', $this->dbprefix.'\\1', $item); } // Do we prefix an item with no segments? elseif ($prefix_single === TRUE && strpos($item, $this->dbprefix) !== 0) { $item = $this->dbprefix.$item; } } if ($protect_identifiers === TRUE && ! in_array($item, $this->_reserved_identifiers)) { $item = $this->escape_identifiers($item); } return $item.$alias; }
如果匹配到()'其中任意字符,直接返回原部分语句;
trim处理
匹配到AS则把$item去掉最后面的AS及AS之后的部分,AS之后的部分做escape_identifiers处理
匹配到空格把最后的空格及其后面的部分拿出来从item去掉并做escape_identifiers处理
对$item做escape_identifiers处理与前面的$alias拼接返回
跟踪一下escape_identifiers
关键语句
preg_replace('/'.$preg_ec[0].'?([^'.$preg_ec[1].'\.]+)'.$preg_ec[1].'?(\.)?/i', $preg_ec[2].'$1'.$preg_ec[3].'$2', $item);
把item用重音符包裹,测试一下如下:($item 是 name%20from%20flash%20union%20select ,$alias是aa,前面只做了一次空格校验)
这个`无法闭合,因为`会被正则检测出来并在其后加上`。
回到前面,发现
strcspn函数,在 php 中进行匹配,只要检测到有相同部分,就停止检测,例如下面
所以,只要我们包含(、)、'任意一个就可以在protect_identifiers处理前直接return......
可以正确执行,但是因为最后显示结果使用的是
所以不能拿到结果,要盲注,测试一下sleep()
可以执行。
原题还过滤了大小于号,逗号等字符,所以最终的payload
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 服务端注入之Flask框架中服务端模板注入问题
- 三, 跨语言微服务框架 - Istio官方示例(自动注入.请求路由.流量控制.故障注入) 原 荐
- Quarkus框架入门之二:依赖注入 原 荐
- Golang依赖注入框架wire全攻略
- Go 工程化(三):依赖注入框架 wire
- 浅析PHP框架Laravel最新SQL注入漏洞
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。