RSS Feed
更好更安全的互联网
  • 从 0 开始入门 Chrome Ext 安全(番外篇) — ZoomEye Tools

    2020-07-31

    作者:LoRexxar@知道创宇404实验室
    时间:2020年01月17日
    英文版本: https://paper.seebug.org/1116/
    系列文章:
    1.《从 0 开始入门 Chrome Ext 安全(一) -- 了解一个 Chrome Ext》
    2.《从 0 开始入门 Chrome Ext 安全(二) -- 安全的 Chrome Ext》

    在经历了两次对Chrome Ext安全的深入研究之后,这期我们先把Chrome插件安全的问题放下来,这期我们讲一个关于Chrome Ext的番外篇 -- Zoomeye Tools.

    链接为:https://chrome.google.com/webstore/detail/zoomeyetools/bdoaeiibkccgkbjbmmmoemghacnkbklj

    这篇文章让我们换一个角度,从开发一个插件开始,如何去审视chrome不同层级之间的问题。

    这里我们主要的目的是完成一个ZoomEye的辅助插件。

    核心与功能设计

    在ZoomEye Tools中,我们主要加入了一下针对ZoomEye的辅助性功能,在设计ZoomEye Tools之前,首先我们需要思考我们需要什么样的功能。

    这里我们需要需要实现的是两个大功能,

    1、首先需要完成一个简易版本的ZoomEye界面,用于显示当前域对应ip的搜索结果。
    2、我们会完成一些ZoomEye的辅助小功能,比如说一键复制搜索结果的左右ip等...

    这里我们分别研究这两个功能所需要的部分:

    ZoomEye minitools

    关于ZoomEye的一些辅助小功能,这里我们首先拿一个需求来举例子,我们需要一个能够复制ZoomEye页面内所有ip的功能,能便于方便的写脚本或者复制出来使用。

    在开始之前,我们首先得明确chrome插件中不同层级之间的权限体系和通信方式:

    在第一篇文章中我曾着重讲过这部分内容。

    我们需要完成的这个功能,可以简单量化为下面的流程:

    当然这是人类的思维,结合chrome插件的权限体系和通信方式,我们需要把每一部分拆解为相应的解决方案。

    • 用户点击浏览器插件的功能

    当用户点击浏览器插件的图标时,将会展示popup.html中的功能,并执行页面中相应加的js代码。

    • 浏览器插件读取当前ZoomEye页面的内容

    由于popup script没有权限读取页面内容,所以这里我们必须通过chrome.tabs.sendMessage来沟通content script,通过content script来读取页面内容。

    • 解析其中内容并提取其中的内容并按照格式写入剪切板中

    在content script读取到页面内容之后,需要通过sendResponse反馈数据。

    当popup收到数据之后,我们需要通过特殊的技巧把数据写入剪切板

    这里我们是通过新建了textarea标签并选中其内容,然后触发copy指令来完成。

    整体流程大致如下

    ZoomEye preview

    与minitools的功能不同,要完成ZoomEye preview首先我们遇到的第一个问题是ZoomEye本身的鉴权体系。

    在ZoomEye的设计中,大部分的搜索结果都需要登录之后使用,而且其相应的多种请求api都是通过jwt来做验证。

    而这个jwt token会在登陆期间内储存在浏览器的local storage中。

    我们可以简单的把架构画成这个样子

    在继续设计代码逻辑之前,我们首先必须确定逻辑流程,我们仍然把流程量化为下面的步骤:

    紧接着我们配合chrome插件体系的逻辑,把前面步骤转化为程序逻辑流程。

    • 用户点击ZoomEye tools插件

    插件将会加载popup.html页面并执行相应的js代码。

    • 插件检查数据之后确认未登录,返回需要登录

    插件将获取储存在chrome.storage的Zoomeye token,然后请求ZoomEye.org/user判断登录凭据是否有效。如果无效,则会在popup.html显示need login。并隐藏其他的div窗口。

    • 用户点击按钮跳转登录界面登录

    当用户点击按钮之后,浏览器会直接打开 https://sso.telnet404.com/cas/login?service=https%3A%2F%2Fwww.zoomeye.org%2Flogin

    如果浏览器当前在登录状态时,则会跳转回ZoomEye并将相应的数据写到localStorage里。

    • 插件获取凭证之后储存

    由于前后端的操作分离,所有bg script需要一个明显的标志来提示需要获取浏览器前端的登录凭证,我把这个标识为定为了当tab变化时,域属于ZoomEye.org且未登录时,这时候bg script会使用chrome.tabs.executeScript来使前端完成获取localStorage并储存进chrome.storage.

    这样一来,插件就拿到了最关键的jwt token

    • 用户打开网站之后点击插件

    在完成了登录问题之后,用户就可以正常使用preview功能了。

    当用户打开网站之后,为了减少数据加载的等待时间,bg script会直接开始获取数据。

    • 插件通过凭据以及请求的host来获取ZoomEye数据

    后端bg script 通过判断tab状态变化,来启发获取数据的事件,插件会通过前面获得的账号凭据来请求

    https://www.zoomeye.org/searchDetail?type=host&title= 并解析json,来获取相应的ip数据。

    • 将部分数据反馈到页面中

    当用户点击插件时,popup script会检查当前tab的url和后端全局变量中的数据是否一致,然后通过

    来获取到bg的全局变量。然后将数据写入页面中。

    整个流程的架构如下:

    完成插件

    在完成架构设计之后,我们只要遵守好插件不同层级之间的各种权限体系,就可以完成基础的设计,配合我们的功能,我们生成的manifest.json如下

    上传插件到chrome store

    在chrome的某一个版本之后,chrome就不再允许自签名的插件安装了,如果想要在chrome上安装,那就必须花费5美金注册为chrome插件开发者。

    并且对于chrome来说,他有一套自己的安全体系,如果你得插件作用于多个域名下,那么他会在审核插件之前加入额外的审核,如果想要快速提交自己的插件,那么你就必须遵守chrome的规则。

    你可以在chrome的开发者信息中心完成这些。

    Zoomeye Tools 使用全解

    安装

    chromium系的所有浏览器都可以直接下载

    初次安装完成时应该为

    使用方法

    由于Zoomeye Tools提供了两个功能,一个是Zoomeye辅助工具,一个是Zoomeye preview.

    zoomeye 辅助工具

    首先第一个功能是配合Zoomeye的,只会在Zoomeye域下生效,这个功能不需要登录zoomeye。

    当我们打开Zoomeye之后搜索任意banner,等待页面加载完成后,再点击右上角的插件图标,就能看到多出来的两条选项。

    如果我们选择copy all ip with LF,那么剪切板就是

    如果我们选择copy all url with port

    Zoomeye Preview

    第二个功能是一个简易版本的Zoomeye,这个功能需要登录Zoomeye。

    在任意域我们点击右上角的Login Zoomeye,如果你之前登陆过Zoomeye那么会直接自动登录,如果没有登录,则需要在telnet404页面登录。登录完成后等待一会儿就可以加载完成。

    在访问网页时,点击右上角的插件图标,我们就能看到相关ip的信息以及开放端口

    写在最后

    最后我们上传chrome开发者中心之后只要等待审核通过就可以发布出去了。

    最终chrome插件下载链接:


    Paper

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

    作者:叶霖 杨 | Categories:安全研究 | Tags:
  • Hessian 反序列化及相关利用链

    2020-07-31

    作者:Longofo@知道创宇404实验室
    时间:2020年2月20日
    英文版本:https://paper.seebug.org/1137/

    前不久有一个关于Apache Dubbo Http反序列化的漏洞,本来是一个正常功能(通过正常调用抓包即可验证确实是正常功能而不是非预期的Post),通过Post传输序列化数据进行远程调用,但是如果Post传递恶意的序列化数据就能进行恶意利用。Apache Dubbo还支持很多协议,例如Dubbo(Dubbo Hessian2)、Hessian(包括Hessian与Hessian2,这里的Hessian2与Dubbo Hessian2不是同一个)、Rmi、Http等。Apache Dubbo是远程调用框架,既然Http方式的远程调用传输了序列化的数据,那么其他协议也可能存在类似问题,例如Rmi、Hessian等。@pyn3rd师傅之前在twiter发了关于Apache Dubbo Hessian协议的反序列化利用,Apache Dubbo Hessian反序列化问题之前也被提到过,这篇文章里面讲到了Apache Dubbo Hessian存在反序列化被利用的问题,类似的还有Apache Dubbo Rmi反序列化问题。之前也没比较完整的去分析过一个反序列化组件处理流程,刚好趁这个机会看看Hessian序列化、反序列化过程,以及marshalsec工具中对于Hessian的几条利用链。

    关于序列化/反序列化机制

    序列化/反序列化机制(或者可以叫编组/解组机制,编组/解组比序列化/反序列化含义要广),参考marshalsec.pdf,可以将序列化/反序列化机制分大体分为两类:

    1. 基于Bean属性访问机制
    2. 基于Field机制
    基于Bean属性访问机制
    • SnakeYAML
    • jYAML
    • YamlBeans
    • Apache Flex BlazeDS
    • Red5 IO AMF
    • Jackson
    • Castor
    • Java XMLDecoder
    • ...

    它们最基本的区别是如何在对象上设置属性值,它们有共同点,也有自己独有的不同处理方式。有的通过反射自动调用getter(xxx)setter(xxx)访问对象属性,有的还需要调用默认Constructor,有的处理器(指的上面列出来的那些)在反序列化对象时,如果类对象的某些方法还满足自己设定的某些要求,也会被自动调用。还有XMLDecoder这种能调用对象任意方法的处理器。有的处理器在支持多态特性时,例如某个对象的某个属性是Object、Interface、abstruct等类型,为了在反序列化时能完整恢复,需要写入具体的类型信息,这时候可以指定更多的类,在反序列化时也会自动调用具体类对象的某些方法来设置这些对象的属性值。这种机制的攻击面比基于Field机制的攻击面大,因为它们自动调用的方法以及在支持多态特性时自动调用方法比基于Field机制要多。

    基于Field机制

    基于Field机制是通过特殊的native(native方法不是java代码实现的,所以不会像Bean机制那样调用getter、setter等更多的java方法)方法或反射(最后也是使用了native方式)直接对Field进行赋值操作的机制,不是通过getter、setter方式对属性赋值(下面某些处理器如果进行了特殊指定或配置也可支持Bean机制方式)。在ysoserial中的payload是基于原生Java Serialization,marshalsec支持多种,包括上面列出的和下面列出的。

    • Java Serialization
    • Kryo
    • Hessian
    • json-io
    • XStream
    • ...

    就对象进行的方法调用而言,基于字段的机制通常通常不构成攻击面。另外,许多集合、Map等类型无法使用它们运行时表示形式进行传输/存储(例如Map,在运行时存储是通过计算了对象的hashcode等信息,但是存储时是没有保存这些信息的),这意味着所有基于字段的编组器都会为某些类型捆绑定制转换器(例如Hessian中有专门的MapSerializer转换器)。这些转换器或其各自的目标类型通常必须调用攻击者提供的对象上的方法,例如Hessian中如果是反序列化map类型,会调用MapDeserializer处理map,期间map的put方法被调用,map的put方法又会计算被恢复对象的hash造成hashcode调用(这里对hashcode方法的调用就是前面说的必须调用攻击者提供的对象上的方法),根据实际情况,可能hashcode方法中还会触发后续的其他方法调用。

    Hessian简介

    Hessian是二进制的web service协议,官方对Java、Flash/Flex、Python、C++、.NET C#等多种语言都进行了实现。Hessian和Axis、XFire都能实现web service方式的远程方法调用,区别是Hessian是二进制协议,Axis、XFire则是SOAP协议,所以从性能上说Hessian远优于后两者,并且Hessian的JAVA使用方法非常简单。它使用Java语言接口定义了远程对象,集合了序列化/反序列化和RMI功能。本文主要讲解Hessian的序列化/反序列化。

    下面做个简单测试下Hessian Serialization与Java Serialization:

    结果如下:

    通过这个测试可以简单看出Hessian反序列化占用的空间比JDK反序列化结果小,Hessian序列化时间比JDK序列化耗时长,但Hessian反序列化很快。并且两者都是基于Field机制,没有调用getter、setter方法,同时反序列化时构造方法也没有被调用。

    Hessian概念图

    下面的是网络上对Hessian分析时常用的概念图,在新版中是整体也是这些结构,就直接拿来用了:

    • Serializer:序列化的接口
    • Deserializer :反序列化的接口
    • AbstractHessianInput :hessian自定义的输入流,提供对应的read各种类型的方法
    • AbstractHessianOutput :hessian自定义的输出流,提供对应的write各种类型的方法
    • AbstractSerializerFactory
    • SerializerFactory :Hessian序列化工厂的标准实现
    • ExtSerializerFactory:可以设置自定义的序列化机制,通过该Factory可以进行扩展
    • BeanSerializerFactory:对SerializerFactory的默认object的序列化机制进行强制指定,指定为使用BeanSerializer对object进行处理

    Hessian Serializer/Derializer默认情况下实现了以下序列化/反序列化器,用户也可通过接口/抽象类自定义序列化/反序列化器:

    序列化时会根据对象、属性不同类型选择对应的序列化其进行序列化;反序列化时也会根据对象、属性不同类型选择不同的反序列化器;每个类型序列化器中还有具体的FieldSerializer。这里注意下JavaSerializer/JavaDeserializer与BeanSerializer/BeanDeserializer,它们不是类型序列化/反序列化器,而是属于机制序列化/反序列化器:

    1. JavaSerializer:通过反射获取所有bean的属性进行序列化,排除static和transient属性,对其他所有的属性进行递归序列化处理(比如属性本身是个对象)
    2. BeanSerializer是遵循pojo bean的约定,扫描bean的所有方法,发现存在get和set方法的属性进行序列化,它并不直接直接操作所有的属性,比较温柔

    Hessian反序列化过程

    这里使用一个demo进行调试,在Student属性包含了String、int、List、Map、Object类型的属性,添加了各属性setter、getter方法,还有readResovle、finalize、toString、hashCode方法,并在每个方法中进行了输出,方便观察。虽然不会覆盖Hessian所有逻辑,不过能大概看到它的面貌:

    下面是对上面这个demo进行调试后画出的Hessian在反序列化时处理的大致面貌(图片看不清,可以点这个链接查看):

    下面通过在调试到某些关键位置具体说明。

    获取目标类型反序列化器

    首先进入HessianInput.readObject(),读取tag类型标识符,由于Hessian序列化时将结果处理成了Map,所以第一个tag总是M(ascii 77):

    case 77这个处理中,读取了要反序列化的类型,接着调用this._serializerFactory.readMap(in,type)进行处理,默认情况下serializerFactory使用的Hessian标准实现SerializerFactory:

    先获取该类型对应的Deserializer,接着调用对应Deserializer.readMap(in)进行处理,看下如何获取对应的Derserializer:

    第一个红框中主要是判断在_cacheTypeDeserializerMap中是否缓存了该类型的反序列化器;第二个红框中主要是判断是否在_staticTypeMap中缓存了该类型反序列化器,_staticTypeMap主要存储的是基本类型与对应的反序列化器;第三个红框中判断是否是数组类型,如果是的话则进入数组类型处理;第四个获取该类型对应的Class,进入this.getDeserializer(Class)再获取该类对应的Deserializer,本例进入的是第四个:

    这里再次判断了是否在缓存中,不过这次是使用的_cacheDeserializerMap,它的类型是ConcurrentHashMap,之前是_cacheTypeDeserializerMap,类型是HashMap,这里可能是为了解决多线程中获取的问题。本例进入的是第二个this.loadDeserializer(Class)

    第一个红框中是遍历用户自己设置的SerializerFactory,并尝试从每一个工厂中获取该类型对应的Deserializer;第二个红框中尝试从上下文工厂获取该类型对应的Deserializer;第三个红框尝试创建上下文工厂,并尝试获取该类型自定义Deserializer,并且该类型对应的Deserializer需要是类似xxxHessianDeserializer,xxx表示该类型类名;第四个红框依次判断,如果匹配不上,则使用getDefaultDeserializer(Class),本例进入的是第四个:

    _isEnableUnsafeSerializer默认是为true的,这个值的确定首先是根据sun.misc.Unsafe的theUnsafe字段是否为空决定,而sun.misc.Unsafe的theUnsafe字段默认在静态代码块中初始化了并且不为空,所以为true;接着还会根据系统属性com.caucho.hessian.unsafe是否为false,如果为false则忽略由sun.misc.Unsafe确定的值,但是系统属性com.caucho.hessian.unsafe默认为null,所以不会替换刚才的ture结果。因此,_isEnableUnsafeSerializer的值默认为true,所以上图默认就是使用的UnsafeDeserializer,进入它的构造方法。

    获取目标类型各属性反序列化器

    在这里获取了该类型所有属性并确定了对应得FieldDeserializer,还判断了该类型的类中是否存在ReadResolve()方法,先看类型属性与FieldDeserializer如何确定:

    获取该类型以及所有父类的属性,依次确定对应属性的FIeldDeserializer,并且属性不能是transient、static修饰的属性。下面就是依次确定对应属性的FieldDeserializer了,在UnsafeDeserializer中自定义了一些FieldDeserializer。

    判断目标类型是否定义了readResolve()方法

    接着上面的UnsafeDeserializer构造器中,还会判断该类型的类中是否有readResolve()方法:

    通过遍历该类中所有方法,判断是否存在readResolve()方法。

    好了,后面基本都是原路返回获取到的Deserializer,本例中该类使用的是UnsafeDeserializer,然后回到SerializerFactory.readMap(in,type)中,调用UnsafeDeserializer.readMap(in)

    至此,获取到了本例中com.longofo.deserialize.Student类的反序列化器UnsafeDeserializer,以各字段对应的FieldSerializer,同时在Student类中定义了readResolve()方法,所以获取到了该类的readResolve()方法。

    为目标类型分配对象

    接下来为目标类型分配了一个对象:

    通过_unsafe.allocateInstance(classType)分配该类的一个实例,该方法是一个sun.misc.Unsafe中的native方法,为该类分配一个实例对象不会触发构造器的调用,这个对象的各属性现在也只是赋予了JDK默认值。

    目标类型对象属性值的恢复

    接下来就是恢复目标类型对象的属性值:

    进入循环,先调用in.readObject()从输入流中获取属性名称,接着从之前确定好的this._fieldMap中匹配该属性对应的FieldDeserizlizer,然后调用匹配上的FieldDeserializer进行处理。本例中进行了序列化的属性有innerMap(Map类型)、name(String类型)、id(int类型)、friends(List类型),这里以innerMap这个属性恢复为例。

    以InnerMap属性恢复为例

    innerMap对应的FieldDeserializer为UnsafeDeserializer$ObjectFieldDeserializer

    首先调用in.readObject(fieldClassType)从输入流中获取该属性值,接着调用了_unsafe.putObject这个位于sun.misc.Unsafe中的native方法,并且不会触发getter、setter方法的调用。这里看下in.readObject(fieldClassType)具体如何处理的:

    这里Map类型使用的是MapDeserializer,对应的调用MapDeserializer.readMap(in)方法来恢复一个Map对象:

    注意这里的几个判断,如果是Map接口类型则使用HashMap,如果是SortedMap类型则使用TreeMap,其他Map则会调用对应的默认构造器,本例中由于是Map接口类型,使用的是HashMap。接下来经典的场景就来了,先使用in.readObject()(这个过程和之前的类似,就不重复了)恢复了序列化数据中Map的key,value对象,接着调用了map.put(key,value),这里是HashMap,在HashMap的put方法会调用hash(key)触发key对象的key.hashCode()方法,在put方法中还会调用putVal,putVal又会调用key对象的key.equals(obj)方法。处理完所有key,value后,返回到UnsafeDeserializer$ObjectFieldDeserializer中:

    使用native方法_unsafe.putObject完成对象的innerMap属性赋值。

    Hessian的几条利用链分析

    在marshalsec工具中,提供了对于Hessian反序列化可利用的几条链:

    • Rome
    • XBean
    • Resin
    • SpringPartiallyComparableAdvisorHolder
    • SpringAbstractBeanFactoryPointcutAdvisor

    下面分析其中的两条Rome和SpringPartiallyComparableAdvisorHolder,Rome是通过HashMap.put->key.hashCode触发,SpringPartiallyComparableAdvisorHolder是通过HashMap.put->key.equals触发。其他几个也是类似的,要么利用hashCode、要么利用equals。

    SpringPartiallyComparableAdvisorHolder

    在marshalsec中有所有对应的Gadget Test,很方便:

    这里将Hessian对SpringPartiallyComparableAdvisorHolder这条利用链提取出来看得比较清晰些:

    看以下触发流程:

    经过HessianInput.readObject(),到了MapDeserializer.readMap(in)进行处理Map类型属性,这里触发了HashMap.put(key,value)

    HashMap.put有调用了HashMap.putVal方法,第二次put时会触发key.equals(k)方法:

    此时key与k分别如下,都是HotSwappableTargetSource对象:

    进入HotSwappableTargetSource.equals

    HotSwappableTargetSource.equals中又触发了各自target.equals方法,也就是XString.equals(PartiallyComparableAdvisorHolder)

    在这里触发了PartiallyComparableAdvisorHolder.toString

    发了AspectJPointcutAdvisor.getOrder

    触发了AspectJAroundAdvice.getOrder

    这里又触发了BeanFactoryAspectInstanceFactory.getOrder

    又触发了SimpleJndiBeanFactory.getTYpe->SimpleJndiBeanFactory.doGetType->SimpleJndiBeanFactory.doGetSingleton->SimpleJndiBeanFactory.lookup->JndiTemplate.lookup->Context.lookup

    Rome

    Rome相对来说触发过程简单些:

    同样将利用链提取出来:

    看下触发过程:

    经过HessianInput.readObject(),到了MapDeserializer.readMap(in)进行处理Map类型属性,这里触发了HashMap.put(key,value)

    接着调用了hash方法,其中调用了key.hashCode方法:

    接着触发了EqualsBean.hashCode->EqualsBean.beanHashCode

    触发了ToStringBean.toString

    这里调用了JdbcRowSetImpl.getDatabaseMetadata,其中又触发了JdbcRowSetImpl.connect->context.lookup

    小结

    通过以上两条链可以看出,在Hessian反序列化中基本都是利用了反序列化处理Map类型时,会触发调用Map.put->Map.putVal->key.hashCode/key.equals->...,后面的一系列出发过程,也都与多态特性有关,有的类属性是Object类型,可以设置为任意类,而在hashCode、equals方法又恰好调用了属性的某些方法进行后续的一系列触发。所以要挖掘这样的利用链,可以直接找有hashCode、equals以及readResolve方法的类,然后人进行判断与构造,不过这个工作量应该很大;或者使用一些利用链挖掘工具,根据需要编写规则进行扫描。

    Apache Dubbo反序列化简单分析

    Apache Dubbo Http反序列化

    先简单看下之前说到的HTTP问题吧,直接用官方提供的samples,其中有一个dubbo-samples-http可以直接拿来用,直接在DemoServiceImpl.sayHello方法中打上断点,在RemoteInvocationSerializingExporter.doReadRemoteInvocation中反序列化了数据,使用的是Java Serialization方式:

    抓包看下,很明显的ac ed标志:

    Apache Dubbo Dubbo反序列化

    同样使用官方提供的dubbo-samples-basic,默认Dubbo hessian2协议,Dubbo对hessian2进行了魔改,不过大体结构还是差不多,在MapDeserializer.readMap是依然与Hessian类似:

    参考

    1. https://docs.ioin.in/writeup/blog.csdn.net/_u011721501_article_details_79443598/index.html
    2. https://github.com/mbechler/marshalsec/blob/master/marshalsec.pdf
    3. https://www.mi1k7ea.com/2020/01/25/Java-Hessian%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
    4. https://zhuanlan.zhihu.com/p/44787200

    Paper

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

    作者:叶霖 杨 | Categories:安全研究技术分享 | Tags:
  • 从反序列化到类型混淆漏洞——记一次 ecshop 实例利用

    2020-07-31

    作者:LoRexxar'@知道创宇404实验室
    时间:2020年3月31日
    English Version: https://paper.seebug.org/1268

    本文初完成于2020年3月31日,由于涉及到0day利用,所以于2020年3月31日报告厂商、CNVD漏洞平台,满足90天漏洞披露期,遂公开。


    前几天偶然看到了一篇在Hackerone上提交的漏洞报告,在这个漏洞中,漏洞发现者提出了很有趣的利用,作者利用GMP的一个类型混淆漏洞,配合相应的利用链可以构造mybb的一次代码执行,这里我们就一起来看看这个漏洞。

    https://hackerone.com/reports/198734

    以下文章部分细节,感谢漏洞发现者@taoguangchen的帮助。

    GMP类型混淆漏洞

    漏洞利用条件

    • php 5.6.x
    • 反序列化入口点
    • 可以触发__wakeup的触发点(在php < 5.6.11以下,可以使用内置类)

    漏洞详情

    gmp.c

    zend_object_handlers.c

    从gmp.c中的片段中我们可以大致理解漏洞发现者taoguangchen的原话。

    __wakeup等魔术方法可以导致ZVAL在内存中被修改。因此,攻击者可以将**object转化为整数型或者bool型的ZVAL,那么我们就可以通过Z_OBJ_P访问存储在对象储存中的任何对象,这也就意味着可以通过zend_hash_copy覆盖任何对象中的属性,这可能导致很多问题,在一定场景下也可以导致安全问题。

    或许仅凭借代码片段没办法理解上述的话,但我们可以用实际测试来看看。

    首先我们来看一段测试代码

    在代码中我展示了多种不同情况下的环境。

    让我们来看看结果是什么?

    我成功修改了第一个声明的对象。

    但如果我将反序列化的类改成b会发生什么呢?

    很显然的是,并不会影响到其他的类变量

    如果我们给class b加一个__Wakeup函数,那么又会产生一样的效果。

    但如果我们把wakeup魔术方法中的变量设置为2

    返回的结果可以看出来,我们成功修改了第二个声明的对象。

    但如果我们把ryat改为4,那么页面会直接返回500,因为我们修改了没有分配的对象空间。

    在完成前面的试验后,我们可以把漏洞的利用条件简化一下。

    如果我们有一个可控的反序列化入口,目标后端PHP安装了GMP插件(这个插件在原版php中不是默认安装的,但部分打包环境中会自带),如果我们找到一个可控的__wakeup魔术方法,我们就可以修改反序列化前声明的对象属性,并配合场景产生实际的安全问题。

    如果目标的php版本在5.6 <= 5.6.11中,我们可以直接使用内置的魔术方法来触发这个漏洞。

    真实世界案例

    在讨论完GMP类型混淆漏洞之后,我们必须要讨论一下这个漏洞在真实场景下的利用方式。

    漏洞的发现者Taoguang Chen提交了一个在mybb中的相关利用。

    https://hackerone.com/reports/198734

    这里我们不继续讨论这个漏洞,而是从头讨论一下在ecshop中的利用方式。

    漏洞环境

    • ecshop 4.0.7
    • php 5.6.9

    反序列化漏洞

    首先我们需要找到一个反序列化入口点,这里我们可以全局搜索unserialize,挨个看一下我们可以找到两个可控的反序列化入口。

    其中一个是search.php line 45

    这是一个前台的入口,但可惜的是引入初始化文件在反序列化之后,这也就导致我们没办法找到可以覆盖类变量属性的目标,也就没办法进一步利用。

    还有一个是admin/order.php line 229

    后台的表单页的这个功能就满足我们的要求了,不但可控,还可以用urlencode来绕过ecshop对全局变量的过滤。

    这样一来我们就找到了一个可控并且合适的反序列化入口点。

    寻找合适的类属性利用链

    在寻找利用链之前,我们可以用

    来确定在反序列化时,已经声明定义过的类。

    在我本地环境下,除了PHP内置类以外我一共找到13个类

    从代码中也可以看到在文件头引入了多个库文件

    这里我们主要关注init.php,因为在这个文件中声明了ecshop的大部分通用类。

    在逐个看这里面的类变量时,我们可以敏锐的看到一个特殊的变量,由于ecshop的后台结构特殊,页面内容大多都是由模板编译而成,而这个模板类恰好也在init.php中声明

    回到order.php中我们寻找与$smarty相关的方法,不难发现,主要集中在两个方法中

    而这里我们主要把视角集中在display方法上。

    粗略的浏览下display方法的逻辑大致是

    比较重要的代码会在make_compiled这个函数中被定义

    当流程走到这一步的时候,我们需要先找到我们的目标是什么?

    重新审视cls_template.php的代码,我们可以发现涉及到代码执行的只有几个函数。

    get_para只在select中调用,但是没找到能触发select的地方。

    然后是pop_vars

    恰好配合GMP我们可以控制$this->_temp_key变量,所以我们只要能在上面的流程中找到任意地方调用这个方法,我们就可以配合变量覆盖构造一个代码执行。

    在回看刚才的代码流程时,我们从编译后的PHP文件中找到了这样的代码

    order_info.htm.php

    在遍历完表单之后,正好会触发pop_vars

    这样一来,只要我们控制覆盖cls_template变量的_temp_key属性,我们就可以完成一次getshell

    最终利用效果

    Timeline

    • 2020.03.31 发现漏洞。
    • 2020.03.31 将漏洞报送厂商、CVE、CNVD等。
    • 2020.07.08 符合90天漏洞披露期,公开细节。

    Paper

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

    作者:叶霖 杨 | Categories:安全研究技术分享 | Tags:
  • F5 BIG-IP hsqldb(CVE-2020-5902)漏洞踩坑分析

    2020-07-31

    作者:Longofo@知道创宇404实验室
    时间:2020年7月10日
    English Version: https://paper.seebug.org/1272/

    F5 BIG-IP最近发生了一次比较严重的RCE漏洞,其中主要公开出来的入口就是tmsh与hsqldb方式,tmsh的利用与分析分析比较多了,如果复现过tmsh的利用,就应该知道这个地方利用有些鸡肋,后面不对tmsh进行分析,主要看下hsqldb的利用。hsqldb的利用poc已经公开,但是java hsqldb的https导致一直无法复现,尝试了各种方式也没办法了,只好换其他思路,下面记录下复现与踩坑的过程。

    利用源码搭建一个hsqldb http servlet

    如果调试过hsqldb,就应该知道hsqldb.jar的代码是无法下断点调试的,这是因为hsqldb中类的linenumber table信息没有了,linenumber table只是用于调式用的,对于代码的正常运行没有任何影响。看下正常编译的类与hqldb类的lineumber table区别:

    使用javap -verbose hsqlServlet.class命令看下hsqldb中hsqlServlet.class类的详细信息:

    使用javap -verbose Test.class看下自己编译的类信息:

    可以看到自己编译的类中,每个method中都有一个 LineNumberTable,这个信息就是用于调试的信息,但是hsqldb中没有这个信息,所以是无法调试下断点的,hsqldb应该在编译时添加了某些参数或者使用了其他手段来去除这些信息。

    没办法调试是一件很难受的事情,我现在想到的有两种:

    1. 反编译hsqldb的代码,自己再重新编译,这样就有linenumber信息了,但是反编译再重新编译可能会遇到一些错误问题,这部分得自己手动把代码修改正确,这样确实是可行的,在后面f5的hsqldb分析中可以看到这种方式
    2. 代码开源,直接用源码跑

    hsqldb的代码正好是开源的,那么这里就直接用源码来开启一个servlet吧。

    环境

    • hsqldb source代码是1.8的,现在新版已经2.5.x了,为了和f5中的hsqldb吻合,还是用1.8的代码吧
    • JDK7u21,F5 BIG-IP 14版本使用的JDK7,所以这里尽量和它吻合避免各种问题

    虽然开源了,但是拖到idea依然还有些问题,我修改了一些代码,让他正常跑起来了,修改好的代码放到github上了,最后项目结构如下:

    使用http方式利用hsqldb漏洞(ysoserial cc6,很多其他链也行):

    利用requests发包模拟hsqldb RCE

    java hsqldb https问题无法解决,那就用requests来发https包就可以了,先模拟http的包。

    抓取上面利用java代码发送的payload包,一共发送了三个,第一个是连接包,连接hsqldb数据库的,第二、三包是执行语句的包:

    根据代码看下第一个数据包返回的具体信息,主要读取与写入的信息都是由Result这个类处理的,一共20个字节:

    • 1~4:总长度00000014,共20字节
    • 5~8:mode,connection为ResultConstants.UPDATECOUNT,为1,00000001
    • 9~12:databaseID,如果直接像上面这样默认配置,databaseID在服务端不会赋值,由jdk初始化为0,00000000
    • 13~16:sessionID,这个值是DatabaseManager.newSession分配的值,每次连接都是一个新的值,本次为00000003
    • 17~20:connection时,为updateCount,注释上面写的 max rows (out) or update count (in),如果像上面这样默认配置,updateCount在服务端不会赋值,由jdk初始化为0,00000000

    连接信息分析完了,接下来的包肯定会利用到第一次返回包的信息,把他附加到后面发送包中,这里只分析下第二个发送包,第三个包和第二个是一样的,都是执行语句的包:

    • 1~4:总长度00000082,这里为130
    • 5~8:mode,这里为ResultConstants.SQLEXECDIRECT,0001000b
    • 9~12:databaseID,为上面的00000000
    • 13~16:sessionID,为上面的00000003
    • 17~20:updateCount,为上面的00000000
    • 21~25:statementID,这是客户端发送的,其实无关紧要,本次为00000000
    • 26~30:执行语句的长度
    • 31~:后面都是执行语句了

    可以看到上面这个处理过程很简单,通过这个分析,很容易用requests发包了。对于https来说,只要设置verify=False就行了。

    反序列化触发位置

    这里反序列化触发位置在:

    其实并不是org.hsqldb.util.ScriptTool.main这个地方导致的,而是hsqldb解析器语法解析中途导致的反序列化。将ScriptTool随便换一个都可以,例如org.hsqldb.sample.FindFile.main

    F5 BIG-IP hsqldb调试

    如果还想调试下F5 BIG-IP hsqldb,也是可以的,F5 BIG-IP里面的hsqldb自己加了些代码,反编译他的代码,然后修改反编译出来的代码错误,再重新打包放进去,就可以调试了。

    F5 BIG-IP hsqldb回显

    • 既然能反序列化了,那就可以结合Template相关的利用链写到response
    • 利用命令执行找socket的fd文件,写到socket
    • 这次本来就有一个fileRead.jsp,命令执行完写到这里就可以了

    hsqldb的连接安全隐患

    从数据包可以看到,hsqldb第一次返回信息并不多,在后面附加用到的信息也就databaseID,sessionID,updateCount,且都只为4字节(32位),但是总有数字很小的连接排在前面,所以可以通过爆破出可用的databaseID、sessionID、updateCount。不过对于本次的F5 BIG-IP,直接用上面默认的就行了,无需爆破。

    总结

    虽然写得不多,写完了看起来还挺容易,不过过程其实还是很艰辛的,一开始并不是根据代码看包的,只是发了几个包对比然后就写了个脚本,结果跑不了F5 BIG-IP hsqldb,后面还是调试了F5 hsqldb代码,很多问题需要解决。同时还看到了hsqldb其实是存在一定安全隐患的,如果我们直接爆破databaseID,sessionID,updateCount,也很容易爆破出可用的databaseID,sessionID,updateCount。


    Paper

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

    作者:叶霖 杨 | Categories:安全研究技术分享 | Tags:
  • 开源=安全?RVN 盗币事件复盘

    2020-07-31

    作者:ACce1er4t0r@知道创宇404区块链安全研究团队
    时间:2020年7月22日

    在7月15号,v2ex上突然出现了一个这样标题的帖子:三行代码就赚走 4000w RMB,还能这么玩?

    帖子内容里,攻击者仅仅只用了短短的几行代码,就成功的获利千万RMB,那么他是怎么做到的呢?

    让我们来回顾一下这次事件。

    事件回顾

    2020年1月16日,开源项目Ravencoin接到这么一则pull request

    image.png

    代码中,提交者将原本定义模糊的报错细分,让人们能够更直观的了解究竟出了什么错误,看起来是在优化项目,但是,事实真是这样么?

    2020年6月29日,Solus Explorer开发团队一位程序员在修bug后同步数据时发现了一个suspected transactions with unbalanced VOUTs被Explorer标记出,之后他检查RVN时发现RVN大约被增发了约275,000,000,并发现了大量可疑地reissue asset Transaction,这些交易不仅仅有Asset Amount,而且获得了RVN。在他发现这一事件后,马上和他的团队一起将事件报告给Ravencoin团队。

    2020年7月3日,Ravencoin团队向社区发布紧急更新

    2020年7月4日,13:26:27 (UTC),Ravencoin团队对区块强制更新了新协议,并确认总增发量为 301,804,400 RVN,即为3.01亿RVN.

    2020年7月5月,Ravencoin团队宣布紧急事件结束

    2020年7月8日,Ravencoin团队公布事件

    image.png

    事件原理

    在解释原理前,我们不妨先重新看看WindowsCryptoDev提交的代码

    这是一段Ravencoin中用于验证的逻辑代码。

    简单来说,提交者改变了CheckTransaction对Asset验证的判断,将原本isAsset && txout.nValue != 0的条件更改为下面的条件:

    1. isAsset && nType == TX_TRANSFER_ASSET && txout.nValue != 0
    2. isAsset && nType == TX_NEW_ASSET && txout.nValue != 0

    这段代码本身利用了开源社区PR的风格(在开源社区中,如果开发者发现提交的PR无关实际逻辑,则不会过度关注代码影响),看似只是细化了交易过程中返回的报错,使得正常使用功能的交易者更容易定位到错误,实则,通过忽略else语句,导致一个通用的限制条件被细化到了nType的两种常见情况下

    而代码中nTypt可能的值有如下:

    由于代码的改变,当nType == TX_REISSUE_ASSET时,txout.nValue可以不为0。

    通过对比正常的交易和存在问题的交易,我们也能验证这一观点。

    image.png

    在正常的Reissue操作中,我们需要向 Address RXReissueAssetXXXXXXXXXXXXXXVEFAWu支付100RVN,之后我们可以得到一个新的Amount为0的Address,如果新的Address的Amount不为0,那么将会返回bad-txns-asset-tx-amount-isn't-zero的错误信息(代码被更改前,修复后会返回bad-txns-asset-reissued-amount-isn't-zero的错误信息)

    image.png

    而攻击者修改了判断条件,导致了在CheckTransaction时并不会检测TX_REISSUE_ASSET,所以能够在Address的Amount不为0的情况下通过判断,最终实现增发RVN。

    看完代码后,我们点开这位叫做WindowsCryptoDev的用户的GitHub主页

    这是个在2020年1月15日新建的账号,为了伪造身份,起了个WindowsCryptoDev的id,并且同天建了个叫Windows的repo,最后的活动便是在1月16号向Ravencoin提交PR。

    而对于这个PR,项目团队的反馈也能印证我们的猜测。


    整个攻击流程如下:

    1. 2020年1月15日,攻击者伪造身份
    2. 1月16日,攻击者提交pull request
    3. 1月16日,当天pull request被合并
    4. 5月9日,攻击者开始通过持续制造非法Reissue Asset操作增发RVN,并通过多个平台转卖换为其他虚拟货币
    5. 6月29日,Solus Explorer开发团队一位程序员发现问题并上报
    6. 7月3日,Ravencoin团队向社区发布紧急更新,攻击者停止增发RVN
    7. 7月4日,13:26:27 (UTC),Ravencoin团队对区块强制更新了新协议
    8. 7月5月,Ravencoin团队宣布紧急事件结束
    9. 7月8日,Ravencoin团队公布事件

    至此,事件结束,最终,攻击者增发了近3亿的RVN。

    总结

    随着互联网时代的发展,开源文化逐渐从小众文化慢慢走向人们的视野中,人们渐渐开始认为开源社区给项目带来源源不断的活力,开源使得人人都可以提交请求、人人都可以提出想法,可以一定层度上提高代码的质量、增加社区的活跃度,形成一种正反馈,这使开源社区活力无限。

    但也因此,无数不怀好意的目光也随之投向了开源社区,或是因为攻击者蓄谋已久,抑或是因为贡献者无心之举,一些存在问题的代码被加入到开源项目中,他们有的直接被曝光被发现被修复,也有的甚至还隐藏在核心代码中深远着影响着各种依赖开源项目生存着的软件、硬件安全。

    开源有利亦有弊,攻击者也在渗透着越来越多开发过程中的不同维度,在经历了这次事件之后,你还能随意的接受开源项目中的PR吗?

    REF

    [1] 三行代码就赚走 4000w RMB,还能这么玩?

    https://s.v2ex.com/t/690286

    [2] commit

    https://github.com/RavenProject/Ravencoin/commit/d23f862a6afc17092ae31b67d96bc2738fe917d2

    [3] Solus Explorer - Address: Illegal Supply

    https://rvn.cryptoscope.io/address/?address=Illegal%20Supply

    [4] Ravencoin — Emergency Update

    https://medium.com/@tronblack/ravencoin-emergency-update-dece62255fd9/https://medium.com/@tronblack/ravencoin-emergency-update-dece62255fd9

    [5] Ravencoin — Emergency Ended

    https://medium.com/@tronblack/ravencoin-emergency-ended-3f3181a0f6d2/https://medium.com/@tronblack/ravencoin-emergency-ended-3f3181a0f6d2

    [6] The anatomy of Ravencoin exploit finding

    https://medium.com/@cryproscope/the-anatomy-of-ravencoin-exploit-finding-8fa4fe7547a9/https://medium.com/@cryproscope/the-anatomy-of-ravencoin-exploit-finding-8fa4fe7547a9

    [7] RavencoinVulnerability — WTF Happened?

    https://medium.com/@tronblack/ravencoin-post-vulnerability-fix-fb3a4bd70b7b/https://medium.com/@tronblack/ravencoin-post-vulnerability-fix-fb3a4bd70b7b


    Paper

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

    作者:叶霖 杨 | Categories:安全研究技术分享 | Tags:
  • CVE-2020-1362 漏洞分析

    2020-07-31

    作者:bybye@知道创宇404实验室
    时间:2020年7月24日

    漏洞背景

    WalletService 服务是 windows 上用来持有钱包客户端所使用的对象的一个服务,只存在 windows 10 中。

    CVE-2020-1362 是 WalletService 在处理 CustomProperty 对象的过程中出现了越界读写,此漏洞可以导致攻击者获得管理员权限,漏洞评级为高危。

    微软在 2020 年 7 月更新对漏洞发布补丁。

    环境搭建

    1. 复现环境:windows 10 专业版 1909 (内部版本号 18363.815)
    2. 设置 WalletService 服务启动类型为自动
    3. 调试环境:windbg -psn WalletService 即可。

    漏洞原理与分析

    漏洞点是设置 CustomProperty 对象的 Group 的 get 方法和 set 方法没有检查边界。

    1. get 方法的 a2 参数没有检查边界导致可以泄露堆上的一些地址。
    2. set 方法的 a2 参数没有检查边界,可以覆盖到对象的虚表指针,从而控制程序流。

    漏洞利用过程

    创建 CustomProperty 对象

    WalletService 服务由 WalletService.dll 提供,WalletService.dll 实际上是一个动态链接库形式的 Com 组件,由 svchost.exe 加载。我们可以在自己写的程序(下面称为客户端)中使用 CoCreateInstance() 或者 CoGetClassObject() 等函数来创建对象,通过调用获得的对象的类方法来使用服务提供的功能。

    如何创建出漏洞函数对应的对象呢?最简单的办法是下载 msdn 的符号表,然后看函数名。

    我们想要创建出 CustomProperty 对象,ida 搜索一下,发现有两个创建该对象的函数:Wallet::WalletItem::CreateCustomProperty() 和 Wallet::WalletXItem::CreateCustomProperty()。

    img

    所以我们创建一个 CustomProperty 需要一个 WalletXItem 对象或者 WalletItem 对象,那么使用哪个呢?继续用 ida 搜索 CreateWalletItem 或者 CreateWalletXItem,会发现只有 CreateWalletItem。

    img

    那到这里我们需要一个 WalletX 对象,继续用 ida 搜索会发现找不到 CreateWalletX,但是如果搜索 WalletX,会发现有个 WalletXFactory::CreateInstance(),如果有过 Com 组件开发经验的同学就会知道,这个是个工厂类创建接口类的函数,上面提到的 CoCreateInstance() 函数会使 WalletService 调用这个函数来创建出接口类返回给客户端。

    img

    那么如何调用 WalletXFactory::CreateInstance() 并创建出 WalletX 对象呢?我们需要在客户端使用 CoCreateInstance() 。

    1. 首先,我们需要 WalletXFactory 的 CLSID,可以使用 OLEViewDotNet 这个工具查看。
    2. 其次,我们需要一个 WalletX 的 IID,这个可以用 ida 直接看 WalletXFactory::CreateInstance() 这个函数。

    有了 WalletXFactory 的 CLSID 和 WalletX 的 IID,然后在客户端调用 CoCreateInstance(),WalletService 就会调用 CLSID 对应的工厂类 WalletXFactory 的 CreateInstance(), 创建出 IID 对应的 WalletX 对象,并返回对象给客户端。

    然后按照上面的分析,使用 WalletX::CreateWalletItem() 创建出 WalletItem 对象,然后使用 WalletItem::CreateCustomProperty() 创建出 CustomProperty 对象。

    对于上面的步骤有疑问的同学可以去学一学 Com 组件开发,尤其是进程外组件开发。

    伪造虚表,覆盖附表指针

    由于同一个动态库,在不同的进程,它的加载基址也是一样的,我们可以知道所有dll里面的函数的地址,所以可以获得伪造的虚表里面的函数地址。

    那么把虚表放哪里呢?直接想到的是放堆上。

    但如果我们继续分析,会发现,CustomProperty 类里面有一个 string 对象,并且可以使用 CustomProperty::SetLabel() 对 string 类进行修改,所以,我们可以通过修改 string 类里面的 beg 指针 和 end 指针,然后调用 CustomProperty::SetLabel() 做到任意地址写。

    img

    有了任意地址写,我们选择把虚表放在 WalletService.dll 的 .data 节区,以避免放在堆上可能破坏堆上的数据导致程序崩溃。

    控制程序流到 LoadLibrary 函数

    使用伪造 vtable 并覆盖虚表指针的办法,我们可以通过调用虚函数控制 WalletService 的程序流到任意地址了。

    那么怎么提权呢?在 windows 服务提权中,通常的办法是把程序流控制到可以执行 LoadLibrary() 等函数来加载一个由我们自己编写的动态链接库,因为在加载 dll 的时候会执行 dll 里面的 DllMain(),这个方法是最强大的也是最实用的。

    这里使用漏洞提交者的方法,把虚表的某个地址覆盖成 dxgi.dll 里面的 ATL::CComObject\::`vector deleting destructor(),因为这个函数调用的 LoadLibraryExW() 会使用一个全局变量作为想要加载的 dll 的路径。

    img

    我们可以通过上面的 SetLabel() 进行任意地址写,修改上图的全局变量 Src,使其指向我们自己实现的动态链接库的路径,然后调用对应的虚表函数,使程序流执行到 LoadLibrarExW() 即可。

    实现一个动态链接库

    在 DllMain() 里面写上我们希望以高权限执行代码,然后调用虚表里面对应的函数是 WalletService 的程序流运行到 LoadLibraryEx() 即可。

    注意,因为 windows 服务运行在后台,所以需要在 DllMain() 里面使用命名管道或者 socket 等技术来进行回显或者交互,其次由于执行的是 LoadLibraryExW(),所以这里的 dll 路径要使用宽字符。

    其它

    在控制虚表函数程序流到 LoadLibraryExW() 时,需要绕过下面两个 check。

    第一个是需要设置 this+0x80 这个地址的值,使得下面的 and 操作为 true。

    image-20200724111317787

    第二个是要调整 qword_C5E88 和 qword_C5E80 是下面的变量 v4 指向具有写权限的内存。

    image-20200724111819875

    漏洞利用结果

    可以获得管理员权限

    image-20200724121314068

    补丁前后对比

    可以看到,打了补丁之后,get 方法和 set 方法都对 a2 参数添加了边界检测。

    img
    img

    参考链接

    [1] PoC链接
    [2] 微软更新公告
    [3] nvd漏洞评级


    Paper

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

    作者:叶霖 杨 | Categories:安全研究技术分享 | Tags:
  • 关于 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轻松编写远程调试工具</