关于安全性:来自“Bobby Tables”XKCD漫画的SQL注入如何工作?

How does the SQL injection from the “Bobby Tables” XKCD comic work?

只是看看:

XKCD Strip(来源:https://xkcd.com/327/)

此SQL的作用是:

1
Robert'); DROP TABLE STUDENTS; --

我知道'--都是供评论的,但DROP这个词不是也被评论了吗,因为它是同一行的一部分?


它把学生的桌子掉了。

学校程序中的原始代码可能看起来像

1
q ="INSERT INTO Students VALUES ('" + FNMName.Text +"', '" + LName.Text +"')";

这是将文本输入添加到查询中的幼稚方法,并且非常糟糕,正如您将看到的那样。

在第一个名称、中间名文本框fnmname.text(即Robert'); DROP TABLE STUDENTS; --和姓氏文本框lname.text(我们称之为Derper)的值与查询的其余部分连接之后,结果实际上是两个由语句终止符(分号)分隔的查询。第二个查询已注入第一个查询中。当代码对数据库执行此查询时,它将如下所示

1
INSERT INTO Students VALUES ('Robert'); DROP TABLE Students; --', 'Derper')

它用简单的英语大致翻译为两个查询:

Add a new record to the Students table with a Name value of 'Robert'

Delete the Students table

第二个查询之后的所有内容都标记为注释:--', 'Derper')

学生姓名中的'不是注释,它是结束字符串分隔符。因为学生的名字是一个字符串,所以需要在语法上完成假设查询。只有当注入的SQL查询产生有效的SQL时,注入攻击才起作用。

根据Dan04的机敏评论重新编辑


假设这个名字是在一个变量中使用的,$Name。然后运行此查询:

1
INSERT INTO Students VALUES ( '$Name' )

代码错误地将用户提供的任何内容作为变量。您希望SQL是:

INSERT INTO Students VALUES ( 'Robert Tables` )

但是聪明的用户可以提供他们想要的任何东西:

INSERT INTO Students VALUES ( 'Robert'); DROP TABLE Students; --' )

你得到的是:

1
INSERT INTO Students VALUES ( 'Robert' );  DROP TABLE STUDENTS; --' )

--只对行的其余部分进行注释。


正如其他人已经指出的那样,');关闭了原始语句,随后又关闭了第二个语句。到目前为止,大多数框架(包括PHP等语言)都有默认的安全设置,不允许在一个SQL字符串中使用多个语句。例如,在PHP中,通过使用mysqli_multi_query函数,只能在一个SQL字符串中运行多个语句。

但是,您可以通过SQL注入操作现有的SQL语句,而不必添加第二个语句。假设您有一个登录系统,它用这个简单的选择来检查用户名和密码:

1
2
$query="SELECT * FROM users WHERE username='" . $_REQUEST['user'] ."' and (password='".$_REQUEST['pass']."')";
$result=mysql_query($query);

如果提供peter作为用户名,提供secret作为密码,则得到的SQL字符串如下:

1
SELECT * FROM users WHERE username='peter' and (password='secret')

一切都很好。现在假设您提供这个字符串作为密码:

1
' OR '1'='1

那么得到的SQL字符串是:

1
SELECT * FROM users WHERE username='peter' and (password='' OR '1'='1')

这将使您能够在不知道密码的情况下登录到任何帐户。因此,您不需要能够使用两个语句来使用SQL注入,尽管如果能够提供多个语句,您可以做更具破坏性的事情。


不,'不是SQL中的注释,而是分隔符。

妈妈认为数据库程序员提出的请求看起来像:

1
INSERT INTO 'students' ('first_name', 'last_name') VALUES ('$firstName', '$lastName');

(例如)添加新的student,其中$xxx变量内容直接从HTML表单中提取,不检查格式也不转义特殊字符。

因此,如果$firstName包含Robert'); DROP TABLE students; --数据库程序将直接对数据库执行以下请求:

1
INSERT INTO 'students' ('first_name', 'last_name') VALUES ('Robert'); DROP TABLE students; --', 'XKCD');

也就是说,它将提前终止insert语句,执行cracker想要的任何恶意代码,然后注释掉可能存在的剩余代码。

嗯,我太慢了,在橙色的带子里我已经看到8个答案了…:-)这似乎是一个热门话题。


DR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- The application accepts input, in this case 'Nancy', without attempting to
-- sanitize the input, such as by escaping special characters
school=> INSERT INTO students VALUES ('Nancy');
INSERT 0 1

-- SQL injection occurs when input into a database command is manipulated to
-- cause the database server to execute arbitrary SQL
school=> INSERT INTO students VALUES ('Robert'); DROP TABLE students; --');
INSERT 0 1
DROP TABLE

-- The student records are now gone - it could have been even worse!
school=> SELECT * FROM students;
ERROR:  relation"students" does not exist
LINE 1: SELECT * FROM students;
                      ^

这将删除学生表。

(此答案中的所有代码示例都在PostgreSQL 9.1.2数据库服务器上运行。)

为了清楚地说明发生了什么,让我们用一个只包含名称字段的简单表来尝试这个方法,并添加一行:

1
2
3
4
5
school=> CREATE TABLE students (name TEXT PRIMARY KEY);
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index"students_pkey" for table"students"
CREATE TABLE
school=> INSERT INTO students VALUES ('John');
INSERT 0 1

假设应用程序使用以下SQL向表中插入数据:

1
INSERT INTO students VALUES ('foobar');

foobar替换为学生的实际姓名。正常的插入操作如下所示:

1
2
3
--                            Input:   Nancy
school=> INSERT INTO students VALUES ('Nancy');
INSERT 0 1

当我们查询表时,我们得到:

1
2
3
4
5
6
school=> SELECT * FROM students;
 name
-------
 John
 Nancy
(2 rows)

当我们把小鲍比的名字放进桌子里会发生什么?

1
2
3
4
--                            Input:   Robert'); DROP TABLE students; --
school=> INSERT INTO students VALUES ('Robert'); DROP TABLE students; --');
INSERT 0 1
DROP TABLE

这里的SQL注入是学生名称终止语句并包含一个单独的DROP TABLE命令的结果;输入末尾的两个破折号用于注释任何可能导致错误的剩余代码。输出的最后一行确认数据库服务器已删除该表。

需要注意的是,在INSERT操作期间,应用程序没有检查输入是否有任何特殊字符,因此允许在SQL命令中输入任意输入。这意味着恶意用户可以将特殊符号(如引号)和任意SQL代码插入通常用于用户输入的字段中,以使数据库系统执行它,从而进行SQL注入。

结果如何?

1
2
3
4
school=> SELECT * FROM students;
ERROR:  relation"students" does not exist
LINE 1: SELECT * FROM students;
                      ^

SQL注入是与操作系统或应用程序中的远程任意代码执行漏洞等效的数据库。成功的SQL注入攻击的潜在影响不可低估——根据数据库系统和应用程序配置,攻击者可以使用它造成数据丢失(在本例中),获得对数据的未授权访问,甚至在主机本身上执行任意代码。

正如XKCD Comic所指出的,防止SQL注入攻击的一种方法是清理数据库输入,例如通过转义特殊字符,使它们无法修改底层SQL命令,因此无法导致执行任意SQL代码。如果使用参数化查询,例如在ADO.NET中使用SqlParameter,则输入将至少自动清理,以防止SQL注入。

但是,在应用程序级别清理输入可能不会停止更高级的SQL注入技术。例如,有一些方法可以绕过mysql_real_escape_stringphp函数。为了增加保护,许多数据库系统支持准备好的语句。如果在后端正确地实现,那么准备好的语句可以通过将数据输入视为与命令的其余部分语义上分离来使SQL注入成为不可能的。


假设你天真地写了这样的学生创造方法:

1
2
3
void createStudent(String name) {
    database.execute("INSERT INTO students (name) VALUES ('" + name +"')");
}

有人输入了Robert'); DROP TABLE STUDENTS; --的名字

在数据库上运行的是此查询:

1
INSERT INTO students (name) VALUES ('Robert'); DROP TABLE STUDENTS --')

分号结束insert命令并启动另一个命令;—注释行的其余部分。执行drop table命令…

这就是为什么绑定参数是一件好事。


单引号是字符串的开始和结束。分号是语句的结尾。因此,如果他们这样选择:

1
2
3
Select *
From Students
Where (Name = '<NameGetsInsertedHere>')

SQL将变成:

1
2
3
4
Select *
From Students
Where (Name = 'Robert'); DROP TABLE STUDENTS; --')
--             ^-------------------------------^

在某些系统上,select将首先运行,然后运行drop语句!消息是:不要在SQL中嵌入值。而是使用参数!


');结束查询,它不启动注释。然后,它将删除students表,并对应该执行的查询的其余部分进行注释。


在这种情况下,'不是注释字符。它用于分隔字符串文本。这位喜剧艺术家正指望着这样一个想法,那就是有关的学校在某个地方有一个动态的SQL,看起来像这样:

1
$sql ="INSERT INTO `Students` (FirstName, LastName) VALUES ('" . $fname ."', '" . $lname ."')";

所以现在,'字符在程序员期望它之前结束了字符串文字。结合;字符结束语句,攻击者现在可以添加他们想要的任何SQL。结尾的--comment是为了确保原始语句中的剩余SQL不会阻止在服务器上编译查询。

fwiw,我也认为漫画有一个重要的细节是错误的:如果你正在考虑清理你的数据库输入,正如漫画所暗示的那样,你仍然在做错误的事情。相反,您应该考虑隔离数据库输入,正确的方法是通过参数化查询。


数据库的作者可能做了一个

1
2
sql ="SELECT * FROM STUDENTS WHERE (STUDENT_NAME = '" + student_name +"') AND other stuff";
execute(sql);

如果给定的是学生名,则选择名为"robert",然后删除表。"-"部分将给定查询的其余部分更改为注释。


SQL中的'字符用于字符串常量。在这种情况下,它用于结束字符串常量,而不是用于注释。


这就是它的工作原理:假设管理员正在查找学生的记录

1
Robert'); DROP TABLE STUDENTS; --

由于管理帐户具有很高的权限,因此可以从此帐户中删除表。

从请求中检索用户名的代码是

现在,查询应该是这样的(搜索学生表)

1
2
3
String query="Select * from student where username='"+student_name+"'";

statement.executeQuery(query); //Rest of the code follows

结果查询变为

1
Select * from student where username='Robert'); DROP TABLE STUDENTS; --

由于用户输入没有被清除,所以上面的查询被操作为两部分

1
2
3
Select * from student where username='Robert');

DROP TABLE STUDENTS; --

双破折号(--)只对查询的其余部分进行注释。

这是危险的,因为它可能会使密码验证无效(如果存在的话)

第一个将进行常规搜索。

如果帐户有足够的权限,第二个帐户将删除表student(通常,学校管理帐户将运行此类查询,并将具有上述权限)。