Step 148. OWASP Juice Shop 1: 현대적 웹앱 입문 — 공격 무대가 바뀌었다

Step 148. OWASP Juice Shop 1: 현대적 웹앱 입문 — 공격 무대가 바뀌었다

Level 2 — 보안 입문과 공격 스킬 기초 | 난이도 ★★★☆☆ | 예상 소요 시간 4시간

전제: Step 145(정보 노출)와 Step 147(DVWA 정리)을 마쳤다. robots.txt와 숨겨진 경로 탐색을 안다.

  • 준비물: Docker가 되는 머신 (Juice Shop 컨테이너), 브라우저 개발자 도구(F12), 파이썬 3 + Flask (로컬 재현)
  • 주의: ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다.

DVWA는 2000년대 PHP 웹앱의 세계였습니다. 오늘 만나는 OWASP Juice Shop은 Node.js와 Angular로 만든 현대적 웹앱 — 화면을 그리는 일이 서버가 아니라 여러분의 브라우저에서 일어납니다. 이 변화가 공격자의 지도를 바꿉니다: 페이지 소스는 텅 비어 있고, 대신 거대한 자바스크립트 파일에 API 주소, 숨겨진 기능, 심지어 개발자의 TODO 주석까지 담겨 있습니다. 오늘은 이 새 무대의 구조를 이해하고, "JS 파일을 읽는 공격"을 직접 재현한 뒤, Juice Shop 입문 챌린지에 도전합니다.


1. 학습 목표

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

  • SPA(싱글 페이지 앱)와 전통 웹앱의 구조 차이를 설명한다
  • 프론트엔드 JS 파일에서 API 경로와 힌트를 발굴한다
  • 개발자 도구의 Network 탭에서 /rest/, /api/ 호출을 수집한다
  • Juice Shop을 Docker로 띄우고 스코어보드로 진행을 추적한다
  • "문서 없는 API도 사용 흔적이 문서다"를 실천한다

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

오늘의 도구 한눈에 보기

구분 내용
언어·환경 Docker + 브라우저 개발자 도구 / 파이썬 3 + Flask (로컬 재현)
오늘의 명령 docker run -d -p 3000:3000 bkimminich/juice-shop, F12 → Sources/Network, JS 내 검색(Ctrl+F)
필요한 개념 SPA, REST API, 클라이언트 측 로직 노출, 해시 라우팅(/#/...)
오늘의 산출물 API 경로 목록 + 스코어보드 챌린지 기록

2-1. SPA — 화면이 브라우저에서 만들어진다

전통 웹앱(DVWA 같은)은 서버가 HTML을 완성해 보냅니다. 페이지 소스를 보면 내용이 보였습니다. SPA(Single Page Application)는 다릅니다. 서버는 뼈대 HTML 하나와 거대한 JS 번들을 보내고, 브라우저의 자바스크립트가 화면을 그립니다. 페이지 소스를 열어 보면 로딩 중... 하나뿐입니다. 주소창의 /#/login, /#/basket처럼 # 뒤로 바뀌는 주소를 해시 라우팅이라 하며, 서버에는 요청이 가지 않고 JS가 화면만 바꿉니다.

2-2. 프론트엔드가 비대하다는 것의 의미

화면을 그리는 로직이 전부 여러분 컴퓨터로 내려온다는 것 — 곧 공격자에게 애플리케이션의 설계도 일부가 통째로 배송된다는 뜻입니다. JS 번들 안에는 호출하는 API의 주소, 아직 메뉴에 공개하지 않은 기능의 경로, 개발자가 지우지 않은 주석이 들어 있습니다. 실제 Juice Shop의 여러 챌린지가 "JS 파일을 읽으면 힌트가 있다"로 설계된 이유입니다.

2-3. REST API — 문서 없는 API는 사용 흔적이 문서다

SPA의 서버는 HTML 대신 JSON 데이터를 주고받는 REST API 서버입니다. /rest/products, /api/Users 같은 경로가 그것입니다. 개발자 도구의 Network 탭을 열고 화면을 조작하면, 브라우저가 어떤 API를 어떤 파라미터로 부르는지 전부 보입니다. 공식 문서가 없어도 사용 흔적 자체가 API 명세서입니다 — 이것이 현대 웹 공격의 정찰법입니다.

2-4. Juice Shop — 채점이 있는 놀이터

Juice Shop은 OWASP가 만든 의도적 취약 웹앱으로, 100개가 넘는 챌린지가 난이도별로 분류돼 있습니다. 숨겨진 스코어보드(/#/score-board)가 진행 상황을 추적하고, 각 챌린지의 💡 아이콘이 힌트를 줍니다. DVWA가 "난이도별 코드 비교"의 교재였다면, Juice Shop은 "현대 웹앱 정찰"의 교과서입니다.


3. 따라 하기

3-1. 로컬 재현 — SPA의 텅 빈 소스와 비대한 JS

Juice Shop을 띄우기 전에, 그 구조가 왜 특별한지 작은 모사 앱으로 체험합니다 (본 교재는 2026-09-09에 실측했습니다).

입력 (mini_shop.py의 핵심)

from flask import Flask, jsonify

app = Flask(__name__)

INDEX_HTML = """<!DOCTYPE html>
<html><head><title>미니 쥬스샵(랩)</title></head>
<body>
<div id="app">로딩 중...</div>
<script src="/main.js"></script>
</body></html>"""

MAIN_JS = """// 미니 쥬스샵 프론트엔드 번들 (모사)
// TODO: 배포 전에 이 주석들 지울 것!
// 관리자 섹션: /#/administration — 아직 권한 체크 미구현
// 채점판: /#/score-board (숨겨진 페이지)
const API = {
  products: "/rest/products",
  user: "/api/Users",
  basket: "/rest/basket/",
  admin: "/rest/admin/application-version"
};
console.warn("개발용 경고: 디버그 모드가 켜져 있습니다");
"""

@app.route("/")
def index():
    return INDEX_HTML

@app.route("/main.js")
def mainjs():
    return MAIN_JS, 200, {"Content-Type": "application/javascript"}

@app.route("/robots.txt")
def robots():
    return "User-agent: *\\nDisallow: /ftp/\\n", 200, {"Content-Type": "text/plain"}

@app.route("/rest/products")
def products():
    return jsonify([{"id": 1, "name": "사과 주스"}, {"id": 2, "name": "오렌지 주스"}])

읽는 법: 홈페이지 HTML은 텅 비어 있고(로딩 중...), 모든 정보는 main.js에 있습니다. Juice Shop의 축소판입니다. 서버를 띄운 뒤 다음 실험으로 넘어가세요.

3-2. JS 파일을 읽는다 — 주석과 API 경로 발굴

공격자의 첫 행동은 화면 구경이 아니라 JS 번들 다운로드입니다. 개발자 도구의 Sources 탭에서 main.js를 열고 Ctrl+F로 검색하는 일을, 여기서는 스크립트로 흉내 냅니다.

출력 (2026-09-09 실측):

=== 1) 홈페이지 HTML — 본문에 정보가 없다 (SPA) ===
<div id="app">로딩 중...</div>
<script src="/main.js"></script>

=== 2) main.js 다운로드 후 API 경로/힌트 추출 ===
--- 주석(TODO/힌트) ---
// TODO: 배포 전에 이 주석들 지울 것!
// 관리자 섹션: /#/administration — 아직 권한 체크 미구현
// 채점판: /#/score-board (숨겨진 페이지)
--- /rest/ · /api/ 경로 ---
/api/Users
/rest/admin/application-version
/rest/basket/
/rest/products

읽는 법: 두 종류의 전리품이 나왔습니다. ① 주석 — 숨겨진 관리자 섹션과 채점판의 주소가 개발자의 메모 그대로입니다. ② API 경로 네 개 — 서버가 무슨 기능을 갖는지의 목록입니다. 브라우저에서라면 개발자 도구 → Sources → main.js → Ctrl+F로 score-board, /rest/를 검색하는 것과 정확히 같은 작업입니다.

3-3. robots.txt와 API 직접 호출

출력 (2026-09-09 실측):

=== 3) robots.txt 확인 ===
User-agent: *
Disallow: /ftp/

=== 4) 발견한 API 직접 호출 ===
GET /rest/products -> 200 [{"id":1,"name":"사과 주스"},{"id":2,"name":"오렌지 주스"}]
GET /ftp/ -> 404 (robots.txt가 알려 준 경로 — 존재 자체가 정보)

읽는 법: Step 145에서 배운 robots.txt 수법이 현대 웹앱에서도 그대로 통합니다. 그리고 JS에서 발굴한 API는 로그인 없이도 호출됐습니다 — JSON이 그대로 돌아옵니다. "API 경로를 안다"와 "호출할 수 있다"는 다르지만, 경로를 모르면 시도조차 못 합니다. 발굴이 모든 공격의 전단계입니다.

3-4. Juice Shop 띄우기와 스코어보드 찾기 (워게임 실습, 출력 예시)

여러분의 Docker 머신에서 진행하세요. 출력은 출력 예시입니다.

입력

docker run -d -p 3000:3000 --name juice bkimminich/juice-shop

브라우저에서 http://localhost:3000으로 접속합니다. 첫 과제는 지도 찾기 — 주소창에 직접 http://localhost:3000/#/score-board를 입력하거나, 3-2처럼 JS 번들에서 score-board를 검색해 스코어보드를 찾습니다. 이 "숨겨진 페이지 발견" 자체가 첫 챌린지로 인정되며, 화면 상단에 축하 배너가 뜹니다 (화면 예시):

[알림 배너] You solved a challenge: Score Board
(Find the carefully hidden 'Score Board' page.)

읽는 법: 스코어보드는 난이도(★1~6)별로 챌린지 목록을 보여 주고, 해결한 항목이 초록색으로 칠해집니다. 오늘의 목표인 ★1~2 챌린지는 대부분 "정찰"입니다 — 숨은 페이지 찾기, JS 속 힌트 읽기, 개발자 도구 콘솔의 경고 메시지 읽기.

3-5. Network 탭으로 API 지도 그리기 (화면 예시)

Juice Shop에서 로그인 화면, 상품 목록, 장바구니를 한 바퀴 돌며 개발자 도구 Network 탭을 열어 둡니다. 수집 결과의 예 (화면 예시):

GET  /rest/products/search?q=apple     → 상품 검색
POST /rest/user/login                  → 로그인 (JSON 본문)
GET  /api/Users/1                      → 사용자 정보
GET  /rest/basket/6                    → 장바구니 (숫자가 사용자마다 다르다!)

읽는 법: 각 요청의 경로·메서드·파라미터를 표로 적으세요. 특히 /rest/basket/6처럼 숫자가 박힌 경로는 "저 숫자를 바꾸면?"이라는 질문의 씨앗입니다. 오늘은 수집까지만 — 바꿔 보는 일(IDOR)은 그 자체로 하나의 주제라 다음에 깊이 다룹니다. 수집한 목록이 길어질수록 여러분의 공격 지도가 넓어집니다.


4. 미션과 연습문제

미션 — 현대 웹앱 정찰 보고서

  1. 3-1의 모사 SPA를 띄우고, 홈페이지 소스에 정보가 없음을 확인한 뒤 main.js에서 API 경로 네 개와 주석 힌트를 발굴해 기록한다
  2. 발굴한 /rest/products를 직접 호출해 JSON 응답을 받는다
  3. Juice Shop을 Docker로 띄우고 스코어보드 페이지를 찾아 첫 챌린지를 해결한다
  4. 개발자 도구 Network 탭으로 /rest/, /api/ 호출을 5개 이상 수집해 표로 정리한다
  5. 스코어보드 챌린지를 누적 15개(★1~2 위주) 해결하고, 각각 "어떻게 발견했는가"를 한 줄씩 기록한다

연습문제

문제 1. SPA에서 "페이지 소스 보기"가 무용해진 이유와, 대신 공격자가 읽는 것이 무엇인지 말해 보세요.

문제 2. 프론트엔드 JS 파일에 개발자의 TODO 주석이 남아 있으면 왜 위험한지, 오늘 실측의 예로 설명해 보세요.

문제 3. "문서 없는 API도 사용 흔적이 문서다"라는 말의 뜻을 Network 탭 활용과 연결해 설명해 보세요.

문제 4. 주소의 # 뒤 부분(해시 라우팅)이 바뀔 때 서버에 요청이 가지 않는 이유와, 그래서 스코어보드 같은 숨겨진 화면을 찾으려면 어디를 봐야 하는지 말해 보세요.


5. 모범 답안과 완료 기준

미션 모범 답안

로컬 재현에서 발굴되는 전리품 (2026-09-09 실측): API 경로 /api/Users, /rest/admin/application-version, /rest/basket/, /rest/products와, 주석 속 힌트(/#/administration, /#/score-board). Juice Shop에서의 챌린지 기록 예:

1. Score Board — 숨겨진 /#/score-board 발견 (JS 검색)
2. Error Handling — 존재하지 않는 경로로 에러 화면 유도
3. Exposed Metrics — /metrics 경로 접속
4. Missing Encoding — 깨진 이미지의 대체 텍스트 조사
...

검증하는 법: ① 로컬에서 JS 발굴 출력이 있는가. ② API 직접 호출의 JSON이 있는가. ③ 스코어보드 진행 화면의 기록이 있는가. ④ API 표가 5행 이상인가. ⑤ 각 챌린지에 "발견 경로"가 적혀 있는가.

연습문제 해답

문제 1 해답. SPA의 서버는 빈 뼈대 HTML만 보내고 화면은 브라우저의 JS가 그리므로, 페이지 소스에는 내용이 없습니다. 대신 공격자는 화면을 그리는 주체 — JS 번들(main.js) — 를 다운로드해 읽고, 개발자 도구 Network 탭으로 실제 API 호출을 관찰합니다.

문제 2 해답. JS 번들은 모든 방문자의 컴퓨터로 내려가는 공개 파일입니다. 오늘 실측에서 주석 하나가 숨겨진 관리자 섹션(/#/administration)과 채점판 주소를 알려 줬듯, TODO·디버그 주석은 곧 공격 힌트입니다. 배포 전 주석 제거와 난독화가 필요한 이유입니다.

문제 3 해답. API에 공식 문서가 없어도, 개발자 도구 Network 탭을 열고 화면을 조작하면 브라우저가 실제로 보내는 요청 — 경로, 메서드, 파라미터 — 이 전부 기록됩니다. 그 기록이 곧 API의 사용 설명서이며, 정찰의 산출물입니다.

문제 4 해답. # 뒤(프래그먼트)는 브라우저 안에서만 처리되고 서버로 전송되지 않기 때문입니다. 따라서 어떤 해시 경로가 존재하는지는 서버 요청으로 알 수 없고, 화면 전환을 담당하는 JS 번들 안의 경로 목록(라우트 정의)을 검색해야 합니다.

완료 기준 체크리스트

  • [ ] SPA의 빈 페이지 소스와 비대한 JS의 대비를 직접 확인했다
  • [ ] JS 파일에서 API 경로와 주석 힌트를 발굴했다
  • [ ] 발굴한 API를 로그인 없이 호출해 JSON을 받았다
  • [ ] Juice Shop을 Docker로 띄우고 스코어보드를 찾았다
  • [ ] Network 탭에서 API 호출 5개 이상을 표로 수집했다
  • [ ] 챌린지 15개를 해결하고 발견 경로를 기록했다
  • [ ] 해시 라우팅과 SPA의 구조 차이를 설명할 수 있다

6. 흔한 실수와 해결

벽 1. docker run이 안 된다

증상: Cannot connect to the Docker daemon 또는 명령 자체가 없습니다.
원인: Docker Desktop이 꺼져 있거나 미설치입니다.
해결: Docker Desktop을 먼저 실행하고 상태 아이콘이 초록이 된 뒤 다시 시도하세요. 윈도우에서는 WSL2 백엔드 설정이 필요할 수 있습니다.

벽 2. localhost:3000이 안 열린다

증상: 컨테이너는 떴는데 브라우저가 연결을 거부합니다.
원인: 컨테이너 시작이 덜 끝났거나(첫 실행은 수십 초), 포트 충돌일 수 있습니다.
해결: docker logs juice로 "Server listening" 같은 메시지를 확인하고, 3000번이 점유됐다면 -p 3100:3000으로 바꿔 띄우세요.

벽 3. JS 파일이 너무 커서 못 읽겠다

증상: main.js가 수만 줄의 한 줄짜리 압축(minify) 코드입니다.
원인: 현대 프론트엔드는 빌드 시 압축됩니다. 통째로 읽는 것은 불가능 — 정상입니다.
해결: 읽지 말고 검색하세요. 개발자 도구의 Pretty print({} 버튼)로 줄바꿈을 복원한 뒤, Ctrl+F로 score-board, /rest/, admin 같은 키워드를 찾습니다. 검색이 독해를 이깁니다.

벽 4. 챌린지를 풀었는데 인정이 안 된다

증상: 스코어보드가 초록으로 안 변합니다.
원인: 조건을 절반만 만족했거나, 브라우저가 오래된 상태를 캐시했을 수 있습니다.
해결: 💡 힌트를 다시 읽고 조건을 정확히 맞추세요. 스코어보드를 새로고침(F5)하면 반영되는 경우가 많습니다.

벽 5. Network 탭에 아무것도 안 찍힌다

증상: 화면을 조작해도 요청 목록이 비어 있습니다.
원인: 개발자 도구를 연 뒤에 화면을 조작해야 기록됩니다. 도구를 열기 전 요청은 지나갔습니다.
해결: F12로 Network 탭을 먼저 열고 화면을 새로고침하세요. 필터가 Fetch/XHR로 좁혀져 있는지도 확인합니다.


7. 정리

오늘의 개념

개념 한 줄 설명
SPA 서버는 뼈대만, 화면은 브라우저의 JS가 그리는 현대 웹앱 구조
JS 번들 모든 방문자에게 배송되는 설계도 — API 경로·힌트의 보고
REST API JSON을 주고받는 서버 기능 단위 (/rest/, /api/)
해시 라우팅 # 뒤 경로 — 서버에 안 가고 JS가 화면만 전환
Network 탭 사용 흔적이 곧 API 문서인 정찰 도구
스코어보드 Juice Shop의 숨겨진 채점판이자 지도 (/#/score-board)

오늘의 명령어

명령 하는 일
docker run -d -p 3000:3000 bkimminich/juice-shop Juice Shop 띄우기
docker logs juice 컨테이너 시작 확인
F12 → Sources → main.js → Ctrl+F JS 번들에서 키워드 검색
F12 → Network (Fetch/XHR) API 호출 수집
http://localhost:3000/#/score-board 스코어보드 직접 접속
curl 주소/robots.txt 숨은 경로 힌트 확인

명령어보다 중요한 감각

DVWA에서 Juice Shop으로의 이동은 도구의 교체가 아니라 시선의 이동입니다. 서버가 뿌려 주는 HTML을 보던 눈에서, 브라우저로 내려오는 JS와 네트워크 대화를 보는 눈으로. 공격 표면은 사라진 게 아니라 여러분의 컴퓨터 안으로 들어와 있습니다.

오늘 수집한 API 목록은 그저 주소 나열이 아닙니다. 각 경로에 박힌 숫자, 로그인 없이 열리는 엔드포인트, 주석 속 미완성 기능 — 전부 "다음에 흔들어 볼 손잡이"입니다. 정찰은 공격의 반이라는 말이 현대 웹에서는 문자 그대로입니다.


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