新冠确诊阳性后的第一篇博客,一文带你学习SQL注入
- 1.你好SQL注入
- 2.盲注
- 3.Timing Attack
- 4.常见的攻击技巧
- 5.SQL CoIumn Truncation
- 6.防御SQL注入
- SQL注入防御的误区
- 使用预编译语句
- 使用存储过程
SQL注入攻击属于注入攻击的一种,注入攻击的本质,是把用户输入的数据当做代码执行。这里有两个关键条件,第一个是用户能够控制输入;第二个是原本程序要执行的代码,拼接了用户输入的数据
1.你好SQL注入
下面是一个SQL注入的典型例子:
var Shipcity;
ShipCity = Request.form ("ShipCity");
var sql = "select * from OrdersTable where ShipCity = '" + ShipCity + "'";
变量ShipCity的值由用户提交,在正常情况下,假如用户输入“Beijing”,那么SQL语句会执行:
SELECT * FROM OrdersTable WHERE ShipCity = 'Beijing'
但假如用户输入一段有语义的SQL语句,比如:
Beijing'; drop table OrdersTable--
那么,SQL语句在实际执行时就会如下:
SELECT * FROM OrdersTable WHERE ShipCity = 'Beijing'; drop table OrdersTable--'
我们看到,原本正常执行的查询语句,现在变成了查询完后,再执行一个drop表的操作,而这个操作,是用户构造了恶意数据的结果!🙌
在SQL注入的过程中,如果网站的Web服务器开启了错误回显,则会为攻击者提供极大的便利,比如攻击者在参数中输入一个单引号“'
”,引起执行查询语句的语法错误,服务器直接返回了错误信息:
Microsoft JET Database Engine错误 ’80040e14'
字符串的语法错误 在查询表达式 ’ID=49'’ 中。
/showdetail.asp,行8
从错误信息中可以知道,服务器用的是Access作为数据库,错误回显披露了敏感信息,对于攻击者来说,构造SQL注入的语句就可以更加得心应手了
2.盲注
所谓“盲注”,就是在服务器没有错误回显时完成的注入攻击。服务器没有错误回显,对于攻击者来说缺少了非常重要的“调试信息”,所以攻击者必须找到一个方法来验证注入的SQL语句是否得到执行。
最常见的盲注验证方法是,构造简单的条件语句,根据返回页面是否发生变化,来判断SQL语句是否得到执行。
如果攻击者构造如下的条件语句:
http://newspaper.com/items.php? id=2 and 1=2
实际执行的SQL语句就会变成:
SELECT title, description, body FROM items WHERE ID = 2 and 1=2
因为“and 1=2
”永远是一个假命题,所以这条SQL语句的“and
”条件永远无法成立。对于Web应用来说,也不会将结果返回给用户,攻击者看到的页面结果将为空或者是一个出错页面。
但是为了进一步确认注入是否存在,攻击者还必须再次验证这个过程。因为一些处理逻辑或安全功能,在攻击者构造异常请求时,也可能会导致页面返回不正常。攻击者继续构造如下请求:
http://newspaper.com/items.php? id=2 and 1=1
当攻击者构造条件“and 1=1
”时,如果页面正常返回了,则说明SQL语句的“and
”成功执行,那么就可以判断“id
”参数存在SQL注入漏洞了。
这就是盲注的工作原理!🍟
3.Timing Attack
在MySQL中,有一个BENCHMARK()
函数,它是用于测试函数性能的。它有两个参数:
BENCHMARK(count, expr)
函数执行的结果,是将表达式expr执行count次。比如:
mysql> SELECT BENCHMARK(1000000, ENCODE('hello', 'goodbye'));
+----------------------------------------------+
| BENCHMARK(1000000, ENCODE('hello', 'goodbye')) |
+----------------------------------------------+
| 0 |
+----------------------------------------------+
1 row in set (4.74 sec)
就将ENCODE('hello', 'goodbye')
执行了1000000次,共用时4.74秒。
因此,利用BENCHMARK()
函数,可以让同一个函数执行若干次,使得结果返回的时间比平时要长;通过时间长短的变化,可以判断出注入语句是否执行成功。这是一种边信道攻击,这个技巧在盲注中被称为Timing Attack。
4.常见的攻击技巧
SQL注入可以猜解出数据库的对应版本,比如下面这段Payload,如果MySQL的版本是4,则会返回TRUE:
http://www.site.com/news.php? id=5 and substring(version,1,1)=4
下面这段Payload,则是利用union select
来分别确认表名admin是否存在,列名passwd是否存在:
id=5 union all select 1,2,3 from admin
id=5 union all select 1,2, passwd from admin
在注入攻击的过程中,常常会用到一些读写文件的技巧。比如在MySQL中,就可以通过LOAD_FILE()
读取系统文件,并通过INTO DUMPFILE
写入本地文件。当然这要求当前数据库用户有读写系统相应文件或目录的权限。
例如,获取服务器上的/etc/passwd
文件:
… union select 1,1, LOAD_FILE('/etc/passwd'),1,1;
除了可以使用INTO DUMPFILE
外,还可以使用INTO OUTFILE
,两者的区别是DUMPFILE
适用于二进制文件,它会将目标文件写入同一行内;而OUTFILE
则更适用于文本文件。
5.SQL CoIumn Truncation
在MySQL的配置选项中,有一个sql_mode
选项。当MySQL的sql-mode
设置为default时,即没有开启STRICT_ALL_TABLES
选项时,MySQL对于用户插入的超长值只会提示warning,而不是error(如果是error则插入不成功),这可能会导致发生一些“截断”问题。
首先开启strict模式。
sql-mode="STRICT_TRANS_TABLES, NO_AUTO_CREATE_USER, NO_ENGINE_SUBSTITUTION"
在strict模式下,因为输入的字符串超出了长度限制,因此数据库返回一个error信息,同时数据插入不成功。
mysql> create table 'truncated_test' (-> `id` int(11) NOT NULL auto_increment,-> `username` varchar(10) default NULL,-> `password` varchar(10) default NULL,-> PRIMARY KEY ('id')-> )DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.08 sec)mysql> select * from truncated_test;
Empty set (0.00 sec)mysql> show columns from truncated_test;
+----------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+-------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| username | varchar(10) | YES | | NULL | |
| password | varchar(10) | YES | | NULL | |
+----------+-------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)mysql> insert into truncated_test('username', 'password') values("admin", "pass");Query OK, 1 row affected (0.03 sec)mysql> select * from truncated_test;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | admin | pass |
+----+----------+----------+
1 row in set (0.00 sec)mysql> insert into truncated_test('username', 'password') values("admin x",
"new_pass");
ERROR 1406 (22001): Data too long for column 'username' at row 1
mysql> select * from truncated_test;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | admin | pass |
+----+----------+----------+
1 row in set (0.00 sec)
当关闭了strict选项时:
sql-mode="NO_AUTO_CREATE_USER, NO_ENGINE_SUBSTITUTION"
数据库只返回一个warning信息,但数据插入成功。
mysql> select * from truncated_test;+----+----------+----------+| id | username | password |+----+----------+----------+| 1 | admin | pass |+----+----------+----------+1 row in set (0.00 sec)mysql> insert into truncated_test('username', 'password') values("admin x",-> "new_pass");Query OK, 1 row affected, 1 warning (0.01 sec)mysql> select * from truncated_test;+----+------------+----------+| id | username | password |+----+------------+----------+| 1 | admin | pass || 2 | admin | new_pass |+----+------------+----------+2 rows in set (0.00 sec)mysql>
6.防御SQL注入
SQL注入防御的误区
SQL注入的防御并不是一件简单的事情,开发者常常会走入一些误区。比如只对用户输入做一些escape
处理,这是不够的。参考如下代码:
$sql = "SELECT id, name, mail, cv, blog, twitter FROM register WHERE
id=".mysql_real_escape_string($_GET['id']);
当攻击者构造的注入代码如下时:
http://vuln.example.com/user.php? id=12, AND,1=0, union, select,1, concat(user,0x3a, passwo
rd),3,4,5,6, from, mysql.user, where, user=substring_index(current_user(), char(64),1)
将绕过mysql_real_escape_string
的作用注入成功。这条语句执行的结果如下。
因为mysql_real_escape_string() 仅仅会转义:
- ’
- “
- \r
- \n
- NULL
- Control-Z
那是不是再增加一些过滤字符,就可以了呢? 比如处理包括“空格”、“括号”在内的一些特殊字符,以及一些SQL保留字,比如SELECT、INSERT等。
其实这种基于黑名单的方法,都或多或少地存在一些问题,我们看看下面的案例。
注入时不需要使用空格的例子:
SELECT/**/passwd/**/from/**/user
SELECT(passwd)from(user)
不需要括号、引号的例子,其中0x61646D696E
是字符串admin的十六进制编码:
SELECT passwd from users where user=0x61646D696E
使用预编译语句
一般来说,防御SQL注入的最佳方式,就是使用预编译语句,绑定变量。比如在Java中使用预编译的SQL语句:
String custname = request.getParameter("customerName");
String query = "SELECT account_balance FROM user_data WHERE user_name = ? ";PreparedStatement pstmt = connection.prepareStatement( query );
pstmt.setString( 1, custname);
ResultSet results = pstmt.executeQuery( );
使用存储过程
除了使用预编译语句外,我们还可以使用安全的存储过程对抗SQL注入。使用存储过程的效果和使用预编语句译类似,其区别就是存储过程需要先将SQL语句定义在数据库中。但需要注意的是,存储过程中也可能会存在注入问题,因此应该尽量避免在存储过程内使用动态的SQL语句。如果无法避免,则应该使用严格的输入过滤或者是编码函数来处理用户的输入数据。
下面是一个在Java中调用存储过程的例子,其中sp_getAccountBalance
是预先在数据库中定义好的存储过程。
String custname = request.getParameter("customerName"); try {CallableStatement cs = connection.prepareCall("{call sp_getAccountBalance(? )}");cs.setString(1, custname);ResultSet results = cs.executeQuery();// … result set handling} catch (SQLException se) {// … logging and error handling}
从数据库自身的角度来说,应该使用最小权限原则,避免Web应用直接使用root、dbowner等高权限账户直接连接数据库。如果有多个不同的应用在使用同一个数据库,则也应该为每个应用分配不同的账户。Web应用使用的数据库账户,不应该有创建自定义函数、操作本地文件的权限。🎶
在最后,网络安全是一门实践性非常强的学科,Web安全又是网络安全的基石,诸位宜搭配各种漏洞靶场进行学习,切不可纸上谈兵!
版权声明:本文教程基于白帽子讲Web安全