파라미터와 탐색

XSS를 찾는 일은 올바른 파라미터를 찾는 데서 시작합니다. Dalfox는 여러 단계에 걸쳐 파라미터를 찾습니다. 단계를 모두 알 필요는 거의 없지만, 스캔을 튜닝할 때는 알아두면 도움이 됩니다.

파이프라인 개요

  1. Discovery(탐색): 요청에 이미 들어 있는 입력을 프로빙합니다. 쿼리 값(과 쿼리 파라미터 이름), 헤더, 쿠키, 경로 세그먼트, 페이지에서 찾은 폼의 필드가 대상입니다. URL 프래그먼트 키도 목록에 올라가고 -p로 지정할 수도 있지만, 프래그먼트는 서버에 전달되지 않으므로 스캔은 이를 퍼징하지 않습니다.
  2. Mining(마이닝): -d 본문의 파라미터(form, JSON, GraphQL 변수, XML, multipart)를 프로빙한 뒤, 요청에 없는 이름을 찾습니다. 사전 워드리스트와 응답 속 <input> 요소의 id/name이 그 출처입니다.
  3. Active probing(능동 프로빙): 각 파라미터를 프로빙해 어떤 특수 문자가 살아남는지 파악하고, URL 디코딩을 여러 번 하는 서버를 찾아냅니다.
  4. Payload generation(페이로드 생성): 컨텍스트를 인식하는 페이로드 세트(HTML, JS, 속성, CSS)를 만듭니다. 파라미터의 페이로드를 보내기 전에 빠른 프로브(fast probe)가 샌드위치 마커 요청을 한 번 보냅니다(영문자를 제거하는 필터를 잡기 위한 숫자 전용 대체 프로브 포함). 아무것도 반사되지 않으면 --deep-scan이 아닌 한 그 파라미터의 무거운 페이로드 루프를 건너뜁니다. 능동 프로빙에서 이미 마커가 돌아온 것을 봤다면, 빠른 프로브는 요청을 따로 보내지 않고 그 결과를 재사용합니다.
  5. Reflection check(반사 확인): 페이로드를 보낸 뒤 그것이 돌아오는지 확인합니다.
  6. DOM verification(DOM 검증): 응답을 파싱하여 페이로드가 실행 가능한 위치에 도달했는지 확인합니다. AST 기반 DOM-XSS 분석은 먼저 랜딩 페이지에서 한 번 실행되고, 이어서 파라미터당 한 번 빠른 프로브의 응답(빠른 프로브를 건너뛴 경우에는 첫 반사 응답)으로 실행됩니다.

특정 파라미터 지정하기

테스트할 파라미터를 직접 지정합니다:

dalfox scan https://target.app/api \
  -p q \
  -p id:query \
  -p auth:header \
  -p token:cookie

위치(Location): query, body, json, multipart, cookie, header, graphql, xml. 주입 지점이 쿼리 문자열이 아니라면 name:location 형식을 쓰세요. graphql과 xml은 요청 본문에서 자동으로 탐지됩니다(GraphQL 및 XML 본문 주입 참조). 힌트는 이미 발견된 파라미터를 필터링할 수는 있지만, 이름만으로 새 본문을 합성하지는 못합니다. path와 fragment도 같은 방식으로 필터로만 동작합니다. 경로 세그먼트는 위치로 이름이 붙습니다(-p path_segment_0:path).

위치 힌트가 없는 경우(-p q만 지정):

  1. 탐색/마이닝이 이미 그 이름의 파라미터를 찾았다면 그대로 유지합니다(필터).
  2. 찾지 못했다면 Dalfox가 파라미터를 새로 만듭니다. 위치는 요청에서 추론하며(URL 쿼리 → 본문 → 쿠키 → 헤더), 기본값은 query입니다.

즉, -p q --skip-discovery --skip-mining 같은 조합도 아무것도 스캔하지 않고 조용히 지나가는 대신 q를 그대로 테스트합니다.

워드리스트로 마이닝하기

URL에 파라미터가 없더라도 Dalfox는 흔히 쓰이는 이름을 시도해 볼 수 있습니다:

# 로컬 워드리스트
dalfox scan https://target.app -W ./params.txt

# 원격 워드리스트(첫 조회 이후 캐시됨)
dalfox scan https://target.app --remote-wordlists burp,assetnote

스캔당 목록은 하나만 사용합니다. --remote-wordlists를 불러오는 데 성공하면 그것이 우선하고 -W는 무시됩니다. -W는 원격 조회가 실패했을 때의 대체 수단입니다. 마이닝한 이름은 쿼리 파라미터로 테스트합니다.

사전과 DOM 후보는 이름마다 요청을 하나씩 보내지 않고, 요청 하나에 최대 64개 이름을 담은 버킷으로 검사합니다(요청 라인은 약 8 KiB 이하로 유지). 카나리(canary)가 반사된 이름은 그 응답 하나로 식별됩니다. 아무것도 반사되지 않았는데 응답이 달라진 버킷은 같은 크기의 대조(control) 요청과 비교한 뒤, 버킷을 나눠 어떤 이름이 변화를 일으켰는지 찾습니다. 추가 요청은 이런 모호한 버킷에만 쓰므로 큰 워드리스트도 저렴하게 확인할 수 있습니다.

같은 요청에도 본문이 매번 달라지는 페이지(순환 위젯, 타임스탬프 등)에서는 상태 코드 변화만 응답 변화로 인정합니다. 이름이 여러 개 든 버킷의 요청이 실패했거나 서버가 크기 때문에 거부했다면(예: 쿼리 길이 제한) 버리지 않고 나눠서 다시 보냅니다.

-W나 원격 워드리스트를 주지 않으면 내장 목록을 씁니다. XSS 중심 이름에 gori의 Param Miner 워드리스트에서 가져온 더 넓은 이름(API, 인증, 페이지네이션, feature flag, 미디어, 운영 관련)을 더한 목록입니다.

자동 축소(Auto-collapse)

보내는 파라미터를 전부 되돌려주는 페이지가 있습니다(쿼리 전체를 그대로 출력하는 검색 페이지 등). 이런 페이지에서는 워드리스트의 모든 이름이 반사되어 스캔이 이름마다 테스트를 하게 됩니다. Dalfox는 두 가지 방법으로 이를 막습니다:

  • Sentinel 사전 프로브(pre-probe): 워드리스트를 돌리기 전에, 실제 앱에서 쓸 리 없는 파라미터 이름 세 개를 테스트합니다. 세 개가 모두 반사되면 무엇이든 되돌려주는 페이지로 보고, 마이닝을 건너뛴 뒤 합성 any 쿼리 파라미터 하나로 대신합니다. 요청은 최대 3개이며, 워드리스트 항목이 15개를 넘을 때만 실행합니다.
  • EWMA 축소: 버킷 처리가 끝나면 이동 반사 비율(rolling reflection ratio)을 봅니다. 후보 이름 15개 이상, 반사 5건 이상에서 비율이 85% 이상이면 sentinel 확인을 실행합니다(목록이 작아 처음에 실행하지 않은 경우). sentinel도 반사되면 마이닝한 쿼리 파라미터를 같은 any 파라미터 하나로 접고, 반사되지 않으면 반사된 이름을 모두 유지하므로 선택적으로 반사하는 페이지에서 커버리지를 잃지 않습니다.

노이즈 정리하기

특정 파라미터 무시:

dalfox scan https://target.app --ignore-param csrf --ignore-param __RequestVerificationToken

URL 패턴으로 범위 지정:

dalfox scan urls.txt \
  --include-url '^https://api\.target\.app/' \
  --exclude-url '/static/|/health'

범위 밖(out-of-scope) 도메인 목록:

dalfox scan urls.txt --out-of-scope-file scope-block.txt
# 또는 와일드카드를 사용해 인라인으로(플래그를 반복하고, 플래그 하나에 패턴 하나)
dalfox scan urls.txt --out-of-scope '*.google.com' --out-of-scope '*.cdn.cloudflare.net'

--out-of-scope는 쉼표로 나누지 않습니다. '*.google.com,*.cdn.cloudflare.net'은 패턴 하나로 읽히고 아무것과도 일치하지 않습니다.

탐색만 하고 공격하지 않기

두 모드 모두 같은 탐색, 마이닝, 능동 프로빙 요청을 실행하고 스캔 단계 전에 멈추므로, 스캔의 XSS 페이로드는 보내지 않습니다. 차이는 무엇을 출력하느냐입니다.

Dry-run은 공격 계획을 출력합니다. 대상 수, 대상별로 찾은 파라미터, 실제 스캔이 보낼 요청 수의 하한 추정치입니다. WAF 자극 프로브(<script> 페이로드를 담은 요청 1건)도 건너뜁니다. --only-discovery는 --skip-waf-probe를 주지 않는 한 이 프로브를 보내므로, 공격 형태의 요청을 하나도 보내면 안 될 때는 dry-run을 쓰세요.

dalfox scan https://target.app --dry-run

Discovery-only는 탐색된 파라미터마다 한 줄(URL, 이름, 위치)을 출력합니다:

dalfox scan https://target.app --only-discovery

두 모드 모두 범위 지정과 CI 사전 점검에 도움이 됩니다.

단계 건너뛰기

더 빠르게 진행하거나 불안정한 대상을 우회하려면 파이프라인의 일부를 건너뛰세요:

플래그 건너뛰는 대상
--skip-discovery 탐색 단계 전체(쿼리, 헤더, 쿠키, 경로, 폼, 프래그먼트)
--skip-mining 모든 워드리스트/DOM 마이닝
--skip-mining-dict 사전 마이닝만
--skip-mining-dom <input>의 id/name 속성에서 파라미터 이름을 수집하는 마이닝만
--skip-reflection-header 내장된 공통 헤더 스윕. -H로 넘긴 헤더는 여전히 프로빙됩니다
--skip-reflection-cookie 요청에 담긴 쿠키 프로빙
--skip-reflection-path 경로 세그먼트 반사 확인

명시적으로 지정한 것은 이 플래그들의 영향을 받지 않습니다. -d 본문 파라미터는 --skip-mining에서도 프로빙되고, -p name:header나 -p name:cookie는 짝이 되는 --skip-reflection-* 플래그가 있어도 프로빙됩니다. 페이로드 쪽 건너뛰기 플래그(--skip-xss-scanning, --skip-ast-analysis, --skip-waf-probe)는 CLI 레퍼런스에 있습니다.

--skip-mining-dom은 응답 HTML에서 파라미터 이름을 수집하는 동작만 멈춥니다. DOM-XSS 탐지 자체를 끄지는 않습니다. 인라인 <script> 블록을 정적 분석해 location.hash → innerHTML 같은 source→sink 흐름을 찾아 [A](AST 탐지) 결과를 내는 패스는 --skip-ast-analysis가 제어하는 별개의 단계입니다. 결과에서 이 항목만 걸러내려면 --only-poc v,r을 사용하세요. 두 서브시스템의 차이와 각 증거 등급의 의미는 탐지 모델 문서를 참고하세요.

주입 마커(Injection markers)

주입 지점을 이미 알고 있다면 --inject-marker로 표시하세요:

dalfox scan https://target.app/api \
  --inject-marker FUZZ \
  -d '{"filter":"FUZZ"}'

마커를 지정하면 탐색, 마이닝, 능동 프로빙을 건너뜁니다. FUZZ를 포함한 쿼리 값, form 본문 값, multipart 필드(가져온 raw HTTP 요청처럼 본문이 raw multipart인 경우), 최상위 JSON 문자열 값, 헤더 값, 쿠키 값이 각각 파라미터가 되고, 각 페이로드는 그 값 전체를 대체합니다. 그 밖의 위치(경로 세그먼트, 중첩된 JSON 필드)에 둔 마커는 인식하지 않습니다.

쿼리 파라미터나 헤더를 직접 지정할 수도 있습니다:

# 쿼리 파라미터
dalfox scan 'https://example.com/?q=FUZZ&page=1' --inject-marker FUZZ

# 헤더
dalfox scan https://example.com -H 'X-Search: FUZZ' --inject-marker FUZZ

자동 사전 인코딩(Auto pre-encoding)

일부 엔드포인트는 페이로드를 원시 텍스트로 받지 않고, 구조적 인코딩(base64, JSON, JWT 등)으로 감싼 형태를 기대합니다. 쿼리 탐색 중 어떤 파라미터에서 일반 마커가 돌아오지 않으면, Dalfox는 아래의 래핑 형태를 시도하고 마커가 반사된 래핑을 유지합니다. 이후 그 파라미터의 페이로드는 같은 래핑을 거쳐 보냅니다. 설정할 것은 없습니다. 쿼리 파라미터에 적용됩니다.

단일 단계 인코딩은 마커를 미리 인코딩해 보내는 방식으로 찾습니다:

탐지된 형태 페이로드 인코딩 방식
base64 BASE64(payload)
2base64 BASE64(BASE64(payload))
2url / 3url 2회 또는 3회 URL 인코딩

능동 프로빙도 <가 필터링되는 쿼리·경로 파라미터에 2url / 3url을 시도해, URL 디코딩을 여러 번 하는 서버를 잡아냅니다.

조합 가능한 파이프라인은 파라미터의 기존 값에서 추론합니다. 값이 구조화된 래퍼로 디코딩되면, Dalfox는 모든 리프(leaf) 문자열 필드를 각각 별도의 가상 하위 파라미터로 프로빙하고, 마커가 반사된 리프를 유지합니다:

래퍼 형태 파이프라인
Base64로 감싼 JSON ?qs=eyJ… JsonField(/leaf) → Base64
Base64URL로 감싼 JSON JsonField(/leaf) → Base64Url
순수 URL 인코딩된 JSON ?blob=%7B…%7D JsonField(/leaf)
JWT/JWS ?token=h.p.s JsonField(/leaf) → Base64Url → JwtAssemble

각 리프는 대괄호 스타일 이름을 가진 별도의 파라미터가 됩니다. qs의 move_url 필드에 있는 페이로드는 qs[move_url]로 표시되고, 배열 요소는 qs[items][0]으로 나타납니다. 실제 요청에서는 원래 부모 파라미터(qs) 값을 바꾸므로, 서버에는 평범한 요청으로 보입니다.

JWT는 원래 헤더와 서명 세그먼트를 그대로 둡니다. 서명이 바뀐 페이로드와 맞지 않으므로, 토큰을 검증하지 않는 엔드포인트에서만 통합니다. 서명을 검사하는 엔드포인트에서 탐지 결과가 없는 것은 정상입니다.

대상이 Dalfox가 자동 탐지하지 못하는 래핑을 쓴다면, --inject-marker(위 참조)로 주입 지점을 고정할 수는 있지만 페이로드는 래핑 없이 그대로 전송됩니다.

GraphQL 및 XML 본문 주입

요청 본문(-d, 또는 캡처된 raw-http / har 요청)이 GraphQL이나 XML 문서라면, Dalfox는 본문 전체를 한 덩어리로 테스트하지 않고 그 안의 값을 각각 주입 지점으로 다룹니다.

GraphQL(JSON 본문. content type이 아니라 본문의 형태로 판별합니다): GraphQL 오퍼레이션(값이 query/mutation/subscription 또는 익명 { … } 축약형으로 시작하는 query/mutation 필드)과 variables 오브젝트를 둘 다 가진 본문은, variables 안의 모든 문자열 리프를 프로빙하고 반사되는 것마다 각각 graphql 파라미터(이름은 variables.<경로>)로 등록합니다. 각 페이로드는 오퍼레이션과 다른 변수를 그대로 둔 채 요청 전체를 다시 만들므로, 서버는 항상 유효한 GraphQL 요청을 받습니다.

dalfox scan https://target.app/graphql \
  -X POST \
  -H 'Content-Type: application/json' \
  -d '{"query":"query($q:String!){ search(term:$q){ id } }","variables":{"q":"seed"}}'
# → variables.q 가 `graphql` 파라미터로 주입됩니다

query라는 이름의 필드만 있는 평범한 REST 엔드포인트(검색창, {"query":"laptop"})는 GraphQL로 보지 않습니다. variables 오브젝트와 오퍼레이션 형태의 값이 둘 다 있어야 하므로, 일반 JSON 본문은 그대로 json 경로를 탑니다.

XML / SOAP(Content-Type이 text/xml, application/xml, 또는 application/soap+xml 같은 +xml 타입이거나, 본문이 <?xml …?> 프롤로그로 시작하는 경우): 각 엘리먼트 텍스트 노드와 속성 값을 프로빙하고, 반사되는 것마다 xml 파라미터가 됩니다. 페이로드는 그 자리에 끼워 넣습니다. 문서의 나머지 바이트(네임스페이스, 형제 엘리먼트, SOAP 엔벨로프)는 그대로이고, 요청의 XML content-type도 유지합니다.

dalfox scan https://target.app/soap \
  -X POST \
  -H 'Content-Type: application/soap+xml' \
  -d '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><search><term>seed</term></search></soap:Body></soap:Envelope>'
# → <term> 텍스트 노드가 `xml` 파라미터로 주입됩니다

모든 반사 탐지 결과와 마찬가지로, 응답이 브라우저가 마크업으로 렌더링하는 문서일 때만 [V]가 됩니다(탐지 모델 참고). application/json으로 응답하는 GraphQL API나 이스케이프된 값을 되돌려주는 XML 서비스는 무해(inert)로 보고됩니다. 실제로 위험한 곳은 반사된 값을 마크업으로 렌더링하는 관리자 화면, 리포트, 에러 페이지입니다.

반사 프로브 형태

모든 탐색 및 마이닝 프로브는 단일 토큰 대신 샌드위치 마커(OPEN + INNER + CLOSE)를 보냅니다. 양 끝이 어떻게 되었든 INNER가 돌아오면 반사된 것으로 봅니다:

돌아온 형태 추정 원인
OPEN+INNER+CLOSE 일반 반사
OPEN+INNER 필터가 값의 끝부분을 제거함
INNER+CLOSE 필터가 값의 앞부분을 제거함
INNER만 정규식이 값 일부를 추출했거나 양 끝이 모두 제거됨

어느 경우든 탐색은 그 파라미터를 기록하고 스캔을 이어 갑니다. 단일 토큰으로 확인했다면 첫 행만 잡고, 접두사나 접미사를 제거하는 엔드포인트는 놓쳤을 것입니다. 마커 토큰은 Dalfox가 시작할 때마다 무작위로 만들어지므로(dlx/dlxmid/xld 뒤에 16진수 8자리) 페이지에 우연히 같은 문자열이 있을 가능성은 매우 낮습니다.

탐지 결과가 "검증됨"이 되는 기준

결과 확인 방식
V (Vulnerable, 취약) Dalfox가 응답 DOM을 파싱해 페이로드가 실행될 수 있는 위치에 있음을 확인합니다. 브라우저 실행이 아니라 실제 응답을 정적으로 파싱한 결과입니다. 탐지 모델 참고. evidence 필드는 이를 입증한 경로를 태깅합니다: DOM 마커(CSS 셀렉터 적중), 실행 가능한 URL(위험한 속성 안의 javascript:/data:), HTML 구조적 증거(값이 싱크 호출인 on* 핸들러를 가진 주입된 요소), JS 컨텍스트 AST(파싱된 AST가 페이로드의 바이트 범위에 포함됨을 보여주는 <script> 내부의 싱크 호출), 인라인 핸들러 브레이크아웃(기존 on* 속성 안의 JS 문자열을 페이로드가 닫음).
A (AST-detected, AST 탐지) 정적 JavaScript 분석이 사용자 제어 소스를 위험한 싱크로 추적함(예: innerHTML = location.hash).
R (Reflected, 반사됨) 페이로드 텍스트가 응답 본문에 나타났지만 아직 DOM 증거는 없음. 여전히 수동으로 조사할 가치가 있음.

V와 A는 신호입니다. R은 힌트입니다.

안전한 컨텍스트

반사된 위치가 모두 <textarea>, <title>, <noscript>, <xmp>, <plaintext> 내부라면 보고하지 않습니다. 그 안의 내용은 텍스트로 렌더링되므로 오탐(false positive)만 낳기 때문입니다. 파라미터 자체는 계속 스캔하므로, 먼저 그 요소를 닫는 페이로드(</textarea><svg onload=…>)는 여전히 찾을 수 있습니다.

같은 게이트가 무해한 형태 몇 가지를 더 걸러냅니다. <script> 안에만 반사되었고 파싱한 JavaScript상 싱크 호출이 생기지 않는 경우, URL 값 속성 밖에서 서버가 이스케이프(퍼센트 또는 엔티티 인코딩)해서 되돌려준 경우, 그리고 javascript: / data: 페이로드가 URL 값 속성의 맨 앞에 한 번도 오지 않는 경우입니다. 브라우저가 마크업으로 렌더링하지 않는 응답(JSON, text/plain 등)은 content type 기준으로 따로 걸러냅니다.

다음 단계

ESC