-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathcve_2023_22515_scan.py
273 lines (224 loc) · 10.7 KB
/
cve_2023_22515_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
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
261
262
263
264
265
266
267
268
269
270
271
272
273
#!/usr/bin/env python3
import argparse
import json
import os
import random
import re
import requests
# disable the warnings about the insecure requests
requests.packages.urllib3.disable_warnings()
def get_args():
parser = argparse.ArgumentParser(description="Scan Atlassian Confluence web instances for CVE-2023-22515")
parser.add_argument("-f", help="File containing a list of URLs to scan", dest="file", required=False)
parser.add_argument("-t", help="Comma-separated list of URLs to scan", dest="targets", required=False)
parser.add_argument("-o", help="Output directory", dest="output_dir", default="./")
args = parser.parse_args()
return args
# add status printing with different Print
class Print:
RED = "\033[01;31m"
GREEN = "\033[01;32m"
YELLOW = "\033[01;33m"
BLUE = "\033[01;34m"
MAGENTA = "\033[01;35m"
RESET = "\033[00m"
def template(status_color, status_symbol, text):
print(f"[ {status_color}{status_symbol}{Print.RESET} ] {text}")
def status(text):
Print.template(Print.BLUE, '*', text)
def good(text):
Print.template(Print.GREEN, '+', text)
def error(text):
Print.template(Print.RED, '-', text)
def warning(text):
Print.template(Print.YELLOW, '!', text)
def unknown(text):
Print.template(Print.MAGENTA, '?', text)
def code_red(text):
Print.template(Print.RED, '!!!', text)
def banner():
print(Print.BLUE + '#' * 80)
print(f"""\t\t {Print.RED}CVE-2023-22515 {Print.GREEN}Atlassian Confluence {Print.RED}Scanner\n\t\t\t\t{Print.BLUE}by {Print.RED}Erik Wynter {Print.BLUE}""")
print('#' * 80)
print(Print.RESET)
# the main class with the core logic
class Confluence:
# user agents to make the requests seem a little more legit
# one of these will be selected at random whenever the script runs
USER_AGENTS = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36', # chrome
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0', # firefox
'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15', # safari
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.61' # edge
]
# vulnerable endpoint. the patch removes this altogether
SERVER_INFO_URI = '/server-info.action'
# Exhaustive list of vulnerable versions taken from the official advisory
VULNERABLE_VERSIONS = ['8.0.0', '8.0.1', '8.0.2', '8.0.3', '8.0.4', '8.1.0', '8.1.1', '8.1.3', '8.1.4', '8.2.0', '8.2.1', '8.2.2', '8.2.3', '8.3.0', '8.3.1', '8.3.2', '8.4.0', '8.4.1', '8.4.2', '8.5.0', '8.5.1']
def __init__(self):
self.results = []
# set a random user agent
self.user_agent = self.USER_AGENTS[random.randint(0, len(self.USER_AGENTS) - 1)]
# sanity check for the provided options
def validate_options(self):
options = get_args()
self.targets = []
if not options.file and not options.targets:
Print.error("Specify either a targets file (-f) and/or a comma-separated list of targets (-t).")
return False
if options.file:
if not os.path.isfile(options.file):
Print.error(f"The provided targets file '{options.file}' does not exist.")
return False
if os.path.getsize(options.file) == 0:
Print.error(f"The provided targets file '{options.file}' is empty.")
return False
# load the targets from the file
with open (options.file, 'r') as f:
self.targets += f.read().splitlines()
if options.targets:
self.targets += options.targets.split(",")
self.targets = list(set(self.targets))
out_dir = os.path.abspath(options.output_dir)
if not os.path.exists(out_dir):
Print.warning(f"The provided output directory '{out_dir}' does not exist, so this script will create it.")
try:
os.makedirs(out_dir)
except Exception as err:
Print.error(f"Failed to create the output directory '{out_dir}'.")
Print.error(f"Error: {err}")
return False
self.output_dir = out_dir
return True
# central method for iterating of the targets and performing all necessary actions
def run(self):
Print.status(f"Proceeding to scan {len(self.targets)} target(s)...")
for target in self.targets:
if target.endswith('/'):
target = target[:-1]
# let's get the target info
target_results = self.identify_target(target)
if not target_results: continue
self.results.append(target_results)
# method for trying to identify the product
def identify_target(self, target) -> dict | None:
target_info = {
'target_url': target,
'product': 'unknown',
'version': 'unknown',
'vulnerability_status': 'unknown',
}
# check if the target is up
Print.status(f"{target} - Scanning target...")
headers = { 'User-Agent': self.user_agent }
try:
res = requests.get(target, headers=headers, verify=False, timeout=5)
except Exception as err:
Print.error(f"Error connecting to {target}: {err}")
return None
if not res.ok:
Print.error(f"{target} - Received status code {res.status_code}. Skipping this target.")
return None
if not res.text:
Print.error(f"{target} - Received empty HTTP response body.")
return None
if not 'Atlassian Confluence' in res.text:
Print.error(f"{target} - Target does not appear to be Atlassian Confluence. Skipping this target.")
return None
version_match = re.search(r'Atlassian Confluence ([\d\.]+)<', res.text)
if not version_match:
version_match = re.search(r'"ajs-version-number" content="([\d\.]+)"', res.text)
if not version_match:
Print.warning(f"{target} - Could not obtain the version. The script will continue, but the target may not be Atlassian Confluence.")
target_info['product'] = 'Atlassian Confluence ?'
else:
version = version_match.group(1)
target_info['product'] = 'Atlassian Confluence'
target_info['version'] = version
initial_vuln_status = 'not vulnerable'
if target_info['version'] != 'unknown':
if version in self.VULNERABLE_VERSIONS:
Print.warning(f"{target} - The target is Atlassian Confluence {version}, which is a vulnerable version.")
initial_vuln_status = "potentially vulnerable"
else:
Print.status(f"{target} - The target is Atlassian Confluence {version}, which is not a vulnerable version.")
target_info['vulnerability_status'] = initial_vuln_status
return target_info
else:
Print.status(f"{target} - The target may be Atlassian Confluence, but the version could not be identified.")
# Check if the vulnerable endpoint is available
Print.status(f"{target} - Checking for the vulnerable endpoint {self.SERVER_INFO_URI}")
try:
res = requests.get(f"{target}{self.SERVER_INFO_URI}", headers=headers, verify=False, allow_redirects=False, timeout=5)
except Exception as err:
Print.error(f"{target} - Error connecting to {target}{self.SERVER_INFO_URI}: {err}")
return target_info
if res.status_code != 200:
Print.good(f"{target} - The vulnerable endpoint {self.SERVER_INFO_URI} is not accessible. Received status code {res.status_code}. The target is likely patched.")
if initial_vuln_status == 'potentially vulnerable':
target_info['vulnerability_status'] = 'likely not exploitable'
else:
target_info['vulnerability_status'] = 'not vulnerable'
return target_info
Print.code_red(f"{target} - The vulnerable endpoint {self.SERVER_INFO_URI} is accessible.")
target_info['vulnerability_status'] = 'likely vulnerable'
return target_info
# save the results and print a nice report
def save_and_report(self):
if not self.results:
Print.error("No systems were identified as Atlassian Confluence devices. Exiting.")
return
# save the results to a file
json_file = f"{self.output_dir}/cve_2023_22515_scan.json"
with open(json_file, 'w') as f:
json.dump(self.results, f, indent=2)
Print.status(f"The full scan results for identified systems were saved to {json_file}\n")
# generate a human-readable report
Print.status("Results:")
print('-' * 80)
human_readable_report = f"{self.output_dir}/cve_2023_22515_scan.txt"
vuln_status_ct_hash = {
'likely vulnerable': 0,
'likely not exploitable': 0,
'unknown': 0,
'not vulnerable': 0
}
with open(human_readable_report, 'w') as f:
finding_sort_order = { 'likely vulnerable':0, 'likely_not_exploitable': 1, 'not vulnerable': 2, 'unknown': 3}
sorted_results = sorted(self.results, key=lambda word: finding_sort_order[word['vulnerability_status']])
for result in sorted_results:
info_line = f"{result['target_url']} - Product: {result['product']} Version: {result['version']}"
vuln_status = result['vulnerability_status']
info_line += f" - vulnerability status: {vuln_status}"
f.write(f"{info_line}\n")
vuln_status_ct_hash[vuln_status] += 1
if vuln_status == 'not vulnerable': Print.good(info_line)
elif vuln_status == 'likely vulnerable': Print.code_red(info_line)
elif vuln_status == 'likely not exploitable': Print.warning(info_line)
elif vuln_status == 'unknown': Print.unknown(info_line)
print('-' * 80)
Print.status(f"The human-readable report was saved to {human_readable_report}\n")
# print the summary
Print.status(f"Summary:")
print('-' * 80)
for vuln_status, vuln_ct in vuln_status_ct_hash.items():
if vuln_ct > 0:
if vuln_status == 'not vulnerable': Print.good(f"Not vulnerable targets: {vuln_ct}")
elif vuln_status == 'likely vulnerable': Print.code_red(f"Likely vulnerable targets: {vuln_ct}")
elif vuln_status == 'likely not exploitable': Print.warning(f"Likely not exploitable targets: {vuln_ct}")
elif vuln_status == 'unknown': Print.unknown(f"Targets with unknown vulnerability status: {vuln_ct}")
print('-' * 80)
def main():
confluence = Confluence()
options_check = confluence.validate_options()
if not options_check: exit(1)
Print.banner()
confluence.run()
if not confluence.results:
Print.status("No relevant results were obtained. Exiting.")
exit(0)
confluence.save_and_report()
print()
Print.status("Done! Taking a nap.")
if __name__ == "__main__":
main()