软件安全实验 SEEDlabs SQL 注入攻击实验
环境设置
实验所需文件已在e-learning系统中提供。 此次实验使用一个已经开发好的Web应用程序,并使用容器1来设置这个Web应用程序。实验使用两 个容器,一个用于托管Web应用程序,另一个用于托管Web应用程序的数据库。Web应用程序的容器IP 地址是 10.9.0.5,Web应用程序的URL见下:
1 | |
我们需要将主机名映射到容器的IP地址。请将以下条目添加到/etc/hosts文件。此文件的修改需要 使用root权限(使用 sudo)。如果主机名由于某些原因已经被添加到文件中,且被映射到一个不同的IP 地址,请删除旧的条目。
1 | |
容器设置与相关命令
将实验配置文件Lab3setup.zip 上传至虚拟机,解压后进入 Labsetup 文件夹,使用其中的 docker-compose.yml 文件来构建实验环境。
docker-compose.yml 文件的相关解释以及有关所有 Dockerfile的内容可查阅用户手册(https: //github.com/seed-labs/seed-labs/blob/master/manuals/docker/SEEDManual-Container.md)。若 为第一次使用容器构建SEED实验环境,建议阅读用户手册。
下面列出了一些DockerCompose的常用命令,这些命令在SEEDUbuntu20.04VM中的.bashrc文件 中拥有别名。
1 | |
令所有容器在后台运行(使用-d参数,即dcup-d),要在容器上运行命令,需要使用该容器的shell。 我们首先需要使用“docker ps”命令来找出容器的ID,然后使用“dockerexec”来启动该容器的shell。命 令已在.bashrc文件中创建别名。
1 | |
Remark如遇问题,请查阅用户手册中的“常见问题”部分,以及本实验手册最后一页的附录。
MySQL数据库:通常情况下,容器是一次性的,一旦被删除,内部的所有数据都会丢失。本次实验中,我们 需要保存MySQL数据库中的数据,这样就不会在关闭容器时丢失数据。为此,我们将主机上的mysql_data 文件夹(位于Labsetup下,于MySQL容器运行后创建)挂载到MySQL容器内的/var/lib/mysql文件 夹中。这个文件夹用于MySQL存储其数据库。这样,即使容器被删除,数据库中的数据仍被保存。如果 需要一个干净的数据库,可以用以下命令删除这个文件夹:
1 | |
这我们先把环境起一下
使用 docker-compose build 构建docker

成功构建

使用docker-compose up -d 后台起一下容器
Web应用程序
我们创建了一个简单的员工管理的Web应用程序。员工可以通过这个Web应用程序查看和更新他们 在数据库中的个人信息。在这个Web应用程序中有两个角色:Administrator是一个特权角色,可以管 理每个员工的个人资料信息;Employee是一个非特权角色,可以查看或更新他/她自己的个人资料信息。 表1为所有员工的信息。
表1: Database
| Name | Employee ID | Password | Salary | Birthday | SSN | Nickname | Address | Phone# | |
|---|---|---|---|---|---|---|---|---|---|
| Admin | 99999 | seedadmin | 400000 | 3/5 | 43254314 | ||||
| Alice | 10000 | seedalice | 20000 | 9/20 | 10211002 | ||||
| Boby | 20000 | seedboby | 50000 | 4/20 | 10213352 | ||||
| Ryan | 30000 | seedryan | 90000 | 4/10 | 32193525 | ||||
| Samy | 40000 | seedsamy | 40000 | 1/11 | 32111111 | ||||
| Ted | 50000 | seedted | 110000 | 11/3 | 24343244 |
说明与提示
测试SQL注入字符串 通常情况下,服务器不会返回语法错误的提示,这导致很难得知SQL注入字符串 是否包含语法错误。为此,可以从php源代码复制SQL语句到MySQL控制台。假设你有以下的SQL语句, 并且注入的字符串是' or 1=1;#。
1 | |
将其中 $name的值替换为所要注入的字符串并在MySQL控制台对其进行测试。由此可以在发起真正 的攻击前构建一个无语法错误的注入字符串。
注释 MySQL支持两种单行注释:
--双中横杠开头的,直至行尾,这一种“–”后面必须至少有一个空格隔开,可以是制表符;#井号符号开头直至行尾。
URL 编码 参照以下转换表,完成Task2.2。
| 符号 | 编码 | 符号 | 编码 | 符号 | 编码 | 符号 | 编码 | 符号 | 编码 | 符号 | 编码 | 符号 | 编码 | 符号 | 编码 | 符号 | 编码 | 符号 | 编码 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ! | %21 | # | %23 | $ | %24 | & | %26 | ‘ | %27 | ( | %28 | ) | %29 | * | %2A | + | %2B | , | %2C |
| / | %2F | : | %3A | ; | %3B | = | %3D | ? | %3F | @ | %40 | [ | %5B | ] | %5D | ||||
| newline | %0A 或 %0D 或 %0D%0A | space | %20 | “ | %22 | % | %25 | - | %2D | . | %2E | < | %3C | > | %3E | \ | %5C | ^ | %5E |
| _ | %5F | ` | %60 | { | %7B | %7C | } | %7D | ~ | %7E |
图1:URL Encoder & Decoder
Task 1:熟悉 SQL 语句
Task 1 的目标是通过使用提供的数据库来熟悉SQL命令。Web应用程序所使用的数据存储在MySQL 数据库中,由MySQL容器托管。我们创建了一个名为 sqllab_users的数据库,其中包含一个名为 credential 的表。表中存储着每个员工的个人信息(例如eid、password、salary、ssn等)。本任务中, 请通过操作数据库熟悉SQL查询语句。
请在MySQL容器上建立shell(相关内容请参阅用户手册),并使用 mysql客户端与数据库进行交互 (用户名为 root,口令为 dees)
1 | |

登录客户端后,可以选择创建新的数据库或者加载已有数据库。由于我们已经创建了 sqllab_users 数据库,只需使用use命令加载这个已有的数据库。想要显示 sqllab_users数据库中有哪些表,可以使 用show tables命令打印出所选数据库的所有表。

运行以上命令后,你需要使用一条SQL命令打印员工Alice的所有资料信息。请提供结果截图。
SELECT * from credential;打印整个表单:
SELECT * from credential WHERE Name='Alice'即可打印员工Alice的所有资料:

Task 2:基于 SELECT 语句的 SQL 注入攻击
攻击者通过SQL注入技术可以执行恶意SQL语句,即恶意负载。通过恶意的SQL语句,攻击者可以 从数据库中窃取数据,甚至对数据库进行修改。我们的Web应用程序数据库含有SQL注入漏洞,模仿了 开发人员的常犯错误。
在登陆网站www.seed-server.com中进行SQL注入攻击的练习,登录页面见图2。Web应用程序根据 用户名和口令认证用户,所以只有知道自己口令的员工才能登录。作为攻击者,请在无法得知任何员工口 令的前提下成功登录Web应用程序。

位于目录/var/www/SQL_Injection 下的PHP代码 unsafe_home.php为进行用户认证的代码。下面 的代码片段显示了用户是如何被认证的。
1 | |
上面的SQL语句从表credential中获取员工的个人信息如ID、姓名、工资、SSN等。该SQL语 句使用两个变量input_uname和hashed_pwd,其中input_uname为用户在登录页面所输入的用户名, hashed_pwd为用户所输入口令的sha1哈希值。程序检查与所提供的用户名和口令匹配的条目。匹配成 功,则用户认证成功,并获得相应的员工信息;匹配失败,则认证失败。
典型SQL注入漏洞。
Task 2.1:基于网页的 SQL 注入攻击
你的任务是以管理员的身份从登录页面登录到 Web 应用程序,这样 你就可以查看所有员工的信息。管理员的用户名是 admin,口令未知。请在用户名与口令输入框中输入 能成功完成攻击的内容。
万能密码:可以构造 admin' OR '1=1 作为我们的登录用户名,这样Sql查询语句就变成了:
1 | |
使得判断恒为true

也可以构造 admin' --+(URL编码) 或admin' #作为用户名,从而注释调后面的语句,变成一个查询语句
1 | |

Task 2.2:基于命令行的 SQL 注入攻击
在不使用网页的情况下完成 Task 2.1 的目标。你可以使用命令行 工具,如 curl,它可以发送 HTTP 请求。如需在 HTTP 请求中包含多个参数,需要把 URL 和参数用一对 单引号括起来。否则,用于分隔参数的特殊字符 (如 &) 会被 shell 曲解,造成命令歧义。
以下给出向Web 应用程序发送带有两个参数(username和Password)的HTTPGET请求的示例。
1 | |
如需在用户名和口令字段中包含特殊字符,特殊字符需要进行编码,否则可能会造成命令歧义。如单 引号需要使用%27来代替,空格需要使用%20。在Task2.2中,使用curl时请正确处理HTTP编码。
将 Task 2.1 的命令进行 URL 编码
1 | |

Task 2.3:增加一条新的 SQL 语句
在Task2.1与Task2.2中,我们只能做到从数据库中窃取信息,进 一步的攻击是通过登录页面上的相同漏洞对数据库的数据进行修改。请尝试在SQL注入攻击中使用两条 SQL语句,第二条是更新或删除语句。在SQL中,分号(;)被用来分隔两条SQL语句。请在登录页面使 用两条SQL语句进行攻击。
在这种情况下,会有阻止运行两条SQL语句的反制措施。请查阅SEEDBook(https://www.handsonsecurity.net/)或上网查明此反制措施,并在实验报告中描述你的发现。
1 | |
尝试执行第二条 SQL 语句 DELETE FROM credential WHERE name='admin'; 删除 admin 表项,但是注入失败:

现代Web开发中,为了防止SQL注入带来的更大危害(如执行多条SQL语句),很多数据库驱动默认不允许一条查询里有多个SQL语句。比如:
- MySQL的默认设置(比如PHP的mysqli、Python的pymysql、Java的JDBC等)通常会关闭multi-statement(多语句)执行,除非显式开启。
- 这样做的目的是防止攻击者通过注入分号(;)来拼接多条SQL语句,从而实现如删除表、删除数据等更危险的操作。
这种防止多语句执行的机制,通常称为**“禁止多语句执行”(Disabling Multiple Statements)或“单条语句执行模式”**(Single Statement Mode)。
- 在MySQL中,如果未显式设置
CLIENT_MULTI_STATEMENTS参数,默认是不允许多条语句的。 - 这样,SQL注入攻击者即使通过分号拼接语句,也无法让数据库执行第二条语句。
Task 3:基于 UPDATE 语句的 SQL 注入攻击
如果SQL注入使用的是UPDATE语句,攻击者将利用SQL注入漏洞修改数据库,危害更加严重。在 我们的Web应用程序中,有一个编辑个人资料的页面(图3),允许员工更新他们的个人信息,包括昵称、 电子邮件、地址、电话号码和口令。员工登录后可进入此页面。
当员工通过编辑页面更新他们的信息时,下面的SQLUPDATE语句将被执行。在unsafe_edit_backend .php 文件中实现的PHP代码用于更新员工的个人信息。此文件位于目录/var/www/SQLInjection中。
1 | |
代码是通过执行一个sql语句进行表单的更新:
1 | |
Task 3.1:修改自己的工资
编辑页面中只能修改员工的昵称、电子邮件、地址、电话号码和口令,而不 能用于修改工资。假设你(Alice)由于老板Boby今年未给你加薪而感到不满。你想利用存在于编辑页面 的SQL注入漏洞来增加自己的工资(数值改为[你的学号后6位])。请展示并解释你是如何实现这一目标 的。已知列 salary用于存储工资数额。
Phone Number 是update的最后一项,可以在 Phone Number 这里注入.
填入 ',salary=460052 WHERE name='Alice' # ,相当于执行了:
1 | |

这会把 Alice 的 Salary 表项改成460052:

Task 3.2:修改他人的工资
在提高自己的工资数额后,你决定惩罚你的老板Boby,将他的工资减少(数 值改为[100+你的学号后3位])。请展示并解释你是如何实现这一目标的。
在 PhoneNumber 项填入 ',salary=152 WHERE name='Boby' #,相当于执行了:
1 | |
这会将 Boby 的工资表项更新成 152

Task 3.3:修改他人的口令
修改完Boby的工资后,你仍心有不甘,所以你想修改Boby的口令(口令改 为“2025”),这样你就可以登录他的账户,做进一步的破坏。请展示并解释你是如何实现这一目标的。你需 要证明你可以用新的口令成功登录Boby的账户。需要注意,数据库存储的并非明文形式的口令,而是口 令的哈希值。请在 unsafe_edit_backend.php中查看口令的存储方式,程序使用SHA1哈希函数来生成 口令的哈希值。

在 PhoneNumber 项填入 ',Password='004be89dd9e070ecb080b9b759e5be29ec24881b' WHERE name='Boby' #,这会修改 Boby 的数据库中的 Password 值
现在我们再尝试使用密码 2025 登录,发现登录成功:

Task 4:对策:语句预处理
SQL 注入漏洞的问题所在是没有把代码和数据分开。当构建一个SQL语句时,程序(如PHP程序)知 道哪部分是数据,哪部分是代码。但当SQL语句发送到数据库时,代码和数据的边界消失,SQL解释器可 能得到与开发者设置的原始边界不同的边界。为了解决这个问题,必须确保服务器端代码和数据库处理 得到的边界一致。最安全的解决方法是使用语句预处理。
为了理解语句预处理是如何防止SQL注入攻击的,我们需要了解SQL服务器执行查询时的流程。执 行SQL查询的整体工作流程见图4。查询语句在编译步骤中,首先经过解析和规范化阶段,此阶段根据语 法和语义检查查询语句。下一阶段为编译阶段,关键词(如SELECT、FROM、UPDATE等)被转换为机器 可理解的形式。查询语句在此阶段基本完成翻译。在查询优化阶段,服务器从执行查询语句的不同方案中 选择最佳优化方案。选定的方案被存储在缓存中,在下一个查询语句进入时,服务器首先检查缓存中的内 容,如此语句已存在于缓存中,则跳过解析、编译和查询优化阶段。执行步骤将实际执行编译后的查询。
预处理语句的生成在编译步骤之后执行步骤之前。预处理语句通过编译步骤,变为一个带有表示数据 的空占位符的预编译查询语句。为此需要提供数据才能运行此预编译查询语句。所提供的数据将被直接 插入到预编译的查询语句中并发送到执行引擎,而不会经过编译步骤。因此,即使数据中含有SQL代码, 由于不经过编译步骤,这些代码将被简单地视为数据的一部分,不会被赋予任何特殊的含义。这就是语句 预处理如何防止SQL注入攻击的。
下面将给出如何在PHP中编写语句预处理机制的示例。代码中使用SELECT语句,展示了如何使用语 句预处理机制来重写有SQL注入漏洞的代码。
1 | |
上面的代码容易受到SQL注入攻击,可被重写如下。
1 | |
使用语句预处理机制,我们将向数据库发送SQL语句的过程分为两个步骤。第一步为准备步骤,发送 代码部分,即没有实际数据的SQL语句。上面的代码片段中,实际数据被问号(?)代替。第二步,使用 bind_param() 将数据发送到数据库。数据库将在此步骤中发送的所有内容仅视为数据而非代码。它将数 据填入预处理的语句中的相应占位符。在 bind_param()方法中,第一个参数“is”表示各参数的类型:“i” 表示 $id中的数据为整数类型,“s”表示 $pwd中的数据为字符串类型。
Task 请使用语句预处理机制来修复SQL注入漏洞。为了简单起见,我们在文件夹 defense内创建了一 个简化程序,你需要对这个文件夹中的文件进行修改。访问下面给出的URL,你会看到一个类似于Web 应用程序登录页面的网页,在网页中提供正确的用户名和口令即可查询员工的信息。
1 | |
在这个页面中输入的数据将被发送到服务器程序 getinfo.php,它调用程序 unsafe.php。程序 unsafe.php 中的SQL查询代码含有SQL注入漏洞。请使用语句预处理机制修改 unsafe.php中的SQL查 询,修复SQL注入漏洞。在文件夹 Labsetup中,unsafe.php程序位于 image_www/Code/defense。请 修改该程序,并重建重启容器以生效。
你也可以在容器运行时修改该文件。在容器中,unsafe.php程序位于/var/www/SQL_Injection/ defense 里面。注意,为了保持docker镜像的小巧,容器内只有一个非常简单的文本编辑器 nano,它足 以进行简单的编辑。但如果你不喜欢 nano,你随时可以使用“apt install”在容器内安装你喜欢的命令 行编辑器。如果喜欢 vim,你可以这样做:
1 | |
编辑器会随着容器关闭和删除而丢失。如果你想让它成为永久性的,请把安装命令添加到 image_www 文件夹中的 Dockerfile中。
修改 www 容器中的 /var/www/SQL_Injection/defense/ 文件夹下的 unsafe.php 文件,修改后的文件如下所示:
1 | |
使用准备好的语句(Prepared Statements):通过 $conn->prepare() 函数创建查询,并使用 ? 占位符来防止直接插入用户输入内容。
绑定参数:使用 $safesql->bind_param() 函数,将用户输入的参数绑定到准备好的语句中。这种做法确保了SQL查询不会直接拼接用户输入,从而避免SQL注入风险。其中 "ss" 表示 $input_uname 和 $hashed_pwd 都是字符串类型。
访问 http://www.seed-server.com/defense 再使用 Alice' # 对 USERNAME 进行 SQL 注入测试:

发现已经无法查询到 Alice 的信息了。
思考题
Q1
假设数据库只存储 password和 eid两列的SHA256值。使用下面SQL语句与数据库交互,其中 $passwd 和 $eid变量的值由用户提供。这个程序是否存在SQL注入问题?如果没有,请解释原因;如果 有,请给出构造范例。(10分)
1 | |
有,SQL注入是代码与数据的混合导致的,像这种方法无法防御,只需闭合即可
比如$eid变量为admin,256)'#
1 | |
这样会导致SQL结构被破坏,实现注入。可以以admin身份登录
Q2
本质目的:两者都旨在防止“拼接字符串”导致的代码注入漏洞。
- 在 C 程序中,system() 直接将字符串作为 shell 命令执行,如果用户输入被直接拼接进这个字符串,就可能被恶意利用,造成命令注入。
- 在 SQL 语句中,如果直接把用户输入拼接进 SQL 字符串,也会造成 SQL 注入漏洞。
原理对比
- system():将整个命令行字符串传递给 shell,由 shell 解析并执行。拼接输入就容易让用户插入额外命令(如
; rm -rf /);execve():不经过 shell,而是将命令和参数分别以数组方式传递。即使参数中有恶意内容,也不会被当成新命令或特殊符号处理,杜绝了注入的可能。 - SQL拼接:直接拼接用户输入到 SQL 语句中,用户可以插入恶意 SQL 片段;预处理/参数化查询:SQL 语句结构和数据分离,用户输入作为参数绑定,即使输入包含特殊字符,也只会被当作数据处理,不能改变 SQL 结构。
相似性总结
- 原理相同:都通过“结构和数据分离”来防止注入。
- execve() 把命令和参数分开传递,不让参数内容影响命令结构。
- SQL 预处理语句把 SQL 结构和数据参数分开,防止参数内容干扰 SQL 结构。
- 效果相同:都能有效防止因拼接用户输入导致的注入攻击。