EJS 是一套简单的模板语言,设计的初衷是用尽可能少的js代码,渲染出丰富的html页面。
本期浅看一下:当ejs渲染遇到了原型链污染,所产生的rce。
示例代码还是群主的:ctfshow web341
拿到源码后先看一下项目的结构,主要文件如下:
bin/www文件定义了基本配置和端口信息

并引入了../app.js,在app.js中定义了网站的路由、渲染引擎ejs、渲染所需的模板路径views

当访问网站根目录时,就会加载indexRouter,也就是包含了./routes/index,非常正常的页面渲染

在app.js同样包含了漏洞页面loginRouter(./routes/login),然后在login是和前几篇一样的原型链污染,允许接受content-type为json的请求体,对象复制时没有严格的过滤导致的原型链污染

在login.js可以看到包含了对象复制的实现,在../utils/common

回到index.js,进行调试,当访问网站根目录时,会渲染index.html

跟进去,定义了一些对象后,调用tryHandleCache

跟进tryHandleCache,依旧没有什么动作,调用了handleCache,这里handleCache后面两个括号,意味着把handleCache函数的返回值当做了函数,并运行该函数,如果返回值可控,就可以代码执行

跟进handleCache,返回值func是由外部函数compile函数决定,读取了模板文件源码传进了compile,进去看下

跟进外部的compile,创建了一个templ对象,并在对象内添加了opts属性,实际上就是把外部传进来的opts对象,传递给了templ的opts

执行完之后调用了Template的子函数compile(),并返回执行结果

跟进子函数compile(),返回和fn对象有关

fn对象是由ctor对象构造的,ctor对象定义位置有两处

位置1,ctor对象变成了匿名函数的构造器,匿名函数的构造器也是一个对象,以此构造器创建的对象,实际上就是匿名函数。函数体可控,后续调用一次可以触发代码执行

位置2更简单粗暴了,直接传递给ctor一个函数的构造器,这个类型的对象创建之后也会是一个函数对象,同样的函数体可控,后续调用一次也可以触发代码执行。

这里创建函数对象的时候,需要两个参数,第一个是函数参数,后面是函数体,只要函数体可控后续调用就可以执行,寻找src的来源

构成src比较重要的部分就是source变量了,默认网站应该不会开启debug的,所以比较大的可能是直接把source传递给src。

source如果为空,则进入逻辑,读取配置信息并把需要渲染模板的源码存入source,并取outputFunctionName值作为prepended放在source的开头,appended作为结尾。这里看到只有prepended是可控的,而且这个outputFunctionName是未定义的(undefined),所以可以触发原型链污染,在原型链上级创建恶意属性outputFunctionName,控制source的值,继而控制函数体的内容,后续调用函数就会造成代码执行。

source在之前Template函数中定义为空,中间没有经过赋值,所以可以进入逻辑(同时可以看到outputFunctionName未定义,client为false)

所以最后的返回值是一个匿名函数

上面提到过,返回值会被当做函数运行一次,并且传参data

data包含了程序运行的一些信息,和渲染的信息,主要关注调用的匿名函数是否可以利用:

这里apply调用了fn去操作context对象,并传参进去,这些参数之前创建函数的时候也可以看到:

这里调用了fn,fn的函数体内的恶意代码也会被执行了

为了测试分析是否正确,构造原型链污染,在source拼接时,prepended如下

闭合这个变量声明即可,构造poc如下:
{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;console.log(123);var __tmp2"}}} 实际运行时应该是
var _tmp1;console.log(123);var __tmp2 = __append; 发送poc至login路由

再访问网站根目录,在返回值被当做函数调用前暂停运行:

未打印出123,运行一步,成功打印出来

代码执行的原因确实是因为渲染。可见ejs的渲染的实现,大体上是读取了模板文件源码,并附加一些参数,然后运行代码,从而展示出对应的效果。
官方不承认这个是漏洞,根本原因是代码不规范导致的原型链污染,所以在需要对代码进行加固。
参考:https://github.com/HoLyVieR/prototype-pollution-nsec18/blob/master/paper/JavaScript_prototype_pollution_attack_in_NodeJS.pdf
之前笔记提到的过滤key值__proto__有些太片面了
1.Object.freeze()冻结原型
1.Object.freeze(Object.prototype);
2. Object.freeze(Object);
3. ({}).__proto__.test = 123
4. ({}).test // this will be undefined 冻结原型后,无法添加新的原型至原型链
2.对JSON 输入验证
npm上有很多库,例如avj,可以对json数据验证,排除json数据中不需要的属性。
或者在复制对象,遍历键名的时候,检查"__proto__"和"constructor"中"prototype",因为constructor.prototype也可以操作原型链。
3.使用map代替{}
4.使用Object.create()安全创建对象
1. var obj = Object.create(null);
2. obj.__proto__ // undefined
3. obj.constructor // undefined 这样创建的对象没有属性
5.nodejs中可以通过--disable-proto直接禁止操作原型链
详见:https://nodejs.org/api/cli.html#cli_disable_proto_mode
关于原型链污染的自动化分析:https://blog.s1r1us.ninja/research/PP