上网络安全课,老师要我们翻译的…


0.前言

原文: http://www.cis.syr.edu/~wedu/seed/Labs_16.04/Web/Web_SQL_Injection/Web_SQL_Injection.pdf


1. 概览

SQL注入是一种代码注入技术,它利用了Web应用程序和数据库服务器接口之间的漏洞。该漏洞的成因为在发送到后端数据库服务器之前未在Web应用程序中正确检查用户的输入。   许多Web应用程序从用户那里获取输入,然后使用这些数据来构造SQL语句,这样他们就可以从数据库中查询信息。Web应用程序还可以执行SQL语句从而在数据库中储存信息。简单来说,通过SQL增删改查是Web开发中的常见做法,但是如果没有正确构造SQL语句,就可能会发生SQL注入。SQL注入是对Web应用攻击中最常见的攻击方法之一。   在本实验中,我们创建了一个存在SQL注入漏洞的Web应用程序,并且囊括了许多Web开发人员常犯的错误。你的目标是找到SQL注入漏洞,利用漏洞,展示攻击带来的损害,并掌握可以帮助抵御此类攻击的技术。本实验包含以下主题:

  • SQL语句:SELECT和UPDATE语句
  • SQL注入
  • 预处理

阅读参考:有关SQL注入的详细介绍可以在SEED书的第11章中找到。Computer Security: A Hands-on Approach,作者Wenliang Du

实验环境:本实验已经在我们预先构建的Ubuntu 16.04 VM上进行了测试,可以从SEED网站下载。

2. 实验环境

我们为本实验开发了一个Web应用程序,安装该Web应用程序的路径和此Web应用程序的URL如下所述:

1
2
URL: http://www.SEEDLabSQLInjection.com
Folder: /var/www/SQLInjection/

  上述URL只能从虚拟机内部访问,因为我们已修改/etc/hosts文件,将每个URL的域名映射到虚拟机的本地IP地址(127.0.0.1)。你可以通过修改/etc/hosts来将任何域名映射到特定的IP地址。例如,你可以把以下条目附加到/etc/hosts来将http://www.example.com映射到本地IP地址:

1
127.0.0.1   http://www.example.com

  如果你的Web服务器和浏览器在两台不同的计算机上运行,则需要相应地修改浏览器计算机上的/etc/hosts来将这些域名映射到Web服务器的IP地址,而不是127.0.0.1

Apache配置 在我们预先构建的VM镜像中,我们使用Apache服务器来托管本实验所使用的Web站点。Apache的虚拟主机功能可用于在同一台计算机上托管多个网站(或多个URL)。路径/etc/apache2/sites-available中名为000-default.conf的配置文件必须包含下列的字段:   在配置文件中,每个Web站点都有一个VirtualHost块,用于指定网站源文件的目录和网站URL。以下示例为如何设置URL为http://www.example1.com和另一个URL为http://www.example2.com的网站:

1
2
3
4
5
6
7
8
9
<VirtualHost *>
    ServerName http://www.example1.com
    DocumentRoot /var/www/Example_1/
</VirtualHost>

<VirtualHost *>
    ServerName http://www.example2.com
    DocumentRoot /var/www/Example_2/
</VirtualHost>

  你可以通过访问上述路径中的源文件来修改Web应用。例如,若使用上述配置,可以通过修改/var/www/Example_1/目录中的源文件来更改URL为http://www.example1.com的Web应用。对配置进行更改后,需要重新启动Apache服务器。请参阅以下命令:

1
$ sudo service apache2 start

3. 实验任务

我们创建了一个Web应用,并托管在www.SEEDLabSQLInjection.com上。此Web应用是一个简单的员工管理系统。员工可以通过此Web应用查看和更新数据库中的个人信息。此Web应用中主要有两个角色:Administrator是一个特权角色,可以管理每个员工的个人资料信息;Employee是一个正常的角色,可以查看或更新他/她自己的个人资料信息。所有员工信息如下表所述:

NameEmployee IDPasswordSalaryBirthdaySSNNicknameEmailAddressPhone#
Admin99999seedadmin4000003543254314
Alice10000seedalice2000092010211002
Boby20000seedboby5000042010213352
Ryan30000seedryan9000041032193525
Samy40000seedsamy4000011132111111
Ted50000seedted11000011324343244

3.1 任务1:熟悉SQL语句

此任务的目的是通过使用所提供的数据库来熟悉SQL命令。我们创建了一个名为Users的数据库,其中包含一个名为credential的表,该表存储每个员工的个人信息(例如eid,密码,工资,ssn等)。在此任务中,你需要使用数据库来熟悉SQL查询操作。   MySQL是一个开源的关系数据库管理系统。我们已经在SEEDUbuntu VM镜像中部署了MySQL。用户名是root,密码是seedubuntu。请使用以下命令登录MySQL控制台:

1
$ mysql -u root -pseedubuntu

  登录后,你可以创建新数据库或加载一个现有数据库。由于我们已经为你创建了Users数据库,你只需使用以下命令加载这个数据库:

1
mysql> use Users;

  要显示Users数据库中的表,可以使用以下命令打印所选数据库中的所有表。

1
mysql> show tables;

  在运行上述命令后,你需要使用SQL命令来打印员工Alice的所有个人信息。请提供结果的屏幕截图。

译者所提供的一种解答:

1
mysql> select * from credential where Name = 'Alice';

3.2 任务2:对SELECT语句的SQL注入攻击

SQL注入简单来说是一种攻击者执行其构造的恶意SQL语句(通常称为恶意有效负载(payload))的技术。通过这些恶意SQL语句,攻击者可以从受害者数据库中窃取信息。更糟糕的是,他们可能有权限对数据库进行更改。我们的员工管理系统存在SQL注入漏洞,这些漏洞模仿了开发人员经常犯的错误。   我们将使用www.SEEDLabSQLInjection.com的登录页面完成此任务。登录页面如图1所示,它要求用户提供用户名和密码。 Web应用会根据这两个数据对用户进行身份验证,因此只有知道其密码的员工才能登录。作为攻击者,你的任务是在不知道任何员工凭据的情况下登录Web应用。

图1

  为了帮助你开始执行此任务,我们将解释如何在Web应用中实现身份验证。位于var/www/SQLInjection目录中的PHP代码unsafe_home.php用于执行用户身份验证。以下代码显示了如何对用户进行身份验证。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
$input_uname = $_GET[’username’];
$input_pwd = $_GET[’Password’];
$hashed_pwd = sha1($input_pwd);
...
$sql = "SELECT id, name, eid, salary, birth, ssn, address, email,
nickname, Password
FROM credential
WHERE name= ’$input_uname’ and Password=’$hashed_pwd’";
$result = $conn -> query($sql);

// 下面是伪代码
if(id != NULL) {
    if(name==’admin’) {
    return All employees information;
    } else if (name !=NULL){
    return employee information;
    }
} else {
    Authentication Fails;
}

  上述SQL语句从credential表中选择个人员工信息,如id,name,salary,ssn等。SQL语句使用两个变量input_unamehashed_ pwd,其中input_uname保存用户在登录页面的username字段中输入的字符串,而hashed_pwd保存用户输入的密码的sha1哈希值。程序检查是否有任何记录与提供的用户名和密码匹配,如果匹配,则用户验证成功,并获得相应的员工信息,如果没有匹配,身份验证失败。

  • 任务2.1:来自网页的SQL注入攻击 你的任务是从登录页面以管理员身份登录Web应用,查看所有员工的信息。我们假设你知道管理员的帐户名称是admin,但不知道密码。你需要决定在UsernamePassword字段中输入怎样的payload才能成功进行攻击。

  • 任务2.2:来自命令行的SQL注入攻击 你的任务是重复任务2.1,但是需要在不使用浏览器的情况下执行此操作。你可以使用命令行工具(例如curl)来发送HTTP请求。需要注意的是,如果要在HTTP请求中包含多个参数,则需要将URL和参数放在一对引号之间。否则,用于分隔参数的特殊字符(例如)将会被shell解释执行,从而改变命令的含义。下面演示如何将HTTP GET请求发送到我们的Web应用,并附加两个参数(UsernamePassword): $ curl 'www.SeedLabSQLInjection.com/index.php?username=alice&Password=111' 如果你需要在用户名或密码字段中包含特殊字符,则需要对其进行正确的编码,否则它们可能会改变请求的含义。如果要在这些字段中包含单引号,应该使用%27,如果要包含空格,应该使用%20。在此任务中,你需要在使用curl发送请求时处理HTTP编码。

  • 任务2.3:拼接新的SQL语句 在上述两次攻击中,我们只能从数据库中窃取信息。如果我们可以使用登录页面中的相同漏洞修改数据库不就美滋滋了? 一个想法是使用SQL注入攻击将一个SQL语句变成两个,第二个是使用更新或删除语句。在SQL中,分号(;)用于分隔两个SQL语句。请描述如何使用登录页面让服务器运行两个SQL语句,并且尝试从数据库中删除记录并记录你观察的内容。

3.3 任务3:对UPDATE语句的SQL注入攻击

如果UPDATE语句发生SQL注入漏洞,将会造成更严重的损害,因为攻击者可以使用此漏洞修改数据库。 在我们的员工管理系统中,有一个编辑个人资料页面(图??),允许员工更新他们的个人资料信息,包括昵称,电子邮件,地址,电话号码和密码。当然,要转到此页面,员工需要先登录。   当员工通过此页面更新其信息时,将执行以下SQL UPDATE查询。在unsafe_edit_backend.php文件中的PHP代码实现了更新员工的个人资料信息的功能。 PHP文件位于/var/www/SQLInjection中。

1
2
3
4
5
6
7
8
9
$hashed_pwd = sha1($input_pwd);
$sql = "UPDATE credential SET
    nickname=’$input_nickname’,
    email=’$input_email’,
    address=’$input_address’,
    Password=’$hashed_pwd’,
    PhoneNumber=’$input_phonenumber’
    WHERE ID=$id;";
$conn->query($sql);

image

  • 任务3.1:修改自己的薪水 正如“编辑个人资料”页面所示,员工只能更新他们的昵称,电子邮件,地址,电话号码和密码,他们无权改变工资。假设你(爱丽丝)是一个心怀不满的员工,因为你的老板Boby今年没有增加你的工资。你希望通过利用“编辑个人资料”页面中的SQL注入漏洞来增加自己的工资。请演示如何实现这一目标。假设你知道工资存储在名为salary的字段中。
  • 任务3.2:修改其他人的工资 增加自己的工资后,你决定恶搞你的老板Boby,想把他的工资减少到1美元。请演示如何实现这一目标。
  • 任务3.3:修改其他人的密码 在改变Boby的工资之后,你仍然难受的一匹,所以你想把Boby的密码改掉,然后登录他的账户并进一步搞事。请演示如何实现这一目标。你需要证明你确实可以使用新密码成功登录Boby的帐户。这里需要注意的是数据库存储的是密码的哈希值而不是明文密码字符串。你可以再次查看unsafe_edit_backend.php代码,来查看密码的存储方式,发现是使用SHA1哈希函数生成密码的哈希值。

为了确保你的注入字符串不包含任何语法错误,你可以在对我们的Web应用进行真实攻击之前在MySQL控制台上测试你的注入字符串。

3.4 任务4:对策——预处理

SQL注入漏洞的根本原因是无法将代码与数据分开处理。在构造SQL语句时,程序(例如PHP程序)知道哪个部分是数据,哪个部分是代码。然而,当SQL语句被发送到数据库时,这个边界已经消失。SQL解释器看到的边界可能与开发人员设置的原始边界不同。要解决此问题,务必确保边界视图在服务器端代码和数据库中保持一致。最安全的方法是使用预处理语句。   要了解预处理如何阻止SQL注入,我们需要了解SQL Server在收到查询时发生了什么。执行查询的抽象流程如图3所示。在编译步骤中,查询首先经历解析和归一化,这将会根据语法和语义再一次检查语句。然后进入编译阶段,其中的关键字(例如,SERECT,FROM,UPDATE等)被转换为机器可理解的格式。简单来说,在此阶段,将解释查询。在查询优化阶段,一条查询可以有多种执行方法,最后都是返回相同结果,从中选择最佳优化计划。所选择的计划存储在缓存中,因此无论何时进入下一个查询,都将检查缓存中的内容,如果它已经存在于缓存中,则跳过解析,编译和查询优化阶段。上述阶段全部结束后将编译的查询传递到执行阶段进行执行。   预处理语句在编译之后但在执行步骤之前出现。预处理语句会通过编译步骤,转换为带有空占位符的数据的预编译查询。要运行此预编译查询,需要提供数据,但这些数据不会经过编译处理。相反,它们将直接插入预编译的查询中,并被发送到执行引擎。因此,即使数据中存在SQL代码,因为不经过编译步骤,代码将被简单地视为数据的一部分,所以没有任何特殊含义。这就是预处理语句如何防止SQL注入攻击的原因。

图3

  以下是在PHP中使用预处理语句的示例。我们将在下列示例中使用SELECT语句,并使用预处理语句来重写易受SQL注入攻击的代码。

1
2
$sql = "SELECT name, local, gender FROM USER_TABLE WHERE id = $id AND password =’$pwd’ "; 
$result = $conn->query($sql))

上面的代码存在SQL注入漏洞,可以将其改写为:

1
2
3
4
5
6
$stmt = $conn->prepare("SELECT name, local, gender FROM USER_TABLE WHERE id = ? and password = ? "); 
// 为语句绑定数据 
$stmt->bind_param("is", $id, $pwd); 
$stmt->execute(); 
$stmt->bind_result($bind_name, $bind_local, $bind_gender); 
$stmt->fetch();

  可以看到,我们将发送SQL语句的过程分为两个步骤。第一步是仅发送代码部分,即没有实际数据的SQL语句,这是准备步骤。正如我们从上面的代码片段中看到的那样,实际数据被问号(?)取代。在此步骤之后,我们使用bind_param()将数据发送到数据库。数据库将此步骤中发送的所有内容仅作为数据处理,不作为代码处理。它将数据绑定到处理好的语句中的相应标记的位置。在bind_param()方法中,第一个参数“is”表示数据的类型:“i”表示$id中的数据具有整数类型(int),“s”表示$pwd中的数据具有字符串类型(string)。

  对于此任务,请使用预处理机制来修复你在先前任务中利用的SQL注入漏洞。 然后,检查你是否仍然可以利用此漏洞。

4. 注意事项

测试SQL注入字符串 在真实环境中,检查SQL注入攻击语句是否存在语法错误可能会很难,因为通常服务器不会返回错误消息。若要进行检查,可以将SQL语句从php源代码复制到MySQL控制台。例如有以下SQL语句,注入的字符串是'or 1 = 1;#

1
SELECT * from credential WHERE name='$name' and password='$pwd';

你可以用注入字符串替换$name的值,然后使用MySQL控制台对其进行测试。此方法可以帮助你在进行实际注入攻击之前构造无语法错误的注入字符串。

5. 课程要求

你需要提交详细的实验报告,描述已完成的工作和观察到的内容,并提供屏幕截图和代码。你还需要对实验结果进行解释。