sql二次注入和截断联合使用

来源:90WIKI
作者:@Spoock
发表时间: 2017-3-16 19:54:57

sql二次注入和截断

这次主要是说明sql的二次注入和sql插入数据库时截断的问题而形成的一种特殊的注入手段,有时会有意想不到的效果。

sql二次注入

二次注入的原理也是非常的简单,在第一次进行数据库插入数据的时候,仅仅只是使用了addslashes或者是借助get_magic_quotes_gpc对其中的特殊字符进行了转义,在写入数据库的时候还是保留了原来的数据,但是数据本身还是脏数据。在将数据存入到了数据库中之后,开发者就认为数据是可信的。在下一次进行需要进行查询的时候,直接从数据库中取出了脏数据,没有进行进一步的检验和处理,这样就会造成SQL的二次注入。比如在第一次插入数据的时候,数据中带有单引号,直接插入到了数据库中;然后在下一次使用中在拼凑的过程中,就形成了二次注入。

在网上搜索的时候,发现这张图对于SQL二次注入的原理解释得很好,我就直接使用了

image

对SQL二次注入不了解的,可以去看文章dedecms鸡肋级注入与细节分析过程

sql二次截断

这个是mysql数据库的特性,当插入的数据超过数据库规定的长度时,数据库会将数据自动地截断进行插入,不会有任何的报错。
下面来进行一个简单的实验。
有如下的表数据:

CREATE TABLE `users` (
  `user_id` int(5) NOT NULL AUTO_INCREMENT,
  `username` varchar(30) DEFAULT NULL,
  `password` varchar(32) DEFAULT NULL,
  `email` varchar(30) DEFAULT NULL,
  `ip` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1; 可以看到其中的username的长度是30

如果我们使用如下的语句插入数据:

insert into users(username,password,email,ip) values('aaaaaaaaaaaaaaaaaaaaaaaaaaaaab','1','2') 

其中的username设置的值是30个字母a和一个字母b,最后的结果为:

可以看到最后字母b被截断了。没有被插入到数据库中。
以上就是一个简单的sql截断的例子。

实例演示

sql的二次注入,如果在第一次插入数据的时候对数据进行了转义,那么在下次使用的过程中也不会出现问题。但是借助于sql截断的方法,有时能够有一种化腐朽为神奇的效果。利用截断的功能,将最后一个的特殊字符去掉,这样就可以绕过限制了。

if (!get_magic_quotes_gpc()){
        foreach($_POST as $key => $value){
                $_POST[$key] = addslashes($value);
        }
        foreach($_GET as $key => $value){
                $_GET[$key] = addslashes($value);
        }
}
if($_GET['action'] == 'login'){
        if($_SESSION['login']){
                header('Location: index.php');
                die();
        }
        if($_POST['username'] && $_POST['password']){
                $username = mysql_real_escape_string($_POST['username']);
                $password = md5($_POST['password']);
                $sql = "SELECT * FROM users WHERE username = '$username' and password = '$password'";
                $query = mysql_query($sql);
                if(mysql_num_rows($query) == 1){
                        $row = mysql_fetch_array($query);
                        $_SESSION['login'] = 1;
                        $_SESSION['username'] = $row['username'];
                        $_SESSION['email'] = $row['email'];
                        header('Location: index.php');
                        die();
                }else{
                        ShowLoginForm();
                        die('<br>login error');
                }
        }
        ShowLoginForm();
        die();
} else if($_GET['action'] == 'register'){
        if($_POST['username'] && $_POST['password'] && $_POST['email']){
                $username = mysql_real_escape_string($_POST['username']);
                $password = md5($_POST['password']);
                $email = mysql_real_escape_string($_POST['email']);
                $ip = getenv('REMOTE_ADDR');
                $sql = "SELECT * FROM users WHERE username = '$username'";
                $query = mysql_query($sql);
                if(mysql_num_rows($query) > 0){
                        ShowRegisterForm();
                        die('<br>user exists!');
                }
                $sql = "INSERT INTO users(username, password, email, ip) VALUES ('$username', '$password', '$email', '$ip')";
                $query = mysql_query($sql);
                if(!$query){
                        die('db error');
                }else{
                        $uid = mysql_insert_id();
                        $sql = "SELECT * FROM users WHERE user_id = $uid";
                        $query = mysql_query($sql);
                        $row = mysql_fetch_array($query);
                        $_SESSION['login'] = 1;
                        $_SESSION['username'] = $row['username'];
                        $_SESSION['email'] = $row['email'];
                        header('Location: index.php');
                        die('success');
                }
        }
        ShowRegisterForm();
        die();
}

if(!$_SESSION['login']){
        header('Location: index.php?action=login');
        die();
}

if($_GET['search']){
        $username = $_SESSION['username'];
        $title = mysql_real_escape_string($_GET['search']);
        $sql = "select * from posts where username='$username' and title like '$title'";
        var_dump($sql);
        $result = mysql_query($sql);
       
        while ($row = mysql_fetch_assoc($result)) {
                echo "title: ".$row['title'];
                echo '<br>';
                echo "content: ".$row['content'];
                echo '<br>';
                echo '<br>';
        }

        die();
} 

首先分析一下逻辑,在注册逻辑中

$username = mysql_real_escape_string($_POST['username']);
$sql = "INSERT INTO users(username, password, email, ip) VALUES ('$username', '$password', '$email', '$ip')";
$query = mysql_query($sql); 

在将数据插入到数据库之前,使用了mysql_real_escape_string对数据进行了转义处理。
在注入成功之后

$sql = "SELECT * FROM users WHERE user_id = $uid";
$query = mysql_query($sql);
$row = mysql_fetch_array($query);
$_SESSION['login'] = 1;
$_SESSION['username'] = $row['username']; 

从数据库取出数据,放入到SESSION中,取出的数据是已经转义过的。

在查询逻辑中

$username = $_SESSION['username'];
$title = mysql_real_escape_string($_GET['search']);
$sql = "select * from posts where username='$username' and title like '$title'"; 

其中的$username = $_SESSION['username'];
username是直接从session中取出来的。

在这种情况下,我们就可以通过sql截断的方法去掉mysql_real_escape_string转义的'。
我们尝试注册一个用户名为admin '长度为30的用户,最后的结果如下:

可以看到最后插入数据库的用户名是admin \

原因很简单

用户名进行mysql_real_escape_string转义之后,变为了admin \'

此时用户名的长度是31,超过了数据库的长度,此时就会进行截断,最后的'就会被截断,所以最后插入到数据库中的的用户名就会变为admin \

这样我们就多一个\可以使用了

下面就是构造一个简单的payload了,由于search是我们可控的,所以我们最终的查询可以写为:

localhost/sql4/index.php?search=union select 1,2,3,@@version%23

最后的输出结果为:

通过sql截断的方式得到了一个\,注释掉后面的',然后就可以利用union语句注出数据了。

后续

经过p牛的指点,我的文章中的还是有一些错误。下面就做一个补充说明吧。

mysql strick-mode

在mysql的配置文件my.cnf中存在一个配置项sql-mode,我的配置选项是:

sql-mode="NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION" 

在上面的配置中,是一个非严格模式。非严格模式就可以发生我的文章中的截断的情况。

如果在sql-mode中加入STRICT_TRANS_TABLES,变为:

sql-mode="NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES" 

STRICT_TRANS_TABLES严格模式下,会进行数据的严格校验,错误数据不能插入,报error错误就会变成严格模式。关于严格模式,可以看文章mysql严格模式说明

我们可以通过select @@sql_mode来查看数据库当前的模式。
在严格模式下,我们再一次进行尝试:

可以看到在严格模式下,无法插入数据。所以如果mysql要进行截断,是需要在非严格模式下。

斜线和单引号

在sql和php中,\'其实标识的就是单引号,是一个字符,而不是反斜线+单引号。这个很容易验证。
在php中

$mystr = '\'';
echo strlen($mystr); 

输出的结果是1不是2
在mysql中

mysql> select length('\'');
+--------------+
| length('\'') |
+--------------+
|            1 |
+--------------+
1 row in set 

结果是1
所以我的文章中说的

用户名进行mysql_real_escape_string转义之后,变为了admin \',此时用户名的长度是31,超过了数据库的长度,此时就会进行截断,最后的'就会被截断

这句话是错的,admin \'的长度还是30而不是31
但为什么进入到数据库中还是截断了呢?

sql截断

为什么我的文章中最后还是可以进行截断呢?
是因为在上面的代码中进行了两次转义,首先使用了addslashes进行了转义,后来又使用了mysql_real_escape_string进行了转义,这种情况下就会多出一个\。

还是用一段简单的代码来说明问题

可以看到'经过两次转义之后变为\\\',结果长度变为了4吗?我们在mysql中看看

mysql> select length('\\\'');
+----------------+
| length('\\\'') |
+----------------+
|              2 |
+----------------+
1 row in set 

可以看到mysql认为\\\'的长度是2。这就说明了'在经过两次转义的情况下会变为字符'(反斜线+单引号),长度加1了,所以最后才会发生截断。

这样就应该理解得更加透彻了。还请各位大佬多多指教。

1 Like