当时的0解题,当初信息搜集找到的确实是郁离歌学长的CVE-2021-41862以及as的官方文档,但其实原题是0CTF/TCTF2023的ezjava,12月9号打的比赛,而出题人关于这题的笔记却是12月14号写的,但是时至今日才复现成功,心里也算是五味杂陈……不过终究也算是学到东西。

题目实现很简单,jar包直接运行就行,也算是从干看着javaweb相关的文章(经典流程之大量的源码和最后一个弹计算器的的截图,教人半懂不懂的)到了能一点点学着做的程度了。

本地搭建:

1
java -jar ezchall.jar

image-20240325140023975

然后访问127.0.0.1就可以在本地测试了。

反编译看源码,定位到控制器:\BOOT-INF\classes\com\example\ctf\IndexController

image-20240325140210048

eval路由存在以下实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public String index(@RequestParam(name = "s",defaultValue = "") String s, Model model) {
ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine engine = sem.getEngineByName("AviatorScript");
AviatorEvaluatorInstance instance = ((AviatorScriptEngine)engine).getEngine();
instance.setOption(Options.FEATURE_SET, Feature.asSet(new Feature[]{Feature.Assignment, Feature.ForLoop, Feature.WhileLoop, Feature.Lambda, Feature.Let}));
HashSet<Object> classes = new HashSet();
instance.setOption(Options.ALLOWED_CLASS_SET, classes);

try {
model.addAttribute("msg", engine.eval(s));
return "index";
} catch (Exception var8) {
model.addAttribute("msg", "error");
return "index";
}
}

当初也是看到这里就去找的aviatorscript相关的rce,但是没有成功利用的。今天才看到文章:https://www.imwxz.com/posts/bd94fce0.html讲得很详细了,针对CVE-2021-41862推出修复后的版本如何继续绕过。

文中提到:安全沙箱主要分为三个防护手段:语言特性、class白名单、反射调用。

  • 语言特性:可以开启和关闭表达式语言特性,例如 instance.setOption(Options.FEATURE_SET, Feature.asSet());
  • class白名单:控制可被调用的class白名单,例如 instance.setOption(Options.ALLOWED_CLASS_SET, new HashSet());
  • 反射调用:控制是否开启反射,例如 instance.setFunctionMissing(null);

poc:exec(invoke(getMethod(loadClass(getClassLoader(getClass(constantly(1))),'java.lang.Runtime'),'getRuntime',nil),nil,nil),'calc')

在网页中传入就可以直接弹出计算器

image-20240325140610618

接下来讨论如何对poc进行理解。

题目允许了Feature.Assignment, Feature.ForLoop, Feature.WhileLoop, Feature.Lambda, Feature.Let这样一些特性,文章中却没有,但白名单不允许任何类这一点是相同的。

1
instance.setOption(Options.ALLOWED_CLASS_SET, new HashSet());

poc的核心原理:

aviator 4.2.5 引入了一个新的方式:基于反射的自动方法发现和调用。原来对象的 object.method(args) 调用方式,转化成 method(object, args) 就可以

而且,要知道默认的类加载器是无法获取的:

1
2
3
4
"".getClass()
// class java.lang.String
"".getClass().getClassLoader()
// null

最简单的解决方案就是使用函数或闭包,此处由于允许lambda,但不允许fn(具体参考:https://www.yuque.com/boyan-avfmj/aviatorscript/yr1oau中的语法特性:FEATURE_SET),也无法进行如文中举例一样的利用。

所以会导致报错,这也是为什么打以前的poc打不动的原因之一。解决方案是寻找一个自带的外部类,as自带的函数库中有constantly(x),作用是返回一个函数。然后需要简单从0先熟悉常规命令执行链:

loadClass->getMethod->invoke->exec

首先最里的一层是getClassLoader(getClass(constantly(1))),先调用getclass获取函数的类,然后获取它的类加载器。这里按照正常的java语言是不可以这样的,因为getclassloder不接受任何参数,是要用constantly(1).getClass().getClassLoder()才能用的,由于as的特性,才能得以实现。

接着,继续利用该特性:method(object, args)来调用。

往外走,来到loadClass()这一层,第一个object的位置是一个classloader,需要用它来加载'java.lang.Runtime',因此有了loadClass(getClassLoader(getClass(constantly(1))),'java.lang.Runtime')

而我们看一下loadclass()的原版实现就不难理解了:

1
ClassLoader().loadClass("java.lang.Runtime");

.前面的CLassLoader()就是object,'java.lang.Runtime'就是args。

继续往外走,getMethod的object是我们加载好的类,两个args是getRuntimenil,不完全等价于java.lang.Runtime.getRuntime(),返回的是method_name为'getRuntime'的method对象,需要通过invoke才能调用。

原版实现:

1
2
3
invoke(Object obj, Object... args)
CLassloder().loadClass("java.lang.Runtime").getMethod("getRuntime").invoke(nil)
//等价于java.lang.Runtime.getRuntime(),由于getRuntime()是静态方法,所以invoke传递null

最后,就是

1
getRuntime().exec("calc")

由于getRuntime()通过的是前面一长串的invoke调用得来,所以改写成as的方式就变成了exec(..., 'calc')

可以改成其他反弹shell类的命令,博主本机用的win来搭建题目,所以执行win类的命令有效,考虑比赛时靶机应当是linux,反弹shell后就随便玩了。

QQ截图20240325162348

至此,想必也算是对javaweb有了一个初步的入门。