MySQL道普請便り

第81回 MySQLプロトコルでハンドシェイクレスポンスを返してみる

この記事を読むのに必要な時間:およそ 2.5 分

第78回でMySQLプロトコルのハンドシェイクパケットを眺めてみましたが,今回はハンドシェイクレスポンスを返してみます。このレスポンスがうまくいくと,認証が完了してコマンドを受け付けてくれるようになります。

実行環境

MySQLは5.7.23を使っています。また,今回ハンドシェイクレスポンスを作成するために,Rubyの2.5.1を利用しています。Rubyのインストールに関しては,割愛させていただきます。

MySQLパケットについて

実際にパケットを送る前に,MySQLのパケットの通信の形式について紹介します。

MySQLにはサーバ/クライアント間でパケットをやり取りする際の決まり事があり,MySQLの公式ドキュメントに記載されています。

簡単に形式をすると,はじめの3バイトはパケットの長さを表しています。最大値はFF FF FFになるのですが,これ以上のサイズのパケットを送信したい場合や受信する場合があると思います。その場合にはFF FF FFを最大値とし,そこまでで区切って送信します。そしてパケットのサイズがFF FF FFでなくなるまで繰り返し送ります。

続いてシーケンス番号が1バイトで送られてきます。最後にpayload(今回の例で言えばハンドシェイクレスポンス)を付けて送るという流れになっています。

ハンドシェイクレスポンスを作ってみる

さて,ハンドシェイクレスポンスと一言で言いましたが,MySQLの公式のドキュメントには2種類記載されています。Protocol::HandshakeResponse41とProtocol::HandshakeResponse320です。この内HandshakeResponse320は,HandshakeResponse41を使えない古いサーバに使います。

今回の環境ではHandshakeResponse41が問題なく使えるので,41レスポンスを利用して作成していきます。

HandshakeResponse41のペイロードに関して

MySQLの公式のドキュメントからハンドシェイクレスポンスの形式を引用したものが,以下になります。

4              capability flags, CLIENT_PROTOCOL_41 always set
4              max-packet size
1              character set
string[23]     reserved (all [0])
string[NUL]    username
  if capabilities & CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA {
lenenc-int     length of auth-response
string[n]      auth-response
  } else if capabilities & CLIENT_SECURE_CONNECTION {
1              length of auth-response
string[n]      auth-response
  } else {
string[NUL]    auth-response
  }
  if capabilities & CLIENT_CONNECT_WITH_DB {
string[NUL]    database
  }
  if capabilities & CLIENT_PLUGIN_AUTH {
string[NUL]    auth plugin name
  }
  if capabilities & CLIENT_CONNECT_ATTRS {
lenenc-int     length of all key-values
lenenc-str     key
lenenc-str     value
   if-more data in 'length of all key-values', more keys and value pairs
  }

if文が入っていて少しわかりにくいですが,if文の意味は以下のとおりです。if文の条件式でcapabiliesフラグと各種オプションでビットのAND演算を行います。その結果がtrue(0以外)になるとしたら,必要な値を入力する必要があります。

Capabilityのフラグに関しては,MySQLの公式ドキュメントに記載があります。

今回は,そのうちCLIENT_PROTOCOL_41(0x00000200)と CLIENT_SECURE_CONNECTION(0x00008000)を設定して認証を行います。その場合,上記のif文を処理して少しわかりやすくすると,以下のようになります。

4              capability flags, CLIENT_PROTOCOL_41 always set
4              max-packet size
1              character set
string[23]     reserved (all [0])
string[NUL]    username
1              length of auth-response
string[n]      auth-response

というわけで,接続するにあたっては,上記のようなpayloadを用意すれば良いということになります。

CLIENT_SECURE_CONNECTIONの接続方法に関して

上記のペイロードの説明で「今回はCLIENT_SECURE_CONNECTIONを利用して接続する」と説明しましたが,その接続方式に関して説明します。

ハンドシェイクパケットのauth_plugin_data_part_1とauth_plugin_data_part_2を利用して,パスワードをハッシュ化して接続します。

20バイトのランダムデータをサーバから送ってもらって,そのデータに対して以下のような計算を行うと,ハッシュ化されたデータが得られます。

SHA1(password) XOR SHA1( "サーバから送られてきた20バイトのランダムデータ" <文字列結合> SHA1( SHA1( password ) ) )

このようなパラメータを使うことで,直接パスワードを伝送しなくてもすむ仕組みが用意されています。

実際に接続を行ってみる(簡易版)

はじめは設定を簡単にして接続を行ってみようと思います。

user名がrootでパスワードが設定されていないアカウントを事前に用意します。そしてCapabilitiesにCLIENT_PROTOCOL_41(0x00000200)だけを設定します。パスワードが無い場合の認証では,なんと上で説明を行ったCLIENT_SECURE_CONNECTIONの計算をサボることができます。日常的に使うようなクライアントを作成したい場合にはこの手法は基本的に取れませんが,1回だけ試してみたい場合などには利用することができます。最低限接続するのに必要なプログラムをRubyで書いてみました。

プログラム全体

require 'socket'

socket = TCPSocket.open("localhost", 3306)
data = [0x00000200, 1024 * 3, 33, "", 'root', ''].pack("VVa*Ca23Z*a")
socket.sync = true
socket.write [data.length % 256, data.length / 256, 1].pack("CvC")
socket.write data
socket.flush

p socket.readpartial(11)

さて上から順に説明をしていきますと,require 'socket'で今回接続に利用するSocket通信用のライブラリを読み込みます。次の行ではTCP接続を行っています。

dataが今回定義するレスポンスパケットの中身です。capabilitiesに先程書いたとおりにCLIENT_PROTOCOL_41だけ用意をしています。次の1024*3はパケットの最大長です。その次の33ということで文字コードをutf8を指定しています。予約されている23文字の空白を入れて,最後にユーザ名とCLIENT_SECURE_CONNECTIONで暗号化されたパスワードを入れます。今回はパスワードが存在しないため,空白をいれてそれぞれの形式に合わせてバイト列を作成しています。

注意しないといけないのは,MySQLのパケットはリトルエンディアンである点です。他の言語で実装する場合には,バイト文字列に変換する際には注意しましょう。

実行すると,以下のような出力が得られると思います。

"\a\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00"

こちらですが,MySQLの認証ではOKパケットと呼ばれるパケットになります。最初の4バイトはMySQLパケットなので取り除いてみると,\x00\x00\x00\x02\x00\x00\x00となります。最初の1バイトがヘッダですが,こちらが00なのでOKとなります。

今度はユーザ名を存在しないユーザに変更して試してみましょう。rootの部分をuserと書き換えて実行してみると,以下のような出力が得られます。

"G\x00\x00\x02\xFF\x15\x04#280"

こちらも最初の4バイトを取り除くと\xFF\x15\x04#280となっています。こちらはエラーパケットと呼ばれるパケットで,ffから始まる場合はエラーが発生しているので,何か設定が間違っていないか確認してみましょう。

まとめ

今回は,レスポンスリクエストを作成してMySQLに接続する方法について説明をしました。これでMySQLと簡易的に接続することができるようになります。みなさんもぜひ一度遊んでみてください!

著者プロフィール

木村浩一郎(きむらこういちろう)

GMOメディア株式会社 技術推進室所属のWebアプリケーションエンジニア。最近はミドルウェア・インフラ周りのことも少しずつ学習しています。趣味は将棋。好きな戦法は四間飛車。

Twitter:@kk2170

コメント

コメントの記入