Digest is secure!
Digest is secure!:digest認証はセキュアです!
digest認証についての問題
前回のbasic認証よりもセキュアな通信ができる。
以下がdigest認証の手順
典型的なDigest認証におけるHTTPクライアントとHTTPサーバの間の通信を紹介する。
だいたいの流れは以下のようになる。
- クライアントは認証が必要なページをリクエストする。しかし、通常ここではユーザ名とパスワードを送っていない。なぜならばクライアントはそのページが認証を必要とするか否かを知らないためである。
- サーバは401レスポンスコードを返し、認証領域 (realm) や認証方式(Digest)に関する情報をクライアントに返す。このとき、ランダムな文字列(nonce)とサーバーがサポートしている qop (quality of protection) を示す引用符で囲まれた1つまたは複数のトークンも返される。
- それを受けたクライアントは、認証領域(通常は、アクセスしているサーバやシステムなどの簡単な説明)をユーザに提示して、ユーザ名とパスワードの入力を求める。ユーザはここでキャンセルすることもできる。
- ユーザによりユーザ名とパスワードが入力されると、クライアントはnonceとは別のランダムな文字列(cnonce)を生成する。そして、ユーザ名とパスワードとこれら2つのランダムな文字列などを使ってハッシュ文字列(response)を生成する。
- クライアントはサーバから送られた認証に関する情報(ユーザ名, realm, nc(nonce count), nonce, cnonce, qop)とともに、responseをサーバに送信する。
- サーバ側では、クライアントから送られてきたランダムな文字列(nonce、cnonce)などとサーバに格納されているハッシュ化されたパスワードから、正解のハッシュを計算する。
- この計算値とクライアントから送られてきたresponseとが一致する場合は、認証が成功し、サーバはコンテンツを返す。不一致の場合は再び401レスポンスコードが返され、それによりクライアントは再びユーザにユーザ名とパスワードの入力を求める。
とりあえず問題のq9.pacpをwiresharkで見てみる。
見やすいようにhttp通信でフィルタして、httpストリームを追跡する。
httpストリームの結果
まずクライアントがhttp://ctfq.u1tramarine.blue/q9にGETリクエストを送っている。
素直にGETできるわけがなく、401 Unauthorized が帰ってきて認証できない。
この時にDigest認証が必要であることが書かれていて、認証に必要なrealm,nonce,algorithm,qopがサーバーから送られてくる。
クライアントはサーバーから送られてきた認証に必要な情報(realm,nonce,algorithm,qop)と、生成したcnonceや、nc,uriの情報をもとにresponseを計算し、それらをサーバーに送信している。
クライアントから送られてきたresponseの値と、サーバーで計算した正解ハッシュが一致したから、flagが書かれていると思われるflag.htmlをクライアントに送信している。
方針
クライアントから送られてきたresponseの値と、サーバーで計算した正解ハッシュが一致すればいいので、一致するようにresponseの計算に必要な認証情報を設定することでflag.htmlを入手する。
クライアントがresponseを作るにはA1(ユーザー名,realm,パスワード),nonce,nc,cnonce,qop,A2(HTTPメソッド,コンテンツURI)が必要。
ユーザー名、realm、パスワード、qop、HTTPメソッド、コンテンツURI、は固定な値。
nonceは一番最初のGETリクエストの応答に含まれていて、毎回ランダムな値。
cnonceはクライアントが生成するものでこれも毎回ランダム値。
解く
この通信に注目すると、GETリクエストと送る時に、header情報としてAuthorization: Digest name,realm,nonce,uri,algrithm,response,qop,nc,cnonceを同時に送信している。
この情報を適切に設定してGETリクエストを送ればflag.htmlを入手できそうだから、まずresponseの中身を調べてみる。
httpストリームから、**response="e9654c012dc42f9f78f81a685073df98"**がわかる。これをMD5で逆変換してnonce,nc等の情報を見てみる。このサイトで逆変換できる。
見やすいように並べると、
|情報名|値|
|:---|:---:|--:|
|MD5(A1)|c627e19450db746b739f41b64097d449|
|nonce|HHj57RG8BQA=4714c627c5195786fc112b67eca599d675d5454b|
|nc|00000002|
|cnonce|656335d78cef6e86|
|qop|auth|
|MD5(A2)|adea3748da59405c1f4c1650442607a1|
c627e19450db746b739f41b64097d449はA1(ユーザー名:realm:パスワード)をMD5変換したものだから逆変換したらユーザー名、realm、パスワードがわかりそう。やってみると
この逆変換はできなかった。もしできたらパスワードがわかったけどそんなに甘くはない。
だけど、ユーザー名、realm、パスワードはサーバーで設定されていて変わらない情報だからMD5(A1)にc627e19450db746b739f41b64097d449を使うことができそう。逆変換はできなかったけど、Wiresharkでユーザー名=q9、realm=sercetは確認できる。
次にadea3748da59405c1f4c1650442607a1の逆変換をしてA2(HTTPメソッド:コンテンツURI)を調べてみる。
こっちは逆変換できた。httpメソッドがGETで、コンテンツURIが/q9/であることがわかった。
これらはWireSharkからも確認できた
今回はhttp://ctfq.u1tramarine.blue/q9/flag.htmlアクセスしたいから、メソッドをGET、コンテンツURIを**/q9/flag.htmlとして、A2をGET:/q9/flag.html**に変更し、これをMD5変換したものをMD5(A2)とする。変換してみると
となって、MD5(A2)=9e2b6bca5d4d92f6ead358623df264c8と設定する。
残りのqopはauthで固定で、cnonce、ncはresponseの計算で使うだけのものだから適当に設定すれば良い(空白でも良い)。
nonceは最初のGETリクエストに対する応答としてサーバーから送られてくるランダムな値でアクセス毎に変更されるからnonceはあらかじめ設定するのではなく、応答情報から抜き取ってくる必要がある。
今回はpythonのrequestを使ってその応答情報からnonceを得た。
実装
import requests
import hashlib
from bs4 import BeautifulSoup as bs
# Authorizationヘッダ作成に用いるパラメタ
url = "http://ctfq.u1tramarine.blue/q9/flag.html"
md5a1 = "c627e19450db746b739f41b64097d449"
nonce = ""
nc = ""
cnonce = ""
qop = "auth"
a2 = "GET:/q9/flag.html"
md5a2 = "9e2b6bca5d4d92f6ead358623df264c8"
username = "q9"
realm = "secret"
algorithm = "MD5"
uri = "/q9/flag.html"
# urlにアクセスして得られた応答のheader情報(WWW-Authenticate)からnonceを抜き出す。
auth_header = requests.get(url).headers['WWW-Authenticate']
nonce = auth_header.split(" ")[2][7:-2]
# responseの計算
not_md5_response = md5a1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + md5a2
md5_response = hashlib.md5(not_md5_response.encode('utf-8')).hexdigest()
# header情報の作成
# headerの形式はhttpストリームの3.に示したAuthrizationの形式に合わせる
headers = {
'Authorization': \
'Digest username="' + username + '"' + ', ' + \
'realm="' + realm + '"' + ', ' + \
'nonce="' + nonce + '"' + ', ' + \
'uri="' + uri + '"' + ', ' + \
'algorithm="' + algorithm + '"' + ', ' + \
'response="' + md5_response + '"' + ', ' + \
'qop=' + qop + ', ' + \
'nc=' + nc + ', ' + \
'cnonce="' + cnonce + '"'
}
# 作成したheader情報をurlに送信する
answer = requests.get(url, headers=headers)
print(answer.text)
まとめ
digest認証について理解できた。
前回のBasic認証よりもセキュアな認証を行っていることがわかった。
しかし、パスワードが分からなくてもAuthrizationの情報をうまく設定すれば突破できてしまう危険性があることがわかった。