가이드

Repeater & Fuzzer

흥미로운 플로우를 캡처했다면, RepeaterFuzzer가 그 플로우를 테스트하는 곳입니다.

Repeater

Repeater는 요청 워크벤치입니다. 플로우를 보내고, 요청의 어느 부분이든 편집한 뒤, 다시 보냅니다. 응답, 소요 시간, 이전 응답과의 diff가 나란히 표시됩니다. 세션은 프로젝트와 함께 유지되므로 나중에 다시 돌아올 수 있습니다.

세션이 수십 개 쌓이면 칩 스트립이 스크롤되기 시작하고, /로 훑어 찾는 건 더 이상 현실적이지 않습니다. 스트립 위 어느 칩에서든 **f**를 누르면 전체 세션 목록이 뜹니다. 타이핑하면 이름·메서드·경로·대상 호스트·#태그로 걸러지고, Enter로 고른 세션으로 점프합니다. 같은 목록이 스트립 왼쪽 끝의 뒤에도 있습니다 — 클릭하거나, 첫 칩에서 로 이동하면 됩니다. Fuzzer, Notes, Decoder, JWT, Comparer, Miner, Sequencer 등 모든 워크벤치 스트립에 동일하게 있습니다.

서브탭은 일괄 처리를 위해 마크할 수도 있습니다. 스트립에서 t는 서 있는 칩을 마크하고 오른쪽으로 한 칸 이동하며, Shift-T/ 필터가 보여주는 칩을 전부 마크하고, Esc는 스트립을 떠나기 전에 먼저 마크를 지웁니다. 마크된 칩에는 막대가 붙습니다. 마크는 서브탭 동작이 무엇에 작용하는지를 바꾸는 것이지 동작 자체를 늘리는 게 아닙니다 — History 목록이 이미 따르는 규칙과 같습니다.

대상은 마크가 있으면 마크 전부, 없으면 활성 칩

그래서 Shift-TCtrl-W는 열린 세션 전부를 confirm 한 번으로 닫고, Ctrl-R은 마크된 세션을 함께 보내며(각각 자기 연결로, 최대 20개, confirm 후), Spaced는 전부 복제하고, Spacet는 입력한 태그를 전부에 붙입니다. 스트립에서 연 space 메뉴는 SPACE · 3 MARKED로 읽히고 항목 이름이 스스로 바뀝니다(Close 3 sub-tabs, Send 3 sub-tabs). 단일 대상으로 남는 동작은 (cursor)라고 말합니다. 필터가 가리고 있는 마크는 조용히 닫히지 않고 confirm에 드러납니다. Fuzzer, Notes, Decoder, JWT, Cookie, Comparer, Miner, Sequencer 등 모든 워크벤치 스트립이 같은 방식으로 마크·닫기·복제하며, 전송은 Repeater의 것입니다.

편집 가능한 HTTP/2 요청 패널, 헤더와 JSON 본문을 보여주는 응답 패널, 그리고 1152ms 만에 재전송된 200 상태 줄을 갖춘 gori Repeater 탭
Repeater: 왼쪽에 편집 가능한 요청, 오른쪽에 실시간 응답과 소요 시간, 이전 전송과의 diff.

Repeater는 HTTP/1 이상을 다룹니다.

WebSocket에서는 이 "하나씩" 순서가 핵심입니다. 소켓은 대화를 실어 나르므로, 세 번째 메시지가 두 번째의 응답에 의존하는 스크립트는 gori가 그 사이에 기다려 줄 때만 충실하게 재생됩니다. 그리고 그때 트랜스크립트는 보낸 것 전부를 받은 것 전부보다 앞에 나열하는 대신 실제 전선 순서대로 읽힙니다. 여기서 세 가지가 따라옵니다.

WebSocket Repeater는 permessage-deflate를 협상하지 않고, 캡처 프록시도 기본적으로 이를 끕니다. Match & Replace head 규칙으로 확장 제안을 의도적으로 복원할 수는 있습니다. 그렇게 캡처한 세션은 확장으로 인코딩된 바이트를 담고 있어 재생할 수 없습니다. 해당 규칙을 끄거나 클라이언트에서 압축을 끈 뒤 다시 캡처하세요.

명령줄에서 Repeater를 실행하고, 선택적으로 새 대상을 지정할 수 있습니다.

gori run repeater <flow-id> --target https://staging.example.com --diff

환경 변수

아웃바운드 요청은 $KEY 스타일 치환을 지원합니다. 토큰은 에디터에서 리터럴 텍스트로 남아 있다가, Repeater, Fuzzer, Miner, Intercept 포워드, gori run, MCP send_request에서 전송 시점에만 확장됩니다.

변수는 두 곳에서 정의합니다(키 충돌 시 프로젝트가 우선).

레이어 위치
Global Preferences(Ctrl-,) → Editor & KeysEnv, Ctrl-PSettings: Env, 또는 settings.jsonenv 섹션
Project Project 탭 → ENV 패널 (a 추가, e 편집, d 삭제)

기본 접두사는 $입니다(ENV space 메뉴의 Change prefix나 설정의 env.prefix로 변경 가능). 키는 A-Z a-z _로 시작해 A-Z a-z 0-9 _가 이어집니다.

알 수 없는 토큰은 요청이 표시되는 곳에서는 리터럴 텍스트로 그대로 남습니다. 에디터는 입력한 그대로를 유지하고, 하이라이터가 미등록 토큰을 등록된 토큰과 다르게 칠합니다. 다만 전송되지는 않습니다. Repeater, Fuzzer, Miner, Sequencer, Discover는 요청 라인, 헤더, 타깃에 아무것으로도 해석되지 않는 변수가 남아 있으면 그 이름을 대며 실행을 거부합니다. minimize, 편집한 intercept forward, WebSocket 메시지도 마찬가지입니다. 변수를 설정하거나 토큰을 지우세요. 검사 범위는 요청 head뿐입니다. 바디 안의 $는 바이트로 취급하므로 바이너리 업로드는 그대로 재전송됩니다. WebSocket 텍스트 메시지는 head가 없으므로 페이로드 전체를 검사하고, 바이너리 메시지는 검사하지도 확장하지도 않습니다.

GET /api/me HTTP/1.1
Host: api.example.com
Authorization: Bearer $TOKEN

캡처된 트래픽에 나타나는 값은 복사하거나 표시할 때 다시 $KEY로 마스킹할 수 있어, 비밀 값이 원시 문자열이 아니라 토큰으로 유지됩니다.

Fuzzer

Fuzzer는 Intruder 스타일 엔진입니다. 요청에서 위치를 표시하고, 페이로드 세트를 붙이고, 응답을 매칭하면서 요청 행렬을 전송합니다.

강조된 마커 위치를 보여주는 요청 템플릿, 페이로드 세트 설정 패널, 전송된 요청 결과 테이블, 분포 사이드바를 갖춘 gori Fuzzer 탭
Fuzzer: 템플릿의 §…§ 마커, CONFIG의 페이로드 세트와 모드, 실시간 결과 테이블, 상태 / 크기 분포 사이드바.

공격 모드

모드 동작
sniper 한 번에 한 위치씩, 단일 페이로드 세트를 순환 (기본값)
batteringram 표시된 모든 위치에 같은 페이로드
pitchfork 병렬 세트: 각 세트의 n 번째 페이로드를 함께
clusterbomb 모든 세트에 걸친 모든 조합

위치와 페이로드

요청에서 §…§ 마커로 위치를 표시하거나, gori가 자동으로 배치하게 하세요. 페이로드 세트는 내장 프리셋(sqli, xss, traversal, format-string, bad-strings, command-injection — 파일 없이 바로 시작), 워드리스트, 명시적 목록, 숫자 범위, N개의 빈(null) 페이로드, 또는 무차별 대입 문자 세트가 될 수 있습니다. 프리셋은 추가 파일을 병합(내장 우선, 중복 제거)할 수 있고 다른 세트와 조합됩니다. 프로세서를 사용하면 나가는 각 페이로드를 변환할 수 있습니다: prefix/suffix, URL/base64/hex 인코딩, 대소문자 변환, 해싱, 정규식 치환.

마커 하나에 자체 Decoder 체인을 붙일 수도 있습니다. 커서를 마커 안에 두고 Ctrl-Y를 누르면 체인 편집기가 열리고, 보내기 전에 값이 각 단계를 거치는 모습을 미리 보여 줍니다. Decoder 라이브러리에 저장해 둔 체인은 여기서 이름으로 부를 수 있어서, 한 번 만들어 둔 체인이 마커 안에서는 단어 하나가 됩니다: §admin¦myenc > url-encode§. Repeater 마커도 동일합니다.

gRPC 메시지는 마커가 유용하게 쓰이지 않는 유일한 곳입니다 — 위치가 바이트 범위가 아니라 스키마가 아는 필드인 gRPC 필드 스윕을 보세요.

매칭

ffuf 스타일 matcher와 filter로 status, size, words, lines, 왕복 시간(--mt/--ft, ms 단위 — 시간 기반 블라인드 페이로드의 유일한 증거가 되는 차원), 본문 정규식에 대해 결과를 필터링합니다. 여기에 시끄러운 기준선을 걸러내는 자동 보정까지 더해집니다. 자동 보정은 스윕 전에 대상을 여러 번 샘플링한 뒤, 각 응답을 모든 샘플 형태와 비교하되 그 샘플들이 스스로 보여 준 흔들림만큼 폭을 넓혀서 비교합니다 — 그래서 요청마다 달라지는 id나 타임스탬프를 품은 페이지는 걸러지고, 샘플이 전부 동일했던 대상은 여전히 정확히 비교됩니다. 매칭된 응답은 강조되며 캡처 정규식으로 추출할 수 있습니다.

실행 저장과 다시 열기

TUI 실행 중 gori는 모든 결과를 비공개 임시 SQLite 스풀에 기록하고, 화면 창은 최대 5,000행 / 동적 결과 데이터 64 MiB로 제한합니다. 최신 행은 계속 조작할 수 있고, 혼자서 지나치게 큰 행은 지표만 표시됩니다. 페이로드/오류 텍스트마저 이 창을 넘으면 해당 필드를 잘라 표시하고 그렇게 표시했음을 알린 뒤, 자리표시자로 요청을 재구성하는 대신 Repeater/Comparer로 보내기를 비활성화합니다. 스풀에는 여전히 완전한 행이 남아 있습니다. 스풀은 소유자 전용이고, 실행을 버리면 작은 백그라운드 트랜잭션으로 정리되며, 프로젝트를 닫으면 통째로 제거됩니다. 스풀 실패는 나가는 트래픽을 결코 멈추지 않으며, 그 실행을 영구 저장할 수 없게 만들 뿐입니다.

비어 있지 않은 실행이 끝나고 스풀이 완전하면, READ 모드에서 Shift-S 를 눌러 스풀된 모든 행을 프로젝트에 영구 저장합니다. 편집 중에는 대문자 S가 평소대로 입력됩니다. 저장은 행 수와 바이트 수가 제한된 백그라운드 배치로 이뤄지고, 상태 줄과 Jobs 패널이 성공 또는 실패를 알립니다. 단축키를 다시 눌러도 사본이 생기지 않으며, 프로젝트 복사가 실패하면 재시도를 위해 임시 스풀이 남습니다.

프로젝트를 다시 열면 처음 선택된 Fuzzer 세션에 대해 마지막으로 성공한 저장 실행이 복원됩니다. 다른 Fuzzer 세션은 처음 선택할 때 지연 복원됩니다. 복원은 가장 최근 5,000행 / 64 MiB만 창에 읽어 들이고 showing N으로 표시합니다 — 아카이브 전체는 페이지 단위 CLI/MCP 리더로 계속 읽을 수 있습니다. 진행 중이거나, 일부 실패했거나, 현재 형식 이전의 불완전한 스냅숏은 자동 복원되지 않습니다. Space → Run history 를 열면 더 오래된 현재 형식 실행을 고를 수 있고, Enter가 불러오고 d가 지웁니다. Fuzzer 세션을 닫으면 그 세션의 저장 실행 기록도 함께 삭제되며, 닫기 확인 창이 그 사실을 말해 줍니다.

헤드리스와 에이전트 표면도 같은 영구 저장소를 씁니다:

gori run fuzz save 42 --auto --preset sqli
gori run fuzz list
gori run fuzz show RUN_ID
gori run fuzz show RUN_ID RESULT_INDEX --format json
gori run fuzz delete RUN_ID --yes

평범한 gori run fuzz …는 계속 일회성입니다. MCP에서는 fuzz_startsave_results: true를 넘긴 뒤 list_fuzz_runs, get_fuzz_run, delete_fuzz_run을 쓰세요. 영구 실행은 History 플로우와 별개입니다 — 개별 전송이 History에도 나타날지는 --record-history / record_history가 계속 결정합니다.

스윕의 프레이밍

Content-Length는 페이로드가 삽입될 때마다 다시 계산되고, 템플릿에 본문은 있는데 길이 선언이 아예 없으면 추가되므로, 일반적인 스윕은 항상 일관된 상태를 유지합니다. --verbatim(MCP update_content_length: false, 또는 Fuzzer ADVANCED 카드의 Auto Content-Length 끄기)은 이 두 가지를 모두 끕니다. 본문과 어긋나는 길이 자체가 CL / CL-TE 디싱크 테스트의 목적이기 때문입니다.

뒤쪽 절반은 들리는 것보다 중요합니다. HTTP/1.1 요청 본문에는 연결 종료로 경계를 잡는 형태가 없어서, Content-Length도 청크 Transfer-Encoding도 없는 본문은 오리진이 길이 0인 본문으로 읽습니다 — 페이로드는 읽히지 않은 채 나가는데 모든 행은 여전히 상태 코드를 보고합니다. --verbatim이 템플릿을 바로 그 형태로 남기는 경우, gori는 실행이 조용히 지나가게 두지 않고 첫 전송 전에 이를 알려줍니다.

gRPC 템플릿에는 두 번째 길이 선언 — 각 메시지 앞의 5바이트 접두사 — 이 있으며, 기본값은 반대입니다. gori는 페이로드가 남긴 그대로 두고, 실행이 끝날 때 한 번 알려줍니다(2 of 3 requests left it stale, MCP fuzz_statusgrpc_stale_prefix). 의도적으로 잘못된 접두사를 테스트할 때는 이것이 맞는 동작이지만, 평범한 단항 호출을 스윕하는데 모든 요청이 프레이밍 계층에서 거부된다면 원하는 동작이 아닙니다. --reframe-grpc(MCP reframe_grpc: true, 또는 Fuzzer ADVANCED 카드의 gRPC reframe (unary) 토글)는 요청마다 접두사를 다시 계산합니다. 세 표면 모두 기본값은 꺼짐이며, 단일 메시지에만 적용됩니다. 클라이언트 스트리밍 본문, grpc-web-text 본문, 그리고 이미 프레이밍이 깨진 시드는 그대로 두고 여전히 보고합니다.

gRPC 필드 스윕

protobuf 메시지 안의 바이트에 마커를 씌우는 건 실무에서 쓸 수 있는 동작이 아닙니다. int32 필드의 값은 varint의 옥텟이고, 그 위에 §…§를 두르면 필드가 아니라 와이어 포맷을 테스트하는 셈입니다. 그래서 gRPC 필드 위치는 마킹이 아니라 이름으로 지정합니다 — --field role, MCP의 fields 인자, 또는 Fuzzer ADVANCED 카드의 gRPC field(s) 행:

gori run fuzz --flow 42 --field role --payloads ROLE_ADMIN,ROLE_USER,99

SPEC은 필드 이름, 중첩 메시지 경로(profile.age), 필드 번호, 또는 반복 필드의 특정 occurrence(tags[1])입니다. 해당 rpc를 해석할 descriptor set이 필요합니다 — protoc --descriptor_set_out 파일이든 gori run grpc reflect로 받아온 것이든. 필드 이름은 같은 플로우에서 Repeater의 ␣E:FIELDS 폼과 History의 protobuf 트리가 이미 보여 주는 그 이름입니다.

스플라이스가 할 수 있는 일에서 두 가지가 따라옵니다. 필드는 캡처된 메시지에 실제로 있어야 합니다 — gori는 occurrence를 교체할 뿐 추가하지 않으므로, 기본값으로 남아 와이어에 없는 proto3 필드는 위치가 될 수 없습니다(거부 메시지가 그 메시지에 실제로 있는 필드들을 나열합니다). 그리고 bytes 필드의 페이로드는 hex로 읽습니다(de ad be ef). 그 선언의 값은 바이너리이고, 텍스트로 받으면 의도한 옥텟 대신 입력한 문자열의 UTF-8이 조용히 나가기 때문입니다.

페이로드는 선언을 거쳐 바이트가 됩니다. 스키마가 필요한 이유가 바로 이것입니다: -3int32에서 부호 확장된 10바이트, sint32에서 지그재그 1바이트, bool이나 enum에서는 또 다른 옥텟입니다. 퍼징 대상이 아닌 바이트는 재직렬화가 아니라 캡처에서 그대로 복사됩니다 — 선언되지 않은 필드 번호, group, 최소가 아닌 varint, 잘린 캡처의 파싱되지 않은 꼬리까지 — 그리고 5바이트 길이 접두사는 실제로 나가는 메시지에 맞춰 다시 계산됩니다.

필드 위치의 ¦chain--encode/--prefix/--hash 등 프로세서 파이프라인은 선언된 타입이 인코딩하기 전의 텍스트를 변환합니다. 그래서 --field name¦base64-encode는 페이로드의 base64를 그 string 필드로 보내고, 같은 체인을 int32 필드에 걸면 base64 텍스트는 정수가 아니므로 미리 거부됩니다. (TUI 행에서는 체인 안 단계 구분에 |>를 쓰세요 — 거기서 쉼표는 필드 구분입니다.)

세 가지는 스윕 도중이 아니라 첫 요청 전에 거부됩니다: 스키마가 선언하지 않은 필드, 선언과 와이어 타입이 충돌하는 필드(둘 다 Repeater 폼이 같은 이유로 read-only로 그리는 것들입니다 — 그 옥텟을 바꾸는 길은 여전히 ^X입니다), 그리고 선언된 타입이 담을 수 없는 페이로드입니다.

한 가지 조합은 거부가 아니라 보고됩니다. --verbatimContent-Length를 캡처 값 그대로 두는데, 재인코딩된 메시지는 크기가 다르므로 모든 요청이 잘못된 본문 길이를 선언하고 gRPC 계층에 닿기도 전에 HTTP 프레이밍 계층에서 거부됩니다. 5바이트 접두사를 다시 계산하는 것과 정확히 같은 논리를 다른 길이 선언에 겨눈 것이지만, CL 디싱크는 실제 테스트이므로 실행은 그 사실을 알리고 진행합니다.

WebSocket 퍼징

WebSocket 세션도 다른 대상과 똑같이 스윕하지만, 프로토콜에서 비롯된 차이가 하나 있습니다. 페이로드 하나가 세션 하나입니다. gori는 연결을 열고 템플릿이 담고 있는 핸드셰이크를 수행한 뒤 — HTTP/1.1 위의 RFC 6455 Upgrade: 요청이든, HTTP/2 위의 RFC 8441 확장 CONNECT든 — 페이로드를 끼워 넣은 프레임 스크립트를 보내고, 오리진의 응답을 모두 읽어낸 다음 소켓을 닫습니다 — 그리고 다음 페이로드에 대해 이 과정을 다시 반복합니다. 소켓은 요청/응답 쌍이 아니라 대화이므로, 다른 방식으로는 어떤 응답이 어떤 페이로드에서 비롯됐는지 짝지을 수 없습니다. 따라서 동시성 N은 동시에 열린 소켓 N개를 뜻합니다.

§…§ 위치는 프레임 안에 표시합니다. WebSocket 애플리케이션의 파라미터가 있는 곳이 바로 거기입니다.

gori run fuzz --repeater 7 \
  --message '{"op":"login","user":"§admin§"}' \
  --payloads-preset sqli

WebSocket 세션에 대한 --repeater N은 핸드셰이크와 세션에 저장된 프레임을 함께 시드하므로, 캡처된 교환을 기록된 그대로 스윕합니다. --flow N도 캡처된 소켓에 대해 같은 일을 합니다. 직접 프레임을 작성하려면 --message / --message-frame으로 대체하면 됩니다 — --message-framegori run repeater send와 동일한 opcode=…,fin=…,rsv=…,mask=…,len=…,hex=|b64=|text= 문법을 쓰므로 PING, 코드를 지정한 CLOSE, 마스킹하지 않은 클라이언트 프레임, 페이로드와 어긋나는 길이 필드까지 모두 만들 수 있습니다. --idle-ms는 세션별 침묵 대기 시간을, --ws-keep-key는 템플릿 자체의 Sec-WebSocket-Key를 보내도록 해서 키가 없거나 잘못된 경우 자체를 시험할 수 있게 합니다(RFC 8441 핸드셰이크에는 그런 키가 없으며, 플래그를 무시하는 대신 그 사실을 알려 줍니다).

핸드셰이크도 위치 공간입니다. 업그레이드 요청의 헤더나 쿼리 값에 마커를 달면 프레임과 같은 실행에서 함께 스윕됩니다. 반대 방향은 --ws-http-only입니다 — 핸드셰이크를 평범한 요청으로 보내고 101을 응답으로 읽으므로, 업그레이드에 200으로 답하는 오리진을 시험할 때 씁니다.

결과는 다른 스윕과 똑같이 읽힙니다. 수신 프레임이 곧 응답 본문이기 때문입니다 — --mr, --mh, --extract, 크기·단어 매칭이 모두 그대로 동작합니다. 다만 핸드셰이크 상태 코드가 표현할 수 없는 두 가지가 별도 필드로 붙습니다. 업그레이드가 성공하면 오리진이 페이로드를 받아들였든 아니든 모든 행이 101이기 때문입니다.

#1     bob                       101   30B   1w   1.4ms  ws 1 frame · close 1000
#2     admin'--                  101   31B   5w   1.5ms  ws 1 frame · close 1008

ws_close_codews_frames_in--format json과 MCP fuzz_results에도 같은 방식으로, WebSocket 행에만 나타납니다.

적용되지 않는 옵션이 넷 있습니다. --race는 거부됩니다 — 레이스 그룹은 바이트가 동일한 요청 복사본들이어서 프레임 교환 형태가 없기 때문입니다. --http2Upgrade: websocket 템플릿에서만 거부됩니다. HTTP/2에는 업그레이드 메커니즘이 없으므로(RFC 9113 §8.1) h2 위의 WebSocket은 RFC 8441 확장 CONNECT로 열리며, 시드 자체가 그 형태라면 플래그 없이도 HTTP/2로 스윕합니다 — 핸드셰이크 바이트가 그렇게 말하기 때문입니다. --record-history도 거부됩니다. 프레임 교환은 요청/응답 플로우가 아니어서, 기록하면 전사가 비어 있는 WebSocket인 척하는 History 항목이 남기 때문입니다. --follow-redirects, --timeout, --ac는 여기서 그냥 무의미하며, 실행 시작 시 한 번 그렇게 알려 줍니다. 각 거부 메시지는 원하는 동작을 얻는 방법이 --ws-http-only일 때 그 사실을 함께 알려 줍니다 — 그 플래그를 쓰면 평범한 HTTP 스윕이므로 셋 다 동작하며 History 기록도 됩니다. 송신 프레임이 없는 WebSocket 시드 역시 평범한 HTTP로 스윕합니다. 핸드셰이크만 있는 “프레임” 실행은 페이로드마다 소켓을 열어 아무것도 보내지 않을 뿐이기 때문입니다.

연결 재사용

스윕은 하나의 연결을 여러 요청에 재사용합니다. 요청마다가 아니라 워커마다 TCP 핸드셰이크를(그리고 https라면 TLS 핸드셰이크까지) 한 번만 치릅니다. 원격 오리진을 대상으로 할 때 대개 이것이 실행 시간의 가장 큰 비용입니다.

HTTP/2도 마찬가지입니다. h2 스윕은 페이로드마다 연결을 새로 맺는 대신 한 연결을 순차적으로 재사용합니다 — 스트림 1, 그다음 3, 그다음 5. 이것이 생각보다 중요한 이유는 h2가 보통 손으로 켜는 옵션이 아니기 때문입니다. 캡처된 h2 플로우로 시드한 스윕(History에서 ⇧I, gori run fuzz <flow-id>)은 h2를 스스로 선택하며, 요즘 대상에서 캡처되는 트래픽은 대부분 그렇습니다. 왕복 시간이 0에 가까워 이득이 오히려 축소되어 나오는 루프백 측정에서, h2+TLS 요청 2000건이 1.56초에서 0.08초로, 핸드셰이크 2000회가 50회로 줄었습니다.

프레이밍이 명확하다고 증명할 수 없는 요청은 설정과 무관하게 소켓을 공유하지 않습니다. 실제 본문 길이와 어긋나는 Content-Length, CL+TE, 난독화된 프레이밍 헤더, Connection: close, Upgrade는 각각 자기 연결을 받습니다. 스머글링 페이로드가 다음 페이로드의 결과를 오프레이밍할 수 없다는 뜻입니다. 대상의 동작이 연결 단위일 때(연결 범위 rate limit, 연결로 고정하는 로드 밸런서) 또는 keep-alive 처리 자체를 시험할 때는 --no-keep-alive(CLI), keep_alive: false(MCP), Fuzzer ADVANCED 오버레이의 Keep-alive 토글로 재사용을 끕니다.

gori run fuzz는 실제로 치른 비용을 함께 출력합니다: connections · 50 dialed · 2950 reused.

헤드리스 실행

gori run fuzz <flow-id> \
  --auto \
  --wordlist params.txt \
  --mode sniper \
  --mc 200,302 \
  --fs 0

소스는 캡처된 플로우(--flow), 저장된 HTTP 리피터 세션(--repeater), 원시 요청 파일(--request), 또는 stdin이 될 수 있습니다. 출력은 text, json, jsonl입니다.

TUI의 Repeater 전송은 History에 기록됩니다. 손으로 요청을 다루는 테스터야말로 증거가 사라지던 쪽이었고, 플로우를 남기지 않는 전송은 비교도 내보내기도 인계도 할 수 없습니다. 상태줄이 방금 쓴 id를 알려 줍니다(sent → 200 in 391ms · History #84). Settings → General → Record Repeater sends에서 끌 수 있습니다. WebSocket 전송과 send-group은 기록되지 않으며(소켓의 증거는 프레임 트랜스크립트이고 세션이 이미 갖고 있습니다) 상태줄이 한 번 그렇게 알려 줍니다.

나머지는 그대로 opt-in이고, 헤드리스 표면은 각자의 호출별 인자를 유지하므로 이 설정 때문에 스크립트 동작이 바뀌지 않습니다. gori run repeater send --record-history는 전송을 플로우로 기록하고 그 id를 출력하며(기본 off), gori run fuzz --record-history=none|matched|all은 전송한 각 요청+응답을 기록하고(matched는 매칭된 행만, all은 매 전송, 5000개 상한), MCP send_requestrecord_history:false를 넘기지 않는 한 기록합니다.

기록된 플로우는 모두 출처를 말합니다 — History의 SRC 열, 그리고 쿼리의 src:repeater / src:fuzzer / src:gori — 그래서 재전송이 대상 클라이언트가 만든 트래픽으로 잘못 읽히지 않습니다. 이 플로우는 어디서 왔나를 보세요.

두 도구 모두, 기록된 플로우는 와이어에 나간 그대로의 요청입니다. 활성 세션 슬롯의 헤더 오버레이와 전송 시점에 치환된 $NAME 값이 그 안에 들어 있으므로, 그 플로우를 재전송·비교·스캔하면 조립 전 초안이나 템플릿이 아니라 실제 전송을 재현합니다. (Fuzzer의 결과 은 여전히 렌더된 템플릿을 보여줍니다. "Repeater로 보내기"가 그 바이트로 탭을 시드하고, 슬롯은 전송할 때마다 적용되기 때문입니다.)

다음 단계