SQL注入实验,PHP连接数据库,Mysql查看binlog,PreparedStatement,mysqli, PDO

看到有人说了判断能否sql注入的方法:

简单的在参数后边加一个单引号,就可以快速判断是否可以进行SQL注入,这个百试百灵,如果有漏洞的话,一般会报错。

下面内容参考了这两篇文章

http://blog.csdn.net/stilling2006/article/details/8526458

http://www.aichengxu.com/view/43982

nginx配置文件位置:/usr/local/etc/nginx/nginx.conf

主目录位置:/usr/local/Cellar/nginx/1.10.1/

Symfony位置:/Users/baidu/Documents/Data/Work/Installed/symfony/mywebsite/web

先在主目录创建一个文件 test.php,内容如下:

<?php
echo "PHP version:" . PHP_VERSION . "<br/>";

$con = mysql_connect(\'10.117.146.21:8306\', \'root\', \'[password]\');
mysql_select_db(\'springdemo\', $con);
$sql = \'select nickname from user where id = 1\';
$result = mysql_query($sql);

print_r(\'rows:\' . mysql_num_rows($result) . \'<br/>\');
while ($row = mysql_fetch_array($result)) {
  print_r($row[\'nickname\'] . \'<br/>\');
}

mysql_close($con);
?>

上面,注意几个细节:

1. mysql连接要关的,使用mysql_close,之前因为没关,始终有连接存在

2. mysql_num_rows获得行数,mysql_fetch_array获得每次结果。

然后,nginx运行之后,得到运行结果:

http://localhost:8080/test.php

PHP version:5.5.30
rows:1
abc

下面,改造成从参数拿sql参数。

<?php
echo "PHP version:" . PHP_VERSION . "<br/>";

$con = mysql_connect(\'10.117.146.21:8306\', \'root\', \'[password]\');
mysql_select_db(\'springdemo\', $con);

$input_id = trim($_GET[\'id\']);
$sql = \'select nickname from user where id = \' . $input_id;
var_dump(\'SQL is:\' . $sql);
$result = mysql_query($sql);

if ($result != null) {
  print_r(\'rows:\' . mysql_num_rows($result) . \'<br/>\');
  while ($row = mysql_fetch_array($result)) {
    print_r($row[\'nickname\'] . \'<br/>\');
  }
}

mysql_close($con);
?>

在上面,会使用id参数添加到sql里面,并且会将sql打印出来。

得到结果

http://localhost:8080/test.php?id=3

PHP version:5.5.30
string(50) "SQL is:select nickname from user where id = 3
" rows:1
micro

http://localhost:8080/test.php?id=3 or 1=1

PHP version:5.5.30
string(57) "SQL is:select nickname from user where id = 3 or 1=1
" rows:4
abc
micro
helloworld
你好

注意,开始的时候上面输出\'你好\'的是中文乱码。只需要在php文件开始加上下面这句,就可以避免乱码:

<?php
header(\'Content-Type: text/html; charset=utf-8\');

从上面可以看出,发生了sql注入。用户可以打印出所有的用户信息。

再引申一下。有的时候,我们会用引号将参数包住,但是这样仍然不能解决问题。

将PHP改成如下:

<?php
header(\'Content-Type: text/html; charset=utf-8\');
echo "PHP version:" . PHP_VERSION . "<br/>";

$con = mysql_connect(\'10.117.146.21:8306\', \'root\', \'[password]\');
mysql_select_db(\'springdemo\', $con);

$input_id = trim($_GET[\'id\']);
$sql = \'select nickname from user where id = \\'\' . $input_id . \'\\'\';
print_r(\'SQL is:\' . $sql . \'<br/>\');
$result = mysql_query($sql);

if ($result != null) {
  print_r(\'rows:\' . mysql_num_rows($result) . \'<br/>\');
  while ($row = mysql_fetch_array($result)) {
    print_r($row[\'nickname\'] . \'<br/>\');
  }
}

mysql_close($con);
?>

上面把var_dump换成了print_r,以免sql语句总是换行。

这时候,不同url访问获得的结果如下:

http://localhost:8080/test.php?id=3

PHP version:5.5.30
SQL is:select nickname from user where id = \'3\'
rows:1
micro

http://localhost:8080/test.php?id=3 or 1=1

PHP version:5.5.30
SQL is:select nickname from user where id = \'3 or 1=1\'
rows:1
micro

http://localhost:8080/test.php? or \'1\'=\'1

PHP version:5.5.30
SQL is:select nickname from user where id = \'3\' or \'1\'=\'1\'
rows:4
abc
micro
helloworld
你好

上面可以看到,加了引号,对于之前的情况是能够避免了。但是只要稍作调整,又能够成功进行sql注入了。

有时候,攻击者还会在参数里面加上\'--\'。这是因为sql会认为 -- 右边的都是注释,这样能够更方便对sql的控制。

首先将test.php改成如下,增加一个参数:

<?php
header(\'Content-Type: text/html; charset=utf-8\');
echo "PHP version:" . PHP_VERSION . "<br/>";

$con = mysql_connect(\'10.117.146.21:8306\', \'root\', \'[password]\');
mysql_select_db(\'springdemo\', $con);

$input_id = trim($_GET[\'id\']);
$name = trim($_GET[\'name\']);
$sql = \'select nickname from user where id = \\'\' . $input_id . \'\\' and nickname = \\'\' . $name . \'\\'\';
print_r(\'SQL is:\' . $sql . \'<br/>\');
$result = mysql_query($sql);

if ($result != null) {
  print_r(\'rows:\' . mysql_num_rows($result) . \'<br/>\');
  while ($row = mysql_fetch_array($result)) {
    print_r($row[\'nickname\'] . \'<br/>\');
  }
}

mysql_close($con);
?>

然后对于不同url的访问结果:

http://localhost:8080/test.php?id=3

PHP version:5.5.30
SQL is:select nickname from user where id = \'3\' and nickname = \'\'
rows:0

http://localhost:8080/test.php?id=3&name=micro

PHP version:5.5.30
SQL is:select nickname from user where id = \'3\' and nickname = \'micro\'
rows:1
micro

http://localhost:8080/test.php?id=3 or 1=1

PHP version:5.5.30
SQL is:select nickname from user where id = \'3 or 1=1\' and nickname = \'\'
rows:0

http://localhost:8080/test.php? or \'1\'=\'1

PHP version:5.5.30
SQL is:select nickname from user where id = \'3\' or \'1\'=\'1\' and nickname = \'\'
rows:1
micro
http://localhost:8080/test.php? or \'1\'=\'1&name=micro
PHP version:5.5.30
SQL is:select nickname from user where id = \'3\' or \'1\'=\'1\' and nickname = \'micro\'
rows:1
micro

以上例子可以看出,即使加了sql注入,但是拼出来的sql仍然受到了限制只能返回一行。不过,加了 -- 注释符号,情况就又不一样了。

http://localhost:8080/test.php? or 1=1 --\'

PHP version:5.5.30
SQL is:select nickname from user where id = \'3\' or 1=1 --\'\' and nickname = \'\'
rows:1
micro

http://localhost:8080/test.php? or 1=1 -- \'

PHP version:5.5.30
SQL is:select nickname from user where id = \'3\' or 1=1 -- \'\' and nickname = \'\'
rows:4
chaoliu
micro
helloworld
你好

http://localhost:8080/test.php? or 1=1; -- \'

PHP version:5.5.30
SQL is:select nickname from user where id = \'3\' or 1=1; -- \'\' and nickname = \'\'
rows:4
chaoliu
micro
helloworld
你好

这3个例子要仔细看。一定要注意,-- 的前后都要加上空格才会生效。

另外,在参数里加上分号; 对于拼接sql的代码,也是会生效的。

现在试试,直接在url里面对数据库内容进行修改。

http://localhost:8080/test.php?; update user set nickname=\'a\' where 

PHP version:5.5.30
SQL is:select nickname from user where id = \'3\'; update user set nickname=\'a\' where \' and nickname = \'\'

貌似运行没有成功。
但是直接把sql贴到Mysql客户端运行是可以成功的。

PHP的error log是在

/usr/local/var/log/php_errors.log

但是没有看到有打印错误内容。

去查看了Mysql的error日志,在Mysql机器上面的

/home/work/.jumbo/var/lib/mysql 目录里面有几种日志,也没有看到信息。

所以想到查binlog看看。binlog的查看方法(在Mysql客户端里面):

mysql> show binlog events in \'mysql-bin.000005\'\G

*************************** 207. row ***************************
   Log_name: mysql-bin.000005
        Pos: 18070
 Event_type: Query
  Server_id: 1
End_log_pos: 18144
       Info: BEGIN
*************************** 208. row ***************************
   Log_name: mysql-bin.000005
        Pos: 18144
 Event_type: Query
  Server_id: 1
End_log_pos: 18254
       Info: use `springdemo`; update user set nickname=\'abc\' where id=1
*************************** 209. row ***************************
   Log_name: mysql-bin.000005
        Pos: 18254
 Event_type: Xid
  Server_id: 1
End_log_pos: 18281
       Info: COMMIT /* xid=7772 */
209 rows in set (0.00 sec)

里面只记录了更新的日志。而且发现通过上面修改的更新是没有的。

还有一些其他binlog相关的命令:

mysql> show master status\G
*************************** 1. row ***************************
            File: mysql-bin.000005
        Position: 18281
    Binlog_Do_DB: 
Binlog_Ignore_DB: 
1 row in set (0.00 sec)


mysql> show binary logs;
+------------------+-----------+
| Log_name         | File_size |
+------------------+-----------+
| mysql-bin.000001 |     29776 |
| mysql-bin.000002 |   1036239 |
| mysql-bin.000003 |       769 |
| mysql-bin.000004 |       397 |
| mysql-bin.000005 |     18281 |
+------------------+-----------+
5 rows in set (0.00 sec)

另外,也可以用mysqlbinlog工具查看。还没有试过。

看起来这条url是不能生效的。

http://localhost:8080/test.php?id=3%27;%20update%20user%20set%20nickname=%27ab%27%20where%20id=1;%20--%20%27

又试了下drop table的命令

http://localhost:8080/test.php?id=3%27;%20drop%20table%20users;%20--%20%27

对于下面这个表:
mysql> create table users ( a varchar(20), b varchar(10), primary key (a) );
Query OK, 0 rows affected (0.12 sec)

运行上面的url之后,仍然存在
PHP version:5.5.30
SQL is:select nickname from user where id = \'3\'; drop table users; -- \'\' and nickname = \'\'

与原文描述的不一样了:http://blog.sina.com.cn/s/blog_6a384fce0100n95f.html

下面来说解决的办法:

1. 通过正则表达式匹配校验,或其他格式的校验(较复杂不规范)

2. 通过addslashes或者str_replace(比如把引号\'替换成\引号\')处理(有漏洞)

3. 通过mysql_real_escape_string处理(有漏洞)

4. 通过mysqli处理,用PreparedStatement(推荐)

5. 通过PDO(PHP Data Object)处理,也是用PreparedStatement(推荐)

对于1,太复杂琐碎,不做讨论。

对于2其中的str_replace,也不规范,不做讨论。

2其中的addslashes和mysql_real_escape_string 都存在由于客户端和Mysql服务器编码方式不一致导致的编码转换丢失和字符一分为二导致的漏洞。后面详述,先看本来的方案。

参考 http://www.aichengxu.com/view/43982

addslashes:

<?php
header(\'Content-Type: text/html; charset=utf-8\');
echo "PHP version:" . PHP_VERSION . "<br/>";

$con = mysql_connect(\'10.117.146.21:8306\', \'root\', \'[password]\');
mysql_select_db(\'springdemo\', $con);

$input_id = addslashes($_GET[\'id\']);
$name = addslashes($_GET[\'name\']);
$sql = \'select nickname from user where id = \\'\' . $input_id . \'\\' and nickname = \\'\' . $name . \'\\'\';
print_r(\'SQL is:\' . $sql . \'<br/>\');
$result = mysql_query($sql);

if ($result != null) {
  print_r(\'rows:\' . mysql_num_rows($result) . \'<br/>\');
  while ($row = mysql_fetch_array($result)) {
    print_r($row[\'nickname\'] . \'<br/>\');
  }
}

mysql_close($con);
?>

测试上面的一些URL:

http://localhost:8080/test.php?3\' and nickname = \'micro\'
rows:1
micro
注:正常
http://localhost:8080/test.php?id=3%27%20or%201=1;%20--%20%27 PHP version:5.5.30 SQL is:select nickname from user where id = \'3\\' or 1=1; -- \\'\' and nickname = \'\' rows:0
注:防御成功

mysql_real_escape_string:

<?php
header(\'Content-Type: text/html; charset=utf-8\');
echo "PHP version:" . PHP_VERSION . "<br/>";

$con = mysql_connect(\'10.117.146.21:8306\', \'root\', \'[password]\');
mysql_select_db(\'springdemo\', $con);

$input_id = mysql_real_escape_string($_GET[\'id\']);
$name = mysql_real_escape_string($_GET[\'name\']);
$sql = \'select nickname from user where id = \\'\' . $input_id . \'\\' and nickname = \\'\' . $name . \'\\'\';
print_r(\'SQL is:\' . $sql . \'<br/>\');
$result = mysql_query($sql);

if ($result != null) {
  print_r(\'rows:\' . mysql_num_rows($result) . \'<br/>\');
  while ($row = mysql_fetch_array($result)) {
    print_r($row[\'nickname\'] . \'<br/>\');
  }
}

mysql_close($con);
?>

注,mysql_real_escape_string函数需要连到Mysql服务器才能够工作。

测试一些URL:

http://localhost:8080/test.php?id=3&name=micro

PHP version:5.5.30
SQL is:select nickname from user where id = \'3\' and nickname = \'micro\'
rows:1
micro

http://localhost:8080/test.php?id=3%27%20or%201=1;%20--%20%27

PHP version:5.5.30
SQL is:select nickname from user where id = \'3\\' or 1=1; -- \\'\' and nickname = \'\'
rows:0

讨论上面两个函数里面的漏洞 http://www.t086.com/article/5027:

主要是针对这个字符:chr(0xbf).chr(0x27).

该漏洞最早2006年被国外用来讨论数据库字符集设为GBK时,0xbf27本身不是一个有效的GBK字符,但经过  addslashes()  转换后变为0xbf5c27,
前面的0xbf5c是个有效的GBK字符,所以0xbf5c27会被当作一个字符0xbf5c和一个单引号来处理,结果漏洞就触发了。 mysql_real_escape_string() 也存在相同的问题,只不过相比 addslashes() 它考虑到了用什么字符集来处理,因此可以用相应的字符集来处理字符。 意思是如果客户端和服务器能够设置一样的字符集,那么可以避免这个漏洞。 当mysql_real_escape_string检测到的编码方式跟client设置的编码方式(big5/bgk)不一致时,mysql_real_escape_string跟addslashes是没有区别的 。 [client] default-character-set=latin1 + mysql_query("SET CHARACTER SET \'gbk\'", $mysql_conn); 这种情况下mysql_real_escape_string 是基于 latin1工作的,是不安全的。 [client] default-character-set=gbk + mysql_query("SET CHARACTER SET \'gbk\'", $mysql_conn); 这种情况下mysql_real_escape_string 是基于 gbk工作的,是安全的。 但是文中作者测试了,仍然是有漏洞的。(存疑)

看了下Mysql 服务器的设置,的确有client这个分组,不过没有加上上面提到的字符集设置:

位置:
/home/work/.jumbo/etc/mysql/my.cfg

里面关于client的内容:
# The following options will be passed to all MySQL clients
[client]
#password       = your_password
port            = 8306
socket          = /home/work/.jumbo/var/run/mysqld/mysqld.sock

可能是需要加上 default-character-set=gbk 这样的设置吧。不过仍然不是很规范,不推荐。

文章作者还提到,可以用iconv来转换,不过更近粗暴,转不成功后面的就会截断,不太好。另外iconv之后,还需要再加上addslashes函数。

$this->sName=iconv(\'gbk//IGNORE\', \'utf-8\', $this->sName);              

下面就讨论两个推荐的方式:mysqli 和 PDO,他们的初始方法分别如下所示:

// PDO  
$pdo = new PDO("mysql:host=localhost;dbname=database", \'username\', \'password\');  
   
// mysqli, procedural way  
$mysqli = mysqli_connect(\'localhost\',\'username\',\'password\',\'database\');  
   
// mysqli, object oriented way  
$mysqli = new mysqli(\'localhost\',\'username\',\'password\',\'database\');  

另外,他们其实也都是有转码的函数的,不过更推荐的是更好的PreparedStatement方式:

推荐采用prepared statements的方式绑定查询来代替PDO::quote() 和 mysqli_real_escape_string().

看mysqli, PDO是否安装,可以通过php_info()打印出来的信息来看:

http://localhost:8080/index.php
调用了php_info()

API Extensions    mysqli,pdo_mysql,mysql

mysqli

MysqlI Support    enabled
Client API library version    mysqlnd 5.0.11-dev - 20120503 - $Id: 15d5c781cfcad91193dceae1d2cdd127674ddb3e $
Active Persistent Links    0
Inactive Persistent Links    0
Active Links    0

PDO
PDO support    enabled
PDO drivers    mysql, sqlite

看起来都安装了。

另起一篇,来看mysqli 和 PDO等的操作和对sql注入的处理吧。