Nmap 스캔 결과 XML 형식 분석 방법

NmapBeginner
지금 연습하기

소개

사이버 보안 분야에서 네트워크 스캔 결과를 이해하고 분석하는 것은 안전한 인프라를 유지하는 데 매우 중요합니다. Nmap (Network Mapper) 은 네트워크 탐색 및 보안 감사를 위해 가장 널리 사용되는 도구 중 하나입니다. 이 튜토리얼은 XML 형식의 Nmap 스캔 결과를 해석하는 과정을 안내하여, 사이버 보안 요구 사항에 맞게 이 강력한 도구를 활용하는 데 필요한 기술을 제공합니다.

이 랩을 마치면, XML 출력을 사용하여 Nmap 스캔을 실행하고, XML 데이터의 구조를 이해하며, 명령줄 도구와 Python 스크립트를 사용하여 귀중한 정보를 추출하고, 스캔 결과에서 잠재적인 보안 문제를 식별하는 방법을 알게 될 것입니다.

Nmap 설치 및 기본 XML 스캔 실행

Nmap 이란 무엇인가요?

Nmap (Network Mapper) 은 네트워크 탐색 및 보안 감사를 위한 무료 오픈 소스 유틸리티입니다. 전 세계의 보안 전문가들은 네트워크에서 어떤 장치가 실행 중인지 식별하고, 사용 가능한 호스트와 제공하는 서비스를 발견하며, 열린 포트를 찾고, 보안 취약점을 탐지하기 위해 이 도구를 사용합니다.

Nmap 설치

시스템에 Nmap 을 설치하는 것으로 시작해 보겠습니다. 터미널 창을 열고 다음 명령을 입력합니다.

sudo apt update
sudo apt install nmap -y

설치가 완료되면 Nmap 버전을 확인하여 Nmap 이 올바르게 설치되었는지 확인합니다.

nmap --version

다음과 유사한 출력을 볼 수 있습니다.

Nmap version 7.80 ( https://nmap.org )
Platform: x86_64-pc-linux-gnu
Compiled with: liblua-5.3.3 openssl-1.1.1f libssh2-1.8.0 libz-1.2.11 libpcre-8.39 nmap-libpcap-1.9.1 nmap-libdnet-1.12 ipv6
Compiled without:
Available nsock engines: epoll poll select

XML 출력을 사용한 기본 Nmap 스캔 실행

Nmap 은 스캔 결과를 XML 형식으로 저장할 수 있으며, 이는 데이터를 프로그래밍 방식으로 분석하는 구조화된 방법을 제공합니다. 로컬 머신에 대한 기본 스캔을 실행하고 결과를 XML 형식으로 저장해 보겠습니다.

sudo nmap -A -T4 -oX ~/project/localhost_scan.xml localhost

이 명령은 다음을 수행합니다.

  • -A: OS 탐지, 버전 탐지, 스크립트 스캔 및 traceroute 를 활성화합니다.
  • -T4: 타이밍 템플릿을 "aggressive"로 설정합니다.
  • -oX: 출력이 XML 형식이어야 함을 지정합니다.
  • localhost: 스캔 대상 (자신의 머신)

스캔을 완료하는 데 1~2 분 정도 걸릴 수 있습니다. 완료되면 터미널에 스캔 결과 요약이 표시됩니다.

XML 스캔 결과 보기

방금 생성한 XML 파일을 살펴보겠습니다.

cat ~/project/localhost_scan.xml

출력은 스캔에 대한 자세한 정보를 포함하는 구조화된 XML 문서가 됩니다. 처음에는 압도적으로 보일 수 있지만, 다음 단계에서 이를 해석하는 방법을 배우게 됩니다.

head 명령을 사용하여 XML 파일의 기본 구조도 확인해 보겠습니다.

head -n 20 ~/project/localhost_scan.xml

이것은 XML 파일의 처음 20 줄을 보여주며, 구조를 엿볼 수 있습니다.

XML 출력 구조 검토

Nmap XML 형식 이해

Nmap XML 출력은 스캔 정보를 논리적인 방식으로 구성하는 계층적 구조를 따릅니다. 이 구조의 주요 요소를 살펴보겠습니다.

  1. <nmaprun>: 모든 스캔 정보를 포함하는 루트 요소
  2. <scaninfo>: 스캔 유형 및 매개변수에 대한 세부 정보
  3. <host>: 각 스캔된 호스트에 대한 정보
    • <status>: 호스트가 작동 중인지 여부
    • <address>: IP 및 MAC 주소
    • <hostnames>: DNS 이름
    • <ports>: 스캔된 포트에 대한 세부 정보
      • <port>: 특정 포트에 대한 정보
        • <state>: 포트가 열려 있는지, 닫혀 있는지 또는 필터링되었는지 여부
        • <service>: 서비스 정보 (사용 가능한 경우)
    • <os>: 운영 체제 탐지 결과
    • <times>: 스캔에 대한 타이밍 정보

명령줄 도구를 사용하여 정보 추출

XML 파일은 원시 형식으로 읽기가 어려울 수 있습니다. 몇 가지 명령줄 도구를 사용하여 스캔 결과에서 특정 정보를 추출해 보겠습니다.

먼저, grepwc를 사용하여 열린 포트 수를 세어 보겠습니다.

grep -c "state=\"open\"" ~/project/localhost_scan.xml

이 명령은 XML 파일에서 state="open"의 인스턴스를 검색하고 개수를 셉니다.

다음으로, -A 옵션과 함께 grep을 사용하여 열린 포트와 해당 서비스를 식별하여 일치하는 줄을 표시합니다.

grep -A 3 "state=\"open\"" ~/project/localhost_scan.xml

이렇게 하면 열린 포트의 각 인스턴스와 그 뒤에 오는 3 줄이 표시되며, 일반적으로 서비스 정보가 포함됩니다.

xmllint를 사용하여 XML 파일을 더 쉽게 읽을 수 있도록 형식을 지정할 수도 있습니다. 먼저 설치해 보겠습니다.

sudo apt install libxml2-utils -y

이제 XML 파일의 형식을 지정해 보겠습니다.

xmllint --format ~/project/localhost_scan.xml > ~/project/formatted_scan.xml

형식이 지정된 파일을 살펴보겠습니다.

head -n 50 ~/project/formatted_scan.xml

이렇게 하면 형식이 지정된 XML 파일의 처음 50 줄이 표시되어 훨씬 쉽게 읽을 수 있습니다.

마지막으로, XPath 를 사용하여 xmllint를 사용하여 호스트 상태에 대한 특정 정보를 추출해 보겠습니다.

xmllint --xpath "//host/status/@state" ~/project/localhost_scan.xml

이 명령은 XPath 를 사용하여 호스트 요소 아래의 모든 상태 요소의 상태 속성을 추출합니다.

Python 을 사용하여 Nmap XML 파싱

Python 을 사용한 XML 파싱 소개

Python 은 XML 파일을 파싱하기 위한 강력한 라이브러리를 제공합니다. 이 단계에서는 Nmap 스캔 결과를 파싱하고 더 읽기 쉬운 형식으로 표시하는 간단한 Python 스크립트를 만들 것입니다.

기본 XML 파서 생성

xml.etree.ElementTree 모듈을 사용하여 Nmap XML 파일을 파싱하는 Python 스크립트를 만들어 보겠습니다. 이 모듈은 Python 표준 라이브러리에 포함되어 있으므로 추가로 설치할 필요가 없습니다.

프로젝트 디렉토리에 parse_nmap.py라는 새 파일을 만듭니다.

nano ~/project/parse_nmap.py

다음 코드를 복사하여 편집기에 붙여넣습니다.

#!/usr/bin/env python3
import xml.etree.ElementTree as ET
import sys

def parse_nmap_xml(xml_file):
    try:
        ## Parse the XML file
        tree = ET.parse(xml_file)
        root = tree.getroot()

        ## Print scan information
        print("Nmap Scan Report")
        print("=" * 50)
        print(f"Scan started at: {root.get('startstr')}")
        print(f"Nmap version: {root.get('version')}")
        print(f"Nmap command: {root.get('args')}")
        print("=" * 50)

        ## Process each host in the scan
        for host in root.findall('host'):
            ## Get host addresses
            for addr in host.findall('address'):
                if addr.get('addrtype') == 'ipv4':
                    ip_address = addr.get('addr')
                    print(f"\nHost: {ip_address}")

            ## Get hostname if available
            hostnames = host.find('hostnames')
            if hostnames is not None:
                for hostname in hostnames.findall('hostname'):
                    print(f"Hostname: {hostname.get('name')}")

            ## Get host status
            status = host.find('status')
            if status is not None:
                print(f"Status: {status.get('state')}")

            ## Process ports
            ports = host.find('ports')
            if ports is not None:
                print("\nOpen Ports:")
                print("-" * 50)
                print(f"{'PORT':<10}{'STATE':<10}{'SERVICE':<15}{'VERSION'}")
                print("-" * 50)

                for port in ports.findall('port'):
                    port_id = port.get('portid')
                    protocol = port.get('protocol')

                    ## Get port state
                    state = port.find('state')
                    port_state = state.get('state') if state is not None else "unknown"

                    ## Skip closed ports
                    if port_state != "open":
                        continue

                    ## Get service information
                    service = port.find('service')
                    if service is not None:
                        service_name = service.get('name', '')
                        service_product = service.get('product', '')
                        service_version = service.get('version', '')
                        service_info = f"{service_product} {service_version}".strip()
                    else:
                        service_name = ""
                        service_info = ""

                    print(f"{port_id}/{protocol:<5} {port_state:<10}{service_name:<15}{service_info}")

            ## Get OS detection information
            os = host.find('os')
            if os is not None:
                print("\nOS Detection:")
                for osmatch in os.findall('osmatch'):
                    print(f"OS: {osmatch.get('name')} (Accuracy: {osmatch.get('accuracy')}%)")

    except ET.ParseError as e:
        print(f"Error parsing XML file: {e}")
        return False
    except Exception as e:
        print(f"Error: {e}")
        return False

    return True

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print(f"Usage: {sys.argv[0]} <nmap_xml_file>")
        sys.exit(1)

    xml_file = sys.argv[1]
    if not parse_nmap_xml(xml_file):
        sys.exit(1)

Ctrl+O를 누른 다음 Enter를 눌러 파일을 저장하고 Ctrl+X로 nano 를 종료합니다.

이제 스크립트를 실행 가능하게 만듭니다.

chmod +x ~/project/parse_nmap.py

파서 실행

이전에 생성한 Nmap XML 파일에서 Python 스크립트를 실행해 보겠습니다.

python ~/project/parse_nmap.py ~/project/localhost_scan.xml

다음과 같은 스캔 결과가 깔끔하게 형식화된 출력을 볼 수 있습니다.

  • 기본 스캔 정보
  • 호스트 세부 정보
  • 열린 포트 및 서비스
  • 사용 가능한 경우 운영 체제 탐지 결과

이 형식화된 출력은 원시 XML 파일보다 훨씬 읽기 쉽고 스캔에서 가장 중요한 정보를 강조 표시합니다.

파서 코드 이해

Python 스크립트가 수행하는 작업을 검토해 보겠습니다.

  1. xml.etree.ElementTree를 사용하여 XML 파일을 파싱합니다.
  2. 루트 요소에서 일반적인 스캔 정보를 추출합니다.
  3. 스캔에서 발견된 각 호스트에 대해:
    • IP 주소와 호스트 이름을 추출합니다.
    • 호스트가 작동 중인지 여부를 결정합니다.
    • 포트 번호, 프로토콜, 서비스 이름 및 버전을 포함하여 모든 열린 포트를 나열합니다.
    • 사용 가능한 경우 OS 탐지 정보를 추출합니다.

이 구조화된 접근 방식을 통해 XML 복잡성을 무시하면서 가장 관련성이 높은 정보에 집중할 수 있습니다.

보안 관련 정보 추출

Nmap 스캔에서 얻는 보안 통찰력

이제 Nmap XML 데이터를 파싱할 수 있으므로 스크립트를 확장하여 보안 관련 정보를 추출해 보겠습니다. 여기에는 다음이 포함됩니다.

  1. 잠재적으로 위험한 열린 포트 식별
  2. 구식 서비스 버전 감지
  3. 보안 문제 요약

보안 분석에 초점을 맞춘 파서의 향상된 버전을 만들어 보겠습니다.

보안 분석 스크립트 생성

security_analysis.py라는 새 파일을 만듭니다.

nano ~/project/security_analysis.py

다음 코드를 복사하여 붙여넣습니다.

#!/usr/bin/env python3
import xml.etree.ElementTree as ET
import sys
import datetime

## Define potentially risky ports
HIGH_RISK_PORTS = {
    '21': 'FTP - File Transfer Protocol (often unencrypted)',
    '23': 'Telnet - Unencrypted remote access',
    '25': 'SMTP - Email transfer (may allow relay)',
    '445': 'SMB - Windows file sharing (potential target for worms)',
    '3389': 'RDP - Remote Desktop Protocol (target for brute force)',
    '1433': 'MSSQL - Microsoft SQL Server',
    '3306': 'MySQL - Database access',
    '5432': 'PostgreSQL - Database access'
}

## Services with known security issues
OUTDATED_SERVICES = {
    'ssh': [
        {'version': '1', 'reason': 'SSHv1 has known vulnerabilities'},
        {'version': 'OpenSSH 7', 'reason': 'Older OpenSSH versions have multiple CVEs'}
    ],
    'http': [
        {'version': 'Apache httpd 2.2', 'reason': 'Apache 2.2.x is end-of-life'},
        {'version': 'Apache httpd 2.4.1', 'reason': 'Apache versions before 2.4.30 have known vulnerabilities'},
        {'version': 'nginx 1.14', 'reason': 'Older nginx versions have security issues'}
    ]
}

def analyze_security(xml_file):
    try:
        ## Parse the XML file
        tree = ET.parse(xml_file)
        root = tree.getroot()

        ## Prepare the report
        report = []
        report.append("NMAP SECURITY ANALYSIS REPORT")
        report.append("=" * 50)
        report.append(f"Report generated on: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
        report.append(f"Scan started at: {root.get('startstr')}")
        report.append(f"Scan command: {root.get('args')}")
        report.append("=" * 50)

        ## Track security findings
        high_risk_services = []
        potentially_outdated = []
        exposed_services = []

        ## Process each host in the scan
        for host in root.findall('host'):
            ## Get host addresses
            ip_address = None
            for addr in host.findall('address'):
                if addr.get('addrtype') == 'ipv4':
                    ip_address = addr.get('addr')

            hostname = "Unknown"
            hostnames = host.find('hostnames')
            if hostnames is not None:
                hostname_elem = hostnames.find('hostname')
                if hostname_elem is not None:
                    hostname = hostname_elem.get('name')

            report.append(f"\nHOST: {ip_address} ({hostname})")
            report.append("-" * 50)

            ## Process ports
            ports = host.find('ports')
            if ports is None:
                report.append("No port information available")
                continue

            open_ports = 0
            for port in ports.findall('port'):
                port_id = port.get('portid')
                protocol = port.get('protocol')

                ## Get port state
                state = port.find('state')
                if state is None or state.get('state') != "open":
                    continue

                open_ports += 1

                ## Get service information
                service = port.find('service')
                if service is None:
                    service_name = "unknown"
                    service_product = ""
                    service_version = ""
                else:
                    service_name = service.get('name', 'unknown')
                    service_product = service.get('product', '')
                    service_version = service.get('version', '')

                service_full = f"{service_product} {service_version}".strip()

                ## Check if this is a high-risk port
                if port_id in HIGH_RISK_PORTS:
                    high_risk_services.append(f"{ip_address}:{port_id} ({service_name}) - {HIGH_RISK_PORTS[port_id]}")

                ## Check for outdated services
                if service_name in OUTDATED_SERVICES:
                    for outdated in OUTDATED_SERVICES[service_name]:
                        if outdated['version'] in service_full:
                            potentially_outdated.append(f"{ip_address}:{port_id} - {service_name} {service_full} - {outdated['reason']}")

                ## Track all exposed services
                exposed_services.append(f"{ip_address}:{port_id}/{protocol} - {service_name} {service_full}")

            report.append(f"Open ports: {open_ports}")

        ## Add security findings to report
        report.append("\nSECURITY FINDINGS")
        report.append("=" * 50)

        ## High-risk services
        report.append("\nHIGH-RISK SERVICES")
        report.append("-" * 50)
        if high_risk_services:
            for service in high_risk_services:
                report.append(service)
        else:
            report.append("No high-risk services detected")

        ## Potentially outdated services
        report.append("\nPOTENTIALLY OUTDATED SERVICES")
        report.append("-" * 50)
        if potentially_outdated:
            for service in potentially_outdated:
                report.append(service)
        else:
            report.append("No potentially outdated services detected")

        ## Exposed services inventory
        report.append("\nEXPOSED SERVICES INVENTORY")
        report.append("-" * 50)
        if exposed_services:
            for service in exposed_services:
                report.append(service)
        else:
            report.append("No exposed services detected")

        ## Write the report to a file
        report_file = "security_report.txt"
        with open(report_file, 'w') as f:
            f.write('\n'.join(report))

        print(f"Security analysis complete. Report saved to {report_file}")

        ## Display a summary
        print("\nSummary:")
        print(f"- High-risk services: {len(high_risk_services)}")
        print(f"- Potentially outdated services: {len(potentially_outdated)}")
        print(f"- Total exposed services: {len(exposed_services)}")

    except ET.ParseError as e:
        print(f"Error parsing XML file: {e}")
        return False
    except Exception as e:
        print(f"Error: {e}")
        return False

    return True

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print(f"Usage: {sys.argv[0]} <nmap_xml_file>")
        sys.exit(1)

    xml_file = sys.argv[1]
    if not analyze_security(xml_file):
        sys.exit(1)

Ctrl+O를 누른 다음 Enter를 눌러 파일을 저장하고 Ctrl+X로 nano 를 종료합니다.

스크립트를 실행 가능하게 만듭니다.

chmod +x ~/project/security_analysis.py

보안 분석 실행

Nmap XML 파일에서 보안 분석 스크립트를 실행해 보겠습니다.

cd ~/project
./security_analysis.py localhost_scan.xml

스크립트는 스캔 결과를 분석하고 잠재적인 취약점에 초점을 맞춘 보안 보고서를 생성하여 security_report.txt라는 파일에 저장합니다.

보고서의 내용을 살펴보겠습니다.

cat ~/project/security_report.txt

보안 분석 이해

보안 분석 스크립트는 몇 가지 중요한 기능을 수행합니다.

  1. 고위험 포트 식별: 공격자가 자주 사용하는 FTP(21), Telnet(23), RDP(3389) 와 같은 일반적으로 악용되는 포트를 식별합니다.

  2. 구식 서비스 감지: 알려진 보안 취약점이 있을 수 있는 SSH, Apache, nginx 와 같은 서비스의 이전 버전을 확인합니다.

  3. 노출된 서비스 인벤토리: 모든 열린 포트 및 서비스의 완전한 인벤토리를 생성하며, 이는 보안 감사에 유용합니다.

  4. 위험 범주화: 보안 개선의 우선 순위를 정하는 데 도움이 되도록 위험 수준별로 결과를 정리합니다.

이러한 유형의 분석은 공격자가 네트워크를 악용하기 전에 보안 전문가가 잠재적인 취약점을 식별하는 데 매우 중요합니다.

분석 확장

실제 시나리오에서는 다음을 수행하여 이 분석을 확장할 수 있습니다.

  1. 감지 목록에 더 많은 고위험 포트 추가
  2. 최신 취약성 정보로 구식 서비스 정의 업데이트
  3. 알려진 CVE(Common Vulnerabilities and Exposures) 를 확인하기 위해 취약성 데이터베이스와 통합
  4. 감지된 문제에 대한 해결 권장 사항 추가

Nmap XML 데이터를 프로그래밍 방식으로 분석하는 기능은 사이버 보안 전문가에게 강력한 기술이며, 자동화된 취약성 평가 및 더 큰 보안 모니터링 시스템과의 통합을 가능하게 합니다.

요약

Nmap 스캔 결과를 XML 형식으로 분석하는 이 랩을 완료하신 것을 축하드립니다. 몇 가지 중요한 기술을 익히셨습니다.

  1. Nmap 설치 및 실행: Nmap 을 설치하고 XML 출력을 사용하여 스캔을 실행하는 방법을 배웠으며, 이는 네트워크 정찰의 기반을 제공합니다.

  2. XML 구조 이해: Nmap XML 파일의 구조를 탐색하고 명령줄 도구를 사용하여 특정 정보를 추출하여 스캔 결과를 빠르게 분석할 수 있는 능력을 갖게 되었습니다.

  3. Python 을 사용한 XML 파싱: Python 스크립트를 생성하여 Nmap 스캔 결과를 읽기 쉬운 형식으로 파싱하고 표시하여 구조화된 데이터를 프로그래밍 방식으로 처리하는 방법을 보여주었습니다.

  4. 보안 분석: Python 기술을 확장하여 보안 문제에 대한 스캔 결과를 분석하고 잠재적으로 위험한 서비스를 식별하며 포괄적인 보안 보고서를 생성했습니다.

이러한 기술은 네트워크 평가, 취약성 스캔 및 보안 감사를 수행해야 하는 사이버 보안 전문가에게 필수적입니다. Nmap 결과 분석을 자동화하는 기능은 보다 효율적이고 철저한 보안 모니터링을 가능하게 합니다.

다음과 같은 방법으로 이러한 기술을 더욱 향상시킬 수 있습니다.

  • 더 발전된 Nmap 스캔 기술 탐색
  • 스캔 결과를 다른 보안 도구와 통합
  • 더 정교한 분석 알고리즘 생성
  • 스캔 데이터를 위한 시각화 도구 개발

무단 스캔은 불법적이거나 비윤리적일 수 있으므로, 네트워크 스캔은 자신이 소유하거나 스캔할 명시적인 권한이 있는 네트워크에서만 수행해야 합니다.