本文发表在NDSS2018,原文链接:https://software-lab.org/publications/ndss2018.pdf

项目地址:https://github.com/sola-da/Synode

第一作者是来自 达姆城工业大学 的Cristian-Alexandru Staicu,发表过多篇顶会文章,主要研究方向是Web:

1

背景知识

在Node.js中,有两种API会造成注入类漏洞。一个是eval及其系列函数,可以接受一个string作为参数,并将其作为javascript代码执行,造成js代码执行攻击;另一个是exec及其系列函数,可以接受一个string作为参数并将其作为shell命令执行,造成系统命令注入漏洞。除此以外,作者还发现,开发者的安全意识还有所欠缺,面对一个潜在的漏洞,开发者可能会采取消极的态度不去修复,或者会认为该模块是其他人负责的部分。所以本文就希望寻求一种方法能在减少开发者参与度的情况下提高node.js程序的安全性。

与在浏览器上执行的js不同,node.js并不会提供一个沙盒来将程序和系统隔离开来,这就导致了node.js代码可以直接与操作系统进行交互。

2

在上图的例子中,在第7行和第10行分别存在两个不同的敏感api,exec和eval。

开发者预期的调用方式是:

1
backupFile("f", "txt");

那么在第7行执行的命令就为:

1
cp f.txt ~/.localBackup/

并在第10行将消息存入mrssages.backup_other文件中。

但是攻击者可以构造这样的攻击字符串:

1
backupFile("-help || rm -rf * || echo ", "");

对应在第7行执行的命令就为:

1
cp -help || rm -rf * || echo . ~/.localBackup/

此时当前目录下的所有文件都会被删除:

3

研究表明,数千个模块可能容易受到命令注入攻击,并且修复它们需要很长时间,即使对于流行的项目也是如此。在这些发现的推动下,作者提出了Synode,一种自动缓解技术,它结合了静态分析和安全策略的运行时执行,以安全的方式使用脆弱的模块。

前期调研

为了了解目前在node.js中潜在的注入类漏洞情况,作者对235,850个npm模块进行了调研。

首先,作者调研了容易出现注入漏洞的API是否在真实的开发中得到了广泛的应用。结果如下图所示:

4

对于直接调用了exec和eval函数的模块,我们称之为injection module。直接调用exec和eval的模块分别占所有模块的3%和4%。exec-level-1表示间接调用该injection模块的模块,后面的1和2表示经过几次模块调用。

除了危险的api函数在模块中的使用频率,在用户数据被传入到敏感api之前,作者还调研了这些数据在多大的程度上被检查了。90%的敏感api对应的执行路径上缺少对敏感数据的检查;9%的敏感api对应的执行路径上会通过正则表达式对其进行检查,如下图所示:

5

令人诧异的是,几乎很少模块会借助第三方过滤库(如shell-escape,escapeshellarg)来防止exec命令注入漏洞。

工具设计

工具流程

6

先介绍下template,在synode中,template就是一串由string constants和一些holes组成的,这个hole会由一些危险的runtime data填充,从而组成一串攻击字符串。

synode的总体工作流程主要分为两个阶段:

  1. 静态分析阶段,对于经过分析后安全的程序,synode不会进行进一步的处理;对于存在漏洞的模块,synode会在对其进行检查的同时静态地重写源代码。
  2. 动态运行阶段,动态地运行程序检查该安全策略是否可用。

静态分析阶段

定义1:Template Tree,是将传入敏感API的值汇总为一个Tree来表示

7

定义2:Template,是形如_t = [c1, … , ck]_的序列,其中_ci_表示常量或是一个变量。下图是对Template Tree进行自底向上遍历转为Template之后的结果:

8

如果某injection API对应的template中的序列项全是常量字符串,那么可以肯定该injection API不会存在注入漏洞,也就无需对其进行运行时检查,如_tc1tc2_就是对应的eval(Figure 1中的第10行)就是安全的,而_td2_对应的exec(Figure 1中的第7行)操作就是存在安全风险的。需要在动态运行阶段判断该处API调用是否存在漏洞。

动态运行阶段

首先介绍下PAST(Partical AST),是将API和其对应的template结合起来的无环有向图_(N, E)_。下图右(a)是Fig.8的PAST树:

9

为了得到如上图所示的PAST,首先使用benign的字符串填充模板的未知变量部分来实例化模板,比如下图是列出的已知填充exec和eval的一组benign值:

10

将每一个进行组合后填充到template的未知变量部分,然后交给API对应的语言解释器进行执行,如exec是命令执行API,对应的解释器是bash,当且仅当字符串被接受为该语言的合法成员时,会将该结果ASP存储到一组合法的example AST中。

现在对于每一个敏感API,都有了一组example AST,接着需要将这些AST合并到一个单独的PAST中:

11

对example AST进行深度遍历,找到example AST的最近公共子节点,将所有的公共子节点都添加到PAST树中,不同的公共子节点添加为_Nsub_,该节点其实就是未知变量对应的节点。比如对于Fig.1中的图代码生成的PAST树就是上图最后的结构。

synode在进行到动态运行阶段时,会重写在敏感API附近的代码,接着将runtime值传入该敏感API时生成新的PAST树对其进行检查。该检查策略为:

  1. P1,该PAST的结构必须和example AST生成的PAST树结构保持一致,也就是说扩展节点必须是从未知变量节点出发的。
  2. P2,从未知变量节点扩展的sub-PAST树的父节点类型必须是safe node type。比如对于exec函数,作者认为仅当该节点类型为_literals_才为安全的节点类型;对于eval函数,允许所有在JSON代码中出现的类型。、

比如下图是传入FIg.1中的runtime values:

13

第一条数据显然同时满足P1和P2。第2条不满足P1,因为该条数据对应的PAST树的扩展节点并不是从未知变量节点(即HOLE)出发的。第3条满足了P1原则,但是违反了P2原则,该template对应的API是exec,从未知变量节点扩展的sub-PAST树的父节点类型并不是safe node type。

12

实验评估

数据集:一开始收集了所有的235,850个node.js模块,然后去除不包含任何敏感API(exec,eval)调用的模块,最后剩下的数据集是15,604个模块。

部署:

  • Lenovo ThinkPad T440s Laptop
  • Intel Core i7 CPU(2.10GHz)
  • 12GB RAM
  • Ubuntu 14.04

静态分析结果

下表是静态分析生成template tree时的结果:

14

静态分析阶段一共找到了51,627处敏感API调用,其中18,924(36.66%,其中exec函数对应的静态参数占比31.05%,eval对应的静态参数占比39.29%)是静态安全的,也就是传入该敏感API的参数是用户不可控的。exec函数对应的template tree中含有未知变量节点(HOLE)的占exec API总数的49.02%,eval则是34.52%。

下图是对每个敏感API可能会生成的template数量情况:

15

对于绝大多数的敏感API都只对应一个template。

动态运行评估

为了对动态运行机制进行评估,synode测试了如下图所示的24个漏洞模块。对于每一个敏感API都有benign和malicious的用户输入。在下图的第3列Injection Vector中,(I)nterface表示我们通过模块自己提供的export接口调用;(N)etwork表示通过request请求调用api;(F)ile system表示该模块会从文件中读取数据;(C)ommand line表示将输入作为command line命令传入模块中:

16

false positive:对于56个良性输入,共发现了5个fp,fp rate=8.92%。

如下图中因为synode没有对_Array.map_方法进行建模,所以在拼接cmd的dmenuArgs参数就是未知的操作,就造成了false negative:

17

false negative:synode会尝试去拦截所有具有潜在风险的字符串执行,所以从这个角度来讲是不会存在false negative的,但是在一些情况下,也可能会遗漏一些潜在的sink点,比如sink点是被动态拼接的:

1
global["ev"+"al"](userInput);

总结

本篇论文对235,850个node.js模块中存在的注入漏洞进行了研究。作者使用了静态分析+动态运行的方法,在静态分析阶段,推断在API处用户输入的template,若是该template中不存在未知变量,则认为该处API调用是静态安全的。对于静态分析阶段无法判断安全性的API,通过代码重写并实时运行来检测API的安全性。运行时使用PAST语法树来确保运行时传入API的值不会超出预期的值。