Skip to content

Commit e5a397e

Browse files
authored
Merge pull request #168 from VladSteopoaie/main
feature: Added crypto writeups for CA CTF 2025
2 parents afad53c + e35d9ff commit e5a397e

7 files changed

Lines changed: 1301 additions & 1 deletion

File tree

config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ params:
108108
- name: h3pha
109109
link: https://github.com/VladSteopoaie/
110110
picture: https://avatars.githubusercontent.com/u/69504268?s=400&u=0bb78ff707338ad58e677d856fbe92766e116010&v=4
111-
tags: [ 'pwn', 'web', 'rev', 'misc', 'network' ]
111+
tags: [ 'pwn', 'web', 'rev', 'misc', 'network', 'crypto' ]
112112
description:
113113
- Don't hate me, but... Try harder!
114114
excludedSections:
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
---
2+
title: Hourcle
3+
date: 2025-03-27T18:33:57+03:00
4+
description: Writeup for Hourcle [Cyber Apocalypse CTF 2025]
5+
author: h3pha
6+
tags:
7+
- crypto
8+
draft: false
9+
---
10+
___
11+
12+
## Challenge Description
13+
14+
> A powerful enchantment meant to obscure has been carelessly repurposed, revealing more than it conceals. A fool sought security, yet created an opening for those who dare to peer beyond the illusion. Can you exploit the very spell meant to guard its secrets and twist it to your will?
15+
16+
## Intuition
17+
18+
We are given this file (`server.py`):
19+
```python
20+
from Crypto.Cipher import AES
21+
from Crypto.Util.Padding import pad
22+
import os, string, random, re
23+
24+
KEY = os.urandom(32)
25+
26+
password = ''.join([random.choice(string.ascii_letters+string.digits) for _ in range(20)])
27+
28+
def encrypt_creds(user):
29+
padded = pad((user + password).encode(), 16)
30+
IV = os.urandom(16)
31+
cipher = AES.new(KEY, AES.MODE_CBC, iv=IV)
32+
ciphertext = cipher.decrypt(padded)
33+
return ciphertext
34+
35+
def admin_login(pwd):
36+
return pwd == password
37+
38+
39+
def show_menu():
40+
return input('''
41+
=========================================
42+
|| ||
43+
|| 🏰 Eldoria's Shadow Keep 🏰 ||
44+
|| ||
45+
|| [1] Seal Your Name in the Archives ||
46+
|| [2] Enter the Forbidden Sanctum ||
47+
|| [3] Depart from the Realm ||
48+
|| ||
49+
=========================================
50+
51+
Choose your path, traveler :: ''')
52+
53+
def main():
54+
while True:
55+
ch = show_menu()
56+
print()
57+
if ch == '1':
58+
username = input('[+] Speak thy name, so it may be sealed in the archives :: ')
59+
pattern = re.compile(r"^\w{16,}$")
60+
if not pattern.match(username):
61+
print('[-] The ancient scribes only accept proper names-no forbidden symbols allowed.')
62+
continue
63+
encrypted_creds = encrypt_creds(username)
64+
print(f'[+] Thy credentials have been sealed in the encrypted scrolls: {encrypted_creds.hex()}')
65+
elif ch == '2':
66+
pwd = input('[+] Whisper the sacred incantation to enter the Forbidden Sanctum :: ')
67+
if admin_login(pwd):
68+
print(f"[+] The gates open before you, Keeper of Secrets! {open('flag.txt').read()}")
69+
exit()
70+
else:
71+
print('[-] You salt not pass!')
72+
elif ch == '3':
73+
print('[+] Thou turnest away from the shadows and fade into the mist...')
74+
exit()
75+
else:
76+
print('[-] The oracle does not understand thy words.')
77+
78+
if __name__ == '__main__':
79+
main()
80+
```
81+
82+
To get the flag we need the secret password that the server generates. We can provide a username and the server will append the password to it, encrypt it and return the ciphertext.
83+
84+
The first thing that I observed is that the server uses `AES` in `CBC` mode, but instead of encrypting the text, it decrypts it and returns the result.
85+
86+
I have solved a similar CTF challenge in which I explained how to attack a system like this [here](https://dothidden.xyz/kalmarctf_2025/very_serious_cryptography/). Actually I used the same script to solve this challenge, I just adapted it for this situation.
87+
88+
## Solution
89+
90+
> Note: the script takes a while to run on a remote target.
91+
92+
Solver:
93+
```python
94+
from pwn import *
95+
96+
charset = string.ascii_letters+string.digits
97+
def send_input_list(p, input_list):
98+
output_list = []
99+
for i in input_list:
100+
p.readuntil(b":: ")
101+
p.sendline(b"1")
102+
p.readuntil(b":: ")
103+
p.sendline(i.encode())
104+
p.readuntil(b"lls: ")
105+
output = bytes.fromhex(p.recvline()[:-1].decode())
106+
output_list.append(output)
107+
return output_list
108+
109+
password = ""
110+
input_size = 16 + 16 + 15
111+
# 16 is the minimum allowed, and we add 15 to make sure that the first character of the password
112+
# is at the end of the block, I also added one more block because the length of the passowrd is 20
113+
114+
# p = process(["python", "server.py"])
115+
p = remote("94.237.55.91", 38990)
116+
117+
while len(password) != 20:
118+
try:
119+
# ensure that the character we are searching is at the end of the block
120+
padding = "_" * (input_size - len(password))
121+
# this is where the original flag is encrypted
122+
original = send_input_list(p, [padding])[0]
123+
# create all possible variants of the characters withing the flag
124+
brute_input = [padding + password + c for c in charset]
125+
# send the variants, and receive all encryptions
126+
brute_output = send_input_list(p, brute_input)
127+
# this is the position of the end of the block
128+
character_position = len(padding + password) + 1
129+
for i in range(len(brute_output)):
130+
if brute_output[i][32:character_position] == original[32:character_position]:
131+
password += charset[i]
132+
print(password)
133+
134+
except EOFError:
135+
p.close()
136+
p = remote("94.237.55.91", 38990)
137+
# p = process(["python", "server.py"])
138+
password = ""
139+
140+
p.sendline(b"2")
141+
p.sendline(password)
142+
p.interactive()
143+
```
144+
145+
146+
### Flag
147+
148+
`HTB{encrypting_with_CBC_decryption_is_as_insecure_as_ECB___they_also_both_fail_the_penguin_test_6f5aea60aea8dee076ad6ff61d768d05}`
149+

0 commit comments

Comments
 (0)