Midnight Monologues

日々勉強したことを書いてきます

RITSEC CTF 2022 - Writeup

2022年4月1日, 16:00 UTC - 4月4日, 04:00 UTCに開催されたRITSEC CTF 2022 に会社のチームで参加した。 他のチームメンバーの陰で6問解いたので、以下にWriteupを記載する。

Web

Pretty Horrible Program 1

Bingus our beloved is found and he can never be replaced

Author : BradHack3r
https://ctf.ritsec.club/php1


指定されたURLへアクセスすると添付のようなフォーム画面が表示される。
ソースコードが以下URLで確認できる。
https://ctf.ritsec.club/php1/index.php?source

<?php
if (isset($_GET['source'])) {
  highlight_file(__FILE__);
  die();
}
define('APP_RAN', true);
require('flag.php');
?>
<!DOCTYPE html>

<head>
  <style>
    body {
      display: flex;
      flex-direction: column;
      align-items: center;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
    }

    code {
      color: orange;
      font-size: 2.5rem;
    }

    .title {
      font-weight: 500;
    }

    .title b {
      color: blue;
    }

    .answer code {
      font-size: 2rem;
    }
  </style>
  <title>PHP 1</title>
</head>

<body>
  <img src="/php1/praise_bingus.webp" width="300" />
  <h1 class="title"><b>P</b>retty <b>H</b>orrible <b>P</b>rogram <b>1</b></h1>
  <a href="/php1/index.php?source">View Source Code</a>
  <br />
  <?php
  if (isset($_GET['bingus'])) {
    $input = $_GET['bingus'];
    $to_replace = 'bingus';
    $clean_string = preg_replace("/$to_replace/", '', $input);
    echo "<p>Your string is: $clean_string</p>";
    if ($clean_string == $to_replace) {
      echo "<h2 class=\"answer\">Bingus <span style=\"color: green;\">IS</span> your beloved</h2>";
      output_flag();
    } else {
      echo "<h2 class=\"answer\">Bingus <span style=\"color: red;\">IS NOT</span> your beloved</h2>";
    }
  }
  ?>
  <form method="get">
    <input type="text" required name="bingus" placeholder="Gimme some input :)" />
    <input type="submit" />
  </form>
</body>


以下の判定処理よりbingusのクエリストリングで渡したGETリクエストの値がpreg_replace関数で置換された後にbingusの文字列と等しければflagが表示される。

    if ($clean_string == $to_replace) {
      echo "<h2 class=\"answer\">Bingus <span style=\"color: green;\">IS</span> your beloved</h2>";
      output_flag();


bingusの文字列が置換されて消えるためbinbingusgusのように指定すれば条件に一致する。
つまり以下のURLでアクセスすればflagが表示される。

https://ctf.ritsec.club/php1?bingus=binbingusgus

flag : RS{B1ngus_0ur_B3lov3d}



Pretty Horrible Program 2

Bingus cereal 👀 duh

Author : BradHack3r
https://ctf.ritsec.club/php2


指定されたURLへアクセスすると添付のようなフォーム画面が表示される。
ソースコードが以下URLで確認できる。
https://ctf.ritsec.club/php2/index.php?source

<?php
if (isset($_GET['source'])) {
  highlight_file(__FILE__);
  die();
}
define('APP_RAN', true);
require('flag.php');

if (!isset($_COOKIE['user'])) {
  $default_user = new User;
  $_COOKIE['user'] = serialize($default_user);
  setcookie(
    'user',
    serialize($default_user),
  );
}

if (isset($_POST['user'])) {
  setcookie(
    'user',
    $_POST['user'],
  );
}

?>
<!DOCTYPE html>
<html>

<head>
  <style>
    body {
      display: flex;
      flex-direction: column;
      align-items: center;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
    }

    hr {
      width: 50%;
    }

    code {
      font-size: 2rem;
      font-weight: 500;
    }

    .error {
      color: red;
    }

    .success {
      color: green;
    }

    .title {
      font-weight: 500;
    }

    .title b {
      color: blue;
    }

    .answer code {
      font-size: 2rem;
    }

    form {
      display: flex;
      flex-direction: column;
      align-items: center;
    }
  </style>
  <title>PHP 2</title>
</head>

<body>
  <img src="/php2/meme.jpeg" width="300" />
  <h1 class="title"><b>P</b>retty <b>H</b>orrible <b>P</b>rogram <b>2</b></h1>
  <?php
  class User
  {
    public $role = 'User';

    public function is_admin()
    {
      if ($this->role == 'Admin') {
        return true;
      } else {
        return false;
      }
    }

    public function __sleep()
    {
      return array($this->role);
    }
  }
  ?>
  <?php
  if (isset($_COOKIE['user'])) {
    echo "<p>Output:<br/>" . $_COOKIE['user'] . '</p>';
  } else {
    echo 'Please provide some input.';
  }
  ?>
  <?php
  if (isset($_COOKIE['user'])) {
    try {
      $user = unserialize($_COOKIE['user']);
      if ($user->is_admin()) {
        echo '<h3 class="success">Welcome Admin</h3>';
        output_flag();
      } else {
        echo '<h3 class="error">Not Admin</h3>';
      }
    } catch (Error $e) {
      echo '<h2 class="error">Uh oh, ur input was <code>cringe</code></h2>';
    }
  }
  ?>
  <hr />
  <form action="/php2/index.php" method="post">
    Serialized User: <input type="text" name="user"><br>
    <input type="submit">
  </form>
  <a href="/php2/index.php?source">View Source Code</a>
</body>

</html>


以下の判定処理よりcookieにセットされたO:4:"User":1:{s:4:"User";N;}のシリアル化された値を改変して is_admin()のrole == 'Admin'をセットできればflagが表示される。

public function is_admin()
    {
      if ($this->role == 'Admin') {
        return true;
      } else {
        return false;
      }


シリアライズされた値の確認

O:4:"User":1:{s:4:"User";N;}

     ↓

object(__PHP_Incomplete_Class)#1 (2) {
  ["__PHP_Incomplete_Class_Name"]=>
  string(4) "User"
  ["User"]=>
  NULL
}

User(4文字)のObjectがあり、その中にUser(4文字)のstring型の要素が1つ構成されている。User要素の値はNullが設定されている。
この中にrole(4文字)という要素を追加して、Admin(5文字)の値をセットする。(つまり要素は2つで構成されることになる。)

PHPシリアライズ形式については以下のURLが参考になる。 phpinternalsbook-ja.com

※安全でないデシリアライゼーションの考え方は以下の大変ありがたいURLを参照するとよいでしょう。 blog.tokumaru.org

O:4:"User":2:{s:4:"User";N;s:4:"role";s:5:"Admin";}

     ↓

object(__PHP_Incomplete_Class)#1 (3) {
  ["__PHP_Incomplete_Class_Name"]=>
  string(4) "User"
  ["User"]=>
  NULL
  ["role"]=>
  string(5) "Admin"
}


cookieの値(user)はURLエンコードされているので作成したシリアル化された値をURLエンコードする。 O%3A4%3A%22User%22%3A2%3A%7Bs%3A4%3A%22User%22%3BN%3Bs%3A4%3A%22role%22%3Bs%3A5%3A%22Admin%22%3B%7D

この値をCookieのuserにセットしてサイトへアクセスするとflagが表示される。

flag : RS{C3re4l_B1ngu5}



Pretty Horrible Program 3

Well, better get cracking I guess

Author : BradHack3r
https://ctf.ritsec.club/php3


指定されたURLへアクセスすると添付のようなフォーム画面が表示される。

ソースコードが以下URLで確認できる。
https://ctf.ritsec.club/php3/index.php?source=true

<?php
if (isset($_GET['source'])) {
  highlight_file(__FILE__);
  die();
}
define('APP_RAN', true);
require 'flag.php';
?>
<!DOCTYPE html>
<html>

<head>
  <style>
    body {
      display: flex;
      flex-direction: column;
      align-items: center;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
      text-align: center;
    }

    hr {
      width: 50%;
    }

    code {
      font-size: 2rem;
      font-weight: 500;
    }

    .error {
      color: red;
    }

    .success {
      color: green;
    }

    .title {
      font-weight: 500;
    }

    .title b {
      color: blue;
    }

    .answer code {
      font-size: 2rem;
    }

    form {
      display: flex;
      flex-direction: column;
      align-items: center;
    }
  </style>
  <title>PHP 3</title>
</head>

<body>
  <img src="/php3/drippy_bingus.jpeg" width="300" />
  <h1 class="title"><b>P</b>retty <b>H</b>orrible <b>P</b>rogram <b>3</b></h1>
  <?php
  if (isset($_GET['input1']) and isset($_GET['input2'])) {
    if ($_GET['input1'] == $_GET['input2']) {
      print '<h3 class="error">Nice try, but it won\'t be that easy ;)</h3>';
    } else if (hash("sha256", $_GET['input1']) === hash("sha256", $_GET['input2'])) {
      output_flag();
    } else {
      print '<h3 class="error">Your inputs don\'t match</h3>';
    }
  }
  ?>
  <p>See if you can make the sha256 hashes match</p>
  <br />
  <a href="/php3/index.php?source=true">Source Code</a>
  <form method="get">
    <input type="text" required name="input1" placeholder="Input 1" />
    <p>Hash: <?php if (isset($_GET['input1'])) print hash("sha256", $_GET['input1']) ?></p>
    <input type="text" required name="input2" placeholder="Input 2" />
    <p>Hash: <?php if (isset($_GET['input2'])) print hash("sha256", $_GET['input2']) ?></p>
    <input type="submit" />
  </form>
</body>

</html>
<?php


以下の判定処理より緩やかな一致では等しくないが、厳密な一致では等しい値をinput1とinput2のGETリクエストで渡すとflagが表示される。

  if (isset($_GET['input1']) and isset($_GET['input2'])) {
    if ($_GET['input1'] == $_GET['input2']) {
      print '<h3 class="error">Nice try, but it won\'t be that easy ;)</h3>';
    } else if (hash("sha256", $_GET['input1']) === hash("sha256", $_GET['input2'])) {
      output_flag();
    } else {
      print '<h3 class="error">Your inputs don\'t match</h3>';
    }


既知の内容でsha256 collision phpで調べると文献がある。

https://stackoverflow.com/questions/53080807/sha256-hash-collisions-between-two-strings/53081240

配列の形式ではbypassできるとのこと
以下URLでフォーム上はエラーになるがflagが表示される。
https://ctf.ritsec.club/php3?input1[0]=1&input2[1]=1


flag : RS{Th3_H@sh_Sl1ng1ng_5lash3r}



Down the Data Streams

Can you find it?
<br>
Some brute-forcing is allowed.
<br>
Directories are found on the first page
https://ctf.ritsec.club/data-streams


指定されたURLへアクセスすると以下のメッセージが表示される。

Start: /6610e477ddefc14511cc4f261c3c608d.txt


以下のURLへアクセスすると数列が表示される。
https://ctf.ritsec.club/data-streams/6610e477ddefc14511cc4f261c3c608d.txt

[0, '89504e470d0a1a0a0000000d49484452'] f1c0647234d033c43d05ed798f34d1af


表示された値を元に以下URLへアクセスする。
https://ctf.ritsec.club/data-streams/f1c0647234d033c43d05ed798f34d1af.txt

[11628, '419b79d96aac9743a332abc4d3f291d3'] 5728c2dc294629e33a44bb5b745f052c

なんとなく1列目がINDEX値、2列目がPNGバイト列(89 50 4e 47で始まるため)、3列目がファイル名と予想される。2列目のPNGバイト列を抽出して結合すればflagが表示されるはず。

普段はやらないプログラミングに挑戦してみる。試しに3回だけ取得する処理を実行する。

>>> import requests
>>> r = requests.get('https://ctf.ritsec.club/data-streams/6610e477ddefc14511cc4f261c3c608d.txt')
>>> png =''
>>> png = png + r.text
>>>
>>> for i in range(3):
...     r = requests.get('https://ctf.ritsec.club/data-streams/'+r.text[-32:]+'.txt')
...     png = png +'\n' + r.text
...     i = i+1
...
>>> print(png)
[0, '89504e470d0a1a0a0000000d49484452'] f1c0647234d033c43d05ed798f34d1af
[11628, '419b79d96aac9743a332abc4d3f291d3'] 5728c2dc294629e33a44bb5b745f052c
[9765, '9b9ffeecc7afbf716d776f1c7643cb8e'] 922371925a235b713156a23da7e8276d
[3212, '38ab28f59e9028651377b9bb27f65399'] 0d069134327851a1be09b71f0f5d70ed
>>>


成功しているようだ。全てのバイト列を取得する。(大体2時間ぐらいかかる。)

import requests
r = requests.get('https://ctf.ritsec.club/data-streams/6610e477ddefc14511cc4f261c3c608d.txt')
png =''
png = png + r.text

for i in range(14421):
    r = requests.get('https://ctf.ritsec.club/data-streams/'+r.text[-32:]+'.txt')
    png = png +'\n' + r.text
    i = i+1


取得したpngバイト列をファイルに出力する。

>>> f = open('png.txt', 'w', encoding='UTF-8')
>>> f.write(png)
>>> f.close()


取得したファイルからExcelを駆使してPNG文字列を抽出したらcyberchefに投げて終わり。


flag : RS{81ngus5_w3b_53rvic3s}



Forensic

Bad C2

Not very versatile malware

Author : degenerat3


添付ファイルのpcapファイルを確認するとhttpプロトコルにてC2サーバとやり取りしているログが確認できる。
やり取りされているC2サーバのIPアドレスグローバルIPが利用されている。
41パケット目でJSON形式でPOSTリクエストを投げている。


上記のjson形式の設定にてfalseをtrueに変えてHTTPプロトコルでアクセスするとflagが表示される。

実際のC2サーバではhttps通信やC3連携(slackなど)で通信内容を秘匿するのでhttpで通信が漏洩するのがイケていない?

flag : RS{m4gic_word_is_4lw4ys_b31ng_p0lit3}



Cyber Survey

Lookin' like a Fujitsu FI-6800

Author : degenerat3


添付ファイルを確認するとTCPプロトコルの通信が確認できる。

Protocol階層でデータストリームを確認するとDataがあるので選択する。

宛先Portポート番号は全て40000のポート番号になっている。

40082
40083
40123
 ・
 ・



末尾の3桁の番号はなんとなくASCII文字っぽい。

>>> chr(82)
'R'
>>> chr(83)
'S'
>>> chr(123)
'{'


むむむ...

宛先Port番号の末尾3桁をASCII文字にしたものがflagになる。

flag :RS{sc4ns_bett3r_than_4n_hp_d3skjet3755}



感想

・なぜ人は他にやらなければならないことが有るときほど、CTFが捗ってしまうのか。不思議だね。