시리즈: autopwn 개발기 ② — Phase 1 트러블슈팅 이전 글: 개발기 ① — 왜 자가 호스팅 풀 킬체인을 직접 짜기 시작했나 이 글의 범위: 빌드는 성공했는데 첫 가동에서 세 군데에서 막혔다. 세 함정의 정체와 수정 패치.
🧨 상황
Phase 0(아카이브 정리), Phase 1a~1d(스켈레톤·proxychains 레이어·라우터·플레이북) 까지는 GitHub main에 푸시까지 끝낸 상태. 다음 세션 진입 후 첫 작업은 타깃 등록 → infra 기동 → recon_basic 한 번 돌리기.
이론상 다섯 줄짜리 작업이다.
$EDITOR scope/allowlist.yaml # 본인 자산 등록
docker compose up -d tor postgres redis msf api worker
curl -H "X-API-Key:$KEY" .../readyz
curl -d '{"playbook":"recon_basic","target":"..."}' .../playbooks/run
curl -N .../events/stream실제로는 세 군데에서 막혔다.
🪤 Trap #1 — msf 컨테이너 무한 재시작 (YAML folded scalar)
증상
docker compose ps
# autopwn-msf Restarting (127) 1 second agodocker compose logs msf 를 보면 같은 메시지가 1초마다 반복:
bash: line 1: msfrpcd: command not found
bash: line 2: -U: command not found
bash: line 3: -a: command not found
addgroup: group 'msf' in usemsfrpcd: command not found 뿐 아니라 그 다음 줄의 -U 와 -a 가 별도 명령으로 해석되고 있다. argv가 줄단위로 쪼개진 것.
원인 — command: > folded scalar의 함정
문제의 docker-compose 조각:
msf:
image: metasploitframework/metasploit-framework:latest
command: >
bash -lc "msfrpcd -P ${MSF_RPC_PASSWORD:-msfrpcpass}
-U ${MSF_RPC_USER:-msf}
-a 0.0.0.0 -p 55553 -S -f"YAML > (folded block scalar)는 이론적으로 줄바꿈을 공백으로 합쳐주지만, 큰따옴표 안에서의 동작과 docker compose의 argv 분리 룰이 합쳐지면 결과가 예측대로 가지 않는다. bash가 받은 스크립트는 멀티라인이 됐고 첫 줄이 msfrpcd ... 라서 PATH에서 찾지 못한다.
게다가 metasploit-framework 이미지의 실제 PATH에는 msfrpcd가 없다. 바이너리는 /usr/src/metasploit-framework/msfrpcd 에 있고 WORKDIR이 그 디렉터리라 ./msfrpcd 로 불러야 한다.
docker run --rm --entrypoint /bin/sh metasploitframework/metasploit-framework:latest \
-c "ls /usr/src/metasploit-framework/msfrpcd; pwd"
# /usr/src/metasploit-framework/msfrpcd
# /usr/src/metasploit-framework패치
문자열 폼 대신 list 폼으로 argv를 명시. 동시에 ./msfrpcd 상대 경로로 호출:
msf:
image: metasploitframework/metasploit-framework:latest
command: ["./msfrpcd", "-P", "${MSF_RPC_PASSWORD:-msfrpcpass}",
"-U", "${MSF_RPC_USER:-msf}",
"-a", "0.0.0.0", "-p", "55553", "-S", "-f"]docker compose up -d msf 후 로그:
[*] MSGRPC starting on 0.0.0.0:55553 (NO SSL):Msg...deprecation warning이 잔뜩 나오지만(Gem::Platform.match deprecated…) RPC는 정상.
교훈
- shell-form
command:는 한 줄에 끝낼 게 아니면 쓰지 말 것. list-form이 항상 안전하다. - 베이스 이미지에
WORKDIR + 상대 실행컨벤션이 있으면bash -lc로 감싸지 말고 직접 호출해야 한다. bash 로그인 셸의 PATH는 컨테이너 운영자가 의도한 게 아니다.
🪤 Trap #2 — API가 import 단계에서 죽음 (grpcio 누락)
증상
docker compose up -d api 직후:
curl -sv http://127.0.0.1:8080/healthz
# * connect to 127.0.0.1 port 8080 failed: Connection refuseddocker compose logs api:
File "/app/app/main.py", line 8, in <module>
from .routers import events, findings, health, listeners, ...
File "/app/app/routers/listeners.py", line 5, in <module>
from ..tools import sliver
File "/app/app/tools/sliver.py", line 15, in <module>
from sliver import SliverClient, SliverClientConfig
File ".../site-packages/sliver/beacon.py", line 22, in <module>
import grpc
ModuleNotFoundError: No module named 'grpc'원인
sliver-py==0.0.18 가 내부에서 grpc 를 import 하는데, 이 패키지는 grpcio 를 자동 의존으로 끌고 오지 않는다. PyPI 메타데이터에서 grpcio가 install_requires 에 빠져있거나 extras로 빠져있는 듯.
비슷한 패턴이 sliver-py 만의 문제가 아니라서 — Python ML/RPC 라이브러리들이 grpcio 같은 무거운 C 확장을 옵셔널로 두는 경우가 많다.
패치
api/requirements.txt:
sliver-py==0.0.18
grpcio==1.66.2
protobuf==5.28.2sliver-py 와 호환되는 버전 페어가 중요. 최신 grpcio가 sliver-py의 proto와 깨질 수도 있어서 마이너 버전까지 핀.
빌드 재실행:
docker compose build api worker
docker compose up -d api worker
docker compose logs api --tail=10
# {"IsTor":true,"IP":"107.189.8.70"}
# INFO: Uvicorn running on http://0.0.0.0:8080엔트리포인트가 부팅 전에 Tor 도달성을 확인하고 시작하는 게 잘 동작했다. IsTor:true 가 정상.
교훈
- Python 의존성은 transitive 가 가장 위험하다. sliver-py 만 핀하지 말고 grpcio/protobuf 까지 명시 핀.
- 부팅 전 헬스체크(엔트리포인트의 Tor 확인)가 있다면 거기서 import 에러가 잡히도록 import 까지 강제 트리거하는 것도 방법.
🪤 Trap #3 — proxychains4가 hostname proxy 를 거부
증상
scope에 타깃 등록, recon_basic 한 번 돌렸더니 30초 만에 실패:
curl -s .../playbooks/runs/$RUN_ID | jq .steps[0].summary
# "nmap failed (rc=1): proxy tor has invalid value or is not numeric
# non-numeric ips are only allowed under the following circumstances:
# chaintype == strict (true), proxy is not first in list (false), pr"원인 — strict_chain 모드의 룰
proxychains4.conf:
strict_chain
proxy_dns
remote_dns_subnet 224
...
[ProxyList]
socks5 ${TOR_HOST} ${TOR_PORT}엔트리포인트가 envsubst 로 ${TOR_HOST} 를 그대로 tor 라는 도커 서비스명으로 치환했다. proxychains4 의 strict_chain 모드는 첫 번째 프록시가 hostname 이면 거부한다. (random_chain 모드 + 첫 번째 아닌 경우만 hostname 허용)
도커 내부 DNS로 풀린다고 proxychains가 알 도리는 없으니까 — proxychains 입장에서는 "numeric IP 줘"가 합리적이다.
패치 — 엔트리포인트에서 미리 resolve
# ops/docker/api-entrypoint.sh
_TOR_HOST_RAW="${TOR_HOST:-tor}"
_TOR_PORT="${TOR_PORT:-9050}"
_TOR_IP=""
for i in 1 2 3 4 5; do
_TOR_IP="$(getent hosts "$_TOR_HOST_RAW" | awk '{print $1; exit}')"
[[ -n "$_TOR_IP" ]] && break
sleep 2
done
TOR_HOST="$_TOR_IP" TOR_PORT="$_TOR_PORT" \
envsubst < /app/ops/proxychains/proxychains4.conf > /etc/proxychains4.conf
echo "[entrypoint] proxychains4.conf rendered (tor=${_TOR_HOST_RAW}→${_TOR_IP}:${_TOR_PORT})"5회 retry 는 docker 네트워크 부팅 직후 DNS가 잠깐 안 풀릴 때를 위한 안전망. 보통 1회에 풀린다.
재기동 후 컨테이너 내부:
docker compose exec api cat /etc/proxychains4.conf | tail -3
# [ProxyList]
# # Default: Tor SOCKS5 from the `tor` service in docker-compose.
# socks5 172.18.0.3 9050엔트리포인트 로그도 깔끔:
[entrypoint] proxychains4.conf rendered (tor=tor→172.18.0.3:9050)
[entrypoint] PROXY_CHAIN=true — verifying Tor reachability...
{"IsTor":true,"IP":"193.189.100.199"}교훈
- proxychains의 strict_chain은 numeric IP를 요구한다. docker 환경에서 서비스명을 그대로 쓰면 안 됨.
- envsubst 같은 단순 치환을 쓸 때는 엔트리포인트에서 한 단계 사전 처리(여기서는
getent hosts)를 끼우는 게 안전. - Tor 도달성 자체는 별도 도구(
curl --socks5-hostname직접 호출)로 검증 — proxychains 와 분리되어 있다.
🧪 패치 후 검증
API readyz:
curl -s -H "X-API-Key:$KEY" http://127.0.0.1:8080/readyz | jq .
# {
# "ok": false,
# "proxy_chain": true,
# "checks": {
# "redis": true,
# "tor": "error: Unknown scheme for proxy URL URL('socks5h://tor:9050')",
# "msf_rpc": true
# }
# }tor 체크가 false 인데 이건 readyz 가 httpx 로 socks5h 를 직접 시도해서 그런 거고(httpx 본체는 socks5h 미지원), 엔트리포인트의 check.torproject.org 호출은 IsTor:true 받아옴. 즉 실제 도구 트래픽은 정상 — readyz 의 cosmetic 버그다. 다음 일감으로 적어두고 통과.
modules manifest:
curl -s -H "X-API-Key:$KEY" http://127.0.0.1:8080/modules/manifest | jq '.endpoints | length, .events | length'
# 20
# 17라우터·이벤트 다 잡힘. Phase 1 인프라 PASS.
🚦 다음 글
autopwn 개발기 ③ — 패치 끝나고 실제로 recon_basic 을 본인 관리 호스트에 한 번 돌려봤다. SSE 67줄, 15분 소요, 그리고 작은 nmap adapter 버그가 한 마리 잡혔다.
📝 정리
- YAML folded scalar + bash -lc + 컨테이너 WORKDIR 컨벤션 세 개가 만나면 argv가 깨진다. list 폼이 답
- sliver-py 는 grpcio/protobuf 를 자동으로 안 끌고 온다. transitive deps 핀 필수
- proxychains4 strict_chain 은 numeric IP 만 받는다. 엔트리포인트에서 resolve 후 치환



