한국에서 KTX 쓰다보면 매진된 좌석을 예약하려고 하루종일 새로고침하는 사람 많을 거다.

 

이게 귀찮기도 하고 가끔 일반석 뜨면 1초만에 예약되는 매직아닌 매직을 보다보니 빡이 쳐서 자동 예매 스크립트 하나 만들었다.

 

사실 이게 꿀통이라 많은 사람이 이용하면 코레일 쪽에서 막지 않을까..? 하는 생각도 들었지만 내 블로그 통계를 보니 그런 걱정은 접어두고 그냥 올리기로 했다.

 

1. 코레일로부터 어찌 좌석 데이터를 받는가?

처음엔 로그인 기능이라든가 결제 API 등 구현하기 힘들거 같아서 간단하게 좌석이 Available 하면 비프음 뜨도록 만들었다가 PC 앞에 앉아 기다리기 싫어서 텔레그램으로 알림 가도록 만들었다.

 

요청 파라미터가 뭐고 URL은 어떤걸 써야하는지는 코레일 홈페이지에서 좌석 조회하는 프로세스를 프록시 툴로 하나 하나 보다보니 하나가 딱 걸리더라 그래서 그걸로 썻다.

import requests
import json
import time
import winsound

def time_to_int(t):
    return int(t.replace(":", ""))

TELEGRAM_BOT_TOKEN = "비밀><"
TELEGRAM_CHAT_ID = "비밀><"
def send_telegram(msg):
    url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
    data = {
        "chat_id": TELEGRAM_CHAT_ID,
        "text": msg
    }
    requests.post(url, data=data)

url = "https://www.korail.com/web_s/ZaIL00KWFZOJM2VSH1Z5TNBVO5eZZXSVSPLGa7JK9ZMPWZGc3U/LVYNcbMSQNPRMOOWfLtXA"

params = {
    "jg_": "....."
}

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
    "Accept": "application/json, text/plain, */*",
    "Origin": "https://www.korail.com",
    "Referer": "https://www.korail.com/ticket/search/list"
}

cookies = {
    "WMONID": "......",
    "JSESSIONID": "......"
}

files = {
    "searchType": (None, "GENERAL"),
    "txtGoStart": (None, "서울"),
    "txtGoEnd": (None, "포항"),
    "txtTrnGpCd": (None, "109"),
    "adjStnScdlOfrFlg": (None, "N"),
    "srtCheckYn": (None, "N"),
    "ebizCrossCheck": (None, "N"),
    "radJobId": (None, "1"),
    "txtPsgFlg_1": (None, "1"),
    "rtYn": (None, "N"),
    "txtGoAbrdDt": (None, "20251220"),
    "txtGoHour": (None, "100000"),
    "txtWkndUseFlg": (None, "Y"),
    "spePersonnel": (None, "[object Object]"),
    "txtMenuId": (None, "11"),
    "txtGoStartCode": (None, "0001"),
    "txtGoEndCode": (None, "0515"),
    "limitStartDate": (None, "20251220"),
    "txtTrainNm": (None, "전체"),
    "txtSeatAttCd_4": (None, "015"),
    "tkTripChgQryFlg": (None, "Y"),
    "Device": (None, "BH"),
    "Version": (None, "999999999")
}

while True:
    response = requests.post(
        url,
        params=params,
        headers=headers,
        cookies=cookies,
        files=files,
        timeout=10
    )

    data = response.json()
    trains = data["trn_infos"]["trn_info"]

    found = False  # 이번 루프에서 조건 충족 여부

    for t in trains:
        dep_time = time_to_int(t["h_dpt_tm_qb"])
        if dep_time > 1800:
            continue

        # 일반실
        if t["h_gen_rsv_nm"] != "매진":
            send_telegram(f"[일반실] {t['h_trn_no']} {t['h_dpt_tm_qb']} {t['h_gen_rsv_nm']}")

        if t["h_stnd_rsv_nm"] != "매진":
            send_telegram(f"[스텐드] {t['h_trn_no']} {t['h_dpt_tm_qb']} {t['h_stnd_rsv_nm']}")

        # 특실
        if t["h_spe_rsv_nm"] != "매진":
            send_telegram(f"[특실] {t['h_trn_no']} {t['h_dpt_tm_qb']} {t['h_spe_rsv_nm']}")

    time.sleep(15)

 

2. 좌석 등급

좌석은 h_gen_rsv_nm, h_stnd _rsv_nm, h_spe_rsv_nm 로 나뉘는데 스탠드, 일반, 특별석이다. 그리고 이에 대한 상태값은 매진, 예약 가능, 임박 등등이 있다. 우리가 필요한 건 매진만 아니면 되기에 다른 값들은 신경쓸 필요가 없다.

 

3. 텔레그램 챗봇 발급

여기 참고바란다.

https://blog.naver.com/lifelectronics/223198582215

 

[텔레그램 봇] 1. 텔레그램 봇 제작 환경 구축, 텔레그램 api 활용하기(2023 텔레그램 업데이트 버전

오랜만에 블로그의 취지에 맞는 글(나름 썸네일도 구해봤다.) 이전에 학교 공지사항을 업데이트 하는 텔레...

blog.naver.com

 

4. Korail 라이브러리

알림이 뜨는건 좋은데 미친 사람들이 매크로를 계속 돌리는 건지 아니면 누구보다 빠른 손가락을 가진 건지 1초만에 매진이 되더라.

그래서 로그인이랑 결제 기능까지 구현해야 하냐..? 하다가 korail 라이브러리를 발견했다. 아마 이 사람도 나랑 똑같이 새로고침 귀찮아서 해당 기능 구현하고 공개한거 같다. (공식 API 형식이 아닌, 내가 기존에 만든거랑 같이 Request, Response 가공 무한 반복이다.)

https://github.com/carpedm20/korail2 

 

GitHub - carpedm20/korail2: Korail (www.letskorail.com) wrapper for Python.

Korail (www.letskorail.com) wrapper for Python. Contribute to carpedm20/korail2 development by creating an account on GitHub.

github.com

 

깜짝놀란게 코레일 패스워드 암호화 방식부터 결제까지 다 가능했다는 것이다.

JS랑 모바일 앱 열심히 뜯어봤을 개발자에게... 찬사를 남긴다.

 

****** 이슈 발생할 수도 있으니 자동 예매 스크립트는 공개하지 않겠다. 문제 해결 과정만 공개함.

 

1. 진짜 간단하게 while이랑 for문 돌려서 좌석 있으면 예매하도록 만들었다.

그런데 문제점이 Korail 라이브러리에서 좌석 검색 및 예매하는 코드에서 발생했는데, 제일 첫번째 리스트 값에 있는 기차를 예매하는 것이다.

예시로 13시부터 좌석을 검색하고 예매하는 거라 가정하고 내가 원하는 건 13시 기차라고 생각하자. 그런데 15시 기차 좌석이 Available 하다. 이때 15시 기차를 예매한다.

 

그래서 코드를 쭉 봤는데, search_train 함수에서는 코레일로부터 이런 저런 값들을 json 형태로 받는데 그중 dep_time은 출발 시간이였다. 그래서 이를 기준으로 필터링해 해당 문제를 해결했다.

 

2. 에러 발생

krail2.py 파일을 보면 863번째 줄에 raise로 에러 핸들링하는 구문이 있었다. 이는 해당 KTX 열차 예매 가능 좌석이 없으면 발생하는 에러다. 그런데 우리는 자동 예매를 해야하기에 이를 print('No Trains found') 로 수정하거나 해당 라인을 지워야 한다. 이를 하지 않으면 while문 돌아가자 마자 바로 에러가 떠서 프로세스가 중지된다.

 

3. 실행 문제

처음엔 PC에서 실행을 했는데, 언제나 PC를 가지고 다닐순 없다. 그래서 방안을 강구하다가 모바일로 돌리는 방법을 떠올리고 즉시 python mobile smartphone 키워드로 구글링했더니 2개가 나왔다.

 

하나는 py 블라블라 라는 앱이 있는데, 앱에서 python 코드 테스트하는 앱이다. 그런데 이건 사용 불가하다. 왜냐하면 우리는 백그라운드로 돌려야 하는데, 앱의 경우 화면이 중지되면(백그라운드 상태) 프로세스가 onparse로 상태가 바뀌면서 상태 중지되기 때문이다.

 

두번째는 Termux 앱이다. 안드로이드폰은 리눅스 기반 커널을 이용한다. 이는 뭐다? 리눅스 쉘 쓸 수 있다! 라는 것이다. 그래서 이 앱을 이용해 리눅스 배시 쉘을 열 수 있는데, 여기에 우리가 늘 하던 것처럼 파이썬 설치하고 스크립트 돌리면 된다.

이는 앱이 백그라운드 상태가 되도 쉘을 백그라운드로 돌릴 수 있다.

 

# 후기

초급 개발자라면 누구나 만들 수 있는 스크립트였고, 코레일 라이브러리도 마찬가지로 모바일 앱 리버싱이랑 패킷 분석만 하면돼서 초급 보안기술자도 충분히 만들 수 있다.

 

요즘 코딩에 맛들렸는데, 데이터 가공은 파이썬, 웹은 자바 그리고 해킹 관련된 건 go, powershell 로 코딩하면서 여러가지를 배워보도록 하겠다.

 

# 갑자기든 생각.. 코레일 쪽에선 이를 어찌 막아야 할까?

1. 매크로 탐지

매크로 탐지 솔루션이 있는지 의문이긴 하다.

스크립트로 돌리는 것들은 요청 시간이 늘 일정하다 보니 매크로 탐지에 충분히 걸릴 수 있다.

 

근데 왜 운영을 하지 않는걸까 생각을 해보니 요청 시간 기반으로 탐지한다면 요청 시간은 1~2초 내로 랜덤한 값을 줘서 하면 뚫리기에 운영을 하지 않는게 아닐까 싶다.

 

그럼 어찌해야 할까? 같은 IP 기준 3~5분 간격 또는 요청수 기반으로 캡차 인증 기능을 쓰면 된다.(이것도 매크로 탐지다) 코레일의 경우 기차역에서 키오스크로 예매하는 시스템과 프론트에서 예매하는 시스템이 있는데, 이들이 쓰는 API와 우리가 모바일, PC로 예매하는 API는 같은 걸 쓸 것으로 추측된다.

 

이를 서로 분리시켜서 모바일, PC의 경우 캡차 인증이 있는 API로 요청시키면 매크로 문제는 해결되지 않을까 싶다.

 

악성 민원이 걱정되지 않냐고? 어차피 어르신들은 모바일 앱, PC로 예매하는거 잘 못 쓰신다. 쓰는 사람들은 대부분 젊은계층이기에 캡차 인증이 어려워서 악성 민원이 들어올 걱정은 하지 않아도 되지 않을까 싶다.

* 뭐 들어오면 그때 롤백시키면 되지

 

2. 모바일 앱 리버싱

코레일 모바일 앱 리버싱 해보진 않았는데, korail2 라이브러리가 있고 여기서 쓰는게 모바일에서 쓰는 API니까... 리버싱하기 쉽게 만들어진 앱이 아닌가 싶다.

리버싱 막는 제일 좋은 방법은 난독화다. 오픈소스로된 난독화 솔루션 말고 돈주고 하나 사시는게...? 돈이 없다면 오픈소스 조금 바꿔서 난독화만 해도 시중에 오픈된 비난독화(Deobfuscation) 코드로 뚫리는 일은 없을 거라 생각한다. 

+ Recent posts