Our recent advisory describes an ASP.NET vulnerability which was recently publicly disclosed. This blog post will give you more information about the vulnerability and the workaround. It will also provide a script which will help you detect ASP.NET applications on your server that are in a vulnerable configuration.

The Impact of the Vulnerability

ASP.Net uses encryption to hide sensitive data and protect it from tampering by the client. However, a vulnerability in the ASP.Net encryption implementation can allow an attacker to decrypt and tamper with this data.

But what can the attacker do with this capability? Part of the answer depends on the ASP.Net application being attacked. For example, if the ASP.Net application stores sensitive information, such as passwords or database connection strings, in the ViewState object this data could be compromised. The ViewState object is encrypted and sent to the client in a hidden form variable, so it is a possible target of this attack.

If the ASP.Net application is using ASP.Net 3.5 SP1 or above, the attacker could use this encryption vulnerability to request the contents of an arbitrary file within the ASP.Net application. The public disclosure demonstrated using this technique to retrieve the contents of web.config. Any file in the ASP.Net application which the worker process has access to will be returned to the attacker.

How the Vulnerability Works

To understand how this vulnerability works, you need to know about cryptographic oracles. An oracle in the context of cryptography is a system which provides hints as you ask it questions. In this case, there is a vulnerability in ASP.Net which acts as a padding oracle. This allows an attacker to send chosen cipher text to the server and learn if it was decrypted properly by examining which error code was returned by the server.

By making many requests the attacker can learn enough to successfully decrypt the rest of the cipher text. The attacker can then alter the plain text and re-encrypt it as well.

The Workaround - Silencing the Oracle

The workaround for this vulnerability is to use the customErrors feature of ASP.NET to configure applications to return the same error page regardless of the error encountered on the server.

By following the steps in the advisory to map all error messages to a single error page, you make it difficult for the attacker to distinguish between the different types of errors, effectively limiting access to the oracle.

How to Detect Vulnerable ASP.Net Applications

Some ASP.Net applications may already be configured to return the same error page for all server errors. To detect ASP.Net applications that are not configured this way and need to have the workaround applied to them, use the following script:

Note: After installing the MS10-070 security update, the workaround is no longer necessary and there is no need to run this script.

'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'  DetectCustomErrorsDisabled.vbs Script
'  Version 3.1
'  
'  This script will help detect vulnerable configuration for the Padding Oracle 
'  ASP.Net vulnerability documented in MS advisory 2416728.
'  
'  http://www.microsoft.com/technet/security/advisory/2416728.mspx
'  
'  Usage: 
'      cscript DetectCustomErrorsDisabled.vbs [RemoteServerName] 
'
'  NOTE: THIS SCRIPT USES THE FILESYSTEM AND SHELL OBJECT AND SHOULD BE
'       RUN AS AN ADMINISTRATOR
'
'  The script works by enumerating all web.config and assessing if the 
'  side-channel leak for the padding oracle vulnerability is mitigated by the 
'  use of homogenizing custom error responses from ASP.Net applications. 
'  
'  Note: On IIS 7 servers, this script requires IIS6 compatibility mode to be
'  installed.
'  
'  More information on: http://blogs.technet.com/b/srd/archive/2010/09/17/understanding-the-asp-net-vulnerability.aspx
'  
'  Version History:
'  1.0 - Initial version
'  2.0 - Added additional checks for app/site root config
'  3.0 - Added error validation for XML parsing and path checks
'  3.1 - Added check for missing root web.config
' 
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
OPTION EXPLICIT
ON ERROR RESUME NEXT
DIM strServer
DIM objWebService, objWebServer, objDir, objFileSys
DIM physicalPath, dir, xmlDoc, nodeList, node, ret
DIM configFile, configFilePath, configLine
DIM childNodes, ErrPage500, ErrPage404, errFound
DIM index, errCount

strServer = "localhost"


' Parse command line input
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
IF WScript.Arguments.Length=1 THEN
    strServer = WScript.Arguments( 0 )
END IF

IF WScript.Arguments.Length>1 THEN
    WScript.Echo "Illegal number of arguments"
    WScript.Echo "Usage: cscript.exe DetectCustomErrorsDisabled.vbs [RemoteServerName]"
    WScript.Quit( 1 )
END IF

' Initializations
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
SET objFileSys = CreateObject("Scripting.FileSystemObject")
SET objWebService = GetObject( "IIS://" & strServer & "/W3SVC" )
IF Err <> 0 THEN
    WScript.Echo "Could not find IIS ADSI object. Make sure you have IIS and IIS6 management compatibility installed."
    WScript.Quit (1)
END IF
SET xmlDoc = CreateObject("Microsoft.XMLDOM")

IF IsNull(objFileSys) THEN
    WScript.Echo "Failed to create FileSystemObject. Please run script as Admin."
    WScript.Quit (1)
END IF

IF IsNull(objWebService) THEN
    WScript.Echo "Failed to connect to IIS ADSI provider. Make sure you have IIS6 "_
    + "management compatibility role service installed."
    WScript.Quit (1)
END IF

WScript.Echo("Enumerating possible paths with ASP.Net configuration that have" _
    +" custom errors turned off.")    
WScript.Echo ("")    


' Search web server for unsafe configuration
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
FindASPNetConfig(objWebService)


' Search all paths on web server for possible web.config  files.
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
SUB FindASPNetConfig(WebService)

    FOR EACH objWebServer IN WebService
        IF objWebserver.Class = "IIsWebServer" THEN
            EnumDirectories(objWebServer)
        END IF
    NEXT
    
END SUB

' Recursively go through vdirs and webdirs
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
SUB EnumDirectories(objDir)
    
    DIM objSubDir
    ' The first call to this is from IIsWebServer, so we can skip that
    FOR EACH objSubDir IN objDir
        IF (objSubDir.Class = "IIsWebVirtualDir") THEN
            GetPhysicalPaths(objSubDir)            
            EnumDirectories(objSubDir)          
        END IF
    NEXT	
    
END SUB


' Get physical paths for web and virtual directories
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
SUB GetPhysicalPaths(objDir)
    
    physicalPath = objDir.Path
    CALL EnumWebConfig(physicalPath,1)

END SUB


' Recursively search for web.config files.
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
SUB EnumWebConfig(Path,IsRoot)

    IF NOT objFileSys.FolderExists(Path) THEN 
        IF IsRoot THEN
            ' WScript.Echo Path & ": Site's disk path is incorrect and root web.config does not exist"
            WScript.Echo Path & ": ** Vulnerable configuration found **"
        END IF
        EXIT SUB
    END IF

    configFilePath = Path & "\web.config"
    IF objFileSys.FileExists(configFilePath) THEN 
        CALL ProcessWebConfig(configFilePath,IsRoot)
    ELSEIF IsRoot = 1 THEN
        ' WScript.Echo Path & ": Site or app root web.config does not exist"
         WScript.Echo Path & ": ** Vulnerable configuration found **"
    END IF
    
    FOR EACH dir IN objFileSys.GetFolder(Path).SubFolders
        CALL EnumWebConfig(dir.Path,0)
    NEXT

END SUB

' Skip known identities that will have Write access by default
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
SUB ProcessWebConfig(Path,IsRoot)
    xmlDoc.async="false"
    xmlDoc.load(Path)
    errFound = 0
    SET nodeList = xmlDoc.getElementsByTagName("customErrors")

    IF IsRoot = 1 AND nodeList.length = 0 THEN
        ' Root web.config does not set defaultRedirect, so this config should 
        ' have a customErrors section present with customErrors turned on and a 
        ' defaultRedirect present. Else this is a vulnerable configuration.
	
        ' WScript.Echo path & ": Root web.config must have customErrors with defaultRedirect defined"

        errFound = errFound + 1

    ELSEIF IsRoot = 1 THEN
        ret = CheckRootCustomErrorsSection(nodeList, Path)
        errFound = errFound + ret
    END IF

    DIM count

    FOR count=0 TO nodeList.length-1
        ret = CheckCustomErrorsDisabled(nodeList.Item(count), Path)
	    errFound = errFound + ret
        ret = CheckCustomErrorsAreHomogenous(nodeList.Item(count), Path)
	    errFound = errFound + ret
    NEXT
            
    IF errFound > 0 THEN
        WScript.Echo Path & ": ** Vulnerable configuration found **"
    ELSE
        WScript.Echo Path & ": ok"
    END IF
END SUB


FUNCTION CheckRootCustomErrorsSection(xmlnodelist, path)

    errCount = 0
    FOR index=0 TO xmlnodeList.length-1
        ret = CheckRootCustomErrorsDisabled(nodeList.Item(index), Path)
	    errCount = errCount + ret
    NEXT

    CheckRootCustomErrorsSection = errCount
END FUNCTION


FUNCTION CheckRootCustomErrorsDisabled(xmlnode, path)
    
    IF StrComp (LCase(xmlnode.getAttribute("mode")), "off") = 0 THEN
        ' WScript.Echo path & ": Custom Error disabled: " & xmlnode.xml
        CheckRootCustomErrorsDisabled = 1
        EXIT FUNCTION
    ELSEIF IsNull(xmlnode.getAttribute("defaultRedirect")) THEN
        ' WScript.Echo path & ": defaultRedirect not set: " & xmlnode.xml
        CheckRootCustomErrorsDisabled = 1
        EXIT FUNCTION
    ELSE
        CheckRootCustomErrorsDisabled = 0
    END IF
    
END FUNCTION


FUNCTION CheckCustomErrorsDisabled(xmlnode, path)
    IF StrComp (LCase(xmlnode.getAttribute("mode")), "off") = 0 THEN
        ' Unsafe config
        ' WScript.Echo path & ": Custom Error disabled: " & xmlnode.xml
        CheckCustomErrorsDisabled = 1
    ELSE
        CheckCustomErrorsDisabled = 0
    END IF
    
END FUNCTION

FUNCTION CheckCustomErrorsAreHomogenous(xmlnode, path)
    IF xmlnode.childNodes.length=0 AND len(xmlNode.getAttribute("defaultRedirect"))>0 THEN
	    CheckCustomErrorsAreHomogenous = 0
        EXIT FUNCTION
    END IF

    SET childNodes = xmlnode.childNodes

    ErrPage404 = ""
    ErrPage500 = ""

    DIM count
    FOR count=0 TO childNodes.length-1
        CALL GetErrorPage(childNodes.Item(count))
    NEXT

    IF StrComp(ErrPage404,"") = 0 AND StrComp(ErrPage500,"") = 0 AND IsNull(xmlNode.getAttribute("defaultRedirect")) THEN
        ' Missing defaultRedirect in this case will cause config to be vulnerable
        'WScript.Echo path & ": missing defaulRedirect URL: " & xmlnode.xml
        CheckCustomErrorsAreHomogenous = 1
        EXIT FUNCTION
    ELSEIF StrComp(ErrPage404,"") = 0 AND StrComp(ErrPage500,"") <> 0 AND StrComp(ErrPage500, xmlNode.getAttribute("defaultRedirect")) <> 0 THEN
        'WScript.Echo path & ": 500 and default error pages differ: " & xmlnode.xml
        CheckCustomErrorsAreHomogenous = 1
        EXIT FUNCTION
    ELSEIF StrComp(ErrPage500,"") = 0 AND StrComp(ErrPage404,"") <> 0 AND StrComp(ErrPage404, xmlNode.getAttribute("defaultRedirect")) <> 0 THEN
        'WScript.Echo path & ": 404 and default error pages differ: " & xmlnode.xml
        CheckCustomErrorsAreHomogenous = 1
        EXIT FUNCTION
    ELSEIF StrComp(ErrPage404, ErrPage500) <> 0 THEN
        'WScript.Echo path & ": 404 and 500 error pages differ: " & xmlnode.xml
        CheckCustomErrorsAreHomogenous = 1
        EXIT FUNCTION
    ELSE 
        CheckCustomErrorsAreHomogenous = 0 
    END IF

END FUNCTION

SUB GetErrorPage(xmlnode)
    IF xmlnode.nodeType <> 1 THEN
        EXIT SUB
    ELSEIF IsNull(xmlnode.getAttribute("statusCode")) THEN
        'Do nothing.
    ELSEIF StrComp(xmlnode.getAttribute("statusCode"), "500") = 0 THEN
        ErrPage500 = xmlnode.getAttribute("redirect")
    ELSEIF StrComp(xmlnode.getAttribute("statusCode"), "404") = 0 THEN
        ErrPage404 = xmlnode.getAttribute("redirect")
    END IF

END SUB

Acknowledgements

Many thanks to Levi Broderick, Nazim Lala, and Stefan Schackow for their contributions to this post.

-Kevin Brown, MSRC Engineering

 

Updated September 18, 2010

Made several improvements to the included script to catch additional corner cases and provide additional error handling. Clarified that an attacker can only retrieve files from within the ASP.Net application.

Updated September 21, 2010

Updated the script to add a check for missing root web.config files, and attached a zip of the script to the post so it can be downloaded directly.

Updated September 29, 2010

Added a note clarifying that the workaround and the detection script are no longer necessary once the security update has been installed. The security update is now available - please see MS10-070 for more information.