Fel
Fel介绍
Fel:Fast Expression Language,是一种开源表达式引擎,基于java1.5开发,适用java1.5及以上版本。
特点:如名所示:fast—快,每秒可执行千万次表达式,速度是Jexl-2.0的20倍以上。
Maven导入
1
2
3
4
5<dependency>
<groupId>org.eweb4j</groupId>
<artifactId>fel</artifactId>
<version>0.8</version>
</dependency>
使用方法
基本算术表达式计算
1 | FelEngine felEngine = new FelEngineImpl(); |
使用变量/调用java方法
1 | FelEngine felEngine = new FelEngineImpl(); |
自定义上下文/常见用法
- 自定义实体类
1 | // Student类 |
- Fel调用
1 | // 创建表达式引擎对象 |
调用静态方法
- 通过
$(‘class’).method
的语法可以调用第三方类包、工具类、自定义类的方法,也可以创建对象,调用对象的方法
1 | // 调用工具类 |
安全管理器
- 从0.8版本开始,为了防止
“${System}.exit(1)$”
这样的表达式导致系统崩溃,Fel加入了安全管理器,对方法访问进行控制
1 | // 加入安全管理器后执行如下代码会抛出异常 |
表达式解析
- 这里以
FelEngine.instance.eval("$('Runtime').getRuntime().exec(\"C:\\\\Windows\\\\System32\\\\calc.exe\")");
为例,展示表达式解析的流程,以及安全管理器的过滤
语法树结构
- Fel表达式语法树有3种节点
- 常量节点:ConstNode(包括类名、包名、方法内的固定参数等)
- 函数节点:FunNode(包括 +、-、*、/、$、.等操作符或者getRuntime()、exec()等方法)
- 变量节点:VarAstNode(若执行表达式
((a+b)*c/d)
,则其中a、b、c、d均为变量节点 - 以上节点都继承自AbstFelNode,所有组成表达式的元素都会被解析成节点
执行表达式
- 解析表达式:默认Antlr解析表达式(antlr-min-3.4.jar),生成AbstNode组成的语法树
- 节点解释:每种节点(常量、变量、函数)都有对应的解释器负责解释该节点
- 语法树执行顺序:按先序遍历执行(根$\rightarrow$左$\rightarrow$右)
获取class/创建对象
Fel中使用‘$’来获取class或创建对象
获取class:
$(‘Runtime’)
结果为java.lang.Runtime
函数调用栈如下
1
2
3AbstFelNode#eval
->FunNode#interpret
->Dollar#call -> #isNew -> #getClass -> Class.forName()创建对象:
$(‘test.Student.new’)
结果为new Student()
函数调用栈如下
1
2
3AbstFelNode#eval
->FunNode#interpret
->Dollar#call -> #isNew -> #newObject -> Class.newInstance()
- Dollar.java部分关键代码如下
1 |
|
获取方法
- Fel中通过点运算符获取方法
- 函数调用栈如下
1 | Dot#call -> #findMethod |
- 取方法代码如下
- 首先使用
Class.getMethods()
方法获取Class对象的方法集合 - 然后for循环逐个比较找出方法
- 返回
finalMethod=public static java.lang.Runtime.getRuntime()
- 若方法不为空,则通过
Method.invoke()
调用方法
- 首先使用
1 | public static Method findMethod(Class<?> cls, String attr, Class<?>[] paramTypes) { |
安全管理器
- 安全管理器中通过黑名单
uncallableMap
+白名单callableMap
的方法来过滤- 若方法在黑名单内,则返回false,无法访问
- 若白名单为空,则返回true
- 若方法在白名单内,返回true
1 |
|
- 跟踪数据流,可发现黑名单如下
java.lang.Runtime.*
com.greenpineyu.fel.compile.*
java.lang.Process.*
java.io.File.*
java.net.*
com.greenpineyu.fel.security.*
java.lang.System.*
- 黑名单配置位置位于
com\greenpineyu\fel\common\FelBuilder.java
处
1 | public class FelBuilder { |
执行系统命令
Fel执行系统命令有以下几种思路,但基于引擎本身执行的方法都不成功,不知道有没有其他方法
首先是
Runtime.getRuntime
,由于安全管理器的存在,这个方法在黑名单内,无法直接通过表达式解析使用,由于Fel版本较老(2013年后就没有维护过),安全管理器的黑名单不够完善,没有禁用
ProcessBuilder
,可以尝试使用ProcessBuilder
执行系统命令- 创建恶意类使用反射
- 套娃,使用别的解析引擎来执行表达式
ProcessBuilder
payload:
felEngine.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
18private 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
8public 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 | // 传入的参数即为class java.lang.ProcessBuilder |
这段代码咋一看没什么问题,使用
newInstance()
实例化的效果和new
也一样,但是这两者有个很关键的不同newInstance()
只能调用无参构造
但是
ProcessBuilder
没有无参构造!这就导致了执行时会抛出java.lang.InstantiationException: java.lang.ProcessBuilder
的异常,罪魁祸首正是因为这个实例化用的是newInstance()
拓展
上面说到ProcessBuilder没有无参构造却依然可以使用ProcessBuilder processBuilder = new ProcessBuilder()
实例化,主要是因为ProcessBuilder
这个类有一个构造方法如下
1 | public ProcessBuilder(String... command) { |
它使用了String... command
这种不定参数的形式,当我们使用ProcessBuilder processBuilder = new ProcessBuilder()
时,其实就相当于调用了这个构造方法,只是传入的参数为空,并不是无参构造
Runtime
Fel有两种执行方式,一种是解释执行,即通过解析表达式来执行语句;一种是编译执行,用于海量数据的快速运算,无法执行获取类、方法等表达式
在解释执行中,表达式解析为java执行语句后,在
invoke()
执行方法前,会经过安全管理器检查,因此暂时没发现什么可以绕过的方法安全检查的函数调用栈如下
1
2
3Dot.java#call -- 点运算符取方法
->#findMethod() ->Method.getMethods()取方法
->#getCallableMethod() -- 调用安全管理器,若黑名单则抛出异常
创建恶意类
- 目前看来Fel只提供创建自定义类对象和调用工具类的api,并没有提供创建类的api
套娃
- 目前唯一一个能成功执行系统命令的方法,本质上并不是在Fel里解析执行的,而是使用了
javax.script.ScriptEngineManager
这个包 - 这个包用于js解析,它可以将js文本格式的代码封装后解析执行对应代码,由于这并没有经过Fel的解析,因此不会触发安全管理器的过滤,可以执行系统命令
1 | // 使用ProcessBuilder执行系统命令 |
防御
- 以下为安全管理器构建源码,可以看到默认只设置了黑名单,白名单默认为空,这里可以手动添加白名单作为过滤
1 | // common/FelBuilder |
- 白名单添加方法
1 | // common/FelBuilder |
后记/总结
这个引擎不知道现在应用得多不多….毕竟从2013年后就没有维护了,由于该引擎加载类的特性(比如newInstance()
在java9后就弃用了,改为使用构造器Constructor
中getConstructor().newInstance()
的方法进行反射,支持有参构造),如今常见的一些依赖反射和类加载形成的表达式注入其实都做不了,可能是因为代码比较老旧的原因吧,反而使得他更“安全”;同时由于不支持创建自定义类(没有loadClass()
、ClassLoader()
等)的原因,也封死了加载恶意类来注入这条路;引擎本身只支持加载已有的自定义类和java工具类,对于需要实例化对象却没有无参构造的类(比如ProcessBuilder),引擎并没有给出解决方案(没想到因为他的落后而显得安全(雾));最后就是安全管理器,黑名单应该是搞不定的,,过滤规则是:“当白名单不为空时,优先根据白名单判断,不在白名单则抛出异常,当白名单为空时,根据黑名单判断,在黑名单内则抛出异常”,因此应该还是要设白名单的吧。