RでRStudio Serverのログイン画面を突破する
ちょっとやろうと思っていることがあったので試してみたときのメモです。
とりあえずGET
何も考えずにGET()
してみます。
library(httr) res <- GET("localhost:8787") res #> Response [http://localhost:8787/unsupported_browser.htm] #> Date: 2016-12-03 22:44 #> Status: 200 #> Content-Type: text/html #> Size: 894 B #> <html> #> #> <head> #> <title>RStudio: Browser Not Supported</title> #> #> <link rel="stylesheet" href="rstudio.css" type="text/css"/> #> <link rel="shortcut icon" href="images/favicon.ico" /> #> #> <style type="text/css"> #> #> ...
unsupported browserと言われるので、User Agentを偽らなくてはいけません。適当に、手元のFirefoxと同じUser Agentにしてみます。
res <- GET("localhost:8787", user_agent("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0")) res #> Response [http://localhost:8787/auth-sign-in] #> Date: 2016-12-03 22:49 #> Status: 200 #> Content-Type: text/html #> Size: 6.18 kB #> <!DOCTYPE html> #> #> <!-- #> # #> # encrypted-sign-in.htm #> # #> # Copyright (C) 2009-12 by RStudio, Inc. #> # #> # This program is licensed to you under the terms of version 3 of the #> # GNU Affero General Public License. This program is distributed WITHOUT #> ...
今度はうまくいってそうです。
ログインフォーム
次に、ログインフォームを探します。
library(rvest) content(res) %>% html_form() #> No encoding supplied: defaulting to UTF-8. #> [[1]] #> <form> '<unnamed>' (POST javascript:void) #> <input text> 'username': #> <input password> 'password': #> <input checkbox> 'staySignedIn': 1 #> <input hidden> 'appUri': #> <button submit> '<unnamed> #> #> [[2]] #> <form> 'realform' (POST auth-do-sign-in) #> <input hidden> 'persist': #> <input hidden> 'appUri': #> <input hidden> 'clientPath': #> <input hidden> 'v':
なぜか2つ出てきます。たぶんrealform
っていう方が名前からして本物っぽいです。<unnamed>
ってなっているのが目に見えている方のフォームで、こっちを送信するとJavaScriptがいろいろ動いてrealform
が送信されるのでしょう。
scriptを解読
script
タグを抽出してみます。
scripts <- content(res) %>% html_nodes("script") scripts #> No encoding supplied: defaulting to UTF-8. #> {xml_nodeset (4)} #> [1] <script language="javascript"><![CDATA[\nfunction verifyMe()\n{\n if(do ... #> [2] <script type="text/javascript" src="js/encrypt.min.js"/> #> [3] <script type="text/javascript"><![CDATA[\nfunction prepare() {\n if (!v ... #> [4] <script type="text/javascript"><![CDATA[\ndocument.getElementById('userna ...
いろいろ見比べた結果、どうやら3つ目が本物のようです。
cat(as.character(scripts[[3]])) #> <script type="text/javascript"><![CDATA[ #> function prepare() { #> if (!verifyMe()) #> return false; #> try { #> var payload = document.getElementById('username').value + "\n" + #> document.getElementById('password').value; #> var xhr = new XMLHttpRequest(); #> xhr.open("GET", "auth-public-key", true); #> xhr.onreadystatechange = function() { #> try { #> if (xhr.readyState == 4) { #> if (xhr.status != 200) { #> var errorMessage; #> if (xhr.status == 0) #> errorMessage = "Error: Could not reach server--check your internet connection"; #> else #> errorMessage = "Error: " + xhr.statusText; #> #> var errorDiv = document.getElementById('errorpanel'); #> errorDiv.innerHTML = ''; #> var errorp = document.createElement('p'); #> errorDiv.appendChild(errorp); #> if (typeof(errorp.innerText) == 'undefined') #> errorp.textContent = errorMessage; #> else #> errorp.innerText = errorMessage; #> errorDiv.style.display = 'block'; #> } #> else { #> var response = xhr.responseText; #> var chunks = response.split(':', 2); #> var exp = chunks[0]; #> var mod = chunks[1]; #> var encrypted = encrypt(payload, exp, mod); #> document.getElementById('persist').value = document.getElementById('staySignedIn').checked ? "1" : "0"; #> document.getElementById('package').value = encrypted; #> document.getElementById('clientPath').value = window.location.pathname; #> document.realform.submit(); #> } #> } #> } catch (exception) { #> alert("Error: " + exception); #> } #> }; #> xhr.send(null); #> } catch (exception) { #> alert("Error: " + exception); #> } #> } #> function submitRealForm() { #> if (prepare()) #> document.realform.submit(); #> } #> ]]></script>
まず、この行で公開鍵を取ってきているようです。
xhr.open("GET", "auth-public-key", true);
そしてそれを用いて、このpayload
というやつを、
var payload = document.getElementById('username').value + "\n" + document.getElementById('password').value;
こんな感じで暗号化しています。
var response = xhr.responseText; var chunks = response.split(':', 2); var exp = chunks[0]; var mod = chunks[1]; var encrypted = encrypt(payload, exp, mod);
encrypt
という関数は、名前からして上のscriptsの2つ目にあったjs/encrypt.min.js
で定義されてそうな雰囲気です。
scripts[[2]] #> {xml_node} #> <script type="text/javascript" src="js/encrypt.min.js">
V8でむりやり動かす
V8パッケージで動くか試してみましょう。
library(V8) encryptjs <- content(GET("http://localhost:8787/js/encrypt.min.js"), as = "text") ct <- new_context() ct$eval(encryptjs) #> Error in context_eval(join(src), private$context) : #> ReferenceError: navigator is not defined
なにかエラーが出ます。ググってみたところ、このnavigator
という変数は、ブラウザの情報を保持しているもののようです。
ということで、これを適当に定義してやればよさそうです。どういう使われ方をしているかもう少し詳しく見てみます。このJavaScriptはminifyされているので、もとのソースを見るほうが読みやすそうです。
navigator
が出てくるのは以下の3か所です。
if(j_lm && (navigator.appName == "Microsoft Internet Explorer")) {
else if(j_lm && (navigator.appName != "Netscape")) {
if(navigator.appName == "Netscape" && navigator.appVersion < "5" && window.crypto) {
どうやら、ブラウザの種類によって条件分岐があるようです。とりあえず、appName
とappVersion
をつくってやればよさそうです。これでもう一度チャレンジしてみます。
ct$eval("var navigator = {appName: 'Netscape', appVersion: '5.0 (windows)'};") ct$eval(encryptjs) #> Error in context_eval(join(src), private$context) : #> ReferenceError: window is not defined
window
がないと言われます。これはググったらタイポさんがIssueを挙げてるのを見つけました。new_context()
に引数を渡せば、global
オブジェクトをwindow
という名前にすることができます。
ct <- new_context("window") ct$eval("var navigator = {appName: 'Netscape', appVersion: '5.0 (windows)'};") ct$eval(encryptjs) #> [1] "function (b,a,c){var d=new $;d.X(c,a);b=d.N(b);d=\"\";for(a=0;a+3<=b.length;a+=3){c=parseInt(b.substring(a,a+3),16);d+=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\".charAt(c>>6)+\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\".charAt(c&63)}if(a+1==b.length){c=parseInt(b.substring(a,a+1),16);d+=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\".charAt(c<<2)}else if(a+2==b.length){c=parseInt(b.substring(a,a+2),16);d+=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\".charAt(c>>\n2)+\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\".charAt((c&3)<<4)}for(;(d.length&3)>0;)d+=\"=\";return d}"
今度はうまくいきました。encrypt
ができているか確かめましょう。
ct$get(JS('Object.keys(window)')) #> [1] "console" "print" "window" "ArrayBuffer" "Int8Array" #> [6] "Uint8Array" "Int16Array" "Uint16Array" "Int32Array" "Uint32Array" #> [11] "Float32Array" "Float64Array" "DataView" "navigator" "g" #> [16] "j" "k" "l" "m" "o" #> [21] "p" "s" "t" "u" "x" #> [26] "z" "A" "B" "D" "E" #> [31] "F" "H" "I" "J" "K" #> [36] "L" "M" "N" "aa" "ba" #> [41] "ca" "da" "ea" "fa" "ga" #> [46] "ha" "ja" "P" "ka" "la" #> [51] "ma" "na" "oa" "pa" "Q" #> [56] "qa" "ra" "sa" "ta" "ua" #> [61] "va" "wa" "xa" "G" "O" #> [66] "R" "ya" "za" "S" "T" #> [71] "U" "V" "W" "X" "Y" #> [76] "Aa" "Ba" "Z" "$" "Ca" #> [81] "Da" "Ea" "encrypt"
ちゃんと定義されていますね。大丈夫そうです。
では、いよいよこれを使ってRStudio Serverにログインしてみましょう。
# 公開鍵を取得 pubkey <- content(GET('http://localhost:8787/auth-public-key')) ct$assign("pubkey", pubkey) ct$eval("var chunks = pubkey.split(':', 2); var exp = chunks[0]; var mod = chunks[1];") # ペイロードを渡して公開鍵で暗号化 ct$assign("payload", paste("rstudio", "rstudio", sep = "\n")) v <- ct$eval("encrypt(payload, exp, mod);") # vは暗号化されたペイロード。後のパラメータは適当に埋める。 res <- POST("http://localhost:8787/auth-do-sign-in", body = list( persist = "0", appUri = "", clientPath = "", v = v ), user_agent("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0"))
成功していれば、http://localhost:8787/
にリダイレクトされているはずです。httrは自動でリダイレクトも追ってくれて、responseオブジェクトをそのまま表示したときは最終的な結果のみが表示されます。
res #> Response [http://localhost:8787/] #> Date: 2016-12-04 00:50 #> Status: 200 #> Content-Type: text/html #> Size: 899 B #> <!DOCTYPE html> #> ...snip...
リダイレクトのレスポンスも見たければ、all_headers
という要素に格納されています。Cookie
も見ることができます。
purrr::map_int(res$all_headers, "status") #> [1] 302 200 res$all_headers[[1]]$headers$location #> [1] "http://localhost:8787/" res$all_headers[[1]]$headers$`set-cookie` #> [1] "user-id=...|...GMT|...; path=/; HttpOnly"
ほんとはV8使わなくてもいいはず...
あまり理解できてないんですが、modulusとpublic exponentがあれば暗号化はできるはずなので、V8は使わなくてもいいはずです。ちなみにこのJavaScriptのソースは以下で公開されているもののようです。
こういう感じ?