레드팀 · AI 자동화 · 시스템 엔지니어링
직접 만들고, 실험하고, 기록한다
© 2026 ZINO LAB
autopwn 개발기 ③ — 첫 라이브 런, SSE 67줄, 그리고 남은 nmap 버그 — ZINO
2026-05-17 · 1분 읽기 ·
scope/allowlist 등록 → /scope/check 거부·허가 동작 확인 → /playbooks/run POST → SSE 라이브 관찰 → 15분 후 succeeded. 백엔드 골격은 의도대로 작동, 다만 nmap adapter의 결과 파싱에 작은 구멍이 남았다.
시리즈 : autopwn 개발기 ③
이전 글 : ② Phase 1을 막은 세 개의 함정
이 글의 범위 : 패치 끝난 백엔드에 실제 타깃 박고 첫 recon_basic 한 번 돌리기. 무엇이 됐고, 무엇이 미흡했나.
타깃 호스트는 본인이 회선·운영권을 가진 호스트지만 글에서는 198.51.100.x (RFC5737 documentation prefix) 로 마스킹한다.
🧭 셋업 — scope 등록
scope/allowlist.yaml 은 디폴트로 빈 상태. 한 줄 추가한다:
networks :
- 198.51.100.42/32 # 본인 관리 호스트 (legacy 홈페이지)
deny :
- 127.0.0.0/8
- 169.254.0.0/16
- 224.0.0.0/4
require_scope_for_discovery : true
proxy_required_for_external : true
요점:
이 글이 도움이 됐나요?
🔥 핵심 💡 인사이트 👍 좋아요 🤯 충격 ⚡ 빠름
networks:
/32
deny: 가 networks: 를 이긴다 (RFC1918 안 풀어줘도 디폴트로 막힘은 유지)
proxy_required_for_external: true — 랩 CIDR 밖이면 PROXY_CHAIN=true 가 아니면 거절
🚦 scope guard 동작 확인 curl -s -H "X-API-Key: $KEY " -H "Content-Type: application/json" \
-d '{"host":"8.8.8.8"}' http://127.0.0.1:8080/scope/check
# {"target":"8.8.8.8","allowed":false,"reason":"target is not in any allow entry"} curl -s -H "X-API-Key: $KEY " -H "Content-Type: application/json" \
-d '{"host":"198.51.100.42"}' http://127.0.0.1:8080/scope/check
# {"target":"198.51.100.42","allowed":true,"reason":"matches network entry"} # 등록 안 된 외부 IP → deny
$ curl -s -H "X-API-Key: $KEY " -d '{"host":"8.8.8.8"}' \
http://127.0.0.1:8080/scope/check
{
"target" : "8.8.8.8",
"allowed" : false ,
"reason" : "target is not in any allow entry"
}
# allowlist 의 lab 네트워크 → allow
$ curl -s -H "X-API-Key: $KEY " -d '{"host":"172.18.0.12"}' \
http://127.0.0.1:8080/scope/check
{
"target" : "172.18.0.12",
"allowed" : true ,
"reason" : "matches network entry"
} 이 두 응답이 의도대로 갈라지는 게 합법성 가드의 핵심. "등록 안 한 IP는 절대 어댑터 진입 못함" 이 코드로 확정.
/scope/check 세 케이스 — 외부 IP (8.8.8.8) 거부, 본인 자산 허가, 그리고 deny 리스트의 127.0.0.1 은 networks: 에 포함돼도 deny 가 우선해서 차단. 정책이 코드로 갈라준다.
🎬 라이브 런 — recon_basic 트리거 먼저 SSE 채널을 별 터미널에 미리 열어둔다:
curl -N -H "X-API-Key: $KEY " http://127.0.0.1:8080/events/stream curl -s -H "X-API-Key: $KEY " -H "Content-Type: application/json" \
-d '{"playbook":"recon_basic","target":"198.51.100.42"}' \
http://127.0.0.1:8080/playbooks/run {
"run_id" : "bf07939f7eca403e87ba7ee8945f4fba" ,
"playbook" : "recon_basic" ,
"target" : "198.51.100.42" ,
"state" : "queued" ,
"steps" : [
{ "id" : "discover" , "state" : "pending" },
{ "id" : "vuln_hints" , "state" : "pending" }
],
"created_at" : "2026-05-17T15:35:02Z"
} /playbooks/run 응답 → 폴링 — queued → running 으로 상태 전이, discover:running step 이 실제로 워커에 잡혀 돌아감. SSE 채널은 ready 이벤트로 즉시 응답.
플레이북 정의는 두 step. discover 가 -sT -Pn --top-ports 1000 -sV 로 깔고 — vuln_hints 가 그 결과에 open port가 있을 때만 banner/http-title 스크립트로 후속 스캔.
# api/playbooks/recon_basic.yaml
name : recon_basic
description : TCP connect 스캔 + 서비스 핑거프린팅. Proxychains/Tor 친화적.
inputs :
- target
disallow_ai : false
steps :
- id : discover
tool : nmap
args :
flags : "-sT -Pn --top-ports 1000 -sV --reason"
- id : vuln_hints
tool : nmap
when : "len(ctx['results']['discover']['open_ports']) > 0"
args :
flags : "-sT -Pn --script=banner,http-title -p {{ results.discover.open_ports_csv }}" -sT (TCP connect) 만 쓴다 — proxychains/Tor 환경에서는 raw 패킷이 통과 안 되기 때문에 -sS 같은 SYN 스캔은 의미가 없다. nmap adapter 자체가 -sS/-sU/-A/-O 같은 raw 플래그를 strip 하고 -sT -Pn 을 강제한다.
📡 SSE 라이브 흐름 event: ready
data: {"ts":"...","channels":["autopwn:events","autopwn:events:scan","autopwn:events:session"]}
event: run.started
data: {"run_id":"bf07939...","playbook":"recon_basic","target":"198.51.100.42"}
event: step.running
data: {"run_id":"bf07939...","step":"discover","summary":null}
event: heartbeat
data: {"ts":"..."}
... (60회 heartbeat × 15초) ...
event: step.succeeded
data: {"run_id":"bf07939...","step":"discover","summary":"1 host(s), 8 open ports [proxied/connect-only]"}
event: step.skipped
data: {"run_id":"bf07939...","step":"vuln_hints","summary":"when=false"}
event: step.succeeded
data: {"run_id":"bf07939...","step":"vuln_hints"}
event: run.succeeded
data: {"run_id":"bf07939...","state":"succeeded"} 실제 lab(172.18.0.12 — Solr 8.11.0)에서 받아 본 SSE 흐름:
$ curl -N -H "X-API-Key:$KEY" http://127.0.0.1:8080/events/stream
event: ready channels=[autopwn:events, …:scan, …:session]
event: run.started playbook=web_vuln_chain target=172.18.0.12
event: step.running step=port_check
event: step.succeeded step=port_check summary=1 host, 1 open port (8983/tcp)
event: step.running step=nuclei_web
event: tool.progress (nuclei stdout 라이브 스트림)
event: step.succeeded step=nuclei_web summary=0 finding(s) (crit=0, high=0)
event: step.skipped step=nuclei_followup summary=when=false
event: run.succeeded
event: heartbeat (15초마다 — autopwn API 가 보내는 SSE keepalive,
실제 활동 아님) grep -c '^event:' /tmp/autopwn-sse.log
# 68 이벤트 타입 횟수 heartbeat 60 step.running 2 step.succeeded 2 step.skipped 1 run.started 1 run.succeeded 1 ready 1
heartbeat 15초 간격 × 60 = 900초 = 15분. 일치.
⏱️ 15분이 긴가? — Tor 회선의 대가 [1 00:35:09] state=running
[2 00:35:24] state=running
...
[59 00:49:41] state=running
[60 00:49:56] state=succeeded -sT --top-ports 1000 -sV 면 nmap 이 1000개 포트에 connect 시도하고 열린 포트는 -sV 로 service detection 까지 한다. 일반 회선이면 10~30초인 스캔이 모든 TCP connect 가 Tor SOCKS5 회선을 새로 잡아서 15분이 됐다.
✅ outbound IP 누설 0
✅ DNS 누설 0 (proxychains proxy_dns 옵션)
❌ 스캔 속도가 회선 대신 Tor 의 동시 회선 한계에 묶임
❌ 일부 포트는 Tor exit 노드 응답 패턴이 다를 수 있어 false negative 가능
자가 호스팅 도구의 디폴트로는 받아들일 만한 트레이드오프. 속도가 필요한 engagement 는 별도 PROXY_CHAIN=false 오버라이드 (그래도 scope 안에 있어야 함)로 전환.
🐛 발견된 버그 — nmap adapter 의 결과 파싱
증상 ① vuln_hints 가 skipped "1 host(s), 8 open ports [proxied/connect-only]" 라고 했는데 vuln_hints 의 when 조건:
"len(ctx['results']['discover']['open_ports']) > 0" 이 false 로 평가됨 → summary: "when=false" 로 skip.
즉 adapter 가 ctx 에 open_ports 리스트를 채우지 않았다. summary 문자열 한 줄만 채우고 구조화된 결과(open_ports: [22, 80, ...] )는 빠짐.
증상 ② findings 가 비었음 curl -s -H "X-API-Key: $KEY " "http://127.0.0.1:8080/findings?run_id= $RUN_ID " | jq .
# [] 8개 포트를 발견했는데 Finding row 가 0건. adapter 가 nmap XML 결과를 파싱해서 Finding 으로 등록하는 단계가 빠져있다.
원인 위치 api/app/tools/nmap.py 의 어댑터가:
nmap -oX - 로 XML 결과를 받음
summary 문자열만 만들고 반환
XML 파싱 → open_ports: List[int] 채움 → Finding row 등록 이 세 줄이 미구현
Task #5 — nmap adapter: open_ports 파싱 / Finding 등록
- ctx['results'][step_id]['open_ports']: list[int]
- ctx['results'][step_id]['open_ports_csv']: str (vuln_hints 의 -p 인자용)
- Finding(host, port, service, banner) row 등록 플레이북의 when 조건과 후속 step의 -p {{ results.discover.open_ports_csv }} 가 의도대로 작동하려면 adapter 측에서 이 두 키를 채워야 한다.
✅ Phase 1 검증 매트릭스 검증 항목 결과 infra up (tor/pg/redis/msf/api/worker) ✅ (msf 엔트리포인트 1회 패치) Tor egress (check.torproject.org IsTor:true ) ✅ API /healthz ✅ {"ok":true} /modules/manifest (20 endpoints, 17 events)✅ scope guard deny (8.8.8.8) ✅ allowed:false scope guard allow (등록 호스트) ✅ /playbooks/run queueing✅ SSE 라이브 흐름 (ready→started→running→succeeded) ✅ 68 이벤트 recon_basic 완료 ✅ 15분, 8 open ports detected nmap adapter 결과 파싱 ⚠️ 후속 일감 (open_ports + findings) readyz tor 체크 (httpx socks5h) ⚠️ cosmetic — entrypoint check 는 정상
Phase 1 백엔드 골격 PASS. 하드코어 익스플로잇이 아니라 풀 스택의 뼈대가 정상 동작하는지 확인하는 단계라 이 매트릭스로 충분.
🚀 다음 단계
Phase 2 — homepage /playground/hacking/autopwn : Next.js 어드민 콘솔. EventSource SSE 라이브 뷰 + 플레이북 트리거 UI + findings 표. 가이드는 docs/integration-homepage.md 에 있어서 그대로 따라가면 됨
Phase 3 — Zino Android :modules:autopwn : Compose UI + OkHttp SSE. 가이드는 docs/integration-zino.md
Phase 4 — AI escalation 토글 on : docker compose --profile ai up -d hexstrike . web_vuln_chain 만 우선 허용 (C2 플레이북은 disallow_ai: true 로 차단된 채 유지)
Cleanup #5 — nmap adapter 결과 파싱 : 위에서 발견된 두 어긋남 패치
📝 정리
scope guard 가 의도대로 갈라줌 — 외부 IP 는 진입 자체가 막힘
SSE 채널이 의도대로 동작 — 운영자가 실시간 흐름을 모니터 가능
Tor 회선이 정상 동작 — IsTor:true + 15분 스캔이 동시 회선 한계로 묶인 것까지 확인
nmap adapter 의 ctx 채움 + Finding 등록이 미구현 — 다음 어댑터 추가 시 같은 함정 피하려면 베이스 어댑터에 helper 메서드를 만들어 두는 게 좋겠다
3편 시리즈는 여기서 마무리. 다음 일감은 Phase 2 (homepage admin 콘솔) — 그쪽으로 넘어가면 또 다른 종류의 함정이 나올 거고, 그 때 다시 ④편으로 잇는다.
Comments
댓글
댓글을 남기려면 로그인이 필요해요. (네이버 · 구글 계정)
댓글 불러오는 중…