본 글은 보안 스터디 및 모의해킹 실습을 목적으로 작성되었습니다. 모든 테스트는 직접 제작하거나 허가받은 환경에서만 진행했습니다.
1. 점검 개요
웹 애플리케이션을 대상으로 Black-Box와 White-Box 방식의 보안 점검을 진행했다.
- Black-Box: HTTP 요청 및 응답을 이용한 동적 점검
- White-Box: PHP 및 SQL 소스코드 분석
실습 환경에는 CTF_MODE가 존재하며, CTF 모드를 활성화하면 일부 취약한 코드 경로가 의도적으로 활성화되도록 구성했다.
따라서 이번 점검은 다음 두 상태를 구분하여 진행했다.
- CTF_MODE = ON: SQL Injection, IDOR, Stored XSS 등의 취약점 확인
- CTF_MODE = OFF: 취약점 대응 코드 적용 여부 확인
점검 결과 주요 취약점으로는 SQL Injection, IDOR, Stored XSS가 확인되었다.
| 취약점 | 위치 | 위험도 | 확인 방식 |
|---|---|---|---|
| SQL Injection | 모델 검색 | High | Black-Box + White-Box |
| Stored XSS | 댓글 및 사용자 입력 출력 | High | Black-Box + White-Box |
| IDOR | 프로필 수정 | Medium | Black-Box + White-Box |
| 민감정보 로그 노출 | Access Log | Medium | Log Analysis |
2. SQL Injection
원인
CTF 모드가 활성화된 모델 검색 기능에서는 사용자 입력값을 SQL 문자열에 직접 연결하는 코드 경로가 존재했다.
$sql = "SELECT * FROM models
WHERE name LIKE '%$q%'
OR summary LIKE '%$q%'
ORDER BY downloads DESC";
$models = db()->query($sql)->fetchAll();
사용자 입력과 SQL 명령이 분리되지 않으므로 입력값에 SQL 구문을 포함하면 기존 쿼리의 의미를 변경할 수 있다.
점검
로그에서는 다음과 같은 SQL Injection 점검 요청을 확인할 수 있었다.
/models.php?q='
/models.php?q=' OR '1'='1' --
/models.php?q=' OR 1=1 #
/model.php?id=1 AND 1=1
/model.php?id=1 AND 1=2
참 조건과 거짓 조건을 각각 요청하여 응답 차이를 비교하는 Boolean-based SQL Injection 형태의 점검도 수행했다.
영향
SQL Injection이 실제 서비스에서 발생하면 검색 조건 우회뿐 아니라 쿼리 구조에 따라 데이터 조회 및 변조로 이어질 가능성이 있다.
이번 실습에서는 허가된 CTF 환경에서 취약 여부만 확인했으며 실제 사용자 정보를 대상으로 테스트하지 않았다.
대응
CTF 모드를 비활성화한 코드에서는 Prepared Statement를 사용해 SQL과 사용자 입력을 분리했다.
$like = "%$q%";
$stmt = db()->prepare(
'SELECT * FROM models
WHERE name LIKE ?
OR summary LIKE ?
OR tags LIKE ?'
);
$stmt->execute([$like, $like, $like]);
Prepared Statement를 적용하면 사용자 입력이 SQL 명령이 아니라 데이터로 처리되므로 SQL Injection을 방지할 수 있다.
3. IDOR
원인
프로필 수정 기능의 취약한 코드 경로에서는 서버 세션의 사용자 ID가 아니라 클라이언트가 전달한 user_id를 수정 대상으로 사용할 수 있었다.
$targetId = isset($_POST['user_id'])
? (int)$_POST['user_id']
: (int)$me['id'];
사용자가 로그인했는지는 확인하지만, 요청으로 전달된 대상 계정을 현재 사용자가 실제로 수정할 권한이 있는지는 확인하지 않는 구조였다.
점검
직접 생성한 테스트 계정 A와 B를 이용해 A 계정의 세션으로 B 계정의 user_id를 전송했다.
로그에서는 다음과 같은 요청을 확인할 수 있었다.
POST /profile_edit.php
user_id=16
display_name=Audit controlled B IDOR verified
bio=AUDIT_IDOR_0930
또 다른 테스트에서도 로그인한 계정과 다른 사용자의 ID를 대상으로 프로필 수정 요청을 보냈다.
POST /profile_edit.php
user_id=19
display_name=IDOR_XSS_PROBE_9f3c
CTF 모드에서는 다른 테스트 계정의 프로필 변경이 가능한 정황을 확인했다.
영향
IDOR 취약점이 존재하면 일반 사용자가 URL이나 요청 파라미터의 객체 ID를 조작해 다른 사용자 데이터를 변경하거나 삭제할 수 있다.
특히 프로필 수정 외에도 게시글, 댓글, 주문, 파일 등의 객체에 같은 문제가 존재한다면 수평적 권한 상승으로 이어질 수 있다.
대응
CTF 모드가 비활성화된 경우 수정 대상을 클라이언트에서 전달받지 않고 로그인 세션에서 가져오도록 변경했다.
$me = require_login();
$userId = (int)$me['id'];
$stmt = db()->prepare(
'UPDATE users
SET display_name=?, bio=?, email=?
WHERE id=?'
);
$stmt->execute([
$display,
$bio,
$email,
$userId
]);
이 방식에서는 공격자가 다음과 같이 임의의 값을 전달해도,
user_id=999
실제 수정 대상은 현재 로그인한 사용자로 고정된다.
관리자처럼 다른 사용자를 수정해야 하는 기능이 있다면 별도의 역할 검증과 객체 단위 권한 검사를 추가해야 한다.
4. Stored XSS
원인
CTF 모드에서는 댓글이나 사용자 입력을 출력할 때 HTML Escape가 적용되지 않는 코드 경로가 존재했다.
<?=$c['content']?>
사용자 입력이 그대로 HTML에 삽입되면 브라우저가 입력값을 단순 문자열이 아니라 HTML 태그나 JavaScript 이벤트로 해석할 수 있다.
점검
다음과 같은 테스트 입력을 게시글과 댓글에 삽입했다.
<script>alert(1)</script>
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
로그에서도 게시글 작성, 수정 및 댓글 등록 과정에서 해당 문자열이 전달된 것을 확인할 수 있었다.
영향
Stored XSS가 존재하면 악성 입력이 DB에 저장되고, 이후 해당 페이지를 열람하는 다른 사용자의 브라우저에서 실행될 가능성이 있다.
특히 관리자나 높은 권한을 가진 사용자가 해당 콘텐츠를 조회하면 피해 범위가 커질 수 있다.
대응
CTF 모드를 비활성화한 코드에서는 사용자 입력을 HTML로 출력하기 전에 escaping을 적용했다.
<?=nl2br(e($c['content']))?>
e() 함수 내부에서 htmlspecialchars()를 사용하면 다음과 같은 입력이
<img src=x onerror=alert(1)>
실제 HTML 태그로 생성되지 않고 문자열로 표시된다.
XSS는 입력값에서 특정 문자열을 단순 삭제하는 방식보다는 출력되는 위치에 맞는 Context-Aware Encoding을 적용하는 것이 중요하다.
5. Reflected XSS
점검
검색 파라미터에도 다음과 같은 값을 전달했다.
/models.php?q=<svg onload=confirm(1337)>
/board.php?q=<img src=x onerror=...>
이는 검색어가 페이지에 다시 출력되는 과정에서 HTML Escape가 정상적으로 이루어지는지 확인하기 위한 테스트이다.
실제 취약했나?
공격 요청 자체는 로그에서 확인됐지만, 접근 로그에는 실제 응답 HTML이나 브라우저 JavaScript 실행 결과가 존재하지 않아 Reflected XSS가 실제로 성공했다고 단정할 수는 없었다.
대응
검색어를 HTML에 다시 출력할 경우 다음과 같이 escaping을 적용해야 한다.
<?= htmlspecialchars($q, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>
HTML 본문, HTML Attribute, JavaScript, URL 등 입력값이 들어가는 위치에 따라 적절한 인코딩 방식을 사용해야 한다.
6. CSRF
상태 변경 요청에는 CSRF Token이 적용되어 있었다.
POST /profile_edit.php
csrf=...
재검증 과정에서는 고의로 잘못된 CSRF Token을 전달하는 요청도 수행했다.
csrf=invalid_token_7d2f
display_name=CSRF_FAIL_MARKER
bio=should_not_apply
서버에서는 CSRF Token을 세션과 비교하고 유효하지 않은 요청은 실제 데이터 변경 전에 차단해야 한다.
다만 CSRF 검증은 IDOR 방어와는 별개의 문제이다.
정상적인 CSRF Token을 가지고 있어도 다른 사용자의 객체를 수정할 수 있다면 IDOR 취약점은 여전히 존재한다.
따라서 상태 변경 요청은 다음과 같은 순서로 검증하는 것이 좋다.
CSRF 검증
↓
로그인 여부 확인
↓
대상 객체 확인
↓
현재 사용자의 객체 접근 권한 확인
↓
데이터 변경
7. 민감정보 로그 노출
모의해킹 로그를 분석하는 과정에서 애플리케이션 취약점 외에 별도의 문제가 발견됐다.
원인
현재 로깅 코드에서는 password 등의 일부 필드만 마스킹하고 있었다.
그 결과 회원가입 시 사용되는 확인용 비밀번호인 password2는 로그에 그대로 남았다.
예를 들어 로그 형태가 다음과 같았다.
password=***
password2=<평문 비밀번호>
CSRF Token 역시 다음과 같이 전체 값이 기록되고 있었다.
csrf=<전체 CSRF Token>
실제 취약했나?
예.
이는 공격 가능성을 추정한 것이 아니라 실제 로그에서 민감값이 기록되고 있는 것을 확인한 문제이다.
대응
로그를 생성하는 공통 함수에서 민감한 키를 일괄 마스킹하도록 변경해야 한다.
예를 들어 다음과 같은 값은 로그에 기록하지 않는 것이 좋다.
password
password2
current_password
new_password
new_password2
csrf
csrf_token
Cookie
Authorization
기록이 필요한 경우에도 다음처럼 값을 제거해야 한다.
password=[REDACTED]
csrf=[REDACTED]
민감정보 마스킹은 각 PHP 페이지마다 따로 구현하기보다는 공통 로깅 함수 또는 미들웨어 단계에서 처리하는 것이 안전하다.
방어(대응) 보고서 — 내 사이트: (URL)
1. 진단 중 관찰한 것 (로그 기반)
| 시각 | 관찰한 요청/패턴 | 추정 점검 관점 | 성공/차단 여부 |
|---|---|---|---|
| 09-29 19:26~19:27 | /models.php?q=', ' OR '1'='1' -- 등 SQL 구문 입력 | 검색 파라미터 SQLi | CTF ON에서 취약 코드 경로 점검, CTF OFF에서는 Prepared Statement 적용 |
| 09-29 19:27 | /login.php에 ' OR '1'='1' -- 입력 | 로그인 SQLi / 인증 우회 | 공격 시도 확인, 인증 우회 성공 근거 없음 |
| 09-29 19:28~19:55 | 게시글/댓글에 <script>, <img onerror>, <svg onload> 삽입 | Stored XSS | CTF ON에서 공격 입력 확인, CTF OFF에서는 HTML Escape 적용 |
| 09-29 20:35~20:40 | 1=1 / 1=2 조건으로 /models.php, /model.php 요청 | Boolean-based SQLi | CTF ON 점검 흔적 확인, CTF OFF Prepared Statement 적용 |
| 09-29 20:36~20:46 | 검색어에 <svg onload>, <img onerror> 삽입 | Reflected XSS | 공격 시도 확인, 실제 실행 여부는 로그만으로 확인 불가 |
| 09-29 21:44 | 다른 user_id를 이용한 /profile_edit.php 수정 | IDOR / BOLA | CTF ON에서는 비인가 수정 성공 정황, CTF OFF에서는 세션 사용자 ID로 고정 |
| 09-30 06:56 | 다른 사용자 ID와 XSS payload를 함께 전달 | IDOR + XSS 연계 점검 | CTF ON 취약 경로 확인, CTF OFF 권한 제한 적용 |
| 09-30 06:57~06:58 | 게시글/댓글에 XSS payload 등록 | Stored XSS | CTF OFF에서 출력 escaping 적용 |
| 전체 로그 | CSRF Token 및 password2 값 기록 | 민감정보 로그 노출 | 미해결 — 추가 대응 필요 |
2. 탐지된 취약점 & 대응
검색 파라미터 SQLi
- 로그에서 본 흔적:
/models.php?q=' OR '1'='1' -- - 실제 취약했나?: 예. CTF ON에서는 의도적으로 취약한 코드 경로가 존재했다.
- 대응: CTF OFF에서는 문자열 연결 방식의 SQL 생성을 제거하고 Prepared Statement + Parameter Binding을 적용했다.
프로필 수정 IDOR
- 로그에서 본 흔적: 다른 사용자의
user_id를 POST 요청으로 전달하여 프로필 변경 - 실제 취약했나?: 예. CTF ON에서 직접 생성한 테스트 계정 간 비인가 프로필 변경을 확인했다.
- 대응: 클라이언트에서 전달되는
user_id를 신뢰하지 않고 로그인 세션의 사용자 ID만 수정 대상으로 사용하도록 변경했다.
Stored XSS
- 로그에서 본 흔적:
<script>alert(1)</script>,<img src=x onerror=alert(1)>,<svg/onload=alert(1)> - 실제 취약했나?: CTF ON에서 취약 출력 경로가 존재했다.
- 대응: CTF OFF에서는 사용자 입력을 출력할 때
htmlspecialchars()기반 HTML Escape를 적용했다.
Reflected XSS
- 로그에서 본 흔적: 검색 파라미터에
<svg onload=...>등의 값을 전달 - 실제 취약했나?: 로그만으로 실제 JavaScript 실행 여부를 확정할 수 없음.
- 대응: 검색 결과에 사용자 입력을 재출력할 때 HTML Context에 맞게 escaping을 수행한다.
로그인 SQL Injection
- 로그에서 본 흔적:
login=' OR 1=1 # - 실제 취약했나?: 로그상 인증 우회 성공 흔적 없음.
- 대응: 로그인 쿼리에 Prepared Statement를 유지하고 안전한 비밀번호 검증 방식을 사용한다.
CSRF
- 로그에서 본 흔적: 상태 변경 요청에 CSRF Token 사용 및
invalid_token재검증 - 실제 취약했나?: CSRF 기능은 구현되어 있으나 접근 로그만으로 모든 요청의 차단 결과를 확정하기는 어려움.
- 대응: 세션과 CSRF Token을 검증한 이후 인증 및 객체 단위 권한 검사도 별도로 수행한다.
민감정보 로그 노출
- 로그에서 본 흔적:
csrf=<token>,password2=<평문> - 실제 취약했나?: 예.
- 대응: 로그 생성 단계에서 비밀번호, 확인용 비밀번호, CSRF Token, Session/Cookie, Authorization 등의 민감값을
[REDACTED]처리한다.
8. CTF 모드에 따른 차이
이번 실습 환경에서 중요한 점은 CTF MODE가 켜져 있는 상태와 꺼져 있는 상태를 구분해야 한다는 것이다.
CTF 모드에서는 취약점 학습 및 공격 실습을 위해 일부 취약한 코드 경로가 의도적으로 활성화된다.
CTF_MODE = ON
↓
SQL Injection 취약 검색 코드
IDOR 취약 프로필 수정 코드
XSS 취약 출력 코드
반대로 운영 환경을 가정한 CTF OFF 상태에서는 방어 코드가 사용된다.
CTF_MODE = OFF
↓
Prepared Statement
세션 기반 객체 권한 제한
HTML Escaping
따라서 CTF ON에서 공격에 성공했다고 해서 보안 패치가 실패한 것은 아니다.
다만 점검 당시 제공된 소스의 config.php에는 CTF_MODE=true가 남아 있었기 때문에, 실제 운영 환경에서 CTF 모드가 확실히 비활성화되어 있는지 별도로 확인할 필요가 있다.
운영 배포에서는 단순히 개발자가 직접 값을 변경하는 방식보다 환경 변수나 운영 전용 설정을 이용하고, CTF 모드가 활성화된 상태에서는 운영 배포 자체가 실패하도록 하는 것이 안전하다.
9. 결론
이번 점검에서는 CTF 모드를 활용해 실제 웹 애플리케이션에서 자주 발생하는 SQL Injection, IDOR, Stored XSS를 직접 확인하고 각각에 대한 방어 코드를 적용했다.
| 취약점 | 위험도 | 대응 |
|---|---|---|
| SQL Injection | High | Prepared Statement 적용 |
| Stored XSS | High | HTML Escaping 적용 |
| IDOR | Medium | 세션 기반 객체 권한 검증 |
| Reflected XSS | Medium | 출력 Context에 따른 Escape |
| 민감정보 로그 노출 | Medium | 민감 필드 로그 마스킹 필요 |
이번 실습에서 가장 크게 느낀 점은 단순히 사용자 입력값을 필터링하는 것만으로는 충분하지 않다는 것이다.
SQL Injection은 SQL과 데이터를 분리해야 하고, XSS는 출력되는 Context에 따라 Encoding해야 하며, IDOR는 클라이언트가 전달하는 ID가 아니라 현재 사용자의 권한을 서버에서 직접 확인해야 한다.
또한 보안 기능을 잘 구현해도 비밀번호나 CSRF Token이 로그에 그대로 저장된다면 또 다른 보안 문제가 생길 수 있다.
Disclaimer 본 글의 모든 보안 테스트는 교육 및 연구 목적으로 직접 제작하거나 허가받은 환경에서만 수행되었습니다. 허가되지 않은 시스템을 대상으로 동일한 테스트를 수행해서는 안 됩니다.
