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?id=3' 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?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:1 micro
http://localhost:8080/test.php?id=3' 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?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: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:4 chaoliu micro helloworld 你好 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:4 chaoliu micro helloworld 你好
这3个例子要仔细看。一定要注意,-- 的前后都要加上空格才会生效。
另外,在参数里加上分号; 对于拼接sql的代码,也是会生效的。
现在试试,直接在url里面对数据库内容进行修改。
http://localhost:8080/test.php?id=3'; update user set nickname='a' where id=1; -- ' PHP version:5.5.30 SQL is:select nickname from user where id = '3'; update user set nickname='a' where id=1; -- '' 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?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
注:防御成功
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注入的处理吧。