CakeCTF 2022 Writeup CakeGEAR [web, warmup]
CakeCTFとは
yoshiking氏, theoremoon氏, ptr-yudai氏らが主催しているJeopardy形式のCTFです。
Jeopardy形式では、独立した様々なジャンルの問題が出題されます。
図1 CakeCTFのトップ画面
難易度は初心者から中級者向けらしいのですが、実際に参加したところ、自分のようなCTF始めたての初心者には難しめかなと感じました。
ちなみに、自分は制限時間内に1問も解くことができませんでした。悲しい、、
CakeGEAR [web, warmup]
PHPの脆弱性を利用してフラグを取得する問題です。丸一日この問題に費やしました。
問題文
図2 CakeGEARの問題文
ログインポータルへのリンクと、サーバ構築時のDockerfile、PHPファイルが配られます。
以下、その詳細です。
図3 ログインポータルの画面
FROM php:8-apache
RUN echo "FakeCTF{neko}" > /flag.txt
RUN chmod 444 /flag.txt
RUN apt update
WORKDIR /var/www/html
ADD index.php ./
ADD admin.php ./
RUN chown -R root:www-data ./
RUN chmod -R 440 ./
RUN chmod 550 ./
<?php
session_start();
$_SESSION = array();
define('ADMIN_PASSWORD', 'f365691b6e7d8bc4e043ff1b75dc660708c1040e');
/* Router login API */
$req = @json_decode(file_get_contents("php://input"));
if (isset($req->username) && isset($req->password)) {
if ($req->username === 'godmode'
&& !in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) {
/* Debug mode is not allowed from outside the router */
$req->username = 'nobody';
}
switch ($req->username) {
case 'godmode':
/* No password is required in god mode */
$_SESSION['login'] = true;
$_SESSION['admin'] = true;
break;
case 'admin':
/* Secret password is required in admin mode */
if (sha1($req->password) === ADMIN_PASSWORD) {
$_SESSION['login'] = true;
$_SESSION['admin'] = true;
}
break;
case 'guest':
/* Guest mode (low privilege) */
if ($req->password === 'guest') {
$_SESSION['login'] = true;
$_SESSION['admin'] = false;
}
break;
}
/* Return response */
if (isset($_SESSION['login']) && $_SESSION['login'] === true) {
echo json_encode(array('status'=>'success'));
exit;
} else {
echo json_encode(array('status'=>'error'));
exit;
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>login - CAKEGEAR</title>
<style>input { margin: 0.5em; }</style>
</head>
<body style="text-align: center;">
<h1>CakeWiFi Login</h1>
<div>
<label>Username</label>
<input type="text" id="username" required>
<br>
<label>Password</label>
<input type="text" id="password" required>
<br>
<button onclick="login()">Login</button>
<p style="color: red;" id="error-msg"></p>
</div>
<script>
function login() {
let error = document.getElementById('error-msg');
let username = document.getElementById('username').value;
let password = document.getElementById('password').value;
let xhr = new XMLHttpRequest();
xhr.addEventListener('load', function() {
let res = JSON.parse(this.response);
if (res.status === 'success') {
window.location.href = "/admin.php";
} else {
error.innerHTML = "Invalid credential";
}
}, false);
xhr.withCredentials = true;
xhr.open('post', '/');
xhr.send(JSON.stringify({ username, password }));
}
</script>
</body>
</html>
<?php
session_start();
if (empty($_SESSION['login']) || $_SESSION['login'] !== true) {
header("Location: /index.php");
exit;
}
if ($_SESSION['admin'] === true) {
$mode = 'admin';
$flag = file_get_contents("/flag.txt");
} else {
$mode = 'guest';
$flag = "***** Access Denied *****";
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>control panel - CAKEGEAR</title>
<style>table, td { margin: auto; border: 1px solid #000; }</style>
</head>
<body style="text-align: center;">
<h1>Router Control Panel</h1>
<table><tbody>
<tr><td><b>Status</b></td><td>UP</td></tr>
<tr><td><b>Router IP</b></td><td>192.168.1.1</td></tr>
<tr><td><b>Your IP</b></td><td>192.168.1.7</td></tr>
<tr><td><b>Access Mode</b></td><td><?= $mode ?></td></tr>
<tr><td><b>FLAG</b></td><td><?= $flag ?></td></tr>
</tbody></table>
</body>
</html>
自分で試したこと
まず始めに、ざっくりコードを読むところから始めました。
コードからは、全体的に以下のような流れで処理を進めていることが分かります。
- ユーザ名とパスワードを入力
- 入力内容をJSON形式に変換してXMLHttpRequestオブジェクトとしてサーバに送信
- サーバ側でJSON文字列をデコードして認証情報を取り出す
- 認証情報をもとに
$_SESSION['login']
,$_SESSION['admin']
を設定 -
$_SESSION['login']
,$_SESSION['admin']
が両方trueであるユーザのみにフラグを表示
どうにかしてセッション変数を設定しなければいけないようです。
コードを見た感じ、入力のサニタイジングが行われていなかったので、インジェクション系の攻撃が利用できるかもしれません。
そこで、以下の入力を渡してみました。
(ユーザ名)
null)||('a'=='a')){$_SESSION['login']=true;$_SESSION['admin']=true;}echo json_encode(array('status'=>'success'));exit;//
(パスワード)
入力なし
この入力がサーバに到達したとき、最初にindex.php
の以下の部分に入力が渡されます。
if (isset($req->username) && isset($req->password)) {
if ($req->username === 'godmode'
&& !in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) {
/* Debug mode is not allowed from outside the router */
$req->username = 'nobody';
}
// 以下略
$req->username
にユーザ名が入るので、今回の入力だと下のコードに置き換わるはずです。(たぶん)
if (isset(null) || ('a' == 'a')){
$_SESSION['login'] = true;
$_SESSION['admin'] = true;
}
echo json_encode(array('status'=>'success'));
exit;
//) && isset($req->password)) {
if ($req->username === 'godmode'
&& !in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) {
/* Debug mode is not allowed from outside the router */
$req->username = 'nobody';
}
// 以下略
isset(null)
は偽になりますが、('a' == 'a')
で必ず真となるためif文が通って、セッション変数 "login" と "admin" が true に設定されます。
そして、echo json_encode(array('status'=>'success'))
の部分でステータスにsuccessを設定し、admin.php
に遷移させて認証をクリアさせます。
その後は、exit
でスクリプトを終了し、それ以降のコードを実行しないようにしています。
(正直ここら辺のコードは自信無いです。間違ったことを書いていたら教えてください。)
結果は失敗でした。
他にもコードを変えたり、HTTPリクエストとレスポンスを観察したりしましたが、解決策が思いつかず時間切れになりました。
解法
CTFイベント終了後もずっと格闘していましたが、結局分からなかったので他の方のwriteupを参考にしました。(https://www.youtube.com/watch?v=-U53PFwi93M&t=2540s)
どうやら、PHPのswitch
文を利用するようです。
PHPの比較演算子には==
と===
がありますが、switch
文は==
を使っており、緩やかな比較を行います。
そのため、厳密に同じ値でなくても、比較判定が真となる場合があります。
具体的には、文字列と論理型(true)の組み合わせのときです。
例えば、"abc"==true
のような条件式は真となります。なので、今回の入力にtrueを渡すと全ての条件式が真となり、switch文全てのケースにマッチします。
このとき、switch文で全てにマッチする場合、どのケースが実行されるかですが、一番最初のケースが実行されるらしいです。
これらの性質を利用すると、switch文で一番最初にあるgodmodeでログインして、フラグを取得することができます。
それでは、入力にtrueを入れてみましょう。
(ユーザ名)
true
(パスワード)
入力なし
ここで一つ注意点があります。入力した値が送信されるとき、入力は文字列として解釈されているため、以下のように一度リクエストをキャッチして、ダブルクォーテーションを外して論理型の値として認識させてあげましょう。
図4 ダブルクォーテーションを外して論理型として認識させる
最後に、止めていたリクエストを送信して終了です。
図5 フラグゲット!
無事、フラグを取得できました。
感想
想像していたよりも数倍難しかったです。実際に問題を解いてみて、自分の知識不足をひしひしと感じました。それから、phpのswitch文の仕様には全然気づけませんでした。気づいた人すごい、、というかこの問題がwarmupって、、難しい。
今回のCakeCTF2022は惨敗でしたが、自分の実力を把握できたうえ、解いていてすごく楽しかったので、有意義な時間を過ごすことができました。またこれからも色々writeup書いていく予定です。
Discussion