电子签约管理控制台
定时任务RCE
契约锁管理控制台的后台存在一个定时任务功能,可通过上传java代码执行。
![]() |
|---|
上传的接口为/api/code/upload,对应控制器为com.qiyuesuo.ucenter.CustomCodeController的upload方法。
![]() |
|---|
在处理上传的过程中,调用到com.qiyuesuo.code.CustomCodeServiceImpl的create方法时,会检测是否存在Runtime、Process、ProcessBuilder三个关键字,这个可以通过加载字节码的方式很容易绕过。
![]() |
|---|
![]() |
后续会调用到com.qiyuesuo.core.compiling.javac.JavaDynamicCompiler的compileFile方法对传入的java源码进行编译,会调用loadClass加载类,但并未调用。
![]() |
|---|
通过CustomCode对象保存包名、类名等信息。
![]() |
|---|
最后返回CustomCode对象的id
![]() |
|---|
配置循环执行的次数和延期的时间或者定时表达式来执行上传的代码。
![]() |
|---|
点击确定以后会请求/api/utask/create接口,该接口对应com.qiyuesuo.utask.UserTaskController的create方法,调用this.timerService.addTask(record)添加到定时任务服务中。
![]() |
|---|
等待一段时间后,会自动调用到com.qiyuesuo.code.CustomCodeStrategyManager的loadCustomCode方法,加载并调用上传的java代码。
![]() |
|---|
权限绕过
每次请求会经过的Filter,顺序从上自下。
1 | 0 = {ApplicationFilterConfig@34795} "ApplicationFilterConfig[name=corsFilter, filterClass=org.springframework.web.filter.CorsFilter]" |
对用户鉴权方法在AccessControlFilter的doFilterInternal方法中,调用了this.isAllow(request, response)方法处理路径鉴权逻辑。
![]() |
|---|
在isAllow方法中,匹配请求路径,如果在白名单中,则放行,如果不在白名单中,则验证cookie,最终如果返回true则执行doFilter,返回false则返回登录失效,并跳转到登录页面。
![]() |
|---|
调用this.securityProperties.getAllowedPatterns()方法获取无需登录即可访问的路径。
![]() |
|---|
1 | 0 = "/api/setup*" |
在matchesRequestURI方法中,调用request.getRequestURI()获取请求的路径,遍历allowedPatterns中的路径,调用this.pathMatcher.matches(pattern, requestUri)方法逐个验证。
![]() |
|---|
当匹配规则为/api/setup*时,只能匹配到/api/setupxxx这类url,不能匹配/api/setup/a
![]() |
|---|
![]() |
当匹配规则为/api/setup/**时,可以匹配到/api/setup/aaaa/aaaa/aaa这类路由
![]() |
|---|
这里使用request.getRequestURI获取请求路径,这种方式存在一个安全隐患,当路径中存在..危险关键字并不会将其..剔除掉,所以在路径中添加/api/setup/../../api/code/upload也会被成功匹配到。
![]() |
|---|
请求经过Filter过滤器之后,会调用到DispatcherServlet类的doService方法,它是Spring MVC的核心,负责接收HTTP请求,并根据请求信息分发到相应的Controller进行处理,会对URL进行路径归一化处理。
调用栈
1 | getRequestUri:382, UrlPathHelper (org.springframework.web.util) |
在调用到Controller之前,会调用this.decodeRequestString(request, uri)对URI进行一次解码。
![]() |
|---|
然后通过request.getServletPath()获取请求路径
![]() |
|---|
![]() |
通过request.getServletPath()获取到的路径路径获取bean
![]() |
|---|
但是当通过/api/setup/../../api/code/upload进行访问时,会被拦截掉。
![]() |
|---|
这是因为在RiskUrlPreventFilter的doFilterInternal方法中,会对请求的url进行检测,如果url中包含RISK_URLS变量中的只,则不允许访问,其中就包括/..。
![]() |
|---|
1 | static { |
由于会对路径做归一化处理,会进行url解码,而这里,我们可以将..进行URL编码即可绕过。
![]() |
|---|
漏洞复现
TestTimerTask1.java
1 | package com.qiyuesuo.utask.java; |
上传,并配置执行方式
![]() |
|---|
![]() |
|---|
电子签约签署平台
表达式注入
在接口/api/template/html/add,该接口对应com.qiyuesuo.api.TemplateHtmlController的addHtmlTemplate方法,该方法接收一个TemplateBean类型的bean,其中包含title、params等变量,首先会校验title不能为空并且不能超过100个字符长度。后续会获取该对象的params参数进行遍历,该参数是一个List对象,保存的是TemplateParam类型的对象,该对象存在一个属性值extensionParam,在160行会调用param.getExtensionParam()获取,并调用this.objectMapper.readValue将其转换成Map,然后会判断是否包含expression键,存在则获取该键,并调用this.checkExpression(newExpression)方法。
![]() |
|---|
在checkExpression方法中,传入newExpression参数会被当做js代码执行。
![]() |
|---|
TemplateBean对象的参数值完全可控,所以可以构造TemplateBean参数的params参数,将expression键值指定JS代码实现代码执行。
权限绕过
签约平台所经过的Filter过滤器
1 | 0 = "ApplicationFilterConfig[name=corsFilter, filterClass=org.springframework.web.filter.CorsFilter]" |
同样用户鉴权方法在AccessControlFilter的doFilterInternal方法中,调用了this.isAllow(request, response)方法处理路径鉴权逻辑。
![]() |
|---|
访问路径白名单
1 | 0 = "/api/captcha/**" |
绕过原理跟管理控制一致。
漏洞复现
执行命令延时判断漏洞是否存在
1 | VChqYXZhLmxhbmcuVGhyZWFkKS5zbGVlcCg1MDAwKQ== |
1 | POST /login/%2e%2e/api/template/html/add HTTP/1.1 |
![]() |
|---|
参考文章
https://www.cnblogs.com/nice0e3/p/14801884.html#%E7%BB%95%E8%BF%87%E6%96%B9%E5%BC%8F






























