MySQL严格模式关闭的安全风险与攻击案例分析

本文深入分析MySQL严格SQL模式关闭时可能引发的安全风险,通过实际攻击案例展示编码不一致如何导致安全绕过,包括Shell通配符扩展、正则表达式量词滥用等攻击手法。

MySQL严格SQL模式关闭可能导致什么问题?

本文展示了一些攻击示例,这些攻击可以利用MySQL在严格SQL模式禁用时的行为,特别是当字符串字符在当前编码中无效时。这种情况通常发生在应用程序编码(如UTF-8)比数据库编码(如ASCII)更宽时。

目录

什么是MySQL严格SQL模式?

严格模式[1]控制MySQL如何处理数据更改语句(如INSERT或UPDATE)中的无效或缺失值。本文详述的情况是值超出范围时。当值对当前编码无效时会发生什么?

在这种情况下严格模式的过度简化:

严格模式开启 严格模式关闭
错误 警告(静默)
无修改 插入"调整后"的值(“最接近的值”)

此时,你已经猜到可能出什么问题了。

设置和检查

实际上,没有单一的"严格模式"是"开启"或"关闭"的。有一个名为sql_mode的系统变量,包含一个值数组。如果该数组包含STRICT_TRANS_TABLES或STRICT_ALL_TABLES值之一,或组合模式TRADITIONAL,则MySQL处于"严格模式"。

MySQL 9.1[2]的文档声称sql_mode的默认值如下:

1
ONLY_FULL_GROUP_BY STRICT_TRANS_TABLES NO_ZERO_IN_DATE NO_ZERO_DATE ERROR_FOR_DIVISION_BY_ZERO NO_ENGINE_SUBSTITUTION

其他版本或替代后端引擎(如MariaDB或Percona Server)的默认值可能不同。@@sql_mode系统变量允许查询当前值。

1
2
[u]> SELECT @@sql_mode\G;
@@sql_mode: STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION

安装程序可能在安装过程中配置SQL模式。因此,即使默认启用严格模式,XAMPP、CMS等也可能静默禁用它。 禁用严格模式(及其他所有内容)的暴力方法是:

1
SET sql_mode='';

观察结果

让我们创建一个使用ASCII编码的表,在其中容易插入无效值,并查看严格模式开启和关闭时发生的情况。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
-- 创建一个故意使用窄空间编码的表
CREATE TABLE uni_sandbox (
  id INT AUTO_INCREMENT PRIMARY KEY,
  data VARCHAR(255) CHARACTER SET ascii
);

-- 严格模式启用
SET sql_mode='STRICT_ALL_TABLES';
INSERT INTO uni_sandbox (data) VALUES ('I ♥ Unicode');
-- => ERROR 1366 (22007): Incorrect string value: '\xE2\x99\xA5 Un...' for column `unicode8`.`uni_sandbox`.`data` at row 1

-- 严格模式禁用
SET sql_mode='';
INSERT INTO uni_sandbox (data) VALUES ('I ♥ Unicode');
SELECT * FROM uni_sandbox\G;
-- => data: I ? Unicode

严格模式开启时触发错误,严格模式关闭时数据被插入但♥被替换为?。Oracle MySQL或MariaDB文档说"将无效值转换为最接近的有效值",但没有确切解释其工作原理。实际上,长于列大小的字符串将被截断,整数四舍五入到最接近的值,但这仅适用于溢出的值。对于无效的值(无法在编码中表示),每个字节被替换为?。

攻击场景

想象以下场景:有一个Web应用程序,严格检查用户输入。用于验证的通用方法是使用正则表达式(RegExp)。无论使用何种语言,几乎所有RegExp引擎都支持POSIX字符类。但多年前,随着Unicode支持的普及,许多引擎扩展了这些类的范围以在Unicode上工作(最初仅限于ASCII)。因此,例如,不仅匹配ASCII范围内的字母数字字符(A-Za-z0-9),默认情况下还会匹配相应Unicode类别中的字符,如字母(L)和数字(N)。

以Ruby中的以下示例为例,使用字符拉丁字母双齿打击音ʭ。

1
2
/[[:alnum:]]/.match('ʭ')
# => #<MatchData "ʭ">

其他语言如JavaScript,也通过实现Unicode属性和类别选择器(\p{…})实现了非POSIX字符类选择器,你可以直接匹配Unicode属性(例如\p{Ll}或Lowercase_Letter用于小写字母)或别名属性[3](例如\p{Alpha}用于匹配字母和字母数字的字母)。

1
2
"ʭ".match(/\p{Alpha}/u)
// Array [ "ʭ" ]

另一方面,如果数据库使用"窄空间"编码,如ASCII、CP-1252,甚至是遗留的部分实现,如结合严格模式禁用的utf8mb3(例如CMS安装自动执行此操作),这将导致一些Unicode字符通过安全检查,但在数据库编码上无效,它们最终会被替换为?。

在这种情况下,有一些攻击场景和安全绕过可以在现实生活中工作。

利用MySQL严格模式回退机制

上下文摘要

本节介绍的所有攻击都将置于以下上下文:应用程序具有安全检查,仅允许包括Unicode在内的字母数字字符,但MySQL数据库使用ASCII编码或类似编码,其中Unicode字符无效,并且严格SQL模式禁用。

Shell通配符扩展

Bash不支持正则表达式(RegExp),但仍可以执行文件名扩展,这称为通配符扩展。Bash然后将扩展称为通配符的字符。最著名的是*匹配任何长度的任何字符串,但也有?恰好匹配一个单个字符(不同于? RegExp量词)。例如,在Unix文件系统的根目录中,???t将匹配boot和root目录。

1
2
$ ls -d /???t
/boot  /root

因此,假设你有一个本地文件泄露(LFD),由于不安全的直接对象引用(IDOR),但文件使用UUIDv4重命名(例如2a0f6947-bd44-449e-94fe-82ebc3ecf115.txt)。从技术上讲,你可以读取所有其他用户的文件,但实际上你不能,因为不可能暴力破解标识符。

但现在,如果你要求读取ʭʭʭʭʭʭʭʭ-ʭʭʭʭ-ʭʭʭʭ-ʭʭʭʭ-ʭʭʭʭʭʭʭʭʭʭʭʭ.txt会怎样?它将允许你列出所有文件,因为ʭ(拉丁字母双齿打击音)以及数十万个其他字符是字母类型的Unicode字符,将通过字母数字安全检查。之后,当存储在MySQL中时,它不会被识别为有效的ASCII字符,因此由于严格SQL模式禁用,它将回退到?。然后,在Bash中,?将扩展为任何单个字符。因此,find命令将列出目录中的所有文件,而不是仅一个,绕过应用程序安全检查以及UUID标识符的使用。

正则表达式量词

对于RegExps,?是一个量词字符,它附加在表达式后,表示必须有零次或一次出现。

例如,RegExp filename\d?.txt将匹配以下任何没有或有一个数字的文件名,但不匹配filename10.txt,因为有两个数字。

1
2
3
4
filename.txt
filename1.txt
filename9.txt

现在想象一个Python应用程序,根据用户输入返回文件,如下所示。

1
2
3
4
5
import re

username = User.username # 从数据库获取的用户输入
check(username, lib.alphanum) # 一些Unicode字母数字检查
re.findall(f'confidential-{username}\.pdf', 'list-files-fetched-fromFS-or-DB', re.IGNORECASE)

因此,与之前类似,注册用户名为aʭbʭ…yʭzʭ0ʭ1…8ʭ9ʭ将转换为a?b?…y?z?0?1?…8?9?,这将允许在插入RegExp时匹配任何ASCII字母和数字一次。如果目标是五个字符长的模式,则此模式必须重复5次。有效负载将非常长且低效,但会工作。

1
2
3
4
5
6
import re
import string

payload = "".join(map(lambda i: i + '?', string.ascii_lowercase + string.digits)) * 5
re.findall(f'confidential-{payload}\.pdf', 'confidential-noraj.pdf', re.IGNORECASE)
# => ['confidential-noraj.pdf']

当然,在允许使用连字符和括号的场景中,并且只需要绕过?限制,这将更容易。

查询参数

在用户应该只能写入字母数字字符的上下文中注入?的能力也可能允许在URL中注入查询参数。

例如,如果Web应用程序内部基于用户名或任何存储在数据库中的过滤用户输入构建URL,则用户可能能够添加查询参数,如debug或admin,这将给他们未经授权的访问或敏感信息。下面的部分Python示例可以说明它可能的样子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from flask import Flask, redirect, url_for
import urllib.parse

app = Flask(__name__)

@app.route('/data/<username>')
@internal # 通过用户名获取用户数据的内部路由
def profile_name(username):
    # 基于用户名从文件系统获取一些数据
    data = "user_secret" if request.args.get("debug") != None else "user_public_stuff"
    return data

@app.route('/profile/name/<userid>')
@internal # 将用户ID转换为用户名的内部路由
def profile_data(userid):
    user = User(userid).username # 通过从数据库获取数据实例化用户类
    return redirect(urllib.parse.unquote(url_for('profile_name', username=user)))

@app.route('/api/public/profile/<userid>')
@auth # 获取配置文件信息的公共路由
def api_profile(userid):
    return redirect(url_for('profile_data', userid=userid))

@app.route('/api/public/profile/name/<userid>')
@auth # 设置用户名的公共路由
def set_name(userid):
    username = request.args.get("name")
    check(username, lib.alphanum) # 一些Unicode字母数字检查
    User(userid).username = username
    return True

if __name__ == '__main__':
    app.run(debug=True)

根据之前所见,数据库会将像norajʭdebug这样的用户名转换为noraj?debug。当然,无法注入=将阻止向参数传递值。但是,根据使用的Web框架或编程语言,应用程序可能仅检查参数是否存在。

WAF绕过

在先前设置的上下文之外,当然间接注入?也可能有助于绕过WAF。例如,在存储文件blob或模板的数据库中,攻击者获得了写入访问权限,WAF将阻止包含<?php的有效负载。在这种情况下,发送<ʭphp可以绕过检测规则,并由于严格SQL模式回退行为在之后转换为原始有效负载。

真实故事

很久以前(2012年),行星对齐允许疯狂的bug。在那些史前时代,Wordpress在设置期间禁用严格SQL模式。另一方面,MySQL链接了糟糕的设计缺陷。MySQL中UTF-8的唯一实现称为utf8(现在称为utf8mb3),其问题是仅处理1到3字节字符[4](而不是有效UTF-8最多4字节)。utf8mb3非常误导,因为声称MySQL支持UTF-8让人们认为UTF-8支持100%完整,而不仅仅是部分实现。另一个危险行为是,当严格SQL模式禁用时,它不像现在那样用?替换无效字符。相反,无效字符以及字符串的其余部分被纯粹简单地删除!因此,任何有效的UTF-8 4字节字符在utf8mb3中被判断为无效,并触发之后任何内容的截断。

这3个主要缺陷的组合在2014年被Cedric’s Cruft利用,在Wordpress[5]核心评论功能中获得存储XSS。

参考资料

  1. https://dev.mysql.com/doc/refman/9.1/en/sql-mode.html#sql-mode-strict
  2. https://dev.mysql.com/doc/refman/9.1/en/server-system-variables.html#sysvar_sql_mode
  3. https://tc39.es/ecma262/multipage/text-processing.html#table-nonbinary-unicode-properties
  4. https://mathiasbynens.be/notes/mysql-utf8mb4
  5. https://cedric.ninja/post/wordpress-stored-xss-vulnerability-4-1-2/
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计