SQL:空格可能无关紧要
介绍
应用
分析
解决方案
结论
参考
介绍
以下是故意暴露漏洞的应用程序的源代码:
https://gitlab.com/AntonyGarand/pwn_fix_repeat_size_does_matter
它很快将作为一项挑战添加到pwnfixrepe.at网站上,您可以在那里修复此类安全问题。
如果你接受这项任务,你的任务是以管理员身份登录该应用程序。
如果您想直接查看详细信息,请前往该solution部分。
应用
该应用程序本身非常简单,
只有三个功能:登录、登出和注册。
表格如下所示:(来源)
CREATE TABLE `users` (
`id` int(11) PRIMARY KEY,
`username` varchar(20) NOT NULL,
`password` varchar(255) NOT NULL,
`description` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `users` (`id`, `username`, `password`, `description`) VALUES (1, 'admin', '$up3rS3cr3tP4$$w0rd!', 'You are admin!');
所有这些功能都使用了预处理语句,因此不存在 SQL 注入漏洞,例如以下登录函数:(来源)
function login() {
global $db;
if ( /* user or pass empty or not string */ ) {
return false;
}
$query = $db->prepare('SELECT * FROM users WHERE username = :username and password = :password');
$query->bindParam(':username', $_POST['user']);
$query->bindParam(':password', $_POST['pass']);
$query->execute();
$result = $query->fetch();
if ($result) {
$_SESSION['username'] = $result['username'];
echo 'Logged in succesfully!';
} else {
echo 'Invalid credentials!';
}
}
最后,如果当前用户已登录,我们会显示一条欢迎信息:
/* if current user is logged in */
{ ?>
<div>
Welcome back, <?= htmlspecialchars($currentUser['username']) ?>!<br/>
<?= $currentUser['description'] ?>
</div>
<!-- ... -->
<?php
}
分析
该漏洞并非出自 PHP,而是由 SQL 本身以及存在漏洞的应用程序的许多糟糕的设计决策所导致的。
以下是对用户表的一些观察结果:
- 用户名不唯一
- 用户名是一个长度为 20 的 varchar 类型字符。
以及应用程序本身:
- 登录用户基于用户名
// If login success:
$_SESSION['username'] = $result['username'];
- 只有当用户名尚未被占用时,才能注册用户。
if (selectUser($_POST['user'])) {
echo 'Username already taken!';
return false;
}
解决方案
在 SQL 中比较字符串很有意思。
你认为以下语句应该是什么?
SELECT * FROM (select 1) as t WHERE 'x' = 'x '
与许多人的预期不同,这个'x' = 'x '条件是true!
在 SQL 中,比较两个字符串时,较短的字符串会用空格填充,直到它们长度相等。
这实际上意味着,在常规字符串比较中,尾随空格是没有意义的!
这是在SQL 规范中的比较运算符中定义的:
a) 如果字符串 X 的字符长度不等于字符串 Y 的字符长度,则为了进行比较,较短的字符串实际上会被替换为自身的一个副本,该副本通过在字符串右侧连接一个或多个填充字符来扩展到较长字符串的长度,其中填充字符的选择基于字符集 CS。如果 CS 具有 NO PAD 属性,则填充字符是一个与实现相关的字符,该字符不能是 X 和 Y 字符集中任何长度小于 CS 下任何字符串长度的字符。否则,填充字符为
<space>.
成功利用该应用程序的关键就在于这一特性。
首先,我们需要创建一个新用户,用户名与“admin”相同。
我们可以输入一个以“admin”开头的用户名admin,后面用空格填充到列长度20,最后再加一个非空格字符。
这样就能满足username exists条件,因为给定的用户名不存在。
插入新用户时,由于该列有20字符限制,值将被截断为201000个字符,因此会显示为“admin admin”,后面带有空格。
请注意,如果该列是唯一的,则无法实现此操作,因为我们会收到重复条目的错误消息。
第二步是使用admin用户名和我们创建的用户的密码登录。
由于 SQL 表中现在有两个用户,一个是常规管理员,另一个是我们创建的虚拟用户,因此登录查询将能够成功,并选中我们的新用户。
最后,当应用程序根据用户名查找当前登录用户时,它会选择第一个具有该admin用户名的用户。这意味着它会选择管理员用户而不是我们自己的用户,现在我们就可以窃取管理员密钥了!
结论
安全问题本质上就是应用程序中的漏洞。
与普通漏洞一样,您可以通过维护高质量的代码来减少它们的数量。
在这个应用程序中,如果username事先将列标记为唯一、使用 ID 来识别当前用户或执行长度验证,就可以避免这个问题。
参考
- https://gitlab.com/AntonyGarand/pwn_fix_repeat_size_does_matter
- https://stackoverflow.com/questions/4166159/sql-where-clause-matching-values-with-trailing-spaces
- http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt