ViewStateViewer: A GUI Tool for deserializing/reserializing ViewState

By: Patrick Toomey

Background

So, I was reading the usual blogs and came across a post by Mike Tracy from Matasano (Matasano has been having some technical difficulties…this link should work once they have recovered).  In the blog post Mike talks about the development of a ViewState serializer/deserializer for his WWMD web application security assessment console  (please see Mike’s original post for more details).  Mike noted that the tools for viewing/manipulating ViewState from an application testing perspective are pretty weak, and I can’t agree more.  There are a number of ViewState tools floating around that do a tolerable job of taking in a Base64 encoded ViewState string and presenting the user with a static view of the deserialized object. However, no tools up until this point, save for Mike’s new implementation, have allowed a user to change the values within the deserialized object and then reserialize them into a new Base64 string. Mike’s tool does exactly this, and is immensely useful for web application testing. The only downside to Mike’s tools is that it is built for his workflow and not mine (how can I fault the man for that).  So, I decided to build an equivalent that works well for me (and hopefully for you as well).

I tend to have a web proxy of some sort running on my machine throughout the day. There are tons of them out there and everyone seems to have their personal favorite. There is Paros, WebScarab, BurpSuite, and I am sure many others. In the last few months I have been using a newer entrant into the category, Fiddler.  Fiddler is a great web proxy whose only big drawback is that it is Windows only.  However, at least for me, the upsides to Fiddler tend to outweigh the negatives.  Fiddler has a fairly refined workflow (don’t get me started on WebScarab), is stable (don’t get me started on Paros), and is pretty extensible.  There are a number of ways to extend Fiddler, most trivially using their own FiddlerScript hooks.  In addition, there is a public API for extending the application using .NET.  Fiddler has a number of interfaces that can be extended to allow for inspecting and manipulating requests or responses. Please see the Fiddler site for more details on extending Fiddler using either FiddlerScript or .NET.  In particular, take a look at the development section to get a better feel for the facilities provided by Fiddler for extending the application.

Anyway, I had been thinking about writing a ViewState serializer/deserializer for Fiddler for the past month or two when I saw Mike’s blog post.  I decided that it was about time to set aside a little time and write some code.  I was lucky that Fiddler uses .NET, as I was able to leverage all of the system assemblies that Mike had to decompile in Reflector. After a bit of coding I ended up with my ViewStateViewer Fiddler inspector.  Let’s take a quick tour…

ViewStateViewer seamlessly integrates into the Fiddler workflow, allowing a user to manipulate ViewState just as they would any other variable in a HTTP request.  An extremely common scenario for testing involves submitting a request in the browser, trapping the request in a proxy, changing a variable’s value, and forwarding the request on to the server. ViewStateViewer tries to integrate into this workflow as seamlessly as possible.  Upon trapping a request that contains ViewState, ViewStateViewer extract the Base64 encoded ViewState, Base64 decodes it, deserializes the ViewState, and allows a user to manipulate the ViewState as they would any other variable sent to the server.  Let’s take a look at some screenshots to get a better idea of how this works.

ViewStateViewer Tour

Serialized ViewStateSerialized ViewState

By Default, Fiddler lets a user trap requests and view/edit a POST body before submitting the request to the server.  In this case, the POST body contains serialized ViewState that we would like to work with.  Without ViewStateViewer this is non-trivial, as Fiddler only shows us the Base64 encoded serialization.

Deserialized ViewStateDeserialized ViewState

ViewStateViewer adds a new “ViewState” tab within Fiddler that dynamically identifies and deserializes ViewState on the fly.  The top half of the inspector shows the original Base64 serialized ViewState string.  The bottom half of the inspector shows an XML representation of the deserialized ViewState.  In between these two views the user can see if the ViewState is MAC protected and the version of the ViewState being deserialized.  In this case we can see that this is .NET 2.X ViewState and that MAC protection is not enabled.

ReSerialized ViewStateReserialized ViewState

Once the ViewState is deserialized we can manipulate the ViewState by changing the values in the XML representation.  In this example we changed one of the string values to “foobar”, as can be seen in the figure above.  Once we change the value we can reserialize the ViewState using the “encode” button.  The reserialized Base64 encoded ViewState string can be seen in the top half of the ViewStateViewer window.  Once we have “encoded” the ViewState with our modifications, ViewStateViewer automatically updates the POST body with the new serialized ViewState string.  This request can now be “Run to Completion”, which lets Fiddler submit the updated request to the server.

Limitations

It should be noted that if the original ViewState had used MAC protection ViewStateViewer would not be able to reserialize the manipulated ViewState with a valid MAC.  While ViewStateViewer will not prevent you from deserializing, manipulating, and reserializing MAC protected ViewState, it will not be able to append a valid MAC to modified ViewState.  ViewStateViewer will warn us that “Modified ViewState does not contain a valid MAC”.  Modified requests made using reserialized MAC protected ViewState will ultimately fail, as we don’t know the machine key used to produce a valid MAC.  Regardless, sometimes simply being able to view what is being stored in MAC protected ViewState can be useful during an application assessment.

In addition to MAC protection, ViewState can be optionally protected using encryption.  Encryption will prevent any attempt by ViewStateViewer to deserialize the ViewState.  If encryption is detected ViewStateViewer will simply show the user the original Base64 ViewState string.  However, as any application security consultant can attest, there are many applications that do not encrypt or MAC protect their ViewState.  ViewStateViewer is aimed squarely at these use cases.

Finally, ViewStateViewer was written entirely with deserializing/serializing ViewState 2.X in mind. While ViewState 1.X is supported, the support at this time is limited, though completely functional. ViewState 1.X, unlike ViewState 2.X, Base64 decodes into a completely human readable ASCII based serialization format (think JSON).  As such, ViewStateViewer simply Base64 decodes ViewState 1.X and displays the result to the user. The user is then free to make any changes they wish to the decoded ViewState. This works exactly the same as the example shown above, except that the decoded ViewState object will not be a nicely formatted XML document. I might get around to adding true ViewState 1.X support, but the benefits would be purely cosmetic, as the current implementation has no functional limitations.

Wrap Up

ViewState is one of those areas that tends to be under-tested during application assessments, as there have been no good tools for efficiently evaluating the effects of modifying ViewState.  Hopefully the release of ViewStateViewer will make evaluation of ViewState a more common practice.  If you want to give ViewStateViewer a try you can download the Fiddler plugin(with source) here. Simply copy ViewStateViewer.dll into your “Inspectors” folder within your Fiddler install directory.  It should be noted that this inspector is for Fiddler2, so please update your copy of Fiddler if you are out of date.  Finally, this is very much a work in progress.  As I found out during development, there is ton of ViewState out there that is either non-trivial to deserialize/reserialize or is impossible to deserialize/reserialize (non-standard objects being serialized for example).  Maybe I’ll do another blog post detailing some of these difficult/impossible to handle edge cases in a subsequent post.  So, while I have done some limited testing, I am sure there are some bugs that will crop up.  If you happen to find one please don’t hesitate to contact me.

7 thoughts on “ViewStateViewer: A GUI Tool for deserializing/reserializing ViewState

  1. Hey,

    Nice work.

    I am facing some issues with this plugin. It smoothly deserializes the viewstate but when i try to r-serialize it, even without making a change, it throws an error: “cannot encode malformed xml”. This is regardless of making any changes to the xml in lower half of plugin window.

    Kindly help.

    /sauby

  2. I ran into the same issue. Replace ViewState.cs with the following:

    /*
    Copyright (c) 2009, Neohapsis, Inc.
    All rights reserved.

    Implementation by Patrick Toomey

    Redistribution and use in source and binary forms, with or without modification,
    are permitted provided that the following conditions are met:

    – Redistributions of source code must retain the above copyright notice, this list
    of conditions and the following disclaimer.
    – Redistributions in binary form must reproduce the above copyright notice, this
    list of conditions and the following disclaimer in the documentation and/or
    other materials provided with the distribution.
    – Neither the name of Neohapsis nor the names of its contributors may be used to
    endorse or promote products derived from this software without specific prior
    written permission.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND
    ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
    ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
    ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    */
    using System;
    using System.Collections.Generic;
    using System.Collections;
    using System.Linq;
    using System.Text;
    using System.Xml;
    using System.IO;
    using System.Collections.Specialized;
    using System.Drawing;
    using System.Web.UI;
    using System.Web;
    using System.Text.RegularExpressions;
    using System.Reflection;
    using System.ComponentModel;

    namespace ViewState
    {
    abstract class ViewState
    {
    protected internal int m_version = 0; // ex. 1 (for ASP.Net 1.1)
    protected internal bool m_encrypted = false;
    protected internal bool m_MACProtected = false;
    protected internal String m_versionString = “”; // ex. ASP.Net 2.0
    protected internal String m_viewStateBase64 = “”; // stores the serialized after decoding the original viewstate and then reenncoding it (all MACs and such will be removed)
    protected internal String m_viewStateXML = “”;
    protected internal String m_MAC = “None”;

    public ViewState(String viewStateBase64)
    {
    m_viewStateBase64 = viewStateBase64;
    computeVersionInfo();
    computeMACInfo();
    m_viewStateXML = getViewStateXMLFromBase64(m_viewStateBase64);

    }

    public ViewState(XmlDocument viewStateXMLDocument)
    {
    m_version = Int32.Parse(viewStateXMLDocument.GetElementsByTagName(“Version”).Item(0).InnerText);
    m_versionString = viewStateXMLDocument.GetElementsByTagName(“VersionString”).Item(0).InnerText;
    m_MAC = viewStateXMLDocument.GetElementsByTagName(“MAC”).Item(0).InnerText;
    if (m_MAC == “None”)
    {
    m_MACProtected = false;
    }
    else
    {
    m_MACProtected = true;
    }
    StringBuilder sb = new StringBuilder();
    //XmlWriterSettings xws = new XmlWriterSettings();
    //xws.Indent = false;
    //XmlWriter xw = XmlWriter.Create(sb, xws);
    StringWriter writer = new StringWriter(sb);
    viewStateXMLDocument.Save(writer);
    m_viewStateXML = sb.ToString();
    m_viewStateBase64 = getViewStateBase64FromXMLTree(viewStateXMLDocument);

    }

    public static byte[] toByteArray(String HexString)
    {

    int NumberChars = HexString.Length;

    byte[] bytes = new byte[NumberChars / 2];

    for (int i = 0; i < NumberChars; i += 2)
    {

    bytes[i / 2] = Convert.ToByte(HexString.Substring(i, 2), 16);

    }

    return bytes;

    }
    protected abstract void computeMACInfo();
    protected abstract String getViewStateXMLFromBase64(String viewStateBase64);
    protected abstract String getViewStateBase64FromXMLTree(XmlDocument dom);
    protected String getViewStateBase64WithMAC()
    {
    if (!m_MACProtected)
    {
    return m_viewStateBase64;
    }
    byte[] viewStateBytes = System.Convert.FromBase64String(m_viewStateBase64);
    byte[] MACBytes = ViewState.toByteArray(m_MAC);
    byte[] combinedBytes = new byte[viewStateBytes.Length + MACBytes.Length];
    viewStateBytes.CopyTo(combinedBytes, 0);
    MACBytes.CopyTo(combinedBytes, viewStateBytes.Length);
    return System.Convert.ToBase64String(combinedBytes);
    }

    protected static object buildObjectElement(XmlNode xmlNode)
    {
    String type = xmlNode.Name;
    if (type == "System.Web.UI.Pair")
    {
    Pair pair = new Pair();
    pair.First = buildObjectElement((XmlNode)xmlNode.ChildNodes.Item(0));
    pair.Second = buildObjectElement((XmlNode)xmlNode.ChildNodes.Item(1));
    return pair;
    }
    else if (type == "System.Web.UI.Triplet")
    {
    Triplet triplet = new Triplet();
    triplet.First = buildObjectElement((XmlNode)xmlNode.ChildNodes.Item(0));
    triplet.Second = buildObjectElement((XmlNode)xmlNode.ChildNodes.Item(1));
    triplet.Third = buildObjectElement((XmlNode)xmlNode.ChildNodes.Item(2));
    return triplet;
    }
    else if (type == "System.Collections.ArrayList")
    {
    ArrayList arrayList = new ArrayList();
    foreach (XmlNode innerXmlNode in xmlNode)
    {
    arrayList.Add(buildObjectElement(innerXmlNode));
    }
    return arrayList;
    }
    else if (type.StartsWith("System.Array-"))
    {
    Array array = null;
    String arrayTypeString = type.Substring("System.Array-".Length);
    Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
    Type arrayType = null;
    foreach (Assembly assembly in assemblies)
    {
    arrayType = assembly.GetType(arrayTypeString);
    if (arrayType != null)
    break;
    }

    if (arrayType != null)
    {
    array = Array.CreateInstance(arrayType,xmlNode.ChildNodes.Count);
    for (int i = array.GetLowerBound(0); i <= array.GetUpperBound(0); i++)
    {
    array.SetValue(buildObjectElement((XmlNode)xmlNode.ChildNodes.Item(i)),i);
    }
    }
    return array;
    }
    else if (type == "System.Collections.Hashtable")
    {
    Hashtable hashTable = new Hashtable();
    return hashTable; // this needs to be fixed…I don't know what the child elements are

    }
    else if (type == "System.Collections.Specialized.HybridDictionary")
    {
    HybridDictionary hybridDictionary = new HybridDictionary();
    foreach (XmlNode innerXmlNode in xmlNode)
    {
    DictionaryEntry dictionaryEntry = (DictionaryEntry)buildObjectElement((XmlNode)innerXmlNode);
    hybridDictionary.Add(dictionaryEntry.Key, dictionaryEntry.Value);
    }
    return hybridDictionary;
    }
    /*else if (type == "System.Collections.IDictionary")
    {
    IDictionary iDictionary = new IDictionary();
    return iDictionary; // hmmm…not quite sure if we will ever see an IDicionary itself since it is an interface
    }*/

    else if (type == "System.Collections.DictionaryEntry")
    {
    DictionaryEntry dictionaryEnry = new DictionaryEntry();
    dictionaryEnry.Key = buildObjectElement((XmlNode)xmlNode.ChildNodes.Item(0));
    dictionaryEnry.Value = buildObjectElement((XmlNode)xmlNode.ChildNodes.Item(1));
    return dictionaryEnry;
    }
    else if (type == "System.String")
    {
    // horrible hack because XML doesn't like to keep around whitespace. All empty strings in our original xml get turned into
    // a newline followed by any number of spaces. We'll just regex it and return the correct empty string.
    // this occurs because we are using xml:space=preserve attributes. If we don't use attributes we have issues with
    // elements composed only of spaces…ugh
    String pattern = @"^\r\n +$";
    Match match = Regex.Match(xmlNode.InnerText, pattern, RegexOptions.Multiline);
    if (match.Success)
    {
    return "";
    }
    else
    {
    return xmlNode.InnerText;
    }
    }
    else if (type == "System.Web.UI.IndexedString")
    {
    return new IndexedString(xmlNode.InnerText);
    }
    else if (type == "System.Int64")
    {
    return Int64.Parse(xmlNode.InnerText);
    }
    else if (type == "System.Int32")
    {
    return Int32.Parse(xmlNode.InnerText);
    }
    else if (type == "System.Int16")
    {
    return Int16.Parse(xmlNode.InnerText);
    }
    else if (type == "System.Boolean")
    {
    if (xmlNode.InnerText == "True")
    {
    return true;
    }
    else
    {
    return false;
    }
    }
    else if (type == "System.Drawing.Color")
    {
    //we use the type converter instead of instantiating a new color, as most colors are not dynamic and are pulled form teh color struct
    String colorString = xmlNode.InnerText;
    #if DEBUG
    try
    {
    Color color = (Color)TypeDescriptor.GetConverter(typeof(Color)).ConvertFromString(colorString);
    return color;
    }
    catch (Exception colorEx)
    {
    return new Color();

    }
    #else
    Color color = (Color)TypeDescriptor.GetConverter(typeof(Color)).ConvertFromString(colorString);

    return color;
    #endif

    }
    else if (type == "Null" && xmlNode.InnerText == "True")
    {
    return null;
    }
    // we try to do a best guess for unknown types using reflection
    else
    {
    Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
    Type realType = null;
    foreach (Assembly assembly in assemblies)
    {
    realType = assembly.GetType(type);
    if (realType != null)
    break;
    }
    object[] args = new object[1];
    args[0] = xmlNode.InnerText;

    // Change being

    //if (Utils.InheritFrom(realType, typeof(System.Enum)))
    //{
    if (realType.IsSubclassOf(typeof(System.Enum)))
    {
    return Enum.Parse(realType, xmlNode.InnerText);
    }
    else
    {

    #if DEBUG
    try
    {
    return System.Activator.CreateInstance(realType, args);
    }
    catch (Exception ex)
    {

    return null;
    }
    }
    #else
    return System.Activator.CreateInstance(realType, args);
    #endif
    }

    }

    public virtual String viewStateXML
    {
    get
    {
    return m_viewStateXML;
    }

    }
    public String viewStateBase64
    {
    get
    {
    return getViewStateBase64WithMAC();
    }
    }

    public String MAC
    {
    get
    {
    return m_MAC;
    }
    }

    public bool MACProected
    {
    get
    {
    return m_MACProtected;
    }
    }
    public int version
    {
    get
    {
    return m_version;
    }
    }

    public String versionString
    {
    get
    {
    return m_versionString;
    }
    }
    public bool encrypted
    {
    get
    {
    return m_encrypted;
    }
    }

    protected void computeVersionInfo()
    {
    if (m_viewStateBase64 == null)
    {
    return;
    }

    m_version = getViewStateVersionFromBase64String(m_viewStateBase64);
    switch (m_version)
    {
    case 2:
    m_versionString = "ASP.Net 2.X";
    m_encrypted = false;
    break;
    case 1:
    m_versionString = "ASP.Net 1.X";
    m_encrypted = false;
    break;
    default:
    m_versionString = "Unknown";
    m_encrypted = true; //this isn't guranteed…but we'll assume it is true for now
    break;
    }

    return;
    }

    public static ViewState newViewStateFromBase64String(String viewStateString)
    {
    ViewState viewState = null;
    int viewStateVersion = getViewStateVersionFromBase64String(viewStateString);
    switch (viewStateVersion)
    {
    case 1:
    viewState = new ViewState_Dot_Net_1_0(viewStateString);
    break;
    case 2:
    viewState = new ViewState_Dot_Net_2_0(viewStateString);
    break;
    default:
    viewState = null;
    break;

    }
    return viewState;
    }

    public static ViewState newViewStateFromXMLString(String viewStateXML)
    {
    ViewState viewState = null;
    int viewStateVersion = 0;
    String versionPattern = @"(.+)”;
    Match match = Regex.Match(viewStateXML, versionPattern, RegexOptions.Singleline);
    if (match.Success)
    {
    viewStateVersion = Int32.Parse(match.Groups[1].Value);
    }
    // if this is viewstate version 1 we need to patch a few things up
    if (viewStateVersion == 1)
    {
    String viewStatePattern = @”(.+)”;
    match = Regex.Match(viewStateXML, viewStatePattern, RegexOptions.Singleline);
    if (match.Success)
    {
    viewStateXML = viewStateXML.Replace(match.Groups[1].Value, HttpUtility.HtmlEncode(match.Groups[1].Value));
    }

    }

    // horrible horrible hack to account for RichTextBox removing CRLF with a LF…anyone have a better idea???
    viewStateXML = viewStateXML.Replace(“\n”, “\r\n”);

    XmlDocument dom = new XmlDocument();

    StringReader reader = new StringReader(viewStateXML);
    //dom.PreserveWhitespace = true;
    dom.Load(reader);
    switch (viewStateVersion)
    {
    case 1:
    viewState = new ViewState_Dot_Net_1_0(dom);
    break;
    case 2:
    viewState = new ViewState_Dot_Net_2_0(dom);
    break;
    default:
    viewState = null;
    break;

    }
    return viewState;
    }

    public static Int32 getViewStateVersionFromBase64String(String viewStateBase64)
    {
    byte[] viewStateBytes;
    try
    {
    viewStateBytes = System.Convert.FromBase64String(viewStateBase64);
    }
    catch (System.ArgumentNullException)
    {
    return 0;
    }
    if (viewStateBytes.Length >= 2 &&
    viewStateBytes[0] == ‘\xff’ &&
    viewStateBytes[1] == ‘\x01′)
    {
    return 2;
    }
    else if (viewStateBytes.Length >= 2 &&
    viewStateBytes[0] == ‘t’ &&
    viewStateBytes[1] == ‘<')
    {
    return 1;
    }
    else
    {
    return 0;
    }
    }

    }
    }

  3. Pingback: Casaba Security » Watcher 1.3.0 released «

  4. Sadly the Plugin seems not to be available on the site anymore. The only plugin currently available does only decode and no recode :(

    • My apologies. We were in the middle of reorganizing the blog static file directory and this slipped through the cracks. I think it should be updated. Thanks for the heads up.

  5. Fiddler extension does not detect MAC or decodes ViewState with None, but OWASP ZAP VEHICLE detect ViewState Mac is enabled. This is on ASP.Net 2.X.

  6. Unfortunately, ASP.Net ViewState for .Net 4.5 web apps appears to not decode with the latest Telerik branded Fiddler. The .Net 3.5 still works fine however.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s