本篇文章仅用于技术交流学习和研究的目的,严禁使用文章中的技术用于非法目的和破坏。

前言

拿到了前段时间ciscn 2024 final的web题附件,来复现一下Fobee这道题,顺便学习一下beetl的模板注入。

3.15.x及以前的版本

过滤的很少,随便绕过。贴几个公开的poc:
https://gitee.com/xiandafu/beetl/issues/I6RUIP
https://gitee.com/xiandafu/beetl/issues/I914H3

3.16.0

3.16.0是题目使用的版本也是本文使用的版本。

测试环境为java8u65, beetl 3.16.0, solon 2.8.5

入口

@Mapping("/render")
    public ModelAndView render(String pass, String tp) throws Exception {
        ModelAndView model = new ModelAndView("render.htm");
        if (pass != null && pass.equals(password)) {
            byte[] decode = Base64.getDecoder().decode(tp);
            String result = BeetlKit.render(new String(decode), new HashMap());
            System.out.println(result);
            model.put("msg", getMD5Hash(result));
        } else {
            model.put("msg", "Render Page");
        }
        return model;
    }

渲染过程

由renderTo进入execute函数。然后遍历所有要执行的语句然后依次执行。 1 每条语句的具体执行代码在org.beetl.core.statement.NativeCallExpression#evaluate函数。

public Object evaluate(Context ctx) {
        Class targetCls = null;
        Object targetObj = null;
        NativeNode lastNode = null;
        if (insNode != null) {
            targetObj = insNode.ref.evaluate(ctx);
            if (targetObj != null) {
                targetCls = targetObj.getClass();
            }

            lastNode = insNode;
        } else {
            targetCls = ctx.gt.loadClassBySimpleName(this.clsNode.clazz);
            if (targetCls == null) {
                throw new BeetlException(BeetlException.NATIVE_CALL_EXCEPTION, "该类不存在")
                        .pushToken(GrammarToken.createToken(clsNode.clazz, token.line));
            }
            lastNode = clsNode;

        }

        for (NativeNode node : chain) {
            ...
            ...
            } else if (node instanceof NativeMethodNode) {
                NativeMethodNode methodNode = (NativeMethodNode) node;
                String method = methodNode.method;
                Expression[] expList = methodNode.params;
                this.checkPermit(ctx, targetCls, targetObj, method);

                Object[] args = expList.length == 0 ? ObjectUtil.EMPTY_OBJECT_ARRAY : new Object[expList.length];
                Class[] parameterType = new Class[args.length];
                for (int i = 0; i < expList.length; i++) {
                    args[i] = expList[i].evaluate(ctx);
                    parameterType[i] = args[i] == null ? null : args[i].getClass();
                }
                this.checkNull(targetCls, lastNode);
                ObjectMethodMatchConf mf = ObjectUtil.findMethod(targetCls, method, parameterType);
                if (mf == null) {
                    throw new BeetlException(BeetlParserException.NATIVE_CALL_INVALID,
                            "根据参数未找到匹配的方法" + method + BeetlUtil.getParameterDescription(parameterType))
                            .pushToken(GrammarToken.createToken(token.text, token.line));
                }

                if (targetObj == null && !Modifier.isStatic(mf.method.getModifiers())) {
                    throw new BeetlException(BeetlException.NULL)
                            .pushToken(GrammarToken.createToken(token.text, token.line));
                }

                try {
					if(mf.contextRequired){
						ArrayList tempList  = new ArrayList(Arrays.asList(args));
						tempList.add(ctx);
						args = tempList.toArray();
					}
                    targetObj = ObjectUtil.invoke(targetObj, mf, args);
                    targetCls = targetObj == null ? null : targetObj.getClass();
                ...
                ...
            }

            lastNode = node;

        }
        return targetObj;
    }

黑名单主要针对方法调用,其他代码省略。

beetl将每条语句拆分为targetCls,targetObj,method和args。在org.beetl.core.DefaultNativeSecurityManager#permit函数中对targetCls进行检查:

public boolean permit(Object resourceId, Class c, Object target, String method) {
		if (c.isArray()) {
			// 允许调用,但实际上会在在其后调用中报错。不归此处管理
			return true;
		}
		String name = c.getName();
		String className = null;
		String pkgName = null;
		int i = name.lastIndexOf('.');
		if (i != -1) {
			pkgName = name.substring(0, i);
			className = name.substring(i + 1);

		} else {
			// 无包名,允许调用
			return true;
		}

		if (pkgName.startsWith("java.lang.reflect")) {
			//反射类,不允许调用 https://gitee.com/xiandafu/beetl/issues/I8RU01
			return false;
		}

		if (pkgName.startsWith("java.lang")) {
			return !className.equals("Runtime")
					&& !className.equals("Process")
					&& !className.equals("ProcessBuilder")
					&& !className.equals("Thread") // https://gitee.com/xiandafu/beetl/issues/I6RUIP
					&& !className.equals("Class") //https://gitee.com/xiandafu/beetl/issues/I6RUIP#note_17223442
					&& !className.equals("System")
					;
		}

		if(pkgName.startsWith("java.beans")){
			//https://gitee.com/xiandafu/beetl/issues/I914H3#note_24939039
			return false;
		}
		if(pkgName.startsWith("org.beetl")){
			//https://gitee.com/xiandafu/beetl/issues/I6RUIP
			return false;
		}

		if(pkgName.startsWith("javax.")){
			return false;
		}

		if(pkgName.startsWith("sun.")){
			return false;
		}

		return true;
	}

在此处执行getRuntime时targetCls为java.lang.Runtime,pkgName位java.lang,被限制了,check不通过。
如果check通过,会解析参数并invoke对应函数:

targetObj = ObjectUtil.invoke(targetObj, mf, args);

出网利用

没办法直接利用,尝试从solon和beetl代码中找找可利用的类。

org.noear.solon.Utils类中有两个静态方法与反射有关:

public static Class<?> loadClass(String className) {
    return ClassUtil.loadClass(className);
}

public static <T> T newInstance(String className) {
    return ClassUtil.tryInstance(className);
}

通过这两个方法可以实例化任意类。

JdbcRowSetImpl没被过滤,可以尝试jndi注入。

不出网利用

newInstance部分绕过了,还差invoke部分。

org.noear.solon.core.wrap.MethodWrap类中实现了invoke功能:

public Object invoke(Object obj, Object[] args) throws Throwable {
    try {
        return method.invoke(obj, args);
    } catch (InvocationTargetException e) {
        Throwable e2 = e.getTargetException();
        throw Utils.throwableUnwrap(e2);
    }
}

org.noear.solon.core.AppContext#methodGet会对method进行包装:

public MethodWrap methodGet(Method method) {
    MethodWrap mw = methodCached.get(method);
    if (mw == null) {
        SYNC_LOCK.lock();

        try {
            mw = methodCached.get(method);
            if (mw == null) {
                mw = new MethodWrap(this, method);
                methodCached.put(method, mw);
            }
        } finally {
            SYNC_LOCK.unlock();
        }
    }

    return mw;
}

@org.noear.solon.Solon.app().context()可以获取上下文,也就是Appcontext对象。

要包装函数需要先获得Method对象。
org.noear.solon.core.util.ReflectUtil#getDeclaredMethods可以获得所有声明方法。

public static Method[] getDeclaredMethods(Class<?> clazz) {
    return global.getDeclaredMethods(clazz);
}

这样就可以调用任意类的任意public函数了。

回显

org.noear.solon.core.handle.ContextUtil#current可获取当前线程的上下文,这里的context和appcontext不一样,此处的context只包含本次请求的请求包和返回包等内容。

public static Context current(){
    Context tmp = threadLocal.get();

    if (tmp == null && Solon.cfg().testing()) {
        tmp = new ContextEmpty();
        threadLocal.set(tmp);
    }

    return tmp;
}

solon使用的是smarthttp。smhttpctx对象有一个output函数用来向返回包写数据。
org.noear.solon.boot.smarthttp.http.SmHttpContext#output

public void output(byte[] bytes) {
    try {
        OutputStream out = this.outputStream();
        if (this._allows_write) {
            out.write(bytes);
        }
    } catch (Throwable var3) {
        throw new RuntimeException(var3);
    }
}

将数据写入byte[]中再代用output就可以回显了。
getInputStream()获得执行结果的inputstream对象,byte[]对象通过反射Array获得。

结尾

目前beetl的3.17.0版本已经默认使用白名单模式 链接