universal_adapter.py)도 만들어서 --auth auto 옵션 하나면 로그인 페이지를 알아서 찾고 폼 필드도 자동 매핑하게 됐다.PR 링크: https://github.com/s2n0n/s2n/pull/139
프로젝트: https://github.com/s2n0n/s2n
s2n은 팀 사이드 프로젝트로 만들고 있는 Python 기반 플러그인형 웹 취약점 스캐너다. SQL Injection, XSS 같은 취약점을 자동으로 스캔하는 툴인데, 나는 여기서 크롤러 파트를 맡았다.
구조 자체는 잘 잡혀 있었다. 플러그인 아키텍처라서 취약점 스캐너를 모듈 단위로 붙였다 뗐다 할 수 있고, 결과도 JSON/HTML로 깔끔하게 뽑힌다. 근데 크롤러 쪽이 문제였다.
기존 크롤러의 핵심 문제는 딱 하나였다.
DVWA 전용으로 하드코딩되어 있었다.
DVWA(Damn Vulnerable Web Application)는 취약점 학습용 실습 환경이다. 기존 구조는 DVWA에서만 로그인하고, DVWA의 URL 목록을 직접 때려박는 방식이었다. 다른 타겟 사이트에 쓰려고 하면 코드를 뜯어야 한다.
스캐너가 "어떤 사이트든 쓸 수 있는 범용 툴"을 지향한다면 크롤러도 범용이어야 한다.
그리고 두 번째 문제가 있었다.
공격 포인트를 수동으로 지정해야 했다.
취약점 스캔을 하려면 "어디에 페이로드를 넣을지"를 알아야 한다. 기존엔 이걸 사람이 직접 URL 목록으로 넘겨줬다. 근데 실제 타겟 사이트는 폼이 어디에 있는지 모른다. 크롤러가 직접 사이트를 돌면서 공격 포인트(폼)를 자동으로 찾아내야 한다.
이 두 가지를 해결하는 게 이번 PR의 목표였다.
크게 세 가지 컴포넌트를 만들었다.
classifier.py크롤링하면서 찾은 폼이 "로그인 폼인지", "검색창인지", "파일 업로드인지" 구분해야 플러그인이 알맞은 페이로드를 쓸 수 있다.
분류 카테고리는 6종이다.
| 클래스 | 설명 | 판별 근거 |
|---|---|---|
LOGIN | 로그인 폼 | password 타입 input 존재, id/username 필드명 |
TEXT_INPUT | 일반 텍스트 입력 | text/textarea 입력 필드 |
FILE_UPLOAD | 파일 업로드 | type="file" input 존재 |
COMMAND | 명령어 실행 가능성 | cmd/command/exec 관련 필드명 |
SEARCH | 검색창 | search/query/q 필드명, type="search" |
GENERIC | 분류 불가 | 위 어디에도 안 걸릴 때 |
HTML 속성에서 패턴을 뽑아 분류하는 휴리스틱 방식이다. 머신러닝 같은 거 없이 필드명, input 타입, 폼 액션 URL을 조합해서 판단한다. 단순하지만 실제로 꽤 잘 맞는다.
smart_crawler.py타겟 URL에서 시작해서 같은 오리진(same-origin) 링크만 따라가면서 BFS(너비 우선 탐색)로 사이트를 순회한다.
시작 URL
└─ 링크 수집 (same-origin만)
└─ 각 페이지 방문
└─ 폼 발견 → PageClassifier로 분류
└─ SiteMap에 기록
BFS를 선택한 이유는 DFS 대비 얕은 depth의 페이지를 먼저 다 긁기 때문이다. 실제 웹 취약점 스캐닝에서는 로그인 직후 페이지나 메인 기능 페이지가 중요한데, 이런 건 보통 depth가 얕다.
크롤링 결과는 SiteMap 객체로 구조화되어서 플러그인에 전달된다. 플러그인 입장에서는 "이 URL에 이 타입의 폼이 있음"이라는 정보를 바탕으로 바로 공격 페이로드를 날릴 수 있다.
universal_adapter.py이게 제일 까다로웠다.
기존엔 DVWA 전용 어댑터가 있었다. DVWA는 로그인 URL도 고정이고 폼 필드명도 고정이라 하드코딩이 가능했다. 근데 범용 어댑터는 아무 사이트나 들어갔을 때 "어디가 로그인 페이지인지", "username 필드가 뭔지", "로그인 성공 여부를 어떻게 판단하는지"를 자동으로 알아내야 한다.
처리 흐름은 이렇다.
1. 로그인 페이지 자동 탐색
- /login, /signin, /auth 같은 일반적인 경로 시도
- 홈페이지에서 로그인 링크 텍스트로 탐색
2. 폼 필드 자동 매핑
- username 관련: id, user, email, login, name ...
- password 관련: pass, pwd, password, secret ...
- 필드명 휴리스틱으로 매핑
3. 로그인 성공 판단 (다중 휴리스틱)
- 로그인 페이지로 리다이렉트 안 됨
- "로그인 실패", "invalid password" 텍스트 없음
- 세션 쿠키 발급됨
세 가지 조건을 종합해서 성공/실패를 판단한다. 어느 하나만 보면 오탐이 많아서 다중 휴리스틱으로 처리했다.
CLI에서는 이렇게 쓴다.
s2n scan -u http://target --auth auto --username admin --password pass
--auth auto만 주면 로그인 페이지 찾는 것부터 자동으로 한다. 타겟 URL을 알고 계정 정보만 있으면 된다.
이게 오픈소스 기여에서 제일 신경 쓴 부분이다.
기존 crawl_recursive(), DVWAAdapter는 그대로 살렸다. 새로 추가한 smart_crawl()은 scan() 시작 시 자동 실행되고, 실패하면 기존 방식으로 fallback한다. 기존 사용자 입장에서 breaking change가 없다.
변경한 파일 목록과 변경 범위를 보면:
| 파일 | 변경 내용 | 변경 규모 |
|---|---|---|
interfaces.py | AuthType.AUTO 추가 | 1줄 |
scan_engine.py | smart_crawl() 연동 + SiteMap 기반 target_urls | 최소 수정 |
cli/runner.py | --auth auto, --login-url 옵션 추가 | 옵션 2개 |
cli/mapper.py | AUTO → AuthType.AUTO 매핑 | 1줄 |
crawler/__init__.py | extract_same_origin_links() 공유 함수 추출 | 중복 코드 제거 |
신규 파일 5개, 기존 파일 수정 5개인데 기존 파일은 죄다 최소 수정이다.
그리고 중요한 게, 기존 테스트 106개가 전부 통과했다. 새 기능 추가하면서 기존 동작 건드린 게 없다는 뜻이다.
s2n/
├── crawler/
│ ├── __init__.py # extract_same_origin_links() 공유 함수
│ ├── classifier.py # HTML 폼 자동 분류 (6종)
│ ├── sitemap.py # 크롤링 결과 구조화 + 플러그인 매핑
│ └── smart_crawler.py # BFS 크롤링 + SiteMap 생성
├── auth/
│ ├── __init__.py
│ └── universal_adapter.py # 범용 로그인 (자동 탐색 + 폼 매핑 + 성공 판단)
├── interfaces.py # AuthType.AUTO 추가
├── scan_engine.py # smart_crawl 연동
└── cli/
├── runner.py # --auth auto, --login-url 옵션
└── mapper.py # AUTO 매핑
처음엔 단순하게 "리다이렉트 발생하면 성공"이라고 봤다. 근데 실제로 해보니 로그인 실패해도 리다이렉트 하는 사이트가 있고, 성공해도 리다이렉트 없이 같은 페이지에서 변화만 생기는 사이트도 있었다.
결국 단일 조건으로는 판단이 안 된다는 걸 깨달았다. 리다이렉트 여부 + 실패 텍스트 유무 + 세션 쿠키 발급 여부를 조합해서 다수결로 판단하는 방식이 가장 안정적이었다.
크롤러 코드 여러 곳에서 same-origin 링크 추출 로직이 중복으로 들어가 있었다. smart_crawler.py 만들면서 이 로직을 또 쓰게 됐는데, 그냥 또 복붙하기보다 crawler/__init__.py에 extract_same_origin_links() 공유 함수로 빼는 게 맞다고 판단했다. 기존 코드 동작은 그대로고 중복만 제거했다.
오픈소스에 실제로 기여해봤다는 게 제일 값졌다.
혼자 만든 프로젝트가 아니라 기존 코드베이스가 있고, 다른 기여자들의 코드 스타일이 있고, 테스트가 있는 환경에서 기능을 추가하는 경험 자체가 달랐다. "내 코드가 기존 106개 테스트를 다 통과해야 한다"는 제약이 오히려 설계를 더 꼼꼼하게 만들었다.
그리고 GUI(Chrome Extension)는 scan_engine.py 연동이기 때문에, 내가 만든 smart_crawl이 별도 수정 없이 확장에도 자동 적용된다. 잘 만든 추상화가 어떤 느낌인지 직접 경험했다.
크롤러라고 하면 단순히 링크 따라가는 거 아닌가 싶을 수 있는데, 실제로 만들어보면 폼 분류, 로그인 자동화, 공격 포인트 매핑까지 해야 "쓸 수 있는 크롤러"가 된다는 걸 알게 된다.
특히 범용 로그인 어댑터 만들면서 "로그인 성공이 뭔지"를 프로그래밍적으로 정의해야 하는 상황이 꽤 재밌었다. 사람은 화면 보면 바로 아는데, 코드로 표현하면 꽤 까다롭다.
이번 PR이 머지되면 s2n이 DVWA 전용 스캐너에서 벗어나 진짜 범용 스캐너로 한 단계 올라간다. 그 기반을 만든 거라서 개인적으로 만족도가 높은 작업이었다.
작성자: HoHK
PR: github.com/s2n0n/s2n/pull/139
프로젝트: github.com/s2n0n/s2n — Python open source vulnerability scanner