0*01 背景
最近 HackerOne 公布了 Nginx 内存内容泄漏的问题, 如果说内存内容泄漏的问题是个 Bug 的话, 那这个 Bug 是个比较典型的程序没有对输入异常数据做适当的过滤处理而形成的.
现实中程序对有限正常系用例的数据处理是定量的, 对无线的异常数据会出现处理的盲点, 如果什么数据都可以作为一个可接受输入程序的输入数据, 那一个程序没有处理好异常系的非业务数据, 就可能造成逻辑 Bug, 或是漏洞.
这篇文章的重点, 不局限于 Bug 问题的代码是如何在异常数据之前出现问题, 如何复现 Bug, 我们还要通过社区给出的防护方案, 学习如何构建安全的代码, 去过滤那些非法的数据输入.
0*02 安全测试
安全测试很多时候, 是构造一个被测程序意料之外的异常输入数据, 让程序出错, 或产生超出正常用户预期的结果.
一个程序功能是为了实现用户某些用例场景的处理, 而安全测试很多时候, 提供给程序输入的数据, 并不一定是用户正常业务使用的正常数据. 安全测试提供的数据, 目的并不是让程序完成正常用户功能作处理, 而是让程序暴露安全问题.
测试人员: 测试的是程序是否能按功能需求实现功能.
安全测试人员: 测试的是程序在收到异常系数据时, 是否出错, 是否可以利用程序出错, 取得系统更大的权限.
这次问题的产生, 一种在有问题的 Nginx 的配置基础, 构造有问题的访问请求, 造成 Nginx 的 Rewrite 功能出问题. 另一种是, 安全测试人员在构造一个 HTTP 请求时, 在 Header 部分注入一些非法的字符, 正常的浏览器 HTTP 请求一般不会有这些奇怪的数据.
Nginx
Nginx 的问题和 %00 有关系, 在请求当中加入 %00, 造成内存内容泄漏.
curl localhost:8337 -d "url=%00asdfasdfasdfasdfasdfasdfasdfasdf" -vv
在静态的 rewrite 配置中加特殊符号,^@是空字节.
- location ~ /memleak {
- rewrite ^.*$ "^@asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdasdf";
- }
- curl localhost:8337/memleak -vv
- ...Location: [http://localhost:8337/WjWj](http://localhost:8337/WjWj)...
WjWj 就是随机的内存数据.
OR
OR 的问题是, Lua 程序员在写 Lua 相关的 URI 设置逻辑, 或是有设定头数据动作时, 不考虑过滤用户请求 Header 中异常数据, 这个数据的会被传递给低层的 Nginx C 代码, 最直接相关的代码就是调用的 ngx.req.set_uri() 这个函数, 如果这个函数也不做 Header 数据的判断, 继续执行下面的逻辑, 就会出现问题.
- location ~ /memleak {
- rewrite_by_lua_block {
- ngx.req.read_body(); local args, err = ngx.req.get_post_args(); ngx.req.set_uri( args["url"], true );
- }
- }
- location / {
- root html; index index.HTML index.htm;
- }
0*03 复现问题
从朋友那得到漏洞消息后, 测试了一些低版本的 Nginx, 发现问题的确是可以复现的, 从漏洞公开时间表, 最后公开这个问题的时间节点是 3.18 号, 发现者已经告知的了 Nginx 和 OR 的厂商相关信息, 并公布了这个问题.
新的版本 Nginx 修复了如果没有问题的话, 但如果企业单位还在用老版本 Nginx 就会出现问题, 对于正常的 Nginx 服务中用到 Rewirte 功能的机率还是很高的.
如果你的 Nginx 服务中用了有问题的 Rewrite 的配置, 或是在 Nginx 中对应使用的 Nginx Lua 服务代码中调用了 ngx.req.set_uri() 这个函数, 会触发的这个问题逻辑代码的执行, 如果没有相关问题 Rewrite 的配置和 API 的调用, 或是过滤过异常 Header 数据, 不一定能复现问题的.
给这个漏洞定位是中低危漏洞. 一般的 Lua 在设置 URI 时大多数不会还考虑过滤 Header 数据, 但如果 Lua 程序是一个 WAF 程序, 其实应该有对非法 Header 数据的检查.
Nginx 问题
Bug 问题原理, 主要还是对应的函数没有对非法的 HTTP 数据做过滤, Hacker One 给出了 Nginx 的问题代码的.
看参考连接: https://hackerone.com/reports/513236
漏洞 Bug 复现的条件:
这个 Bug 的被归为中低危漏洞, 原因也是因为想利用漏洞需要前提条件成立.
A). 低版本 Nginx 或 Openresty 系统服务, 在 nginx.conf 中配置有问题的 Rewrite 的.
B). 低版本 Nginx 或 Openresty 系统服务, 在 nginx.conf 中配置的 Lua 代码, 并且代码调用了 ngx.req.set_uri() 函数.
0*04 测试漏洞
HackerOne 给出复现例子.
Nginx 目录遍历
- location ~ /rewrite {
- rewrite ^.*$ $arg_x;
- }
- location / {
- root HTML; index index.HTML index.htm;
- }
^.*$ 匹配所有的路径映射到入口文件,$arg_x 取变量 x 的值, 这种静态的 rewrite 设定, 就会出现目录遍历, 如果老版本 Nginx 中配置文件中有这种代码就有问题.
curl localhost:80/rewrite?x=/../../../../../../../etc/passwd
测试:
如果可以显示出系统文件 / etc/passwd 中的内容, 实现目录遍历达成.
- location ~ /memleak {
- rewrite ^.*$ "^@asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdasdf";
- }
- location / {
- root HTML; index index.HTML index.htm;
- }
测试:
curl localhost:8337/memleak -vv...Location: [http://localhost:8337/WjWj](http://localhost:8337/WjWj)...
WjWj 是随机的泄漏的内存内容数据, /WjWj 这种路由也不是用户提前正常设置的.
0*05 修复方案发布
OR 社区今天发布新版本, 修复了这个问题. 相对造成这个问题的代码, 也比较关注这个问题的修复方案. 对于安全测试来说的, 理论和 URI 相关的函数, 其实都可以关注一下, 如果是 WAF 系统, 这些地方处理的是否全面, 会决定 WAF 是否可能被绕过.
Nginx C 级别的这些与 URI,HTTP 输入数据直接相关的代码, 最应该被关注, 因为这些函数和对请求中异常数据的过滤息息相关, 一旦没有过滤充分就可能会引起问题.
WAF 某些时候是在给, 被保护的生产业务做过滤, 让生产业务专注于自己的功能, 由 WAF 处理攻击者的业务数据. 一旦, 业务和 WAF 都没有对非法数据做检查, 这些数据就会交给低层的 Nginx C 来处理, 如果 C 也没有检查, 再向后执行, 原本期待正常业务数据的 C 代码, 面对异常输入时, 没有过滤好就会出错.
修复方案
之前的漏洞解析, 更多的关注的造成问题的代码, 而作为一个代码开发人员来说, 还应该关注, 如果写出可靠的安全代码, 我们学习回顾一下, 最新发布的 OR 是如何安全过滤攻击者注入数据的处理.
经老师提醒, 代码方案有初期版和终期版, 经历了最开始没安全检查, 到有安全检查的过程, 代码如下:
- static ngx_inline size_t ngx_int_t ngx_http_lua_check_header_safe(ngx_http_request_t *r, u_char *str,
- ngx_http_lua_safe_header_value_len(u_char *str, size_t len) size_t len);
- {
- size_t i;
- for (i = 0; i <len; i++, str++) {
- if (*str == '\r' || *str == '\n') {
- return i;
- }
- }
- return len;
- }
被删除的一个版本的安全处理函数, 在计算头值长度的时候, 遇到换行回车就停止长度计数.
新发布的代码中加入安全检查函数, 代码如下.
- ngx_inline ngx_int_t
- ngx_http_lua_check_header_safe(ngx_http_request_t *r, u_char *str, size_t len)
- {
- size_t i, buf_len;
- u_char c;
- u_char *buf, *src = str;
- /* %00-%1F, %7F */
- static uint32_t unsafe[] = {
- 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */
- /* ?>=<;:98 7654 3210 /.-, +*)( '&%$ #"! */
- 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */
- /* _^]\ [ZYX WVUT SRQP ONML KJIH GFED CBA@ */
- 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */
- /* ~}| {zyx wvut srqp onml kjih gfed cba` */
- 0x80000000, /* 1000 0000 0000 0000 0000 0000 0000 0000 */
- 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */
- 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */
- 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */
- 0x00000000 /* 0000 0000 0000 0000 0000 0000 0000 0000 */
- };
- for (i = 0; i < len; i++, str++) {
- c = *str;
- if (unsafe[c>> 5] & (1 <<(c & 0x1f))) {
- buf_len = ngx_http_lua_escape_log(NULL, src, len);
- buf = ngx_palloc(r->pool, buf_len);
- if (buf == NULL) {
- return NGX_ERROR;
- }
- ngx_http_lua_escape_log(buf, src, len);
- ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
- "unsafe byte \"0x%uxd\"in header \"%*s\"",
- (unsigned) c, buf_len, buf);
- ngx_pfree(r->pool, buf);
- return NGX_ERROR;
- }
- }
- return NGX_OK;
- }
最新的防护方案是, 当发现请求中有非法数据, 释放空间然后抛出错误异常. 检查也从过去的没到检查, 判断回车换行计算头长度, 变成直接抛出异常错误.
WAF 系统一个主要的功能就是过滤用户非法请求数据, 特别是基于 Nginx + Lua 的 WAF 方案更是这样, 而如果只是单纯检查过滤请求 Header 中的数据, 其实小语言 DSL, 更简洁, 比 Lua 还简洁.
- req-header("Content-Type") contains "multipart/form-data",
- req-header("Content-Type") !contains rx{
- ^multipart/form-data[\s\S]+
- } =>
0*06 总结
在 Nginx 过去历史发展过程, 不只是这一次出现过类似 %00 的问题, 以后安全测试人员和黑客, 还会通过构建类似这种的异常数据输入, 敲开系统的门. 安全生态中的人和系统, 也会在不断发生的威胁事件中, 演进彼此的技法, 在存量和增加的代码中, 发现安全问题, 解决安全问题, 动态的变化.
最新 OR 版本发行, 解决了文中提到的问题: https://openresty.org/en/ann-1015008003.html
来源: http://www.tuicool.com/articles/aUNBJjJ