-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathscan.py
executable file
·185 lines (156 loc) · 6.84 KB
/
scan.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
#!/usr/bin/env python3
# Find Microsoft Exchange servers that are vulnerable to CVE-2020-0688
#
# Note that this RCE requires valid user credentials which prevents mass-exploitation,
# however the risk of targeted attacks is very high.
#
# This script determines whether Exchange is vulnerable by checking its version.
# Following is a list of internal version numbers for patched revisions:
#
# Exchange Server 2013: 15.0.1497.6
# Exchange Server 2016: 15.1.1847.7, 15.1.1913.7
# Exchange Server 2019: 15.2.464.11, 15.2.529.8
#
# There are two sources of known false negatives (vulnerable Exchange servers that will
# be missed by this script):
#
# 1. Potential Exchange servers are prescreened by identifying hosts that have both
# port 25 and 443 open. If there are any setups that block one of these, they will
# get missed.
#
# 2. Since we're not doing any fancy fingerprinting and only rely on the version
# that Exchange server reports itself, we are limited to first three fields in
# version number.
# For example, latest cumulative update for Exchange Server 2019 is 15.2.529.5
# which has been released in December and is vulnerable. The latest update
# that contains the patch is 15.2.529.8. Both of these self-report their version
# as 15.2.529, so the script will assume that the server is patched even if does
# not have the latest patch installed and is vulnerable.
#
# In practice though it seems that there are either Exchange admins that update their
# systems as soon as patches come out or those that live by "if it works, don't touch
# it" principle. There's no in between.
def check_exchange(ip):
version, servername = lib.get_exchange_version(ip, TIMEOUT)
if version is None:
# not an Exchange Server
return None
vuln = lib.is_vulnerable(version)
if not INCLUDE_NOT_VULNERABLE and not vuln:
# we're only looking for vulnerable Exchanges, skip the ones that look patched
return None
# otherwise we're looking for stats on all Exchanges, both patched and not
# determine reverse DNS (rDNS/PTR)
ptr = lib.get_reverse(ip)
# extract Common Name from the TLS certificate
cert = lib.get_cn(ip)
if INCLUDE_NOT_VULNERABLE:
return (ip, version, vuln, servername, ptr, cert)
else:
return (ip, version, servername, ptr, cert)
if __name__ == "__main__":
import sys
if sys.version_info.minor < 5:
print("At least Python 3.5 is required!")
sys.exit()
import shutil
if shutil.which("zmap") is None:
print("Zmap not found, check that it's installed and placed in your $PATH!")
sys.exit()
from concurrent.futures import ThreadPoolExecutor
import lib # majority of goodies are hidden here
import re
import random
import csv
from argparse import ArgumentParser
parser = ArgumentParser(description="Identify Exchange servers vulnerable to CVE-2020-0688.")
parser.add_argument('input', help="Input file, each line contains one IP/subnet")
parser.add_argument('output', help="File to save the results (in CSV format)")
parser.add_argument('-b', '--blacklist', help="File containing IPs/subnets to avoid")
parser.add_argument('-f', '--full', help="Don't filter seemingly up-to-date Exchange servers",
action="store_true")
parser.add_argument('-p', '--parallel', help="How many requests to send in parallel (default: 1000)")
parser.add_argument('-t', '--timeout', help="Timeout (seconds) to use for network connections (default: 5)")
args = parser.parse_args()
INCLUDE_NOT_VULNERABLE = args.full
TIMEOUT = int(args.timeout) if args.timeout else 5
PARALLEL = int(args.parallel) if args.parallel else 1000
# get enough file descriptors
import resource
nofile_limit = max(1024, PARALLEL)
try:
resource.setrlimit(resource.RLIMIT_NOFILE,
(nofile_limit, nofile_limit))
except:
print(f"Failed setting NOFILE limit to {nofile_limit}!")
sys.exit()
# find hosts that have both ports 25 and 443 open
print("Running mass-scan through zmap.")
candidates = lib.find_candidates(args.input, args.blacklist)
# avoid sending multiple requests to the same subnet
random.shuffle(candidates)
print(f"Short-listed {len(candidates)} mail servers.")
#with open("debug.txt", "w") as f:
# f.write('\n'.join(candidates) + '\n')
# pick Exchange servers, profile them, write output to csv
results = []
with open(args.output, "w") as fout:
writer = csv.writer(fout)
if INCLUDE_NOT_VULNERABLE:
writer.writerow(["IP",
"Exchange Version",
"Vulnerable?",
"Server Name",
"PTR",
"Common Name (from TLS cert)"])
else:
writer.writerow(["IP",
"Exchange Version",
"Server Name",
"PTR",
"Common Name (from TLS cert)"])
print(f"Identifying Exchange versions (concurrency: x{PARALLEL}).")
pool = ThreadPoolExecutor(max_workers=PARALLEL)
for line in pool.map(check_exchange, candidates):
if line is not None:
results.append(line)
writer.writerow(line)
# display basic summary
count = { 2013: 0, 2016: 0, 2019: 0 }
gov, mil = [], []
for line in results:
if INCLUDE_NOT_VULNERABLE:
ip, version, is_vulnerable, server_name, ptr, cn = line
# only count vulnerable servers
if not is_vulnerable:
continue
else:
# all servers are vulnerable in this mode
ip, version, server_name, ptr, cn = line
if version[:4] == "15.0":
count[2013] += 1
elif version[:4] == "15.1":
count[2016] += 1
elif version[:4] == "15.2":
count[2019] += 1
# looks like gov?
if ptr and re.search('\.gov', ptr):
gov.append(ptr)
elif cn and re.search('\.gov', cn):
gov.append(cn)
# looks like mil?
if ptr and re.search('\.mil', ptr):
mil.append(ptr)
elif cn and re.search('\.mil', cn):
mil.append(cn)
print(f"Found {count[2013] + count[2016] + count[2019]} vulnerable Exchange servers:")
print(f" Exchange Server 2013: {count[2013]}")
print(f" Exchange Server 2016: {count[2016]}")
print(f" Exchange Server 2019: {count[2019]}")
if len(gov) + len(mil) > 0:
print("")
print("Servers to check first:")
for host in gov:
print(f" {host}")
for host in mil:
print(f" {host}")