카테고리 없음

초초초미니 프로젝트: 피싱 URL 검사 웹사이트, Phishing Check!!! + 회고

min8282 2025. 1. 21. 13:28

프로젝트 소개 - 피싱 URL 검색 웹사이트

이 프로젝트는 SK쉴더스 교육 과정 중 파이썬을 활용한 인프라 자동화 수업의 마지막 단계에서 진행한 미니 프로젝트입니다. 조를 구성하여, 그동안 배운 내용을 기반으로 간단한 웹사이트를 만드는 것이 목표였습니다.
저는 개발 관련 전공자라 교육 내용과 실습 진행을 비교적 수월하게 따라갈 수 있었지만, 팀원들 중에는 파이썬을 처음 배우는 분들도 있었습니다.
결국, 팀원들은 다음 세 그룹으로 나뉘었습니다:

  1. 파이썬을 처음 접한 사람
  2. 파이썬을 사용해 본 적 있는 사람
  3. 프로그래밍에 익숙한 전공자

이렇게 다양한 수준의 팀원이 함께 프로젝트를 진행하면서 예상치 못한 어려움과 문제점도 많이 발생했습니다.


프로젝트 주제 선정 - 쉬워야 하나, 의미 있어야 하나?

강사님께서는 프로젝트의 목표를 "그동안 배운 내용을 활용하여 간단한 웹사이트를 만들어 보는 것"이라고 정해주셨습니다.
우리가 일주일 동안 배운 내용은 다음과 같습니다:

  • 파이썬 기본 문법과 파일 목록 컨트롤
  • 웹 크롤링 및 정규 표현식
  • API 사용법
  • FTP
  • Flask 프레임워크와 MongoDB

전공자의 입장에서 보면 이 모든 내용을 단일 주제로 연결해 프로젝트를 진행하려면 일주일 이상 걸릴 수도 있는 수준입니다. 하지만, 이 교육에서는 빠르게 개념을 익히고 실습하는 데 초점이 맞춰져 있었기 때문에 프로젝트 난이도를 높게 설정하기보다는 비교적 간단한 주제를 선택해야 했습니다.

결국, 우리 팀은 피싱 URL 검색 웹사이트를 주제로 선정했습니다.


주요 기능

우리 팀이 개발한 웹사이트는 다음과 같은 기능을 제공합니다:

  1. URL 검색 기능
    • 사용자가 검색창에 URL을 입력하면, 해당 URL이 피싱 사이트인지 아닌지 판별하여 결과를 반환합니다.
  2. RSS 연동 기능
    • 검색창 아래에 보안 관련 뉴스를 제공하기 위해, 보안뉴스, 데일리시큐, 한국인터넷진흥원(KISA) 등에서 제공하는 RSS 피드를 불러옵니다.

이 두 가지 기능은 교육 과정에서 배운 내용을 기반으로 비교적 간단하게 구현할 수 있었고, 팀원 모두가 이해할 수 있는 수준으로 설계되었습니다.


주요 코드

1. app.py - 메인 애플리케이션 로직

 

URL 검사 로직

  • 사용자가 입력한 URL을 OpenPhish 데이터베이스와 Google Safe Browsing API를 통해 검사.
  • 검사 결과는 안전/위험 여부와 함께 메시지로 반환.
@app.route('/check', methods=['POST'])
def check_url():
    url = request.form['url']
    if is_phishing_url(url):
        return render_template('index.html', result=f"위험: {url}")
    else:
        return render_template('index.html', result=f"안전: {url}")

 

RSS 피드 로딩

  • 보안 관련 RSS 피드를 불러와 사용자에게 보여주는 기능
@app.route('/')
def home():
    rss_news = fetch_rss_feed()
    return render_template('index.html', rss_news=rss_news)

 

 

2. db_setup.py - OpenPhish 데이터베이스 초기화

MongoDB를 사용하여 OpenPhish 데이터를 관리합니다. 데이터를 초기화하고 최신 데이터를 주기적으로 업데이트하는 로직이 포함되어 있습니다.

def initialize_database():
    client = pymongo.MongoClient("mongodb://localhost:27017/")
    db = client['phishing']
    collection = db['urls']
    collection.drop()  # 기존 데이터를 초기화
    new_data = fetch_openphish_data()
    collection.insert_many(new_data)

 

 

3. index.html - 프론트엔드 템플릿

Bootstrap을 사용하여 깔끔한 UI를 구성했습니다. 사용자 입력 폼과 결과를 보여주는 부분, 그리고 RSS 뉴스 표시 영역이 포함되어 있습니다.

<form action="/check" method="POST">
    <input type="text" name="url" placeholder="URL을 입력하세요">
    <button type="submit">검사</button>
</form>

<h3>보안 관련 뉴스</h3>
<ul>
    {% for news in rss_news %}
    <li><a href="{{ news.link }}">{{ news.title }}</a></li>
    {% endfor %}
</ul>

전체 코드

 

1. app.py

from flask import Flask, render_template, request, jsonify
import requests
from pymongo import MongoClient
import feedparser

app = Flask(__name__)

# MongoDB 설정
MONGO_URI = "mongodb://localhost:27017"
DATABASE_NAME = "phishing_checker"
COLLECTION_NAME = "openphish"
client = MongoClient(MONGO_URI)
db = client[DATABASE_NAME]
collection = db[COLLECTION_NAME]

# Google Safe Browsing API 설정
GOOGLE_API_KEY = "your_api_key"
SAFE_BROWSING_API_URL = f"https://safebrowsing.googleapis.com/v4/threatMatches:find?key={GOOGLE_API_KEY}"

# 보안뉴스 RSS URL
RSS_FEEDS = {
    "SECURITY": "http://www.boannews.com/media/news_rss.xml?mkind=1",
    "IT": "http://www.boannews.com/media/news_rss.xml?mkind=2",
    "SAFETY": "http://www.boannews.com/media/news_rss.xml?mkind=4",
    "SecurityWorld": "http://www.boannews.com/media/news_rss.xml?mkind=5"
}

@app.route("/", methods=["GET", "POST"])
def index():
    phishing_result = None
    selected_topic = "SECURITY"  # 기본 RSS 주제
    rss_feed = []

    # URL 피싱 검사
    if request.method == "POST" and "url" in request.form:
        url_to_check = request.form.get("url")

        # OpenPhish DB 확인
        if collection.find_one({"url": url_to_check}):
            phishing_result = {"source": "OpenPhish", "status": "Phishing"}
        else:
            # Google Safe Browsing API 확인
            payload = {
                "client": {
                    "clientId": "yourclientid",
                    "clientVersion": "1.0"
                },
                "threatInfo": {
                    "threatTypes": ["MALWARE", "SOCIAL_ENGINEERING"],
                    "platformTypes": ["ANY_PLATFORM"],
                    "threatEntryTypes": ["URL"],
                    "threatEntries": [{"url": url_to_check}]
                }
            }
            try:
                response = requests.post(SAFE_BROWSING_API_URL, json=payload)
                data = response.json()
                if "matches" in data:
                    phishing_result = {"source": "Google Safe Browsing", "status": "Phishing", "details": data}
                else:
                    phishing_result = {"source": "Google Safe Browsing", "status": "Safe"}
            except Exception as e:
                phishing_result = {"error": str(e)}

        # 터미널에 결과 로그 출력해서 확인 
        print("==== URL Check Result ====")
        print(f"URL: {url_to_check}")
        print(f"Status: {phishing_result.get('status')}")
        print(f"Source: {phishing_result.get('source')}")
        if phishing_result.get("details"):
            print(f"Details: {phishing_result.get('details')}")
        print("==========================")

    # RSS 주제 변경 및 피드 로드
    if request.method == "POST" and "topic" in request.form:
        selected_topic = request.form.get("topic", "SECURITY")

    try:
        rss_url = RSS_FEEDS[selected_topic]
        feed = feedparser.parse(rss_url)
        for entry in feed.entries[:5]:  # 상위 5개 뉴스만 표시
            rss_feed.append({
                "title": entry.title,
                "link": entry.link,
                "   ": entry.summary,
                "date": entry.published if "published" in entry else "No date available"
            })
    except Exception as e:
        print(f"Error fetching RSS feed: {e}")

    # 리턴값 - 판결 여부, rss 데이터, 토픽, index 페이지
    return render_template("index.html", phishing_result=phishing_result, rss_feed=rss_feed, selected_topic=selected_topic, topics=RSS_FEEDS.keys())

if __name__ == "__main__":
    app.run(debug=True)

 

2. db_setup.py

import requests
from pymongo import MongoClient

# MongoDB 설정
MONGO_URI = "mongodb://localhost:27017"
DATABASE_NAME = "phishing_checker"
COLLECTION_NAME = "openphish"

client = MongoClient(MONGO_URI)
db = client[DATABASE_NAME]
collection = db[COLLECTION_NAME]

# OpenPhish 데이터 가져오기
OPENPHISH_API_URL = "https://openphish.com/feed.txt"
response = requests.get(OPENPHISH_API_URL)
openphish_urls = response.text.splitlines()

# MongoDB에 데이터 저장
def populate_database():
    collection.delete_many({})  # 기존 데이터 삭제
    urls_to_insert = [{"url": url} for url in openphish_urls]
    collection.insert_many(urls_to_insert)
    print(f"Inserted {len(urls_to_insert)} URLs into the database.")

if __name__ == "__main__":
    populate_database()

 

3. index.html(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>피싱 췤</title>
    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container-fluid">
            <a class="navbar-brand" href="#">Phishing Check - 피싱 췤</a>
        </div>
    </nav>
    <div class="container my-5">
        <div class="text-center">
            <img src="../static/phishing_check.png" class="center-image" alt="Phishing Check">
        </div>

        <!-- URL 검사 폼 -->
        <div class="card p-4 shadow mb-4">
            <form method="POST">
                <h5>의심되는 URL을 입력하세요!</h5>
                <input type="text" name="url" class="form-control mb-3" placeholder="Enter URL to check" required>
                <button type="submit" class="btn btn-primary w-100">Check!!!</button>
            </form>
            {% if phishing_result %}
                <div class="alert mt-3 {% if phishing_result.status == 'Phishing' %}alert-danger{% else %}alert-success{% endif %}">
                    <h5>Result: {{ phishing_result.status }}</h5>
                </div>
            {% endif %}
        </div>

        <!-- RSS 주제 선택 및 피드 표시 -->
        <form method="POST" class="mb-4">
            <div class="input-group">
                <label for="topic" class="input-group-text">Select Topic:</label>
                <select name="topic" id="topic" class="form-select">
                    {% for topic in topics %}
                        <option value="{{ topic }}" {% if topic == selected_topic %}selected{% endif %}>
                            {{ topic }}
                        </option>
                    {% endfor %}
                </select>
                <button type="submit" class="btn btn-primary">Load News</button>
            </div>
        </form>

        <div>
            {% if rss_feed %}
                <h2 class="text-center">Top News for {{ selected_topic }}</h2>
                <ul class="list-group">
                    {% for item in rss_feed %}
                        <li class="list-group-item">
                            <h5><a href="{{ item.link }}" target="_blank">{{ item.title }}</a></h5>
                            <p>{{ item.description }}</p>
                            <small class="text-muted">{{ item.date }}</small>
                        </li>
                    {% endfor %}
                </ul>
            {% else %}
                <p class="text-center text-muted">No news available for this topic.</p>
            {% endif %}
        </div>
    </div>
    <!-- Bootstrap JS -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

 

안전한 URL 검색
피싱 URL 검색


데이터 수집 - 예상치 못한 장애물

1. OpenPhish와 PhishTank의 정책 변경

피싱 URL 데이터를 수집하기 위해 OpenPhishPhishTank를 먼저 조사했습니다.
OpenPhish는 무료로 피싱 데이터를 제공하는 오픈소스 플랫폼이지만, 정책이 변경되면서 데이터를 다운로드하려면 API 키를 발급받아야 했습니다. API 키를 얻으려면 사이트 관리자에게 이메일로 요청해야 했고, 즉시 발급받을 수 없었습니다.

PhishTank 역시 비슷한 상황이었습니다. 무료 데이터를 얻으려면 API 키가 필요했고, 이 역시 이메일을 통해 요청해야만 했습니다.

우리는 발표를 하루 앞두고 있었기 때문에, 이러한 절차를 기다릴 여유가 없었습니다. 따라서 OpenPhish에서 제공하는 Phishing Feeds(약 500개의 URL 데이터를 포함한 엔드포인트)를 사용하고, PhishTank는 일단 보류하기로 했습니다.

2. 구글 Safe Browsing API

이때 대안으로 떠오른 것이 구글 Safe Browsing API였습니다. 이 API를 활용하면 실시간으로 URL이 악성인지 여부를 확인할 수 있었습니다.
결국, 우리는 두 가지 방식으로 피싱 URL 판별 기능을 구현했습니다:

  1. OpenPhish의 500개 데이터베이스
  2. 구글 Safe Browsing API

추가 기능 - 보안 관련 RSS 피드

기능을 확장하기 위해, 보안뉴스, 데일리시큐, KISA 등에서 제공하는 RSS 피드를 연동했습니다. 이를 통해 사용자는 검색창 아래에서 실시간으로 최신 보안 뉴스를 확인할 수 있도록 설계했습니다.

RSS 피드는 주제별로 정리되어 있어, 직관적이고 깔끔한 UI를 구현할 수 있었습니다.


프로젝트를 마치며 - 협업과 성장의 경험

처음 프로젝트를 시작하면서 전공자로서 저의 역할을 어떻게 할지 고민이 많았습니다. 모두가 동일한 이해도를 가지고 출발하는 상황이 아니었기 때문에, 팀의 전반적인 학습을 도와야 하는 동시에 프로젝트의 완성도도 고려해야 했습니다.

그래서 다음과 같은 방법으로 진행 방향을 잡았습니다:

  1. 각자 파트를 나누지 않고 일정 시간 동안 전부 완성해보기
    프로젝트 초반에는 각자 배우면서 코드를 작성해 보도록 했습니다. 특정한 역할을 미리 정해 분담하면 오히려 팀원들이 전반적인 흐름을 놓칠 수 있다고 생각했기 때문입니다. 이렇게 하다 보면, 비전공자들도 전체 과정을 조금씩 이해할 수 있고, 이후 논의할 때 더 적극적으로 참여할 수 있을 거라 기대했습니다.
  2. 다 같이 모여 코드 설명 및 질의응답 진행
    일정 시간이 지난 뒤에는 팀원들이 작성한 코드를 모아, 각자가 맡은 부분을 설명하는 시간을 가졌습니다. 내가 작성한 코드가 왜 이렇게 작동하는지, 어떤 원리가 적용되었는지를 최대한 쉽게 설명하려 노력했습니다. 이 과정에서 비전공자들이 궁금한 점을 질문하며 기본적인 기술 이해도를 쌓을 수 있었습니다.
  3. 최종적으로 하나의 코드로 통합
    마지막 단계에서는 팀원들과 논의해 최종적으로 하나의 코드를 결정했습니다. 저는 구글 Safe Browsing API를 활용한 조금 더 복잡한 구현 방식을 사용했지만, 다른 팀원들이 이를 이해하기 어렵다고 판단했습니다. 그래서 OpenPhish에서 제공하는 500개 데이터를 활용해 간단하게 구성된 코드를 기반으로 프로젝트를 마무리하기로 했습니다.

이 과정에서 제 코드가 최종 코드로 채택되지 않아 아쉬움이 남았지만, 팀 전체의 합의를 존중하는 것이 더 중요하다고 생각했습니다. 프로젝트의 목표는 단순히 결과물을 완성하는 데 그치지 않고, 팀원 모두가 함께 학습하고 성장하는 데 있다는 점을 잊지 않으려 했습니다.

 

이번 프로젝트는 단순히 웹사이트를 만드는 것을 넘어, 서로 다른 배경을 가진 팀원들과 협업하는 소중한 경험이었습니다. 특히 비전공자 팀원들에게는 프로그래밍 개념 자체가 생소했기 때문에, 기본적인 문법 이해나 코드 작성에서 많은 어려움을 겪는 모습이 보였습니다. 저는 팀원들과 함께 코드를 설명하며 복잡한 기술 용어를 쉽게 풀어 설명하려고 노력했고, 이해를 돕기 위해 적극적으로 소통하며 팀의 협업을 이끌어 나가고자 했습니다.