LA CTF 2024 Writeup

この大会は2024/2/17 13:00(JST)~2024/2/19 7:00(JST)に開催されました。
今回もチームで参戦。結果は5359点で1074チーム中110位でした。
自分で解けた問題をWriteupとして書いておきます。

rules (welcome)

HomeのページのScoringの説明の中にフラグが書いてあった。

lactf{i_read_the_rules}

discord (welcome)

Discordに入り、#generalチャネルのピン止めされたメッセージを見ると、フラグが書いてあった。

lactf{i'm_in_the_discord_server!}

infinite loop (misc)

HTMLソースを見ると、以下の部分が見つかる。

[1736602043,"Flag part 1: lactf{l34k1ng_4h3",null,6,null,null,null,null,null,null,null,[null,"Flag part 1: lactf{l34k1ng_4h3"]]],["Flag part 2: _f04mz_s3cr3tz}",1,0,0,0]
lactf{l34k1ng_4h3_f04mz_s3cr3tz}

mixed signals (misc)

Go Transcribeで文字に起こすと、以下のようになった。

Lima. Alpha. Charlie. Tango. Foxtrot. Open. Brace. Charlie. 
4th November. Underscore. Yankee. Zero. Uniform. 
Underscore. Papa. Lima. Zulu. Underscore. Uniform. 
November. Mike one. X-ray. Underscore. Mike. Yankee. 
Underscore. Sierra one. Golf. November 4th. Lima. Zulu. End. Brace. 

Braceは"{"や"}"、Underscoreはそのまま"_"にして、その他は頭文字を取ったり、数字に置き換えたりする。

lactf{c4n_y0u_plz_unm1x_my_s1gn4lz}

one by one (misc)

HTMLソースのJSON部分を整形してみてみる。

            [
                1904478792,
                "0 - Select a letter, any letter:",
                null,
                3,
                [
                    [
                        561191505,
                        [
                            [
                                "0",
                                null,
                                556692759
                            ],
                            [
                                "1",
                                null,
                                556692759
                            ],
                        :
                        :
                            [
                                "k",
                                null,
                                556692759
                            ],
                            [
                                "l",
                                null,
                                1540876785
                            ],
                            [
                                "m",
                                null,
                                556692759
                        :
                        :

"l"だけ値が異なる。次も見てみる。

            [
                810769981,
                "1 - Select a letter, any letter:",
                null,
                3,
                [
                    [
                        670140814,
                        [
                            [
                                "0",
                                null,
                                1950887260
                            ],
                        :
                        :
                            [
                                "9",
                                null,
                                1950887260
                            ],
                            [
                                "a",
                                null,
                                1973202083
                            ],
                            [
                                "b",
                                null,
                                1950887260
                            ],
                        :
                        :

"a"だけ値が異なる。異なる値の文字を連結すればフラグになりそう。各インデックスで値が異なるので、ここまでと同様に一つ一つ見ていき、異なる値の文字を書き出していく。

lactf{1_by_0n3_by3_un0_*,"g1'}

shattered-memories (rev)

$ strings shattered-memories | grep -A 5 -B 5 lactf
What was the flag again?
No, I definitely remember it being a different length...
t_what_f
t_means}
nd_forge
lactf{no
orgive_a
No, that definitely isn't it.
I'm pretty sure that isn't it.
I don't think that's it...
I think it's something like that but not quite...

文がつながるように結合する。

lactf{not_what_forgive_and_forget_means}

aplet321 (rev)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  int iVar1;
  size_t sVar2;
  char *pcVar3;
  int iVar4;
  int iVar5;
  char local_238;
  char acStack_237 [519];
  
  setbuf(stdout,(char *)0x0);
  puts("hi, i\'m aplet321. how can i help?");
  fgets(&local_238,0x200,stdin);
  sVar2 = strlen(&local_238);
  if (5 < sVar2) {
    iVar4 = 0;
    iVar5 = 0;
    pcVar3 = &local_238;
    do {
      iVar1 = strncmp(pcVar3,"pretty",6);
      iVar5 = iVar5 + (uint)(iVar1 == 0);
      iVar1 = strncmp(pcVar3,"please",6);
      iVar4 = iVar4 + (uint)(iVar1 == 0);
      pcVar3 = pcVar3 + 1;
    } while (pcVar3 != acStack_237 + ((int)sVar2 - 6));
    if (iVar4 != 0) {
      pcVar3 = strstr(&local_238,"flag");
      if (pcVar3 == (char *)0x0) {
        puts("sorry, i didn\'t understand what you mean");
        return 0;
      }
      if ((iVar5 + iVar4 == 0x36) && (iVar5 - iVar4 == -0x18)) {
        puts("ok here\'s your flag");
        system("cat flag.txt");
        return 0;
      }
      puts("sorry, i\'m not allowed to do that");
      return 0;
    }
  }
  puts("so rude");
  return 0;
}

以下の条件を満たす必要がある。

iVar5 + iVar4 == 0x36
iVar5 - iVar4 == -0x18

連立方程式になっているので、解いてみる。

iVar5 * 2 = 30 → iVar5 = 15 → iVar4 = 39

入力文字列に"pretty"が15個、"please"が39個と"flag"が含まれていればよい。

>>> "pretty" * 15 + "please" * 39
'prettyprettyprettyprettyprettyprettyprettyprettyprettyprettyprettyprettyprettyprettyprettypleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleaseplease'
$ nc chall.lac.tf 31321
hi, i'm aplet321. how can i help?
prettyprettyprettyprettyprettyprettyprettyprettyprettyprettyprettyprettyprettyprettyprettypleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleaseflag
ok here's your flag
lactf{next_year_i'll_make_aplet456_hqp3c1a7bip5bmnc}
lactf{next_year_i'll_make_aplet456_hqp3c1a7bip5bmnc}

flaglang (web)

app.jsを確認すると、/switchでは以下のようになっていることがわかる。

app.get('/switch', (req, res) => {
  if (!req.query.to) {
    res.status(400).send('please give something to switch to');
    return;
  }
  if (!countries.has(req.query.to)) {
    res.status(400).send('please give a valid country');
    return;
  }
  const country = countryData[req.query.to];
  if (country.password) {
    if (req.cookies.password === country.password) {
      res.cookie('iso', country.iso, { signed: true });
    }
    else {
      res.status(400).send(`error: not authenticated for ${req.query.to}`);
      return;
    }
  }
  else {
    res.cookie('iso', country.iso, { signed: true });
  }
  res.status(302).redirect('/');
});

クエリーパラメータ"to"で国名を指定し、countries.yamlで該当する国でパスワードを持っている場合、クエリーパラメータ"password"も一致している必要がある。
countries.yamlを見ると、フラグに関係する部分が国として以下の設定がされている。

Flagistan:
  iso: FL
  msg: "<REDACTED>"
  password: "<REDACTED>"
  deny: 
    ["AF","AX","AL","DZ","AS","AD","AO","AI","AQ","AG","AR","AM","AW","AU","AT","AZ","BS","BH","BD","BB","BY","BE","BZ","BJ","BM","BT","BO","BA","BW","BV","BR","IO","BN","BG","BF","BI","KH","CM","CA","CV","KY","CF","TD","CL","CN","CX","CC","CO","KM","CG","CD","CK","CR","CI","HR","CU","CY","CZ","DK","DJ","DM","DO","EC","EG","SV","GQ","ER","EE","ET","FK","FO","FJ","FI","FR","GF","PF","TF","GA","GM","GE","DE","GH","GI","GR","GL","GD","GP","GU","GT","GG","GN","GW","GY","HT","HM","VA","HN","HK","HU","IS","IN","ID","IR","IQ","IE","IM","IL","IT","JM","JP","JE","JO","KZ","KE","KI","KR","KP","KW","KG","LA","LV","LB","LS","LR","LY","LI","LT","LU","MO","MK","MG","MW","MY","MV","ML","MT","MH","MQ","MR","MU","YT","MX","FM","MD","MC","MN","ME","MS","MA","MZ","MM","NA","NR","NP","NL","AN","NC","NZ","NI","NE","NG","NU","NF","MP","NO","OM","PK","PW","PS","PA","PG","PY","PE","PH","PN","PL","PT","PR","QA","RE","RO","RU","RW","BL","SH","KN","LC","MF","PM","VC","WS","SM","ST","SA","SN","RS","SC","SL","SG","SK","SI","SB","SO","ZA","GS","ES","LK","SD","SR","SJ","SZ","SE","CH","SY","TW","TJ","TZ","TH","TL","TG","TK","TO","TT","TN","TR","TM","TC","TV","UG","UA","AE","GB","US","UM","UY","UZ","VU","VE","VN","VG","VI","WF","EH","YE","ZM","ZW"]

もちろんmsgもpasswordもわからない。
app.jsを確認すると、他に/viewがあり、以下のようになっていることがわかる。

app.get('/view', (req, res) => {
  if (!req.query.country) {
    res.status(400).json({ err: 'please give a country' });
    return;
  }
  if (!countries.has(req.query.country)) {
    res.status(400).json({ err: 'please give a valid country' });
    return;
  }
  const country = countryData[req.query.country];
  const userISO = req.signedCookies.iso;
  if (country.deny.includes(userISO)) {
    res.status(400).json({ err: `${req.query.country} has an embargo on your country` });
    return;
  }
  res.status(200).json({ msg: country.msg, iso: country.iso });
});

クエリーパラメータ"country"を指定して該当する。特にパスワードやシグネチャの比較はしていないので、ここでFlagistanのmsgを取得できそう。
https://flaglang.chall.lac.tf/で、「You are from」にJapanを指定すると、クッキー"iso"に以下が設定されていた。

s%3AJP.wQN7%2F%2B4NXf1pZjstKNN7%2B3c97EqMdizyrZVxoiG2kFQ

s%3Aの後ろに国コード(iso)が付いている。
https://flaglang.chall.lac.tf/view?country=Japanにアクセスすると、以下のレスポンスがあった。

{"msg":"こんにちは、世界","iso":"JP"}

https://flaglang.chall.lac.tf/view?country=Flagistanにアクセスすると、以下のレスポンスがあった。

{"err":"Flagistan has an embargo on your country"}

試しにクッキー"iso"の国コード部分の"JP"を"FL"に修正して、再度アクセスしてみると、メッセージにフラグがあった。

{"msg":"lactf{n0rw3g7an_y4m7_f4ns_7n_sh4mbl3s}","iso":"FL"}
lactf{n0rw3g7an_y4m7_f4ns_7n_sh4mbl3s}

la housing portal (web)

SQLインジェクションの問題。検索条件にnameは指定できるが、SQLにこのnameは含まれない。また、keyやvalueに"--"や"/*"を含めることはできない。コメントを使わずにSQLインジェクションすることを考える。
BurpSuiteでInterceptしながら、POSTするパラメータを変更して、検索する。試しにawakeに以下を指定してみる。

' union select 0,1,2,3,4,5 '

URLデコードすると、以下のようになるので、これを指定する。

'%20union%20select%200%2C1%2C2%2C3%2C4%2C5%20'


これでForwardすると、以下のように表示された。

SQLインジェクションできた。flagテーブルのflagカラムにフラグが設定されているので、それを取得するようにする。
awakeに以下を指定する。

' union select 0,1,2,3,4,flag from flag '

URLデコードすると、以下のようになるので、これを指定する。

'%20union%20select%200%2C1%2C2%2C3%2C4%2Cflag%20from%20flag%20'


これでForwardすると、以下のように表示され、フラグを取得できた。

lactf{us3_s4n1t1z3d_1npu7!!!}

very-hot (crypto)

RSA暗号でnの素因数のp, q, rは以下を満たすことがわかっている。

q = p + 6
r = p + 12

nはpの3次方程式になるので、pを算出でき、q, rも算出できる。あとは通常通り復号する。

#!/usr/bin/env python3
from Crypto.Util.number import *
import sympy

with open('out.txt', 'r') as f:
    params = f.read().splitlines()

n = int(params[0].split(' ')[1])
e = int(params[1].split(' ')[1])
ct = int(params[2].split(' ')[1])

p = sympy.Symbol('p')
eq = p * (p + 6) * (p + 12) - n
ps = sympy.solve(eq)
for p in ps:
    if p.is_Integer:
        p = int(p)
        break

q = p + 6
r = p + 12
assert p * q * r == n

phi = (p - 1) * (q - 1) * (r - 1)
d = inverse(e, phi)
m = pow(ct, d, n)
flag = long_to_bytes(m).decode()
print(flag)
lactf{th4t_w45_n0t_so_53xY}

valentines-day (crypto)

Vigenere暗号だが、鍵の長さが161バイトもある。intro.txtにはスペース含めて先頭133バイトの平文がある。まずわかっている範囲で鍵を求める。

NEVERGONNAGIVEYOUUPNEVERGONNALETYOUDOWNNEVERGONNARUNAROUNDANDDESERTYOUNEVERGONNAMAKEYOUCRYNEVERGONNASAYGOO

英文になっているようなのでスペースを入れてわかりやすく整形する。

NEVER GONNA GIVE YOU UP
NEVER GONNA LET YOU DOWN
NEVER GONNA RUN AROUND AND DESERT YOU
NEVER GONNA MAKE YOU CRY
NEVER GONNA SAY GOO

Never gonna give you upの歌詞になっているようなので、その続きを161バイトが判明するまで書き下す。

NEVER GONNA GIVE YOU UP
NEVER GONNA LET YOU DOWN
NEVER GONNA RUN AROUND AND DESERT YOU
NEVER GONNA MAKE YOU CRY
NEVER GONNA SAY GOODBYE             ※ここまで110バイト
NEVER GONNA TELL A LIE AND HURT YOU ※ここまで138バイト
WEVE KNOWN EACH OTHER FOR SO LONG   ※ここまで165バイト

鍵がわかったらそれを元に復号する。

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

with open('intro.txt', 'r') as f:
    intro = f.read()

with open('ct.txt', 'r') as f:
    ct = f.read()

key = ''
for i in range(len(intro)):
    if ct[i] in ascii_uppercase:
        index = ascii_uppercase.index(ct[i]) - ascii_uppercase.index(intro[i])
        key += ascii_uppercase[index]
    elif ct[i] in ascii_lowercase:
        index = ascii_lowercase.index(ct[i]) - ascii_lowercase.index(intro[i])
        key += ascii_uppercase[index]

key += 'DBYE'
key += 'NEVERGONNATELLALIEANDHURTYOU'
key += 'WEVEKNOWNEACHOTHERFORSO'

pt = ''
i_key = 0
for i in range(len(ct)):
    if ct[i] in ascii_uppercase:
        index = ascii_uppercase.index(ct[i]) - ascii_uppercase.index(key[i_key % len(key)])
        pt += ascii_uppercase[index]
        i_key += 1
    elif ct[i] in ascii_lowercase:
        index = ascii_lowercase.index(ct[i]) - ascii_uppercase.index(key[i_key % len(key)])
        pt += ascii_lowercase[index]
        i_key += 1
    else:
        pt += ct[i]
print(pt)

復号結果は以下の通りで、文中にフラグが書いてあった。

On this Valentine's day, I wanted to show my love for professor Paul Eggert. This challenge is dedicated to him. Enjoy the challenge!

I was sitting in the hot, steamy room of Fddgk 1178 wlrcwduxp homgzak bvuh and every exam problem, then searching through my slide print-outs for the closest match and then just writing it down and hoping for the best, when all of a sudden I stumnoxo oaol kqn clucflfa: "axn ting banned in China?"

I remembered the wise words that my TA once told me:

Bing was never banned in China. Naive undergrad. Rent a virtual machine and check.

That's when I realilhw, ebp evrv fmj q dqvk npi vdong. And I had managed to crack the code. I had to rent a lnxsrv and check. Under my breath, I muttered the words export PATH=/usr/local/cs/bin:$PATH, and then the following:

cat ~eggqum/qcyajvgjyrdcihixiv.opt | less
lactf{known_plaintext_and_were_off_to_the_races}

Suddenly, chilling wind brushed over my back.

After one and a half hours, they had finally managed to turn on the AC in fkx wyntsin qmcb.

S odlgmlpkly read through the answer key, slowly copying down the right answers onto my long, thick answer sheet when I hear Eggert scream C-s Student Who Is Cheating Off The Answer Whr th siq ejcumu oydtf xliyue. I was in total shock. Shivering, I looked down at the front of the classroom, and watched as Eggert slowly turned his head until it locked onto me. "I will pkill you," he echoep. L plm qrmqnw mk ci phjx eky uould only watch as he summoned his stand [BIRTHPLACE OF THE INTERNET].

I boelted from my seat, jumping over several unlucky students' desks and sprinting up the stairs and axm zz Qryei rzke dth Tbyoo gf Sciences. But I could sense that Eggert was close on my tail. He summoned a dozen copies of himself with calls to pthread_create(), and they proceeded to chase me as I ran towmuwd nse krcqety lglcqmkb. A hastily grabbed the door to enter mathsci, when one of the Eggert threads shouted "chmod a-x MATHSCI DOOR." I tried opening the door, but it shouted at me: "Permission denied." I edbo "lx -f bfxa" met bmq zaxl oze mathsci building.

I ran desperately from the Eggert clones, but soon found myself out of breath, because I'm a CS major and I sit at my desk all day. Suddenly, I saw one of Eggedw'l nfzncj adz uegz wyr lxgdway. I quickly opened the door next to me and stepped inside, breathing a sigh of relief. But then I turned myself around and screamed.

"Welcome to my office." Eggert echoed. "Yog'yx xuoe gk bx rrh. S ode giig lhat you have a good grasp of the fundamentals of commonly-used software tools and environments, particularly open-source tools likely to be used in upper-division combxmpl dcgvwlq teedvvf."

M tcampered and backed into the corner of his office.

"You may be wondering how you got here. I created a symbolic link between mathsci 6229 and my office here in Engineering VI. I wanf bhf nz klff ctrj S ire'g lrml you. I was merely testing you."

I gulped and looked into his eyes. "Really?"

"Yes..." Eggert replied, "In fact, I brought you here into my office to ask you one very important question."

"Wtdm?" T meurknaqu.

Uqshig pbvfed back in his chair, smiling. "You see, for the past several years I have been building up a team of highly capable individuals, ones that can anticipate foreign cybersecudlmj nsrcrcb riew borpip nmch as USC and stop them. I use the class CS35L in order to sort through all of UCLA's undergraduate CS students, and find the ones with the potential to join this league of cybed kxcips."

"Ryjc'e... Kxkf'v zztlnkible..." I said.

"You probably haven't realized it yet," Eggert explained, "but while running away from me you were able to utilize a multitude of linux commands in order to escapq pr hlltf. Ux hal kxphifxxiv what that means?"

"Uh...." I said, realizing it was quite strange I was able to verbally incantate gnu coreutils, "y-yeah, I guess...?"

Eggert gazed so deeply into my soul I felt as if he hap uny nse afvvmet das fa qv dfternal organs. "Join me, and together we will curve down the world."

source: https://www.reddit.com/r/ucla/comments/b2d4zz/my_experience_with_the_cs35l_final/
lactf{known_plaintext_and_were_off_to_the_races}

selamat pagi (crypto)

フラグの形式からわかる範囲で復号する。

*** a*ala* ***a* *a** a*a* ****a* *******a
T**a* a*a *a** ta** a*a *a** *a*a *ata*a* :*
******a *a** a*a ** ****: lactf{**la*at_*a**_a*a*a*_*a**_***a_a*al****_f********}

インドネシア語の英単語から予測しながら、対応付けする。

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

with open('message.txt', 'r') as f:
    ct = f.read()

C = 'abcdefghijklmnopqrstuvwxyz'
P = 'ylpmin**tuabf***d*grkcsh*e'

pt = ''
for c in ct:
    if c in ascii_uppercase:
        index = C.index(c.lower())
        pt += P[index].upper()
    elif c in ascii_lowercase:
        index = C.index(c)
        pt += P[index]
    else:
        pt += c

print(pt)
Ini adalah pesan yang aman dengan sempurna
Tidak ada yang tahu apa yang saya katakan :D
Bendera kamu ada di sini: lactf{selamat_pagi_apakah_kamu_suka_analisis_frekuensi}
lactf{selamat_pagi_apakah_kamu_suka_analisis_frekuensi}