-
Notifications
You must be signed in to change notification settings - Fork 4
/
certina.py
397 lines (316 loc) · 13.3 KB
/
certina.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
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
import ssl
import datetime
import argparse
import warnings
import requests
import re
import socket
import base64
from cryptography import x509
warnings.filterwarnings("ignore")
RED = "\033[91m"
YELLOW = "\033[93m"
LIGHT_GREEN = "\033[92;1m"
LIGHT_BLUE = "\033[96m"
RESET = "\033[0m"
USERAGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
"""
Grabs certificate from endpoint with SSL library
NOTE: Some endpoints may return error. Check domain or add www.
"""
def grabCertificate(endpoint, filePointer):
try:
certificate: bytes = ssl.get_server_certificate((endpoint, 443)).encode('utf-8')
x509Cert = x509.load_pem_x509_certificate(certificate)
except Exception as e:
printWriter(f"Failed {endpoint}: {e}", filePointer)
exit()
printWriter(f"<<<<-----Analysing {endpoint} Certificate----->>>>", filePointer, YELLOW)
return x509Cert
"""
Alternative method using raw sockets to grab certificate
WARNING: Might be dangerous with use of self created sockets, use with caution
"""
def grabWithSocket(endpoint, filePointer):
printWriter(f"<<<<-----Analysing {endpoint} Certificate----->>>>", filePointer, YELLOW)
printWriter("[!] WARNING: Grabbing certificate with socket", filePointer, YELLOW)
dst = (endpoint, 443)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5) # Set 5s timeout for unreachable hosts
try:
s.connect(dst)
except TimeoutError:
return None
# Upgrade the socket to SSL (Try, Except to detect non SSL sites and handshake errors)
try:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
s = ctx.wrap_socket(s, server_hostname=dst[0])
except:
return None
cert_bin = s.getpeercert(True)
certb64 = base64.b64encode(cert_bin).decode('ascii')
s.shutdown(socket.SHUT_RDWR)
s.close()
# Convert DER to PEM and loading it like grabCertificate()
pem_cert = "-----BEGIN CERTIFICATE-----\n"
pem_cert += certb64
pem_cert += "\n-----END CERTIFICATE-----\n"
certificate_bytes = pem_cert.encode('utf-8')
x509Cert = x509.load_pem_x509_certificate(certificate_bytes)
return x509Cert
"""
Parses certificate information of x509 certificate
"""
def getCertificateInfo(x509Cert, filePointer):
# Obtaining basic cert information
try:
commonName = x509Cert.subject.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME)[0].value
except:
printWriter("[!] Failed to get cert info, please check domain or add www.", filePointer, RED)
exit()
# Multi try blocks to catch empty values
try:
organizationName = x509Cert.subject.get_attributes_for_oid(x509.oid.NameOID.ORGANIZATION_NAME)[0].value
except IndexError:
organizationName = "<Not Part Of Certificate>"
try:
subjectSerialNumber = x509Cert.subject.get_attributes_for_oid(x509.oid.NameOID.SERIAL_NUMBER)[0].value
except IndexError:
subjectSerialNumber = "<Not Part Of Certificate>"
try:
countryName = x509Cert.subject.get_attributes_for_oid(x509.oid.NameOID.COUNTRY_NAME)[0].value
except IndexError:
countryName = "<Not Part Of Certificate>"
try:
localityName = x509Cert.subject.get_attributes_for_oid(x509.oid.NameOID.LOCALITY_NAME)[0].value
except IndexError:
localityName = "<Not Part Of Certificate>"
try:
stateProvince = x509Cert.subject.get_attributes_for_oid(x509.oid.NameOID.STATE_OR_PROVINCE_NAME)[0].value
except IndexError:
stateProvince = "<Not Part Of Certificate>"
# Add on accordingly if more fields are needed
## Check validity of certificate
validityStart = x509Cert.not_valid_before
validityEnd = x509Cert.not_valid_after
validity = True
today = datetime.datetime.utcnow()
if not validityStart < today < validityEnd:
validity = False
# Obtaining SAN domains from the certificate extension section
sanExtension = x509Cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
sanDomains = sanExtension.value.get_values_for_type(x509.DNSName)
# Obtaining issuer information
issuer = x509Cert.issuer
country = issuer.get_attributes_for_oid(x509.oid.NameOID.COUNTRY_NAME)[0].value
organization = issuer.get_attributes_for_oid(x509.oid.NameOID.ORGANIZATION_NAME)[0].value
issuerCN = issuer.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME)[0].value
# I only grabbed a few fields here, for intel use, if you need other fields, just add on
certInfo = {
"CommonName" : commonName,
"Organization" : organizationName,
"SubjectSerial" : subjectSerialNumber,
"Country" : countryName,
"Locality" : localityName,
"stateProvince" : stateProvince,
"Validity" : validity,
"Issuer" : f"CN={issuerCN}, O={organization}, C={country}",
"SAN" : sanDomains
}
return certInfo
"""
Returns SAN domains extracted from x509 cert
"""
def extractSAN(endpoint, sanList, requestFlag, filePointer):
uniqEndpoints = []
for domain in sanList:
validDomain, cleanedDomain = cleanDomain(endpoint, domain)
if validDomain:
uniqEndpoints.append(cleanedDomain)
sanUniq = set(uniqEndpoints)
printWriter(f"[+] {len(sanUniq)} Domains retrieved from SAN", filePointer, LIGHT_BLUE)
for d in sanUniq:
if requestFlag:
status, title, urlScheme = reqDomain(d)
if status:
if title:
printWriter(f"{d} [{urlScheme}] [{status}] [{title}]", filePointer)
else:
printWriter(f"{d} [{urlScheme}] [{status}]", filePointer)
else:
printWriter(f"{d}", filePointer)
else:
printWriter(d, filePointer)
return sanUniq
"""
Returns domains from certificate transparency databases (crt.sh)
"""
def crtshQuery(domain, requestFlag, filePointer):
crtResult = True
crtList = []
# Warning, sometimes crt.sh will throw a "Flush request" error, this is their server issue and can't be fixed
try:
r = requests.get(f"https://crt.sh/?q={domain.strip()}&output=json", headers={'User-Agent':USERAGENT})
jsonResult = r.json()
except Exception as e:
crtResult = False
printWriter(f"-----Error or no results from crt.sh-----", filePointer, RED)
return crtList
if r.status_code != 200:
printWriter(f"-----Error resp from crt.sh [status: {r.status_code}]-----", filePointer, RED)
return crtList
if crtResult:
for result in jsonResult:
cName = result["common_name"]
if cName is not None:
inScopeDomain, cleanedDomain = cleanDomain(domain, cName)
if inScopeDomain:
crtList.append(cleanedDomain)
matchingIdentities = result["name_value"].strip().split("\n")
if len(matchingIdentities) > 0:
for singleDomain in matchingIdentities:
inScopeDomain, cleanedDomain = cleanDomain(domain, singleDomain)
if inScopeDomain:
crtList.append(cleanedDomain)
crtUniq = set(crtList)
printWriter(f"[+] {len(crtUniq)} Domains retrieved from crt.sh", filePointer, LIGHT_BLUE)
for d in crtUniq:
if requestFlag:
status, title, urlScheme = reqDomain(d)
if status:
if title:
printWriter(f"{d} [{urlScheme}] [{status}] [{title}]", filePointer)
else:
printWriter(f"{d} [{urlScheme}] [{status}]", filePointer)
else:
printWriter(f"{d}", filePointer)
else:
printWriter(d, filePointer)
return crtUniq
"""
Query Censys API for intel
"""
def censysQuery():
print("Future works")
"""
Sets validity of domain after checking scope, validity and cleaning it
"""
def cleanDomain(domain, testDomain):
validDomain = False
if '*.' in testDomain:
testDomain = testDomain.replace('*.', '')
if f".{domain}" in testDomain:
validDomain = True
if re.search(r'[^a-zA-Z0-9-.]', testDomain):
validDomain = False
return validDomain, testDomain
"""
Requests the title page to check
"""
def reqDomain(domain, timeout=2, urlScheme="https"):
title = None
failedRequest = False
try:
r = requests.get('https://' + domain.strip(), timeout=timeout, allow_redirects=True, verify=True, headers={'User-Agent':USERAGENT})
except TimeoutError:
failedRequest = True
except:
failedRequest = True
if failedRequest:
try:
r = requests.get('http://' + domain.strip(), timeout=timeout, allow_redirects=True, headers={'User-Agent':USERAGENT})
urlScheme="http"
except TimeoutError:
return None, None, None
except:
return None, None, None
searchTitle = re.search(r'(?<=<title>).*(?=</title>)', r.text, re.IGNORECASE)
if searchTitle is not None:
title = searchTitle.group(0)
return str(r.status_code), title, urlScheme
"""
Prints and write to file if --output is set
"""
def printWriter(stdout, filePointer=None, color=None):
if color:
print(f"{color}{stdout}{RESET}")
else:
print(stdout)
if filePointer is not None:
filePointer.write(stdout + "\n")
def printBannerArt():
art = rf"""{LIGHT_BLUE} ____________ ___________ _____
/ ___/ __/ _ \/_ __/ _/ |/ / _ |
/ /__/ _// , _/ / / _/ // / __ |n0mi1k
\___/___/_/|_| /_/ /___/_/|_/_/ |_| v1.0{RESET}
"""
print(art)
def main():
printBannerArt()
parser = argparse.ArgumentParser(prog='certinfo.py',
description='A certificate enumeration and information gathering tool.',
usage='%(prog)s -e ENDPOINTS')
parser.add_argument("-d", "--domain", help="Endpoints to scan separated by commas", required=False)
parser.add_argument("-s", "--socket", help="Use self-defined socket to grab certificate", default=False, action=argparse.BooleanOptionalAction)
parser.add_argument("-i", "--input", help="Input file containing domains to analyse", required=False)
parser.add_argument("-o", "--output", help="File to output the results", required=False)
parser.add_argument("-c", "--certonly", help="Show only certificate info without further enumeration", default=False, action=argparse.BooleanOptionalAction)
parser.add_argument("-r", "--request", help="Follow up with GET request", default=False, action=argparse.BooleanOptionalAction)
args = parser.parse_args()
inputFile = args.input
outputFile = args.output
requestFlag = args.request
certOnlyFlag = args.certonly
domainString = args.domain
filePointer = None
if outputFile:
filePointer = open(outputFile, 'w')
if domainString:
endpoints = domainString.replace(" ", "").split(",")
if inputFile:
endpoints = []
try:
with open(inputFile, 'r') as inFile:
for domains in inFile:
endpoints.append(domains.strip())
except FileNotFoundError:
print(f"{RED}[!] Error: Input file does not exist{RESET}")
exit()
if domainString is None and inputFile is None:
print(f"{RED}[!] Error: No endpoints specified with -e or -i{RESET}")
exit()
for endpoint in endpoints:
if 'http://' in endpoint:
print(f"{RED}[!] Scheme http:// included, stripping away...{RESET}")
endpoint = endpoint.lstrip('http://')
elif 'https://' in endpoint:
print(f"{RED}[!] Scheme http:// included, stripping away...{RESET}")
endpoint = endpoint.lstrip('https://')
if not args.socket:
parsedCert = grabCertificate(endpoint, filePointer)
else:
parsedCert = grabWithSocket(endpoint, filePointer)
certInfo = getCertificateInfo(parsedCert, filePointer)
for fieldKey in certInfo:
if fieldKey != "SAN":
printWriter(f"-> {fieldKey}: {certInfo[fieldKey]}", filePointer)
if certOnlyFlag:
printWriter(f"-> SAN DNS Name(s): {certInfo['SAN']}", filePointer)
continue
sanUniq = extractSAN(endpoint, certInfo["SAN"], requestFlag, filePointer) # Use this set if you need to implement on other tools
crtUniq = crtshQuery(endpoint, requestFlag, filePointer) # Use this set if you need to implement on other tools
if len(sanUniq) != 0 and len(crtUniq) != 0:
combinedDomains = (sanUniq).union(crtUniq)
printWriter(f"-----Total {endpoint} domains discovered: {len(combinedDomains)}-----", filePointer, LIGHT_GREEN)
elif len(sanUniq) != 0:
printWriter(f"-----Total {endpoint} domains discovered: {len(sanUniq)}-----", filePointer, LIGHT_GREEN)
elif len(crtUniq) != 0:
printWriter(f"-----Total {endpoint} domains discovered: {len(crtUniq)}-----", filePointer, LIGHT_GREEN)
else:
printWriter(f"-----No {endpoint} domains discovered-----", filePointer, RED)
if outputFile:
filePointer.close()
if __name__ == '__main__':
main()