웹 기반 TCP 포트 스캐너 만들기

HTMLBeginner
지금 연습하기

소개

이전 프로젝트에서는 스레딩과 소켓을 활용하여 TCP 포트를 스캔하는 Python 포트 스캐너를 개발했습니다. 기능적으로는 충분했지만, 외부 패키지를 활용하면 더 효율적으로 개선할 수 있는 여지가 있습니다.

이번 프로젝트에서는 python-nmap 라이브러리를 통합하여 더욱 강력한 스캐닝 기능을 제공하도록 포트 스캐너를 고도화할 것입니다. 또한, Flask 를 사용하여 스캐너를 위한 사용자 친화적인 인터페이스를 갖춘 웹 애플리케이션을 구축해 보겠습니다. 이 단계별 프로젝트는 여러분이 기존 지식을 바탕으로 차근차근 따라 하며 완성할 수 있도록 안내합니다.

👀 미리보기

🎯 학습 과제

이 프로젝트를 통해 다음 내용을 학습하게 됩니다:

  • Flask 프로젝트 설정 및 구조 구성 방법
  • Flask-WTF 를 사용하여 웹 폼을 안전하게 생성하고 처리하는 방법
  • 웹 페이지 요청 및 제출을 처리하기 위한 Flask 라우트 구현 방법
  • Python 에서 Nmap 라이브러리를 활용하여 포트 스캔을 수행하는 방법
  • Flask 와 HTML 템플릿을 사용하여 스캔 결과를 웹 페이지에 동적으로 표시하는 방법
  • Tailwind CSS 를 적용하여 프론트엔드 디자인을 개선하는 방법

🏆 최종 목표

이 프로젝트를 마치면 다음과 같은 역량을 갖추게 됩니다:

  • 라우팅, 템플릿 렌더링, 폼 처리를 포함한 Flask 웹 개발의 기초 이해
  • Python 스크립트와 웹 인터페이스를 통합하는 실무 경험
  • 네트워크 스캐닝 작업을 위한 Nmap 라이브러리 활용 능력
  • 웹 애플리케이션에서 폼 생성 및 유효성 검사를 위한 Flask-WTF 사용법 숙지
  • Tailwind CSS 를 활용한 웹 페이지 스타일링 및 사용자 인터페이스 디자인 개선 능력
  • 백엔드 Python 스크립트와 상호작용하여 네트워크 스캔을 수행하는 기능적인 웹 애플리케이션 제작

메인 페이지 구현하기

먼저 templates/index.html 파일을 열고, 스캔 요청을 제출할 수 있는 폼이 올바르게 설정되도록 다음 코드를 추가합니다.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>TCP Port Scanner</title>
    <link
      href="https://cdn.jsdelivr.net/npm/tailwindcss@2.0.3/dist/tailwind.min.css"
      rel="stylesheet"
    />
    <script>
      function updateButton() {
        var submitButton = document.getElementById("scanButton");
        submitButton.value = "Scanning...";
        submitButton.classList.add("cursor-not-allowed", "opacity-50");
        submitButton.disabled = true;
      }
    </script>
  </head>
  <body class="bg-gray-100 flex items-center justify-center h-screen">
    <div class="bg-white p-8 rounded-lg shadow-md">
      <h1 class="text-2xl font-bold mb-4">TCP Port Scanner</h1>
      <form action="" method="post" class="space-y-4" onsubmit="updateButton()">
        {{ form.hidden_tag() }}
        <div>
          {{ form.host.label(class="block text-sm font-medium text-gray-700") }}
          {{ form.host(class="mt-1 block w-full rounded-md border-gray-300
          shadow-sm focus:border-indigo-500 focus:ring-indigo-500") }}
        </div>
        <div>
          {{ form.ports.label(class="block text-sm font-medium text-gray-700")
          }} {{ form.ports(class="mt-1 block w-full rounded-md border-gray-300
          shadow-sm focus:border-indigo-500 focus:ring-indigo-500") }}
        </div>
        <div>
          {{ form.submit(id="scanButton", class="w-full flex justify-center py-2
          px-4 border border-transparent rounded-md shadow-sm text-sm
          font-medium text-white bg-indigo-600 hover:bg-indigo-700
          focus:outline-none focus:ring-2 focus:ring-offset-2
          focus:ring-indigo-500") }}
        </div>
      </form>
    </div>
  </body>
</html>

index.html 템플릿은 스캔할 호스트와 포트 범위를 입력받기 위한 사용자 친화적인 인터페이스를 제공합니다. HTML 폼에는 호스트 주소와 포트 범위를 지정하는 두 개의 주요 필드가 포함되어 있습니다.

WTForms 를 편리하게 사용할 수 있게 해주는 Flask 확장 기능인 Flask-WTF 를 사용하여 템플릿에 이러한 필드를 렌더링합니다. 폼 제출 시 간단한 JavaScript 함수인 updateButton()이 실행되어 제출 버튼의 텍스트를 "Scanning..."으로 변경합니다. 이러한 시각적 피드백은 사용자에게 스캔 요청이 현재 처리 중임을 알려줍니다.

페이지 스타일링은 유틸리티 우선 CSS 프레임워크인 Tailwind CSS 를 사용하여 신속하게 UI 를 구축했습니다.

✨ 솔루션 확인 및 연습

스캔 결과 페이지 구현하기

templates/results.html 파일을 열고 스캔 결과를 표시하기 위해 다음 코드를 작성합니다.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Scan Results</title>
    <link
      href="https://cdn.jsdelivr.net/npm/tailwindcss@2.0.3/dist/tailwind.min.css"
      rel="stylesheet"
    />
  </head>
  <body class="bg-gray-100">
    <div class="flex flex-col items-center justify-center min-h-screen">
      <div class="bg-white p-8 rounded-lg shadow-lg w-full max-w-4xl">
        <h1 class="text-2xl font-bold mb-4 text-center">
          Scan Results for {{ host }}
        </h1>
        <div class="overflow-x-auto">
          <table class="table-auto w-full text-left whitespace-no-wrap">
            <thead>
              <tr
                class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50"
              >
                <th class="px-4 py-3">PORT</th>
                <th class="px-4 py-3">STATE</th>
                <th class="px-4 py-3">SERVICE</th>
                <th class="px-4 py-3">VERSION</th>
              </tr>
            </thead>
            <tbody
              class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800"
            >
              {% for result in scan_results %}
              <tr class="text-gray-700 dark:text-gray-400">
                <td class="px-4 py-3 text-sm">{{ result.port }}/tcp</td>
                <td class="px-4 py-3 text-sm">{{ result.state }}</td>
                <td class="px-4 py-3 text-sm">{{ result.name }}</td>
                <td class="px-4 py-3 text-sm">
                  {{ result.product }} {{ result.version }} {{ result.extra }}
                </td>
              </tr>
              {% endfor %}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  </body>
</html>

results.html 템플릿은 포트 스캔의 최종 결과를 보여주는 역할을 합니다. 스캔 결과를 테이블 형식으로 나타내며, 각 포트의 상태, 서비스 이름, 그리고 가능한 경우 서비스 버전 정보까지 나열합니다.

이 템플릿 역시 Tailwind CSS 를 사용하여 결과 페이지가 반응형이면서도 시각적으로 깔끔하게 보이도록 했습니다. Flask 에 통합된 Jinja2 템플릿 엔진을 사용하면 Flask 애플리케이션에서 전달된 스캔 결과를 동적으로 렌더링하고, 반복문을 통해 테이블 내용을 채울 수 있습니다.

✨ 솔루션 확인 및 연습

Flask 애플리케이션 초기화

이번 단계에서는 Flask 애플리케이션의 메인 Python 스크립트를 작성하겠습니다.

app.py 파일에 다음 코드를 추가합니다.

## 필요한 모듈 임포트
from flask import Flask, render_template, request, redirect, url_for
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired, Regexp
import nmap

## Flask 앱 초기화
app = Flask(__name__)
app.config['SECRET_KEY'] = 'labex'

## Nmap PortScanner 초기화
nm = nmap.PortScanner()

## 스캔 입력을 위한 Flask-WTF 폼 정의
class ScanForm(FlaskForm):
    host = StringField('Host', validators=[DataRequired()])
    ports = StringField('Port Range', validators=[DataRequired(), Regexp(r'^\d+-\d+$', message="Format must be start-end")])
    submit = SubmitField('Scan')

이 단계에서는 Flask 애플리케이션과 폼 클래스가 초기화됩니다.

app.py 스크립트는 Flask 본체, 폼 처리를 위한 Flask-WTF, 그리고 포트 스캔을 수행하기 위한 Nmap 등 필요한 모듈을 임포트하며 시작합니다. Flask 애플리케이션 인스턴스가 생성되고, CSRF 공격으로부터 보호하기 위해 비밀 키 (Secret Key) 가 설정됩니다.

ScanForm 클래스는 호스트와 포트 범위 입력을 위한 폼 필드를 정의합니다. 유효성 검사기 (Validators) 를 사용하여 데이터가 반드시 입력되었는지, 그리고 포트 범위가 올바른 형식 (예: 시작 - 끝) 인지 확인합니다.

'your_secret_key' 부분은 실제 환경에서는 폼 보안을 위해 실제 비밀 키로 대체하여 사용해야 합니다.

✨ 솔루션 확인 및 연습

메인 라우트 처리하기

이번 단계에서는 사용자가 스캔하고자 하는 호스트와 포트 범위를 제출할 수 있는 메인 라우트를 처리합니다. app.py에 다음 함수를 추가하세요.

## 메인 페이지 라우트 정의
@app.route('/', methods=['GET', 'POST'])
def index():
    form = ScanForm()  ## 폼 인스턴스 생성
    if form.validate_on_submit():
        ## 폼에서 데이터 가져오기
        host = form.host.data
        ports = form.ports.data  ## 형식: "시작 - 끝"
        ## 폼 데이터와 함께 스캔 라우트로 리다이렉트
        return redirect(url_for('scan', host=host, ports=ports))
    ## 폼과 함께 메인 페이지 템플릿 렌더링
    return render_template('index.html', form=form)

app.py 스크립트의 이 부분은 웹 애플리케이션의 메인 페이지 라우트를 정의합니다. index() 함수는 ScanForm 인스턴스와 함께 index.html 템플릿을 렌더링합니다.

폼이 제출되고 유효성 검사를 통과하면, 함수는 사용자를 스캔 라우트로 리다이렉트하며 URL 파라미터를 통해 폼 데이터 (호스트 및 포트 범위) 를 전달합니다. 이 리다이렉션이 실제 스캐닝 프로세스를 시작하게 됩니다.

✨ 솔루션 확인 및 연습

스캔 라우트 구현하기

이 단계에서는 실제 스캐닝을 수행하고 결과를 표시하는 라우트를 생성합니다. app.py에 다음 함수를 추가하세요.

## 스캔 결과 라우트 정의
@app.route('/scan')
def scan():
    ## 쿼리 스트링에서 호스트와 포트 가져오기
    host = request.args.get('host')
    ports = request.args.get('ports')
    ## Nmap 을 사용하여 스캔 수행
    nm.scan(hosts=host, ports=ports, arguments='-sV')  ## -sV 는 서비스/버전 감지 옵션
    scan_results = []

    ## 스캔 결과를 처리하여 리스트에 저장
    for host in nm.all_hosts():
        for proto in nm[host].all_protocols():
            lport = nm[host][proto].keys()
            for port in lport:
                service = nm[host][proto][port]
                scan_results.append({
                    'port': port,
                    'state': service['state'],
                    'name': service.get('name', 'Unknown'),
                    'product': service.get('product', ''),
                    'version': service.get('version', ''),
                    'extra': service.get('extrainfo', '')
                })

    ## 스캔 결과와 함께 결과 페이지 템플릿 렌더링
    return render_template('results.html', scan_results=scan_results, host=host)

scan() 함수는 실제 포트 스캔을 수행하고 결과를 표시하는 라우트를 담당합니다. URL 을 통해 전달된 쿼리 스트링 파라미터에서 호스트와 포트 범위를 가져옵니다.

Nmap PortScanner 인스턴스를 사용하여 지정된 호스트와 포트에 대해 스캔을 실시하며, -sV 인자를 사용하여 서비스 버전을 감지합니다.

스캔 결과는 가공되어 각 포트의 상세 정보를 담은 딕셔너리 리스트로 정리됩니다. 이 정보들은 results.html 템플릿으로 전달되어 사용자에게 보여집니다.

✨ 솔루션 확인 및 연습

Flask 애플리케이션 실행하기

모든 구성 요소가 준비되었으므로 이제 Flask 애플리케이션을 실행하여 TCP 포트 스캐너를 작동시킬 차례입니다. app.py의 마지막 코드 부분은 스크립트가 직접 실행될 때만 Flask 애플리케이션이 구동되도록 보장합니다. 이는 실행 가능한 스크립트를 포함하는 Python 애플리케이션에서 흔히 사용되는 패턴입니다.

app.py 파일의 맨 끝에 다음 코드 조각을 추가하세요.

if __name__ == '__main__':
    app.run(debug=True, port=8080, host='0.0.0.0')

이 코드는 Flask 가 디버그 모드를 활성화한 상태로 애플리케이션을 시작하도록 지시하여 오류 추적을 용이하게 합니다. 애플리케이션은 모든 네트워크 인터페이스(host='0.0.0.0')에서 요청을 기다리며 8080 포트를 사용합니다. 디버그 모드는 보안상 위험할 수 있으므로 개발 환경에서만 사용해야 합니다.

Flask 애플리케이션을 실행하려면 app.py가 있는 프로젝트 디렉토리에 있는지 확인한 후, 터미널에서 다음 명령을 실행하세요.

python app.py

Web 8080 탭으로 전환하여 TCP 포트 스캐너에 접속합니다. 이제 스캔할 호스트와 포트 범위를 입력하고 결과 페이지에서 스캔 결과를 확인할 수 있습니다.

참고로 포트 22 와 3306 은 각각 SSH 및 MySQL 서비스와 밀접하게 연관되어 있으며, 포트 3000 은 WebIDE 환경에서 주로 사용됩니다.

✨ 솔루션 확인 및 연습

요약

이 프로젝트를 통해 Flask 와 Nmap 을 사용하여 간단하면서도 강력한 웹 기반 TCP 포트 스캐너를 구축하는 방법을 배웠습니다. 프로젝트 환경 설정과 필요한 의존성 설치부터 시작하여, Flask 애플리케이션 생성, 폼 제출 처리, 포트 스캔 수행, 그리고 결과를 사용자 친화적으로 표시하는 과정까지 전 단계를 살펴보았습니다. 이 프로젝트는 Flask 를 활용한 웹 개발과 Nmap 을 이용한 네트워크 스캐닝을 결합한 실용적인 예제로, 두 분야의 기초를 다지는 데 훌륭한 입문서가 될 것입니다.