緣起:原本我受朋友之託,要協助搶購將會追加的十場的演唱會門票,所以稍稍研究了一下該網站,想說能不能利用自己的專長之便偷點雞,確保自己開賣當日可以穩穩當當地不出差錯把票入手(朋友原本說每張多加 NT$1,000 跟我轉購呢,看在四個孩子的份上...)。
原本這篇文章是打算在搶票成功後第一時間公開,打算用諾曼地搶灘的勝利之姿洋洋灑灑解說,結果過了數日後整件事被我拋到雲霄(我不懂台語,不聽台語歌,所以江蕙對我而言無意義)。
開賣當天週日我就帶著全家到金山上泡天籟溫泉,當中午我躺在室外湯享受山海之色之際,手機 LINE 的訊息不斷傳來:「搶到票沒?」
我心裡暗幹一聲,然後下午的泡湯過程,我心中的陰霾與失落慢慢滋生...。
本來也想說:算了,當沒這回事。當初研究這個售票網的過程記錄就讓他永久塵封在我的 Evernote 之中。
正巧今天看到宏碁慶功的大好消息,想說對於門票開賣當下完全忘了這回事而跑去泡湯的自己,講什麼馬後砲都是多餘的...
不過想想不寫有點可惜,還是拿出來聊聊好了。
請注意:以下很多推論都純粹為本人猜測,未經實戰驗證,僅能當作茶餘飯後嗑瓜子閒聊之用。
首先這個售票網徹底改變了原本寬宏售票網的業務流程!這帖良藥應該才算真正解決售票網癱瘓的主因,而其他技術優勢其實只是點綴強化而已。用分區取代自行選位,由系統直接決定座位,是這次售票成功的真正關鍵(我個人認為啦)。
而且採行的預存購票清單的動作,把大量開賣期間會發生的網路 IO 操作分散在提前幾天,只保留最後一個訂單送出的動作,這也是業務流程的優化,而非技術問題,但卻真正解決了技術上無法解決的問題。而且大幅縮短了程式購票機器人和人類購票的速度差距。
這就是我常說的:技術不是萬能,有時候 User 改變一下僵化的業務流程和頭腦,往往遠比不斷責備、刁難 IT 來得有效。
1月20當天我也登入系統預購如下,所以接下來真正的問題是:1/25 中午開賣,我要如何比別人搶更早送出訂單?
要搶快當然不能依賴『金手指』,滑鼠狂點把電腦搞當機或是太激動搞到中風,這都不是上策。所以,就要靠程式自動化囉!
要做自動化,就要認識我們的對手是什麼來頭,才好接招出招。首先透過瀏覽器本身的開發工具先探探敵手的出身背景,讓我們閱讀一下 Response Header 看看敵手的基本資料:
Okay,從上述資訊,原來對手系出名門,是採用 .NET 2.0 runtime 的 ASP.NET,跑在 Windows 2008 Server 的 IIS,並且從 network 的清單內,可以找到幾個 .aspx 的 request。認識對手的流派,接著就是要觀其言行,我們接著看看他所夾帶的餅乾包了些什麼料?
如果讀者熟悉 ASP.NET 技術的話,應該會從上面的 cookie 清單發現一件事,就是少了最基本的『ASP.NET_SessionId』這個 cookie。一般技術開發的 ASP.NET 網站,如果有透過 Server 來記憶特定使用者的資訊,就會自動產生這樣的 cookie。
基於 http Stateless 的特性,宏碁售票這個網站顯然是完全沒有使用到 Session,也就是說基本上網站的伺服器根本不認識使用者,所以伺服器處理每一個 request 只能夠透過 client 端自己送上來的資訊做驗證。
所以上面的 cookie 放入加密過的亂碼字串就是這個用途。理論上只要我們能夠破解加解密的方式,就可以跟這個售票網宣稱自己的身份,伺服器只能全盤接受。
其實這個售票網不依賴 Session 算是相當合理的設計,為了能應付擠爆大量的 request,前方負載平衡 NLB 的分流機制,不一定會把同一個使用者每次的 request 送往相同的伺服器,而且使用者人數一多,Session 是會大量消耗記憶體資源的。
而如果要做中控的 State Server 或 Distributed Cache(Session) 這些機制,不但拖垮效能耗費硬體資源,而且還要讓工程師煩惱如何做(反)序列化。而且購票網搶票,肯定很多網友開著網頁等待著,網站也不需要處理 Session expire 的麻煩,所以直接規定捨棄 Session 機制算是個正確的 design decision。
但是既然沒了 Session,所有的驗證與安全度只能依賴加密 token 的強健度。
因為 token 需要反解做驗證,可以推測上面用的“可能是”對稱式 AES 的加密法,不過要達到我搶票的目的,破解加密並非必要,可以忽略。
回到我們的目的:自動化。
這個網頁防堵自動化機器人攻進來的唯一防守門檻只有 captch 這個肉眼圖像辨識的機制,如果要走正規方式破解這關卡,不是做不到,但卻十分麻煩。但是剛剛我長篇大論對方沒有採用 Session 機制,就是為了替破解這個關卡鋪路。
因為伺服器沒有記錄任何資訊,只能全盤相信 client 端上傳的資訊,所以伺服器要驗證 captcha 的答案是否正確,正確解答肯定也是由瀏覽器告知的!
沒錯,就是 “ORDER_ValidateCode” 這個 cookie!
伺服器做的事情很簡單,只要把你送上來的 cookie 解密後,跟你手動輸入的字串做比對,兩者相同就是通過驗證!所以我們只要事先準備好一組已經知道答案的 ORDER_ValidateCode 的 Cookie 內容和解答就可以了!完全不需要理會網頁上 Server 最新產生的,因為伺服器根本就不記得他剛剛產生給你的辨識圖樣是什麼。
大家可以點擊上面的聯結進去隨便玩:
如何破解圖像識別輸入的方法也找到了,接著就是,如何進入『真正的購票流程』?
開賣之前,購票網只提供『預存自願單』這樣隔靴搔癢的鳥功能,這不合我們所用啊!我們必須提前知道如何進入真正的購票流程,才能在開賣當下的零秒差之下直接攻取我們要的票券!
不過,身為軟體工程師的你我,可以捫心自問,是不是開發趕工時,偶而會做些能偷懶就偷懶的取巧法?我們就來看看頁面上藏了什麼線索?
不看不知道,一看嚇一跳。網頁原始碼內,『預存志願單』的按鈕旁邊好像藏了些東西?
首先,真正下訂單的按鈕只是透過 CSS 在頁面上被藏起來(style=“display:none”),移除掉該 style 就會顯現真正的下單按鈕:
不過顯示這個按鈕倒也沒什麼特別作用,好玩而已。最有趣的部分,在於『預存志願單』的 onclick 綁在 doPreSave(); 這樣的 javascript function 上,這支程式負責處理『預存』志願單這樣的功能。但是根據工程師有良心的命名習慣,有 do“Pre”Save,那是不是代表應該也有 doSave() 囉?
還真的有!而且就直接寫在頁面上放著,名稱這麼符合直覺,應該給他嘉獎鼓勵!
就在 sign_up.html 頁面地 684 行的位置開始。
雖然該嘉獎這位命名規則直覺的工程師,但是把尚未用到的功能呈現在頁面上,曝露攻擊點....ㄎㄎㄎ。
真正的關鍵在 ajaxCall 那行,才是送出訂單跟伺服器溝通購買。跟doPreSave 之間的差異,只在於 PreSave 帶入 type=1 的參數,而真正的購買,帶入 type=3 的參數
ajaxCall 這支的實作寫在 master.js(line:146) 內。
不浪費時間,我們直接把 onclick 改綁到 doSave() 來看看網路上實際傳送 request 的模樣。
從 Form Data 看出伺服器在處理 type=3 的作業所需要的資料格式,只要到時候依樣畫葫蘆,讓程式在時間點到的時候發送相同的內容即可。
但由於伺服器有依賴開賣時間阻擋,所以提前發送並無效果,而伺服器和使用者的電腦之間又可能存在時差,誤差可能從秒到分鐘,為了精准判定伺服器的時間是否已達開賣時刻,可以一樣透過 UTK0195_.aspx 這支網頁,給 type=6 取得目前伺服器倒數的毫秒數,只要等這個數字為 0 即可。
有了上面這些資訊,我們可以選擇直接在瀏覽器上嵌入一段自動化的 javascript 即可,這應該是最經濟簡便的做法,但要提前一直開著瀏覽器才可以。
不過走此方案的人,還要注意是否網頁有倒數結束自動重新刷新頁面的手法,這樣剛剛提前嵌入的 javascript 程式就會被清除掉喔!
PS. 根據留言的網友Zion(好像是Acer 的PM)表示,開賣當天的程式其實已經不是我先前看到的模樣,可能是說其實網站有多做一道防線,原本的 doSave 並無法直接使用。果然這篇文章沒有經過實戰驗證,不過就分享一下,當作示範外界有心人士可能會如何推敲目標網站的設計與運作。(^^)
以上,就是我嘗試研究宏碁售票網站的過程,只要註明出處原始網址,歡迎轉載分享本文。
沒有留言:
張貼留言