RSS Feed
更好更安全的互联网
  • 关于 Java 中的 RMI-IIOP

    2020-01-02

    作者:Longofo@知道创宇404实验室 
    时间:2019年12月30日

    在写完《Java中RMI、JNDI、LADP、JRMP、JMX、JMS那些事儿(上)》的时候,又看到一个包含RMI-IIOP的议题[1],在16年Blackhat JNDI注入议题[2]中也提到了这个协议的利用,当时想着没太看到或听说有多少关于IIOP的漏洞(可能事实真的如此吧,在下面Weblogic RMI-IIOP部分或许能感受到),所以那篇文章写作过程中也没去看之前那个16年议题IIOP相关部分。网上没怎么看到有关于IIOP或RMI-IIOP的分析文章,这篇文章来感受下。

    环境说明

    • 文中的测试代码放到了github
    • 测试代码的JDK版本在文中会具体说明,有的代码会被重复使用,对应的JDK版本需要自己切换

    RMI-IIOP

    在阅读下面内容之前,可以先阅读下以下几个链接的内容,包含了一些基本的概念留个印象:https://docs.oracle.com/javase/8/docs/technotes/guides/idl/GShome.html[3]
    https://docs.oracle.com/javase/8/docs/technotes/guides/rmi-iiop/rmi_iiop_pg.html[4]
    https://docs.oracle.com/javase/8/docs/technotes/guides/rmi-iiop/tutorial.html#7738[5]

    Java IDL是一种用于分布式对象的技术,即对象在网络上的不同平台上进行交互。Java IDL使对象能够进行交互,而不管它们是以Java编程语言还是C,C ++,COBOL或其他语言编写的。这是可能的,因为Java IDL基于通用对象请求代理体系结构(CORBA),即行业标准的分布式对象模型。CORBA的主要功能是IDL,一种与语言无关的接口定义语言。每种支持CORBA的语言都有自己的IDL映射-顾名思义,Java IDL支持Java映射。为了支持单独程序中对象之间的交互,Java IDL提供了一个对象请求代理或ORB(Object Request Broker)。ORB是一个类库,可在Java IDL应用程序与其他符合CORBA的应用程序之间进行低层级的通信。

    CORBA,Common ObjectRequest Broker Architecture(公共对象请求代理体系结构),是由OMG组织制订的一种标准的面向对象应用程序体系规范。CORBA使用接口定义语言(IDL),用于指定对象提供给外部的接口。然后,CORBA指定从IDL到特定实现语言(如Java)的映射。CORBA规范规定应有一个对象请求代理(ORB),通过该对象应用程序与其他对象进行交互。通用InterORB协议(GIOP)摘要协议的创建是为了允许ORB间的通信,并提供了几种具体的协议,包括Internet InterORB协议(IIOP),它是GIOP的实现,可用于Internet,并提供GIOP消息和TCP/IP层之间的映射。

    IIOP,Internet Inter-ORB Protocol(互联网内部对象请求代理协议),它是一个用于CORBA 2.0及兼容平台上的协议;用来在CORBA对象请求代理之间交流的协议。Java中使得程序可以和其他语言的CORBA实现互操作性的协议。

    RMI-IIOP出现以前,只有RMI和CORBA两种选择来进行分布式程序设计,二者之间不能协作。RMI-IIOP综合了RMI 和CORBA的优点,克服了他们的缺点,使得程序员能更方便的编写分布式程序设计,实现分布式计算。RMI-IIOP综合了RMI的简单性和CORBA的多语言性兼容性,RMI-IIOP克服了RMI只能用于Java的缺点和CORBA的复杂性(可以不用掌握IDL)。

    CORBA-IIOP远程调用

    在CORBA客户端和服务器之间进行远程调用模型如下:

    在客户端,应用程序包含远程对象的引用,对象引用具有存根方法,存根方法是远程调用该方法的替身。存根实际上是连接到ORB的,因此调用它会调用ORB的连接功能,该功能会将调用转发到服务器。

    在服务器端,ORB使用框架代码将远程调用转换为对本地对象的方法调用。框架将调用和任何参数转换为其特定于实现的格式,并调用客户端想要调用的方法。方法返回时,框架代码将转换结果或错误,然后通过ORB将其发送回客户端。

    在ORB之间,通信通过共享协议IIOP进行。基于标准TCP/IP Internet协议的IIOP定义了兼容CORBA的ORB如何来回传递信息。

    编写一个Java CORBA IIOP远程调用步骤:

    1. 使用idl定义远程接口
    2. 使用idlj编译idl,将idl映射为Java,它将生成接口的Java版本类以及存根和骨架的类代码文件,这些文件使应用程序可以挂接到ORB。在远程调用的客户端与服务端编写代码中会使用到这些类文件。
    3. 编写服务端代码
    4. 编写客户端代码
    5. 依次启动命名服务->服务端->客户端

    好了,用代码感受下(github找到一份现成的代码可以直接用,不过做了一些修改):

    1、2步骤作者已经帮我们生成好了,生成的代码在EchoApp目录

    服务端:

    客户端:

    客户端使用了两种方式,一种是COSNaming查询,另一种是Jndi查询,两种方式都可以,在jdk1.8.0_181测试通过。

    首先启动一个命名服务器(可以理解为rmi的registry),使用ordb启动如下,orbd默认自带(如果你有jdk环境的话):

    然后启动服务端corba-iiop/src/main/java/com/longofo/example/Server.java,在启动corba-iiop/src/main/java/com/longofo/example/Client.java或JndiClient.java即可。

    这里看下JndiClient的结果:

    注意到那个class不是没有获取到原本的EchoImpl类对应的Stub class,而我们之前rmi测试也用过这个list查询,那时候是能查询到远程对象对应的stub类名的。这可能是因为Corba的实现机制的原因,com.sun.corba.se.impl.corba.CORBAObjectImpl是一个通用的Corba对象类,而上面的narrow调用EchoHelper.narrow就是一种将对象变窄的方式转换为Echo Stub对象,然后才能调用echoString方法,并且每一个远程对象的调用都要使用它对应的xxxHelper。

    下面是Corba客户端与服务端通信包:

    第1、2个包是客户端与ordb通信的包,后面就是客户端与服务端通信的包。可以看到第二个数据包的IOR(Interoperable Object Reference)中包含着服务端的ip、port等信息,意思就是客户端先从ordb获取服务端的信息,然后接着与服务端通信。同时这些数据中也没有平常所说的ac ed 00 05 标志,但是其实反序列化的数据被包装了,在后面的RMI-IIOP中有一个例子会进行说明。

    IOR几个关键字段:

    • Type ID:接口类型,也称为存储库ID格式。本质上,存储库ID是接口的唯一标识符。例如上面的IDL:omg.org/CosNaming/NamingContext:1.0
    • IIOP version:描述由ORB实现的IIOP版本
    • Host:标识ORB主机的TCP/IP地址
    • Port:指定ORB在其中侦听客户端请求的TCP/IP端口号
    • Object Key:唯一地标识了被ORB导出的servant
    • Components:包含适用于对象方法的附加信息的序列,例如支持的ORB服务和专有协议支持等
    • Codebase:用于获取stub类的远程位置。通过控制这个属性,攻击者将控制在服务器中解码IOR引用的类,在后面利用中我们能够看到。

    只使用Corba进行远程调用很麻烦,要编写IDL文件,然后手动生成对应的类文件,同时还有一些其他限制,然后就有了RMI-IIOP,结合了Corba、RMI的优点。

    RMI-IIOP远程调用

    编写一个RMI IIOP远程调用步骤:

    1. 定义远程接口类
    2. 编写实现类
    3. 编写服务端
    4. 编写客户端
    5. 编译代码并为服务端与客户端生成对应的使用类

    下面直接给出一种恶意利用的demo场景。

    服务端:

    客户端:

    假设在服务端中存在EvilMessage这个能进行恶意利用的类,在客户端中编写同样包名类名相同的类,并继承HelloInterface.sayHello(Message msg)方法中Message类:

    先编译好上面的代码,然后生成服务端与客户端进行远程调用的代理类:

    执行完成后,在下面生成了两个类(Tie用于服务端,Stub用于客户端):

    启动一个命名服务器:

    启动服务端rmi-iiop/src/main/java/com/longofo/example/HelloServer.java,再启动客户端rmi-iiop/src/main/java/com/longofo/example/HelloClient.java即可看到计算器弹出,在JDK 1.8.1_181测试通过。

    服务端调用栈如下:

    注意那个_HelloImpl_Tie.read_value,这是在19年BlackHat议题"An-Far-Sides-Of-Java-Remote-Protocols"[1]提到的,如果直接看那个pdf中关于RMI-IIOP的内容,可能会一脸懵逼,因为议题中没有上面这些前置信息,有了上面这些信息,再去看那个议题的内容可能会轻松些。通过调用栈我们也能看到,IIOP通信中的某些数据被还原成了CDRInputStream,这是InputStream的子类,而被包装的数据在下面Stub data这里:

    最后通过反射调用到了EvilMessage的readObject,看到这里其实就清楚一些了。不过事实可能会有些残酷,不然为什么关于RMI-IIOP的漏洞很少看到,看看下面Weblogic RMI-IIOP来感受下。

    Weblogic中的RMI-IIOP

    Weblogic默认是开启了iiop协议的,如果是上面这样的话,看通信数据以及上面的调用过程极大可能是不会经过Weblogic的黑名单了。

    直接用代码测试吧(利用的Weblogic自带的JDK 1.6.0_29测试):

    list查询结果如下:

    这些远程对象的名称和通过默认的rmi://协议查询的结果是一样的,只是class和interfaces不同。

    但是到managementHome.remove就报错了,managementHome为null。在上面RMI-IIOP的测试中,客户端要调用远程需要用到客户端的Stub类,去查找了下ejb/mgmt/MEJB对应的实现类weblogic.management.j2ee.mejb.Mejb_dj5nps_HomeImpl,他有一个Stub类为weblogic.management.j2ee.mejb.Mejb_dj5nps_HomeImpl_1036_WLStub,但是这个Stub类是为默认的RMI JRMP方式生成的,并没有为IIOP调用生成客户端与服务端类,只是绑定了一个名称。

    通过一些查找,每一个IIOP远程对象对应的Tie类和Stub类都会有一个特征:

    根据这个特征,在Weblogic中确实有很多这种已经为IIOP调用生成的客户端Stub类,例如_MBeanHomeImpl_Stub类,是MBeanHomeImpl客户端的Stub类:

    一个很尴尬的事情就是,Weblogic默认绑定了远程名称的实现类没有为IIOP实现服务端类与客户端类,但是没有绑定的一些类却实现了,所以默认无法利用了。

    刚才调用失败了,来看下没有成功调用的通信:

    在COSNaming查询包之后,服务端返回了type_ip为RMI:javax.management.j2ee.ManagementHome:0000000000000000的标志,

    然后下一个包又继续了一个_is_a查询:

    下一个包就返回了type_id not match:

    可以猜测的是服务端没有生成IIOP对应的服务端与客户端类,然后命名服务器中找不到关于的RMI:javax.management.j2ee.ManagementHome:0000000000000000标记,通过查找也确实没有找到对应的类。

    不过上面这种利用方式只是在代码层调用遵守了Corba IIOP的一些规范,规规矩矩的调用,在协议层能不能通过替换、修改等操作进行构造与利用,能力有限,未深入研究IIOP通信过程。

    在今年的那个议题RMI-IIOP部分,给出了Websphere一个拦截器类TxServerInterceptor中使用到read_any方法的情况,从这个名字中可以看出是一个拦截器,所以基本上所有请求都会经过这里。这里最终也调用到read_value,就像上面的_HelloImpl_Tie.read_value一样,这里也能进行可以利用,只要目标服务器存在可利用的链,作者也给出了一些Websphere中的利用链。可以看到,不只是在远程调用中会存在恶意利用的地方,在其他地方也可能以另一种方式存在,不过在方法调用链中核心的几个地方依然没有变,CDRInputStreamread_value,可能手动去找这些特征很累甚至可能根本找不到,那么庞大的代码量,不过要是有所有的方法调用链,例如GatgetInspector那种工具,之前初步分析过这个工具。这是后面的打算了,目标是自由的编写自己的控制逻辑。

    JNDI中的利用

    在JNDI利用中有多种的利用方式,而RMI-IIOP只是默认RMI利用方式(通过JRMP传输)的替代品,在RMI默认利用方式无法利用时,可以考虑用这种方式。但是这种方式依然会受到SecurityManager的限制。

    在RMI-IIOP测试代码中,我把client与server放在了一起,客户端与服务端使用的Tie与Stub也放在了一起,可能会感到迷惑。那下面我们就单独把Client拿出来进行测试以及看下远程加载。

    服务端代码还是使用RMI-IIOP中的Server,但是加了一个codebase:

    Client代码在新建的rmi-iiop-test-client模块,这样模块之间不会受到影响,Client代码如下:

    然后我在remote-class模块增加了一个com.longofo.example._HelloInterface_Stub

    启动远程类服务remote-class/src/main/java/com/longofo/remoteclass/HttpServer.java,再启动rmi-iiop/src/main/java/com/longofo/example/HelloServer.java,然后运行客户端rmi-iiop-test-client/src/main/java/com/longofo/example/HelloClient.java即可弹出计算器。在JDK 1.8.0_181测试通过。

    至于为什么进行了远程调用,在CDRInputStream_1_0.read_object下个断点,然后跟踪就会明白了,最后还是利用了rmi的远程加载功能:

    总结

    遗憾就是没有成功在Weblogic中利用到RMI-IIOP,在这里写出来提供一些思路,如果大家有关于RMI-IIOP的其他发现与想法也记得分享下。不知道大家有没有关于RMI-IIOP比较好的真实案例。

    参考

    1. https://i.blackhat.com/eu-19/Wednesday/eu-19-An-Far-Sides-Of-Java-Remote-Protocols.pdf
    2. https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.pdf
    3. https://docs.oracle.com/javase/8/docs/technotes/guides/idl/GShome.html
    4. https://docs.oracle.com/javase/8/docs/technotes/guides/rmi-iiop/rmi_iiop_pg.html
    5. https://docs.oracle.com/javase/8/docs/technotes/guides/rmi-iiop/tutorial.html#7738
    作者:吴烦恼 | Categories:安全研究 | Tags:
  • Linux HIDS agent 概要和用户态 HOOK(一)

    2019-12-24

    作者:u2400@知道创宇404实验室
    时间:2019年12月19日

    前言:最近在实现linux的HIDS agent, 搜索资料时发现虽然资料不少, 但是每一篇文章都各自有侧重点, 少有循序渐进, 讲的比较全面的中文文章, 在一步步学习中踩了不少坑, 在这里将以进程信息收集作为切入点就如何实现一个HIDS的agent做详细说明, 希望对各位师傅有所帮助.

    1. 什么是HIDS?

    主机入侵检测, 通常分为agent和server两个部分

    其中agent负责收集信息, 并将相关信息整理后发送给server.

    server通常作为信息中心, 部署由安全人员编写的规则(目前HIDS的规则还没有一个编写的规范),收集从各种安全组件获取的数据(这些数据也可能来自waf, NIDS等), 进行分析, 根据规则判断主机行为是否异常, 并对主机的异常行为进行告警和提示.

    HIDS存在的目的在于在管理员管理海量IDC时不会被安全事件弄的手忙脚乱, 可以通过信息中心对每一台主机的健康状态进行监视.

    相关的开源项目有OSSEC, OSquery等, OSSEC是一个已经构建完善的HIDS, 有agent端和server端, 有自带的规则, 基础的rootkit检测, 敏感文件修改提醒等功能, 并且被包含到了一个叫做wazuh的开源项目, OSquery是一个facebook研发的开源项目, 可以作为一个agent端对主机相关数据进行收集, 但是server和规则需要自己实现.

    每一个公司的HIDS agent都会根据自身需要定制, 或多或少的增加一些个性化的功能, 一个基础的HIDS agent一般需要实现的有:

    • 收集进程信息
    • 收集网络信息
    • 周期性的收集开放端口
    • 监控敏感文件修改

    下文将从实现一个agent入手, 围绕agent讨论如何实现一个HIDS agent的进程信息收集模块

    2. agent进程监控模块提要

    2.1进程监控的目的

    在Linxu操作系统中几乎所有的运维操作和入侵行为都会体现到执行的命令中, 而命令执行的本质就是启动进程, 所以对进程的监控就是对命令执行的监控, 这对运维操作升级和入侵行为分析都有极大的帮助

    2.2 进程监控模块应当获取的数据

    既然要获取信息那就先要明确需要什么, 如果不知道需要什么信息, 那实现便无从谈起, 即便硬着头皮先实现一个能获取pid等基础信息的HIDS, 后期也会因为缺少规划而频繁改动接口, 白白耗费人力, 这里参考《互联网企业安全高级指南》给出一个获取信息的基础列表, 在后面会补全这张表的的获取方式

    数据名称含义
    path可执行文件的路径
    ppath父进程可执行文件路径
    ENV环境变量
    cmdline进程启动命令
    pcmdline父进程启动命令
    pid进程id
    ppid父进程id
    pgid进程组id
    sid进程会话id
    uid启动进程用户的uid
    euid启动进程用户的euid
    gid启动进程用户的用户组id
    egid启动进程用户的egid
    mode可执行文件的权限
    owner_uid文件所有者的uid
    owner_gid文件所有者的gid
    create_time文件创建时间
    modify_time最近的文件修改时间
    pstart_time进程开始运行的时间
    prun_time父进程已经运行的时间
    sys_time当前系统时间
    fd文件描述符

    2.3 进程监控的方式

    进程监控, 通常使用hook技术, 而这些hook大概分为两类:

    应用级(工作在r3, 常见的就是劫持libc库, 通常简单但是可能被绕过 - 内核级(工作在r0或者r1, 内核级hook通常和系统调用VFS有关, 较为复杂, 且在不同的发行版, 不同的内核版本间均可能产生兼容性问题, hook出现严重的错误时可能导致kenrel panic, 相对的无法从原理上被绕过

    首先从简单的应用级hook说起

    3. HIDS 应用级hook

    3.1 劫持libc库

    库用于打包函数, 被打包过后的函数可以直接使用, 其中linux分为静态库和动态库, 其中动态库是在加载应用程序时才被加载, 而程序对于动态库有加载顺序, 可以通过修改 /etc/ld.so.preload 来手动优先加载一个动态链接库, 在这个动态链接库中可以在程序调用原函数之前就把原来的函数先换掉, 然后在自己的函数中执行了自己的逻辑之后再去调用原来的函数返回原来的函数应当返回的结果.

    想要详细了解的同学, 参考这篇文章

    劫持libc库有以下几个步骤:

    3.1.1 编译一个动态链接库

    一个简单的hook execve的动态链接库如下.
    逻辑非常简单

    1. 自定义一个函数命名为execve, 接受参数的类型要和原来的execve相同
    2. 执行自己的逻辑

    通过gcc编译为so文件.

    3.1.2 修改ld.so.preload

    ld.so.preload是LD_PRELOAD环境变量的配置文件, 通过修改该文件的内容为指定的动态链接库文件路径,

    注意只有root才可以修改ld.so.preload, 除非默认的权限被改动了

    自定义一个execve函数如下:

    image.png

    可以输出当前进程的Pid和所有的环境变量, 编译后修改ld.so.preload, 重启shell, 运行ls命令结果如下

    3.1.3 libc hook的优缺点

    优点: 性能较好, 比较稳定, 相对于LKM更加简单, 适配性也很高, 通常对抗web层面的入侵.

    缺点: 对于静态编译的程序束手无策, 存在一定被绕过的风险.

    3.1.4 hook与信息获取

    设立hook, 是为了建立监控点, 获取进程的相关信息, 但是如果hook的部分写的过大过多, 会导致影响正常的业务的运行效率, 这是业务所不能接受的, 在通常的HIDS中会将可以不在hook处获取的信息放在agent中获取, 这样信息获取和业务逻辑并发执行, 降低对业务的影响.

    4 信息补全与获取

    如果对信息的准确性要求不是很高, 同时希望尽一切可能的不影响部署在HIDS主机上的正常业务那么可以选择hook只获取PID和环境变量等必要的数据, 然后将这些东西交给agent, 由agent继续获取进程的其他相关信息, 也就是说获取进程其他信息的同时, 进程就已经继续运行了, 而不需要等待agent获取完整的信息表.

    /proc/[pid]/stat

    /proc是内核向用户态提供的一组fifo接口, 通过伪文件目录的形式调用接口

    每一个进程相关的信息, 会被放到以pid命名的文件夹当中, ps等命令也是通过遍历/proc目录来获取进程的相关信息的.

    一个stat文件内容如下所示, 下面self是/proc目录提供的一个快捷的查看自己进程信息的接口, 每一个进程访问/self时看到都是自己的信息.

    会发现这些数据杂乱无章, 使用空格作为每一个数据的边界, 没有地方说明这些数据各自表达什么意思.

    一般折腾找到了一篇文章里面给出了一个列表, 这个表里面说明了每一个数据的数据类型和其表达的含义, 见文章附录1

    最后整理出一个有52个数据项每个数据项类型各不相同的结构体, 获取起来还是有点麻烦, 网上没有找到轮子, 所以自己写了一个

    具体的结构体定义:

    从文件中读入并格式化为结构体:

    和我们需要获取的数据做了一下对比, 可以获取以下数据

    ppid父进程id
    pgid进程组id
    sid进程会话id
    start_time父进程开始运行的时间
    run_time父进程已经运行的时间

    /proc/[pid]/exe

    通过/proc/[pid]/exe获取可执行文件的路径, 这里/proc/[pid]/exe是指向可执行文件的软链接, 所以这里通过readlink函数获取软链接指向的地址.

    这里在读取时需要注意如果readlink读取的文件已经被删除, 读取的文件名后会多一个 (deleted), 但是agent也不能盲目删除文件结尾时的对应字符串, 所以在写server规则时需要注意这种情况

    /proc/[pid]/cmdline

    获取进程启动的是启动命令, 可以通过获取/proc/[pid]/cmdline的内容来获得, 这个获取里面有两个坑点

    1. 由于启动命令长度不定, 为了避免溢出, 需要先获取长度, 在用malloc申请堆空间, 然后再将数据读取进变量.
    2. /proc/self/cmdline文件里面所有的空格和回车都会变成 '\0'也不知道为啥, 所以需要手动换源回来, 而且若干个相连的空格也只会变成一个'\0'.

    这里获取长度的办法比较蠢, 但是用fseek直接将文件指针移到文件末尾的办法每次返回的都是0, 也不知道咋办了, 只能先这样

    获取cmdline的内容

    小结

    这里写的只是实现的一种最常见最简单的应用级hook的方法具体实现和代码已经放在了github上, 同时github上的代码会保持更新, 下次的文章会分享如何使用LKM修改sys_call_table来hook系统调用的方式来实现HIDS的hook.

    参考文章

    https://www.freebuf.com/articles/system/54263.htmlhttp://abcdefghijklmnopqrst.xyz/2018/07/30/Linux_INT80/https://cloud.tencent.com/developer/news/337625https://github.com/g0dA/linuxStack/blob/master/%E8%BF%9B%E7%A8%8B%E9%9A%90%E8%97%8F%E6%8A%80%E6%9C%AF%E7%9A%84%E6%94%BB%E4%B8%8E%E9%98%B2-%E6%94%BB%E7%AF%87.md

    附录1

    这里完整的说明了/proc目录下每一个文件具体的意义是什么.
    http://man7.org/linux/man-pages/man5/proc.5.html


    Paper

    本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1102/

    作者:吴烦恼 | Categories:安全研究技术分享 | Tags:
  • 认识 JavaAgent –获取目标进程已加载的所有类

    2019-12-24

    之前在一个应用中搜索到一个类,但是在反序列化测试的时出错,错误不是class notfound,是其他0xxx这样的错误,通过搜索这个错误大概是类没有被加载。最近刚好看到了JavaAgent,初步学习了下,能进行拦截,主要通过Instrument Agent来进行字节码增强,可以进行字节码插桩,bTrace,Arthas 等操作,结合ASM,javassist,cglib框架能实现更强大的功能。Java RASP也是基于JavaAgent实现的。趁热记录下JavaAgent基础概念,以及简单使用JavaAgent实现一个获取目标进程已加载的类的测试。

    JVMTI与Java Instrument

    Java平台调试器架构(Java Platform Debugger Architecture,JPDA)是一组用于调试Java代码的API(摘自维基百科):

    • Java调试器接口(Java Debugger Interface,JDI)——定义了一个高层次Java接口,开发人员可以利用JDI轻松编写远程调试工具
    • Java虚拟机工具接口(Java Virtual Machine Tools Interface,JVMTI)——定义了一个原生(native)接口,可以对运行在Java虚拟机的应用程序检查状态、控制运行
    • Java虚拟机调试接口(JVMDI)——JVMDI在J2SE 5中被JVMTI取代,并在Java SE 6中被移除
    • Java调试线协议(JDWP)——定义了调试对象(一个 Java 应用程序)和调试器进程之间的通信协议

    JVMTI 提供了一套"代理"程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能。JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者去扩展自己的逻辑。

    JVMTIAgent是一个利用JVMTI暴露出来的接口提供了代理启动时加载(agent on load)、代理通过attach形式加载(agent on attach)和代理卸载(agent on unload)功能的动态库。Instrument Agent可以理解为一类JVMTIAgent动态库,别名是JPLISAgent(Java Programming Language Instrumentation Services Agent),是专门为java语言编写的插桩服务提供支持的代理

    Instrumentation接口

    以下接口是Java SE 8 API文档中[1]提供的(不同版本可能接口有变化):

    redefineClasses与redefineClasses

    重新定义功能在Java SE 5中进行了介绍,重新转换功能在Java SE 6中进行了介绍,一种猜测是将重新转换作为更通用的功能引入,但是必须保留重新定义以实现向后兼容,并且重新转换操作也更加方便。

    Instrument Agent两种加载方式

    在官方API文档[1]中提到,有两种获取Instrumentation接口实例的方法 :

    1. JVM在指定代理的方式下启动,此时Instrumentation实例会传递到代理类的premain方法。
    2. JVM提供一种在启动之后的某个时刻启动代理的机制,此时Instrumentation实例会传递到代理类代码的agentmain方法。

    premain对应的就是VM启动时的Instrument Agent加载,即agent on load,agentmain对应的是VM运行时的Instrument Agent加载,即agent on attach。两种加载形式所加载的Instrument Agent都关注同一个JVMTI事件 – ClassFileLoadHook事件,这个事件是在读取字节码文件之后回调时用,也就是说premain和agentmain方式的回调时机都是类文件字节码读取之后(或者说是类加载之后),之后对字节码进行重定义或重转换,不过修改的字节码也需要满足一些要求,在最后的局限性有说明

    premain与agentmain的区别

    premainagentmain两种方式最终的目的都是为了回调Instrumentation实例并激活sun.instrument.InstrumentationImpl#transform()(InstrumentationImpl是Instrumentation的实现类)从而回调注册到Instrumentation中的ClassFileTransformer实现字节码修改,本质功能上没有很大区别。两者的非本质功能的区别如下:

    • premain方式是JDK1.5引入的,agentmain方式是JDK1.6引入的,JDK1.6之后可以自行选择使用premain或者agentmain
    • premain需要通过命令行使用外部代理jar包,即-javaagent:代理jar包路径agentmain则可以通过attach机制直接附着到目标VM中加载代理,也就是使用agentmain方式下,操作attach的程序和被代理的程序可以是完全不同的两个程序。
    • premain方式回调到ClassFileTransformer中的类是虚拟机加载的所有类,这个是由于代理加载的顺序比较靠前决定的,在开发者逻辑看来就是:所有类首次加载并且进入程序main()方法之前,premain方法会被激活,然后所有被加载的类都会执行ClassFileTransformer列表中的回调。
    • agentmain方式由于是采用attach机制,被代理的目标程序VM有可能很早之前已经启动,当然其所有类已经被加载完成,这个时候需要借助Instrumentation#retransformClasses(Class<?>... classes)让对应的类可以重新转换,从而激活重新转换的类执行ClassFileTransformer列表中的回调。
    • 通过premain方式的代理Jar包进行了更新的话,需要重启服务器,而agentmain方式的Jar包如果进行了更新的话,需要重新attach,但是agentmain重新attach还会导致重复的字节码插入问题,不过也有HotswapDCE VM方式来避免。

    通过下面的测试也能看到它们之间的一些区别。

    premain加载方式

    premain方式编写步骤简单如下:

    1.编写premain函数,包含下面两个方法的其中之一:

    java public static void premain(String agentArgs, Instrumentation inst); public static void premain(String agentArgs);

    如果两个方法都被实现了,那么带Instrumentation参数的优先级高一些,会被优先调用。agentArgspremain函数得到的程序参数,通过命令行参数传入

    2.定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项

    3.将 premain 的类和 MANIFEST.MF 文件打成 jar 包

    4.使用参数 -javaagent: jar包路径启动代理

    premain加载过程如下:

    1.创建并初始化 JPLISAgent 
    2.MANIFEST.MF 文件的参数,并根据这些参数来设置 JPLISAgent 里的一些内容 
    3.监听 VMInit 事件,在 JVM 初始化完成之后做下面的事情: 
    (1)创建 InstrumentationImpl 对象 ; 
    (2)监听 ClassFileLoadHook 事件 ; 
    (3)调用 InstrumentationImpl 的loadClassAndCallPremain方法,在这个方法里会去调用 javaagent 中 MANIFEST.MF 里指定的Premain-Class 类的 premain 方法

    下面是一个简单的例子(在JDK1.8.0_181进行了测试):

    PreMainAgent

    MANIFEST.MF:

    Testmain

    将PreMainAgent打包为Jar包(可以直接用idea打包,也可以使用maven插件打包),在idea可以像下面这样启动:

    命令行的话可以用形如java -javaagent:PreMainAgent.jar路径 -jar TestMain/TestMain.jar启动

    结果如下:

    可以看到在PreMainAgent之前已经加载了一些必要的类,即PreMainAgent get loaded class:xxx部分,这些类没有经过transform。然后在main之前有一些类经过了transform,在main启动之后还有类经过transform,main结束之后也还有类经过transform,可以和agentmain的结果对比下。

    agentmain加载方式

    agentmain方式编写步骤简单如下:

    1.编写agentmain函数,包含下面两个方法的其中之一:

    如果两个方法都被实现了,那么带Instrumentation参数的优先级高一些,会被优先调用。agentArgspremain函数得到的程序参数,通过命令行参数传入

    2.定义一个 MANIFEST.MF 文件,必须包含 Agent-Class 选项,通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项

    3.将 agentmain 的类和 MANIFEST.MF 文件打成 jar 包

    4.通过attach工具直接加载Agent,执行attach的程序和需要被代理的程序可以是两个完全不同的程序:

    agentmain方式加载过程类似:

    1.创建并初始化JPLISAgent 
    2.解析MANIFEST.MF 里的参数,并根据这些参数来设置 JPLISAgent 里的一些内容 
    3.监听 VMInit 事件,在 JVM 初始化完成之后做下面的事情: 
    (1)创建 InstrumentationImpl 对象 ; 
    (2)监听 ClassFileLoadHook 事件 ; 
    (3)调用 InstrumentationImpl 的loadClassAndCallAgentmain方法,在这个方法里会去调用javaagent里 MANIFEST.MF 里指定的Agent-Class类的agentmain方法。

    下面是一个简单的例子(在JDK 1.8.0_181上进行了测试):

    SufMainAgent

    MANIFEST.MF

    TestSufMainAgent

    Testmain

    将SufMainAgent和TestSufMainAgent打包为Jar包(可以直接用idea打包,也可以使用maven插件打包),首先启动Testmain,然后先列下当前有哪些Java程序:

    attach SufMainAgent到Testmain:

    在Testmain中的结果如下:

    和前面premain对比下就能看出,在agentmain中直接getloadedclasses的类数目比在premain直接getloadedclasses的数量多,而且premain getloadedclasses的类+premain transform的类和agentmain getloadedclasses基本吻合(只针对这个测试,如果程序中间还有其他通信,可能会不一样)。也就是说某个类之前没有加载过,那么都会通过两者设置的transform,这可以从最后的java/lang/Shutdown看出来。

    测试Weblogic的某个类是否被加载

    这里使用weblogic进行测试,代理方式使用agentmain方式(在jdk1.6.0_29上进行了测试):

    WeblogicSufMainAgent

    WeblogicTestSufMainAgent:

    列出正在运行的Java应用程序:

    进行attach:

    Weblogic输出:

    假如在进行Weblogic t3反序列化利用时,如果某个类之前没有被加载,但是能够被Weblogic找到,那么利用时对应的类会通过Agent的transform,但是有些类虽然在Weblogic目录下的某些Jar包中,但是weblogic不会去加载,需要一些特殊的配置Weblogic才会去寻找并加载。

    Instrumentation局限性

    大多数情况下,使用Instrumentation都是使用其字节码插桩的功能,笼统说是类重转换的功能,但是有以下的局限性:

    1. premain和agentmain两种方式修改字节码的时机都是类文件加载之后,就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。这里需要注意的就是上面提到过的重新定义,刚才这里说的不能重新定义是指不能重新换一个类名,字节码内容依然能重新定义和修改,不过字节码内容修改后也要满足第二点的要求。
    2. 类转换其实最终都回归到类重定义Instrumentation#retransformClasses()方法,此方法有以下限制: 
      1.新类和老类的父类必须相同; 
      2.新类和老类实现的接口数也要相同,并且是相同的接口; 
      3.新类和老类访问符必须一致。 新类和老类字段数和字段名要一致; 
      4.新类和老类新增或删除的方法必须是private static/final修饰的; 
      5.可以删除修改方法体。

    实际中遇到的限制可能不止这些,遇到了再去解决吧。如果想要重新定义一全新类(类名在已加载类中不存在),可以考虑基于类加载器隔离的方式:创建一个新的自定义类加载器去通过新的字节码去定义一个全新的类,不过只能通过反射调用该全新类的局限性。

    小结

    • 文中只是描述了JavaAgent相关的一些基础的概念,目的只是知道有这个东西,然后验证下之前遇到的一个问题。写的时候也借鉴了其他大佬写的几篇文章[4]&[5]
    • 在写文章的过程中看了一些如一类PHP-RASP实现的漏洞检测的思路[6],利用了污点跟踪、hook、语法树分析等技术,也看了几篇大佬们整理的Java RASP相关文章[2]&[3],如果自己要写基于RASP的漏洞检测/利用工具的话也可以借鉴到这些思路

    代码放到了github上,有兴趣的可以去测试下,注意pom.xml文件中的jdk版本,在切换JDK测试如果出现错误,记得修改pom.xml里面的JDK版本。

    参考

    1.https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html 
    2.https://paper.seebug.org/513/#0x01-rasp 
    3.https://paper.seebug.org/1041/#31-java-agent 
    4.http://www.throwable.club/2019/06/29/java-understand-instrument-first/#Instrumentation%E6%8E%A5%E5%8F%A3%E8%AF%A6%E8%A7%A3 
    5.https://www.cnblogs.com/rickiyang/p/11368932.html 
    6.https://c0d3p1ut0s.github.io/%E4%B8%80%E7%B1%BBPHP-RASP%E7%9A%84%E5%AE%9E%E7%8E%B0/


    Paper

    本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1099/

    作者:吴烦恼 | Categories:安全研究技术分享 | Tags:
  • 从 0 开始入门 Chrome Ext 安全(二) — 安全的 Chrome Ext

    2019-12-09

    作者:LoRexxar'@知道创宇404实验室
    时间:2019年12月5日

    在2019年初,微软正式选择了Chromium作为默认浏览器,并放弃edge的发展。并在19年4月8日,Edge正式放出了基于Chromium开发的Edge Dev浏览器,并提供了兼容Chrome Ext的配套插件管理。再加上国内的大小国产浏览器大多都是基于Chromium开发的,Chrome的插件体系越来越影响着广大的人群。

    在这种背景下,Chrome Ext的安全问题也应该受到应有的关注,《从0开始入门Chrome Ext安全》就会从最基础的插件开发开始,逐步研究插件本身的恶意安全问题,恶意网页如何利用插件漏洞攻击浏览器等各种视角下的安全问题。

    上篇我们主要聊了关于最基础插件开发,之后我们就要探讨关于Chrome Ext的安全性问题了,这篇文章我们主要围绕Chrome Ext的api开始,探讨在插件层面到底能对浏览器进行多少种操作。

    从一个测试页面开始

    为了探讨插件的功能权限范围,首先我们设置一个简单的页面

    接下来我们将围绕Chrome ext api的功能探讨各种可能存在的安全问题以及攻击层面。

    Chrome ext js

    content-script

    content-script是插件的核心功能代码地方,一般来说,主要的js代码都会出现在content-script中。

    它的引入方式在上一篇文章中提到过,要在manfest.json中设置

    而content_script js 主要的特点在于他与页面同时加载,可以访问dom,并且也能调用extension、runtime等部分api,但并不多,主要用于和页面的交互。

    content_script js可以通过设置run_at来设置相对应脚本加载的时机。

    • document_idle 为默认值,一般来说会在页面dom加载完成之后,window.onload事件触发之前
    • document_start 为css加载之后,构造页面dom之前
    • document_end 则为dom完成之后,图片等资源加载之前

    并且,content_script js还允许通过设置all_frames来使得content_script js作用于页面内的所有frame,这个配置默认为关闭,因为这本身是个不安全的配置,这个问题会在后面提到。

    content_script js中可以直接访问以下Chrome Ext api:

    • i18n
    • storage
    • runtime:
      • connect
      • getManifest
      • getURL
      • id
      • onConnect
      • onMessage
      • sendMessage

    在了解完基本的配置后,我们就来看看content_script js可以对页面造成什么样的安全问题。

    安全问题

    对于content_script js来说,首当其中的一个问题就是,插件可以获取页面的dom,换言之,插件可以操作页面内的所有dom,其中就包括非httponly的cookie.

    这里我们简单把content_script js中写入下面的代码

    然后加载插件之后刷新页面

    可以看到成功获取到了页面内dom的信息,并且如果我们通过xhr跨域传出消息之后,我们在后台也成功收到了这个请求。

    这也就意味着,如果插件作者在插件中恶意修改dom,甚至获取dom值传出都可以通过浏览器使用者无感的方式进行。

    在整个浏览器的插件体系内,各个层面都存在着这个问题,其中content_script jsinjected script jsdevtools js都可以直接访问操作dom,而popup js和background js都可以通过chrome.tabs.executeScript来动态执行js,同样可以执行js修改dom。

    除了前面的问题以外,事实上content_script js能访问到的chrome api非常之少,也涉及不到什么安全性,这里暂且不提。

    popup/background js

    popup js和backround js两个主要的区别在于加载的时机,由于他们不能访问dom,所以这两部分的js在浏览器中主要依靠事件驱动。

    其中的主要区别是,background js在事件触发之后会持续执行,而且在关闭所有可见视图和端口之前不会结束。值得注意的是,页面打开、点击拓展按钮都连接着相应的事件,而不会直接影响插件的加载。

    而除此之外,这两部分js最重要的特性在于,他们可以调用大部分的chrome ext api,在后面我们将一起探索一下各种api。

    devtools js

    devtools js在插件体系中是一个比较特别的体系,如果我们一般把F12叫做开发者工具的话,那devtools js就是开发者工具的开发者工具。

    权限和域限制大体上和content js 一致,而唯一特别的是他可以操作3个特殊的api:

    • chrome.devtools.panels:面板相关;
    • chrome.devtools.inspectedWindow:获取被审查窗口的有关信息;
    • chrome.devtools.network:获取有关网络请求的信息;

    而这三个api也主要是用于修改F12和获取信息的,其他的就不赘述了。

    Chrome Ext Api

    chrome.cookies

    chrome.cookies api需要给与域权限以及cookies权限,在manfest.json中这样定义:

    当申请这样的权限之后,我们可以通过调用chrome.cookies去获取google.com域下的所有cookie.

    其中一共包含5个方法

    • get - chrome.cookies.get(object details, function callback)
      获取符合条件的cookie
    • getAll - chrome.cookies.getAll(object details, function callback)
      获取符合条件的所有cookie
    • set - chrome.cookies.set(object details, function callback)
      设置cookie
    • remove - chrome.cookies.remove(object details, function callback)
      删除cookie
    • getAllCookieStores - chrome.cookies.getAllCookieStores(function callback)
      列出所有储存的cookie

    和一个事件

    • chrome.cookies.onChanged.addListener(function callback)
      当cookie删除或者更改导致的事件

    当插件拥有cookie权限时,他们可以读写所有浏览器存储的cookie.

    chrome.contentSettings

    chrome.contentSettings api 用来设置浏览器在访问某个网页时的基础设置,其中包括cookie、js、插件等很多在访问网页时生效的配置。

    在manifest中需要申请contentSettings的权限

    在content.Setting的api中,方法主要用于修改设置

    因为没有涉及到太重要的api,这里就暂时不提

    chrome.desktopCapture

    chrome.desktopCapture可以被用来对整个屏幕,浏览器或者某个页面截图(实时)。

    在manifest中需要申请desktopCapture的权限,并且浏览器提供了获取媒体流的一个方法。

    • chooseDesktopMedia - integer chrome.desktopCapture.chooseDesktopMedia(array of DesktopCaptureSourceType sources, tabs.Tab targetTab, function callback)
    • cancelChooseDesktopMedia - chrome.desktopCapture.cancelChooseDesktopMedia(integer desktopMediaRequestId)

    其中DesktopCaptureSourceType被设置为"screen", "window", "tab", or "audio"的列表。

    获取到相应截图之后,该方法会将相对应的媒体流id传给回调函数,这个id可以通过getUserMedia这个api来生成相应的id,这个新创建的streamid只能使用一次并且会在几秒后过期。

    这里用一个简单的demo来示范

    这里获取的是一个实时的视频流

    chrome.pageCapture

    chrome.pageCapture的大致逻辑和desktopCapture比较像,在manifest需要申请pageCapture的权限

    它也只支持saveasMHTML一种方法

    • saveAsMHTML - chrome.pageCapture.saveAsMHTML(object details, function callback)

    通过调用这个方法可以获取当前浏览器任意tab下的页面源码,并保存为blob格式的对象。

    唯一的问题在于需要先知道tabid

    chrome.tabCapture

    chrome.tabCapture和chrome.desktopCapture类似,其主要功能区别在于,tabCapture可以捕获标签页的视频和音频,比desktopCapture来说要更加针对。

    同样的需要提前声明tabCapture权限。

    主要方法有

    • capture - chrome.tabCapture.capture( CaptureOptions options, function callback)
    • getCapturedTabs - chrome.tabCapture.getCapturedTabs(function callback)
    • captureOffscreenTab - chrome.tabCapture.captureOffscreenTab(string startUrl, CaptureOptions options, function callback)
    • getMediaStreamId - chrome.tabCapture.getMediaStreamId(object options, function callback)

    这里就不细讲了,大部分api都是用来捕获媒体流的,进一步使用就和desktopCapture中提到的使用方法相差不大。

    chrome.webRequest

    chrome.webRequest主要用户观察和分析流量,并且允许在运行过程中拦截、阻止或修改请求。

    在manifest中这个api除了需要webRequest以外,还有有相应域的权限,比如*://*.*:*,而且要注意的是如果是需要拦截请求还需要webRequestBlocking的权限

    在具体了解这个api之前,首先我们必须了解一次请求在浏览器层面的流程,以及相应的事件触发。

    在浏览器插件的世界里,相应的事件触发被划分为多个层级,每个层级逐一执行处理。

    由于这个api下的接口太多,这里拿其中的一个举例子

    当访问baidu的时候,请求会被block

    当设置了redirectUrl时会产生相应的跳转

    此时访问www.baidu.com会跳转lorexxar.cn

    在文档中提到,通过这些api可以直接修改post提交的内容。

    chrome.bookmarks

    chrome.bookmarks是用来操作chrome收藏夹栏的api,可以用于获取、修改、创建收藏夹内容。

    在manifest中需要申请bookmarks权限。

    当我们使用这个api时,不但可以获取所有的收藏列表,还可以静默修改收藏对应的链接。

    chrome.downloads

    chrome.downloads是用来操作chrome中下载文件相关的api,可以创建下载,继续、取消、暂停,甚至可以打开下载文件的目录或打开下载的文件。

    这个api在manifest中需要申请downloads权限,如果想要打开下载的文件,还需要申请downloads.open权限。

    在这个api下,提供了许多相关的方法

    • download - chrome.downloads.download(object options, function callback)
    • search - chrome.downloads.search(object query, function callback)
    • pause - chrome.downloads.pause(integer downloadId, function callback)
    • resume - chrome.downloads.resume(integer downloadId, function callback)
    • cancel - chrome.downloads.cancel(integer downloadId, function callback)
    • getFileIcon - chrome.downloads.getFileIcon(integer downloadId, object options, function callback)
    • open - chrome.downloads.open(integer downloadId)
    • show - chrome.downloads.show(integer downloadId)
    • showDefaultFolder - chrome.downloads.showDefaultFolder()
    • erase - chrome.downloads.erase(object query, function callback)
    • removeFile - chrome.downloads.removeFile(integer downloadId, function callback)
    • acceptDanger - chrome.downloads.acceptDanger(integer downloadId, function callback)
    • setShelfEnabled - chrome.downloads.setShelfEnabled(boolean enabled)

    当我们拥有相应的权限时,我们可以直接创建新的下载,如果是危险后缀,比如.exe等会弹出一个相应的危险提示。

    除了在下载过程中可以暂停、取消等方法,还可以通过show打开文件所在目录或者open直接打开文件。

    但除了需要额外的open权限以外,还会弹出一次提示框。

    相应的其实可以下载file:///C:/Windows/System32/calc.exe并执行,只不过在下载和执行的时候会有专门的危险提示。

    反之来说,如果我们下载的是一个标识为非危险的文件,那么我们就可以静默下载并且打开文件。

    chrome.history && chrome.sessions

    chrome.history 是用来操作历史纪录的api,和我们常见的浏览器历史记录的区别就是,这个api只能获取这次打开浏览器中的历史纪律,而且要注意的是,只有关闭的网站才会算进历史记录中。

    这个api在manfiest中要申请history权限。

    api下的所有方法如下,主要围绕增删改查来

    • search - chrome.history.search(object query, function callback)
    • getVisits - chrome.history.getVisits(object details, function callback)
    • addUrl - chrome.history.addUrl(object details, function callback)
    • deleteUrl - chrome.history.deleteUrl(object details, function callback)
    • deleteRange - chrome.history.deleteRange(object range, function callback)
    • deleteAll - chrome.history.deleteAll(function callback)

    浏览器可以获取这次打开浏览器之后所有的历史纪录。

    在chrome的api中,有一个api和这个类似-chrome.sessions

    这个api是用来操作和回复浏览器会话的,同样需要申请sessions权限。

    • getRecentlyClosed - chrome.sessions.getRecentlyClosed( Filter filter, function callback)
    • getDevices - chrome.sessions.getDevices( Filter filter, function callback)
    • restore - chrome.sessions.restore(string sessionId, function callback)

    通过这个api可以获取最近关闭的标签会话,还可以恢复。

    chrome.tabs

    chrome.tabs是用于操作标签页的api,算是所有api中比较重要的一个api,其中有很多特殊的操作,除了可以控制标签页以外,也可以在标签页内执行js,改变css。

    无需声明任何权限就可以调用tabs中的大多出api,但是如果需要修改tab的url等属性,则需要tabs权限,除此之外,想要在tab中执行js和修改css,还需要activeTab权限才行。

    • get - chrome.tabs.get(integer tabId, function callback)
    • getCurrent - chrome.tabs.getCurrent(function callback)
    • connect - runtime.Port chrome.tabs.connect(integer tabId, object connectInfo)
    • sendRequest - chrome.tabs.sendRequest(integer tabId, any request, function responseCallback)
    • sendMessage - chrome.tabs.sendMessage(integer tabId, any message, object options, function responseCallback)
    • getSelected - chrome.tabs.getSelected(integer windowId, function callback)
    • getAllInWindow - chrome.tabs.getAllInWindow(integer windowId, function callback)
    • create - chrome.tabs.create(object createProperties, function callback)
    • duplicate - chrome.tabs.duplicate(integer tabId, function callback)
    • query - chrome.tabs.query(object queryInfo, function callback)
    • highlight - chrome.tabs.highlight(object highlightInfo, function callback)
    • update - chrome.tabs.update(integer tabId, object updateProperties, function callback)
    • move - chrome.tabs.move(integer or array of integer tabIds, object - moveProperties, function callback)
    • reload - chrome.tabs.reload(integer tabId, object reloadProperties, function callback)
    • remove - chrome.tabs.remove(integer or array of integer tabIds, function callback)
    • detectLanguage - chrome.tabs.detectLanguage(integer tabId, function callback)
    • captureVisibleTab - chrome.tabs.captureVisibleTab(integer windowId, object options, function callback)
    • executeScript - chrome.tabs.executeScript(integer tabId, object details, function callback)
    • insertCSS - chrome.tabs.insertCSS(integer tabId, object details, function callback)
    • setZoom - chrome.tabs.setZoom(integer tabId, double zoomFactor, function callback)
    • getZoom - chrome.tabs.getZoom(integer tabId, function callback)
    • setZoomSettings - chrome.tabs.setZoomSettings(integer tabId, ZoomSettings zoomSettings, function callback)
    • getZoomSettings - chrome.tabs.getZoomSettings(integer tabId, function callback)
    • discard - chrome.tabs.discard(integer tabId, function callback)
    • goForward - chrome.tabs.goForward(integer tabId, function callback)
    • goBack - chrome.tabs.goBack(integer tabId, function callback)

    一个比较简单的例子,如果获取到tab,我们可以通过update静默跳转tab。

    同样的,除了可以控制任意tab的链接以外,我们还可以新建、移动、复制,高亮标签页。

    当我们拥有activeTab权限时,我们还可以使用captureVisibleTab来截取当前页面,并转化为data数据流。

    同样我们可以用executeScript来执行js代码,这也是popup和当前页面一般沟通的主要方式。

    这里我主要整理了一些和敏感信息相关的API,对于插件的安全问题讨论也将主要围绕这些API来讨论。

    chrome 插件权限体系

    在了解基本的API之后,我们必须了解一下chrome 插件的权限体系,在跟着阅读前面相关api的部分之后,不难发现,chrome其实对自身的插件体系又非常严格的分割,但也许正是因为这样,对于插件开发者来说,可能需要申请太多的权限用于插件。

    所以为了省事,chrome还给出了第二种权限声明方式,就是基于域的权限体系。

    在权限申请中,可以申请诸如:

    • "http://*/*",
    • "https://*/*"
    • "*://*/*",
    • "http://*/",
    • "https://*/",

    这样针对具体域的权限申请方式,还支持<all_urls>直接替代所有。

    在后来的权限体系中,Chrome新增了activeTab来替代<all_urls>,在声明了activeTab之后,浏览器会赋予插件操作当前活跃选项卡的操作权限,且不会声明具体的权限要求。

    • 当没有activeTab
    • 当申请activeTab后

    当activeTab权限被声明之后,无需任何其他权限就可以执行以下操作:

    • 调用tabs.executeScript 和 tabs.insertCSS
    • 通过tabs.Tab对象获取页面的各种信息
    • 获取webRequest需要的域权限

    换言之,当插件申请到activeTab权限时,哪怕获取不到浏览器信息,也能任意操作浏览的标签页。

    更何况,对于大多数插件使用者,他们根本不关心插件申请了什么权限,所以插件开发者即便申请需要权限也不会影响使用,在这种理念下,安全问题就诞生了。

    真实世界中的数据

    经过粗略统计,现在公开在chrome商店的chrome ext超过40000,还不包括私下传播的浏览器插件。

    为了能够尽量真实的反映真实世界中的影响,这里我们随机选取1200个chrome插件,并从这部分的插件中获取一些结果。值得注意的是,下面提到的权限并不一定代表插件不安全,只是当插件获取这样的权限时,它就有能力完成不安 全的操作。

    这里我们使用Cobra-W新增的Chrome ext扫描功能对我们选取的1200个目标进行扫描分析。

    https://github.com/LoRexxar/Cobra-W

    <all-url>

    当插件获取到<all-url>或者*://*/*等类似的权限之后,插件可以操作所有打开的标签页,可以静默执行任意js、css代码。

    我们可以用以下规则来扫描:

    在我们随机挑选的1200个插件中,共585个插件申请了相关的权限。

    其中大部分插件都申请了相对范围较广的覆盖范围。

    其他

    然后我们主要扫描部分在前面提到过的敏感api权限,涉及到相关的权限的插件数量如下:

    后记

    在翻阅了chrome相关的文档之后,我们不难发现,作为浏览器中相对独立的一层,插件可以轻松的操作相对下层的会话层,同时也可以在获取一定的权限之后,读取一些更上层例如操作系统的信息...

    而且最麻烦的是,现代在使用浏览器的同时,很少会在意浏览器插件的安全性,而事实上,chrome商店也只能在一定程度上检测插件的安全性,但是却没办法完全验证,换言之,如果你安装了一个恶意插件,也没有任何人能为你的浏览器负责...安全问题也就真实的影响着各个浏览器的使用者。

    ref

    作者:吴烦恼 | Categories:安全研究 | Tags:
  • Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事儿(上)

    2019-12-06

    作者:Longofo@知道创宇404实验室
    时间:2019年11月4日

    之前看了SHIRO-721这个漏洞,然后这个漏洞和SHIRO-550有些关联,在SHIRO-550的利用方式中又看到了利用ysoserial中的JRMP exploit,然后又想起了RMI、JNDI、LDAP、JMX、JMS这些词。这些东西也看到了几次,也看过对应的文章,但把他们联想在一起时这些概念又好像交叉了一样容易混淆。网上的一些资料也比较零散与混乱,所以即使以前看过,没有放在一起看的话很容易混淆。下面是对RMI、JNDI、LDAP、JRMP、JMX、JMS一些资料的整理。

    :这篇先写了RMI、JNDI、LDAP的内容,JRMP、JMX、JMS下篇再继续。文章很长,阅读需要些耐心。

    测试环境说明

    • 文中的测试代码放到了github
    • 测试代码的JDK版本在文中会具体说明,有的代码会被重复使用,对应的JDK版本需要自己切换

    RMI

    在看下以下内容之前,可以阅读下这篇文章[1],里面包括了Java RMI相关的介绍,包括对Java RMI的简介、远程对象与非远程对象的区别、Stubs与skeletons、远程接口、UnicastRemoteObject类、RMI注册表、RMI动态加载等内容。

    Java RMI

    远程方法调用是分布式编程中的一个基本思想。实现远程方法调用的技术有很多,例如CORBA、WebService,这两种是独立于编程语言的。而Java RMI是专为Java环境设计的远程方法调用机制,远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法并获取执行结果,使分布在不同的JVM中的对象的外表和行为都像本地对象一样。

    这篇文章[2]中,作者举了一个例子来描述RMI:

    假设A公司是某个行业的翘楚,开发了一系列行业上领先的软件。B公司想利用A公司的行业优势进行一些数据上的交换和处理。但A公司不可能把其全部软件都部署到B公司,也不能给B公司全部数据的访问权限。于是A公司在现有的软件结构体系不变的前提下开发了一些RMI方法。B公司调用A公司的RMI方法来实现对A公司数据的访问和操作,而所有数据和权限都在A公司的控制范围内,不用担心B公司窃取其数据或者商业机密。

    对于开发者来说,远程方法调用就像我们本地调用一个对象的方法一样,他们很多时候不需要关心内部如何实现,只关心传递相应的参数并获取结果就行了。但是对于攻击者来说,要执行攻击还是需要了解一些细节的。

    :这里我在RMI前面加上了Java是为了和Weblogic RMI区分。Java本身对RMI规范的实现默认使用的是JRMP协议,而Weblogic对RMI规范的实现使用T3协议,Weblogic之所以开发T3协议,是因为他们需要可扩展,高效的协议来使用Java构建企业级的分布式对象系统。

    JRMP:Java Remote Message Protocol ,Java 远程消息交换协议。这是运行在Java RMI之下、TCP/IP之上的线路层协议。该协议要求服务端与客户端都为Java编写,就像HTTP协议一样,规定了客户端和服务端通信要满足的规范。

    Java RMI远程方法调用过程

    几个tips

    1. RMI的传输是基于反序列化的。
    2. 对于任何一个以对象为参数的RMI接口,你都可以发一个自己构建的对象,迫使服务器端将这个对象按任何一个存在于服务端classpath(不在classpath的情况,可以看后面RMI动态加载类相关部分)中的可序列化类来反序列化恢复对象。

    使用远程方法调用,会涉及参数的传递和执行结果的返回。参数或者返回值可以是基本数据类型,当然也有可能是对象的引用。所以这些需要被传输的对象必须可以被序列化,这要求相应的类必须实现 java.io.Serializable 接口,并且客户端的serialVersionUID字段要与服务器端保持一致。

    在JVM之间通信时,RMI对远程对象和非远程对象的处理方式是不一样的,它并没有直接把远程对象复制一份传递给客户端,而是传递了一个远程对象的Stub,Stub基本上相当于是远程对象的引用或者代理(Java RMI使用到了代理模式)。Stub对开发者是透明的,客户端可以像调用本地方法一样直接通过它来调用远程方法。Stub中包含了远程对象的定位信息,如Socket端口、服务端主机地址等等,并实现了远程调用过程中具体的底层网络通信细节,所以RMI远程调用逻辑是这样的:

    从逻辑上来说,数据是在Client和Server之间横向流动的,但是实际上是从Client到Stub,然后从Skeleton到Server这样纵向流动的:

    1. Server端监听一个端口,这个端口是JVM随机选择的;
    2. Client端并不知道Server远程对象的通信地址和端口,但是Stub中包含了这些信息,并封装了底层网络操作;
    3. Client端可以调用Stub上的方法;
    4. Stub连接到Server端监听的通信端口并提交参数;
    5. 远程Server端上执行具体的方法,并返回结果给Stub;
    6. Stub返回执行结果给Client端,从Client看来就好像是Stub在本地执行了这个方法一样;

    怎么获取Stub呢?

    假设Stub可以通过调用某个远程服务上的方法向远程服务来获取,但是调用远程方法又必须先有远程对象的Stub,所以这里有个死循环问题。JDK提供了一个RMI注册表(RMIRegistry)来解决这个问题。RMIRegistry也是一个远程对象,默认监听在传说中的1099端口上,可以使用代码启动RMIRegistry,也可以使用rmiregistry命令。

    使用RMI Registry之后,RMI的调用关系应该是这样的:

    所以从客户端角度看,服务端应用是有两个端口的,一个是RMI Registry端口(默认为1099),另一个是远程对象的通信端口(随机分配的),通常我们只需要知道Registry的端口就行了,Server的端口包含在了Stub中。RMI Registry可以和Server端在一台服务器上,也可以在另一台服务器上,不过大多数时候在同一台服务器上且运行在同一JVM环境下。

    模拟Java RMI利用

    我们使用下面的例子来模拟Java RMI的调用过程并执行攻击:

    1.创建服务端对象类,先创建一个接口继承java.rmi.Remote

    2.创建服务端对象类,实现这个接口

    3.创建服务端远程对象骨架skeleton并绑定在Registry上

    4.创建恶意客户端

    上面这个例子是在CVE-2017-3241分析[3]中提供代码基础上做了一些修改,完整的测试代码已经放到github上了,先启动RMI Server端java-rmi-server/src/main/java/com/longofo/javarmi/RMIServer,在启动RMI客户端java-rmi-client/src/main/java/com/longofo/javarmi/RMIClient就可以复现,在JDK 1.6.0_29测试通过。

    在ysoserial中的RMIRegistryExploit提供另一种思路,利用其他客户端也能向服务端的Registry注册远程对象的功能,由于对象绑定时也传递了序列化的数据,在Registry端(通常和服务端在同一服务器且处于同一JVM下)会对数据进行反序列化处理,RMIRegistryExploit中使用的CommonsCollections1这个payload,如果Registry端也存在CommonsCollections1这个payload使用到的类就能恶意利用。对于一些CommonsCollections1利用不了的情况,例如CommonsCollections1中相关利用类被过滤拦截了,也还有其他例如结合JRMP方式进行利用的方法,可以参考下这位作者的思路

    这里还需要注意这时Server端是作为RMI的服务端而成为受害者,在后面的RMI动态类加载或JNDI注入中可以看到Server端也可以作为RMI客户端成为受害者

    上面的代码假设RMIServer就是提供Java RMI远程方法调用服务的厂商,他提供了一个Services接口供远程调用;

    在客户端中,正常调用应该是stub.sendMessage(Message),这个参数应该是Message类对象的,但是我们知道服务端存在一个公共的已知PublicKnown类(比如经典的Apache Common Collection,这里只是用PublicKnown做一个类比),它有readObject方法并且在readObject中存在命令执行的能力,所以我们客户端可以写一个与服务端包名,类名相同的类并继承Message类(Message类在客户端服务端都有的),根据上面两个Tips,在服务端会反序列化传递的数据,然后到达PublicKnown执行命令的地方(这里需要注意的是服务端PublicKnown类的serialVersionUID与客户端的PublicKnown需要保持一致,如果不写在序列化时JVM会自动根据类的属性等生成一个UID,不过有时候自动生成的可能会不一致,不过不一致时,Java RMI服务端会返回错误,提示服务端相应类的serialVersionUID,在本地类重新加上服务端的serialVersionUID就行了):

    上面这个错误也是从服务端发送过来的,不过不要紧,命令在出现错误之前就执行了。

    来看下调用栈,我们在服务端的PublicKnown类中readObject下个断点,

    sun.rmi.server.UnicastRef开始调用了readObject,然后一直到调用PublicKnown类的readObject

    抓包看下通信的数据:

    可以看到PublicKnown类对象确实被序列化传递了,通信过程全程都有被序列化的数据,那么在服务端也肯定会会进行反序列化恢复对象,可以自己抓包看下。

    Java RMI的动态加载类

    java.rmi.server.codebasejava.rmi.server.codebase属性值表示一个或多个URL位置,可以从中下载本地找不到的类,相当于一个代码库。代码库定义为将类加载到虚拟机的源或场所,可以将CLASSPATH视为“本地代码库”,因为它是磁盘上加载本地类的位置的列表。就像CLASSPATH"本地代码库"一样,小程序和远程对象使用的代码库可以被视为"远程代码库"。

    RMI核心特点之一就是动态类加载,如果当前JVM中没有某个类的定义,它可以从远程URL去下载这个类的class,动态加载的class文件可以使用http://、ftp://、file://进行托管。这可以动态的扩展远程应用的功能,RMI注册表上可以动态的加载绑定多个RMI应用。对于客户端而言,如果服务端方法的返回值可能是一些子类的对象实例,而客户端并没有这些子类的class文件,如果需要客户端正确调用这些子类中被重写的方法,客户端就需要从服务端提供的java.rmi.server.codebaseURL去加载类;对于服务端而言,如果客户端传递的方法参数是远程对象接口方法参数类型的子类,那么服务端需要从客户端提供的java.rmi.server.codebaseURL去加载对应的类。客户端与服务端两边的java.rmi.server.codebaseURL都是互相传递的。无论是客户端还是服务端要远程加载类,都需要满足以下条件:

    1. 由于Java SecurityManager的限制,默认是不允许远程加载的,如果需要进行远程加载类,需要安装RMISecurityManager并且配置java.security.policy,这在后面的利用中可以看到。
    2. 属性 java.rmi.server.useCodebaseOnly 的值必需为false。但是从JDK 6u45、7u21开始,java.rmi.server.useCodebaseOnly 的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前虚拟机的java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止虚拟机从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。

    :在JNDI注入的利用方法中也借助了这种动态加载类的思路。

    远程方法返回对象为远程接口方法返回对象的子类(目标Server端为RMI客户端时的恶意利用)

    远程对象象接口(这个接口一般都是公开的):

    恶意的远程对象类的实现:

    恶意的RMI服务端:

    RMI客户端:

    这样就模拟出了一种攻击场景,这时受害者是作为RMI客户端的,需要满足以下条件才能利用:

    1. 可以控制客户端去连接我们的恶意服务端
    2. 客户端允许远程加载类
    3. 还有上面的说的JDK版本限制

    可以看到利用条件很苛刻,如果真的满足了以上条件,那么就可以模拟一个恶意的RMI服务端进行攻击。完整代码在github上,先启动remote-class/src/main/java/com/longofo/remoteclass/HttpServer,接着启动java-rmi-server/src/main/java/com/longofo/javarmi/RMIServer1.java,再启动java-rmi-client/src/main/java/com/longofo/javarmi/RMIClient1.java即可复现,在JDK 1.6.0_29测试通过。

    远程方法参数对象为远程接口方法参数对象的子类(目标Server端需要为RMI Server端才能利用)

    刚开始讲Java RMI的时候,我们模拟了一种攻击,那种情况和这种情况是类似的,上面那种情况是利用加载本地类,而这里的是加载远程类。

    RMI服务端:

    远程对象接口:

    恶意远程方法参数对象子类:

    恶意RMI客户端:

    这样就模拟出了另一种攻击场景,这时受害者是作为RMI服务端,需要满足以下条件才能利用:

    1. RMI服务端允许远程加载类
    2. 还有JDK限制

    利用条件也很苛刻,如果真的满足了以上条件,那么就可以模拟一个恶意的RMI客户端进行攻击。完整代码在github上,先启动remote-class/src/main/java/com/longofo/remoteclass/HttpServer,接着启动java-rmi-server/src/main/java/com/longofo/javarmi/RMIServer2.java,再启动java-rmi-client/src/main/java/com/longofo/javarmi/RMIClient2.java即可复现,在JDK 1.6.0_29测试通过。

    Weblogic RMI

    Weblogic RMI与Java RMI的区别

    为什么要把Weblogic RMI写这里呢?因为通过Weblogic RMI作为反序列化入口导致的漏洞很多,常常听见的通过Weblogic T3协议进行反序列化...一开始也没去了详细了解过Weblogic RMI和Weblogic T3协议有什么关系,也是直接拿着python weblogic那个T3脚本直接打。然后搜索的资料大多也都是讲的上面的Java RMI,用的JRMP协议传输,没有区分过Java RMI和Weblogic RMI有什么区别,T3和JRMP又是是什么,很容易让人迷惑。

    这篇文中[5]我们可以了解到,WebLogic RMI是服务器框架的组成部分。它使Java客户端可以透明地访问WebLogic Server上的RMI对象,这包括访问任何已部署到WebLogic的EJB组件和其他J2EE资源,它可以构建快速、可靠、符合标准的RMI应用程序。当RMI对象部署到WebLogic群集时,它还集成了对负载平衡和故障转移的支持。WebLogic RMI与Java RMI规范完全兼容,上面提到的动态加载加载功能也是具有的,同时还提供了在标准Java RMI实现下更多的功能与扩展。下面简要概述了使用WebLogic版本的RMI的一些其他好处:

    1.性能和可扩展性

    WebLogic包含了高度优化的RMI实现。它处理与RMI支持有关的所有实现问题:管理线程和套接字、垃圾回收和序列化。标准RMI依赖于客户端与服务器之间以及客户端与RMI注册表之间的单独套接字连接。WebLogic RMI将所有这些网络流量多路复用到客户端和服务器之间的单个套接字连接上(这里指的就是T3协议吧)。相同的套接字连接也可重用于其他类型的J2EE交互,例如JDBC请求和JMS连接。通过最小化客户端和WebLogic之间的网络连接,RMI实现可以在负载下很好地扩展,并同时支持大量RMI客户端,它还依赖于高性能的序列化逻辑。

    此外,当客户端在与RMI对象相同的VM中运行时,WebLogic会自动优化客户端与服务器之间的交互。它确保您不会因调用远程方法期间对参数进行编组或取消编组而导致任何性能损失。相反,当客户端和服务器对象并置时,并且在类加载器层次结构允许时,WebLogic使用Java的按引用传递语义。

    2.客户端之间的沟通

    WebLogic的RMI提供了客户端和服务器之间的异步双向套接字连接。 RMI客户端可以调用由服务器端提供的RMI对象以及通过WebLogic的RMI Registry注册了远程接口的其他客户端的RMI对象公开的方法。因此,客户端应用程序可以通过服务器注册表发布RMI对象,而其他客户端或服务器可以使用这些客户端驻留的对象,就像它们将使用任何服务器驻留的对象一样。这样,您可以创建涉及RMI客户端之间对等双向通信的应用程序。

    3.RMI注册中心

    只要启动WebLogic,RMI注册表就会自动运行。WebLogic会忽略创建RMI注册表的多个实例的尝试,仅返回对现有注册表的引用。

    WebLogic的RMI注册表与JNDI框架完全集成。可以使用JNDI或RMI注册表(可以看到上面Java RMI我使用了Registry,后面Weblogic RMI中我使用的是JNDI方式,两种方式对RMI服务都是可以的)来绑定或查找服务器端RMI对象。实际上,RMI注册中心只是WebLogic的JNDI树之上的一小部分。我们建议您直接使用JNDI API来注册和命名RMI对象,而完全绕过对RMI注册表的调用。JNDI提供了通过其他企业命名和目录服务(例如LDAP)发布RMI对象的前景。

    4.隧道式

    RMI客户端可以使用基于多种方案的URL:标准 rmi://方案,或分别通过HTTP和IIOP隧道传输RMI请求的 http://和iiop://方案。这使来自客户端的RMI调用可以穿透大多数防火墙。

    5.动态生成存根和骨架

    WebLogic支持动态生成客户端存根和服务器端框架,从而无需为RMI对象生成客户端存根和服务器端框架。将对象部署到RMI注册表或JNDI时,WebLogic将自动生成必要的存根和框架。唯一需要显式创建存根的时间是可集群客户端或IIOP客户端需要访问服务器端RMI对象时。

    T3传输协议是WebLogic的自有协议,Weblogic RMI就是通过T3协议传输的(可以理解为序列化的数据载体是T3),它有如下特点:

    1. 服务端可以持续追踪监控客户端是否存活(心跳机制),通常心跳的间隔为60秒,服务端在超过240秒未收到心跳即判定与客户端的连接丢失。
    2. 通过建立一次连接可以将全部数据包传输完成,优化了数据包大小和网络消耗。

    Weblogic T3协议和http以及其他几个协议的端口是共用的:

    Weblogic会检测请求为哪种协议,然后路由到正确的位置。

    查看Weblogic默认注册的远程对象

    Weblogic服务已经注册了一些远程对象,写一个测试下(参考了这篇文章[5]中的部分代码,代码放到github了,运行weblogic-rmi-client/src/main/java/com/longofo/weblogicrmi/Client即可,注意修改其中IP和Port),在JDK 1.6.0_29测试通过:

    结果如下:

    在Weblogic控制台,我们可以通过JNDI树看到上面这些远程对象:

    注:下面这一段可能省略了一些过程,我也不知道具体该怎么描述,所以会不知道我说的啥,可以跳过,只是一个失败的测试

    在客户端的RemoteWrapper中,我还写了一个readExternal接口方法,远程对象的RemoteWrapper接口类是没有这个方法的。但是weblogic.jndi.internal.WLContextImpl这个实现类中有,那么如果在本地接口类中加上readExternal方法去调用会怎么样呢?由于过程有点繁杂,很多坑,做了很多代码替换与测试,我也不知道该怎么具体描述,只简单说下:

    1.直接用T3脚本测试

    使用JtaTransactionManager这条利用链,用T3协议攻击方式在未打补丁的Weblogic测试成功,打上补丁的Weblogic测试失败,在打了补丁的Weblogic上JtaTransactionManager的父类AbstractPlatformTransactionManager在黑名单中,Weblogic黑名单在weblogic.utils.io.oif.WebLogicFilterConfig中。

    2.那么根据前面Java RMI那种恶意利用方式能行吗,两者只是传输协议不一样,利用过程应该是类似的,试下正常调用readExternal方式去利用行不行?

    这个测试过程实在不知道该怎么描述,测试结果也失败了,如果调用的方法在远程对象的接口上也有,例如上面代码中的remoteWrapper.getRemoteDelegate(),经过抓包搜索"getRemoteDelegate"发现了有bind关键字,调用结果也是在服务端执行的。但是如果调用了远程接口不存在的方法,比如remoteWrapper.readExternal(),在流量中会看到"readExternal"有unbind关键字,这时就不是服务端去处理结果了,而是在本地对应类的方法进行调用(比如你本地存在weblogic.jndi.internal.WLContextImpl类,会调用这个类的readExternal方法去处理),如果本地没有相应的类就会报错。当时我是用的JtaTransactionManager这条利用链,我本地也有这个类...所以我在我本地看到了计算器弹出来了,要不是使用的虚拟机上的Weblogic进行测试,我自己都信了,自己造了个洞。(说明:readExternal的参数ObjectOutput类也是不可序列化的,当时自己也没想那么多...后面在Weblogic上部署了一个远程对象,参数我设置的是ObjectInputStream类,调用时才发现不可序列化错误,虽然之前也说过RMI传输是基于序列化的,那么传输的对象必须可序列化,但是写着就忘记了)

    想想自己真的很天真,要是远程对象的接口没有提供的方法都能被你调用了,那不成了RMI本身的漏洞吗。并且这个过程和直接用T3脚本是类似的,都会经过Weblogic的ObjectInputFilter过滤黑名单中的类,就算能成功调用readExternal,JtaTransactionManager这条利用链也会被拦截到。

    上面说到的Weblogic部署的远程对象的例子根据这篇文章[2]做了一些修改,代码在github上了,将weblogic-rmi-server/src/main/java/com/longofo/weblogicrmi/HelloImpl打包为Jar包部署到Weblogic,然后运行weblogic-rmi-client/src/main/java/com/longofo/weblogicrmi/Client1即可,注意修改其中的IP和Port,在JDK 1.6.0_29测试通过。

    正常Weblogic RMI调用与模拟T3协议进行恶意利用

    之前都是模拟T3协议的方式进行恶意利用,来看下不使用T3脚本攻击的方式(找一个远程对象的有参数的方法,我使用的是weblogic.management.j2ee.mejb.Mejb_dj5nps_HomeImpl_1036_WLStub#remove(Object obj)方法),它对应的命名为ejb/mgmt/MEJB,其中一个远程接口为javax.ejb.EJBHome,测试代码放到github上了,先使用ldap/src/main/java/LDAPRefServer启动一个ldap服务,然后运行weblogic-rmi-client/src/main/java/com/longofo/weblogicrmi/Payload1即可复现,注意修改Ip和Port。

    在没有过滤AbstractPlatformTransactionManager类的版本上,使用JtaTransactionManager这条利用链测试,

    在过滤了AbstractPlatformTransactionManager类的版本上使用JtaTransactionManager这条利用链测试,

    可以看到通过正常的调用RMI方式也能触发,不过相比直接用T3替换传输过程中的反序列化数据,这种方式利用起来就复杂一些了,关于T3模拟的过程,可以看下这篇文章[2]。Java RMI默认使用的JRMP传输,那么JRMP也应该和T3协议一样可以模拟来简化利用过程吧。

    小结

    从上面我们可以了解到以下几点:

    1. RMI标准实现是Java RMI,其他实现还有Weblogic RMI、Spring RMI等。
    2. RMI的调用是基于序列化的,一个对象远程传输需要序列化,需要使用到这个对象就需要从序列化的数据中恢复这个对象,恢复这个对象时对应的readObject、readExternal等方法会被自动调用。
    3. RMI可以利用服务器本地反序列化利用链进行攻击。
    4. RMI具有动态加载类的能力以及能利用这种能力进行恶意利用。这种利用方式是在本地不存在可用的利用链或者可用的利用链中某些类被过滤了导致无法利用时可以使用,不过利用条件有些苛刻。
    5. 讲了Weblogic RMI和Java RMI的区别,以及Java RMI默认使用的专有传输协议(或者也可以叫做默认协议)是JRMP,Weblogic RMI默认使用的传输协议是T3。
    6. Weblogic RMI正常调用触发反序列化以及模拟T3协议触发反序列化都可以,但是模拟T3协议传输简化了很多过程。

    Weblogic RMI反序列化漏洞起源是CVE-2015-4852,这是@breenmachine最开始发现的,在他的这篇分享中[7],不仅讲到了Weblogic的反序列化漏洞的发现,还有WebSphere、JBoss、Jenkins、OpenNMS反序列化漏洞的发现过程以及如何开发利用程序,如果之前没有看过这篇文章,可以耐心的读一下,可以看到作者是如何快速确认是否存在易受攻击的库,如何从流量中寻找反序列化特征,如何去触发这些流量。

    我们可以看到作者发现这几个漏洞的过程都有相似性:首先判断了是否存在易受攻击的库/易受攻击的特征->搜集端口信息->针对性的触发流量->在流量中寻找反序列化特征->开发利用程序。不过这是建立在作者对这些Web应用或中间件的整体有一定的了解。

    JNDI

    JNDI (Java Naming and Directory Interface) ,包括Naming Service和Directory Service。JNDI是Java API,允许客户端通过名称发现和查找数据、对象。这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),公共对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)。

    Naming Service:命名服务是将名称与值相关联的实体,称为"绑定"。它提供了一种使用"find"或"search"操作来根据名称查找对象的便捷方式。 就像DNS一样,通过命名服务器提供服务,大部分的J2EE服务器都含有命名服务器 。例如上面说到的RMI Registry就是使用的Naming Service。

    Directory Service:是一种特殊的Naming Service,它允许存储和搜索"目录对象",一个目录对象不同于一个通用对象,目录对象可以与属性关联,因此,目录服务提供了对象属性进行操作功能的扩展。一个目录是由相关联的目录对象组成的系统,一个目录类似于数据库,不过它们通常以类似树的分层结构进行组织。可以简单理解成它是一种简化的RDBMS系统,通过目录具有的属性保存一些简单的信息。下面说到的LDAP就是目录服务。

    几个重要的JNDI概念

    • 原子名是一个简单、基本、不可分割的组成部分
    • 绑定是名称与对象的关联,每个绑定都有一个不同的原子名
    • 复合名包含零个或多个原子名,即由多个绑定组成
    • 上下文是包含零个或多个绑定的对象,每个绑定都有一个不同的原子名
    • 命名系统是一组关联的上下文
    • 名称空间是命名系统中包含的所有名称
    • 探索名称空间的起点称为初始上下文
    • 要获取初始上下文,需要使用初始上下文工厂

    使用JNDI的好处

    JNDI自身并不区分客户端和服务器端,也不具备远程能力,但是被其协同的一些其他应用一般都具备远程能力,JNDI在客户端和服务器端都能够进行一些工作,客户端上主要是进行各种访问,查询,搜索,而服务器端主要进行的是帮助管理配置,也就是各种bind。比如在RMI服务器端上可以不直接使用Registry进行bind,而使用JNDI统一管理,当然JNDI底层应该还是调用的Registry的bind,但好处JNDI提供的是统一的配置接口;在客户端也可以直接通过类似URL的形式来访问目标服务,可以看后面提到的JNDI动态协议转换。把RMI换成其他的例如LDAP、CORBA等也是同样的道理。

    几个简单的JNDI示例

    JNDI与RMI配合使用:

    JNDI与LDAP配合使用:

    JNDI动态协议转换

    上面的两个例子都手动设置了对应服务的工厂以及对应服务的PROVIDER_URL,但是JNDI是能够进行动态协议转换的。

    例如:

    上面没有设置对应服务的工厂以及PROVIDER_URL,JNDI根据传递的URL协议自动转换与设置了对应的工厂与PROVIDER_URL。

    再如下面的:

    即使服务端提前设置了工厂与PROVIDER_URL也不要紧,如果在lookup时参数能够被攻击者控制,同样会根据攻击者提供的URL进行动态转换。

    在使用lookup方法时,会进入getURLOrDefaultInitCtx这个方法,转换就在这里面: