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
发表评论