Traces
14 minutes to read
We are given the Python source code of a server:
from db import *
from Crypto.Util import Counter
from Crypto.Cipher import AES
import os
from time import sleep
from datetime import datetime
def err(msg):
print('\033[91m'+msg+'\033[0m')
def bold(msg):
print('\033[1m'+msg+'\033[0m')
def ok(msg):
print('\033[94m'+msg+'\033[0m')
def warn(msg):
print('\033[93m'+msg+'\033[0m')
def menu():
print()
bold('*'*99)
bold(f"* 🏰 Welcome to EldoriaNet v0.1! 🏰 *")
bold(f"* A mystical gateway built upon the foundations of the original IRC protocol 📜 *")
bold(f"* Every message is sealed with arcane wards and protected by powerful encryption 🔐 *")
bold('*'*99)
print()
class MiniIRCServer:
def __init__(self, host, port):
self.host = host
self.port = port
self.key = os.urandom(32)
def display_help(self):
print()
print('AVAILABLE COMMANDS:\n')
bold('- HELP')
print('\tDisplay this help menu.')
bold('- JOIN #<channel> <key>')
print('\tConnect to channel #<channel> with the optional key <key>.')
bold('- LIST')
print('\tDisplay a list of all the channels in this server.')
bold('- NAMES #<channel>')
print('\tDisplay a list of all the members of the channel #<channel>.')
bold('- QUIT')
print('\tDisconnect from the current server.')
def output_message(self, msg):
enc_body = self.encrypt(msg.encode()).hex()
print(enc_body, flush=True)
sleep(0.001)
def encrypt(self, msg):
encrypted_message = AES.new(self.key, AES.MODE_CTR, counter=Counter.new(128)).encrypt(msg)
return encrypted_message
def decrypt(self, ct):
return self.encrypt(ct)
def list_channels(self):
bold(f'\n{"*"*10} LIST OF AVAILABLE CHANNELS {"*"*10}\n')
for i, channel in enumerate(CHANNELS.keys()):
ok(f'{i+1}. #{channel}')
bold('\n'+'*'*48)
def list_channel_members(self, args):
channel = args[1] if len(args) == 2 else None
if channel not in CHANNEL_NAMES:
err(f':{self.host} 403 guest {channel} :No such channel')
return
is_private = CHANNELS[channel[1:]]['requires_key']
if is_private:
err(f':{self.host} 401 guest {channel} :Unauthorized! This is a private channel.')
return
bold(f'\n{"*"*10} LIST OF MEMBERS IN {channel} {"*"*10}\n')
members = CHANNEL_NAMES[channel]
for i, nickname in enumerate(members):
print(f'{i+1}. {nickname}')
bold('\n'+'*'*48)
def join_channel(self, args):
channel = args[1] if len(args) > 1 else None
if channel not in CHANNEL_NAMES:
err(f':{self.host} 403 guest {channel} :No such channel')
return
key = args[2] if len(args) > 2 else None
channel = channel[1:]
requires_key = CHANNELS[channel]['requires_key']
channel_key = CHANNELS[channel]['key']
if (not key and requires_key) or (channel_key and key != channel_key):
err(f':{self.host} 475 guest {channel} :Cannot join channel (+k) - bad key')
return
for message in MESSAGES[channel]:
timestamp = message['timestamp']
sender = message['sender']
print(f'{timestamp} <{sender}> : ', end='')
self.output_message(message['body'])
while True:
warn('You must set your channel nickname in your first message at any channel. Format: "!nick <nickname>"')
inp = input('guest > ').split()
if inp[0] == '!nick' and inp[1]:
break
channel_nickname = inp[1]
while True:
timestamp = datetime.now().strftime('%H:%M')
msg = input(f'{timestamp} <{channel_nickname}> : ')
if msg == '!leave':
break
def process_input(self, inp):
args = inp.split()
cmd = args[0].upper() if args else None
if cmd == 'JOIN':
self.join_channel(args)
elif cmd == 'LIST':
self.list_channels()
elif cmd == 'NAMES':
self.list_channel_members(args)
elif cmd == 'HELP':
self.display_help()
elif cmd == 'QUIT':
ok('[!] Thanks for using MiniIRC.')
return True
else:
err('[-] Unknown command.')
server = MiniIRCServer('irc.hackthebox.eu', 31337)
exit_ = False
while not exit_:
menu()
inp = input('> ')
exit_ = server.process_input(inp)
if exit_:
break
Source code analysis
Basically, the server implements a kind of IRC server to allow users to send and receive messages in different channels. Each of the channels has a key to encrypt every message that is sent within the channel:
def __init__(self, host, port):
self.host = host
self.port = port
self.key = os.urandom(32)
The encryption method is AES in CTR mode using the previous key:
def encrypt(self, msg):
encrypted_message = AES.new(self.key, AES.MODE_CTR, counter=Counter.new(128)).encrypt(msg)
return encrypted_message
def decrypt(self, ct):
return self.encrypt(ct)
Notice that the counter is generated on every function call, which will cause a nonce-reuse situation.
The purpose of using encryption is to hide previous messages on a channel. So, if we join a channel right now, the history of messages will be encrypted:
def output_message(self, msg):
enc_body = self.encrypt(msg.encode()).hex()
print(enc_body, flush=True)
sleep(0.001)
However, we can still gather some information about usernames and timestamps:
for message in MESSAGES[channel]:
timestamp = message['timestamp']
sender = message['sender']
print(f'{timestamp} <{sender}> : ', end='')
self.output_message(message['body'])
For instance, we must send special commands to join a channel or to leave a channel:
while True:
warn('You must set your channel nickname in your first message at any channel. Format: "!nick <nickname>"')
inp = input('guest > ').split()
if inp[0] == '!nick' and inp[1]:
break
channel_nickname = inp[1]
while True:
timestamp = datetime.now().strftime('%H:%M')
msg = input(f'{timestamp} <{channel_nickname}> : ')
if msg == '!leave':
break
Solution
So, let’s connect to this IRC server and see what we have here:
$ nc 83.136.252.13 57111
***************************************************************************************************
* 🏰 Welcome to EldoriaNet v0.1! 🏰 *
* A mystical gateway built upon the foundations of the original IRC protocol 📜 *
* Every message is sealed with arcane wards and protected by powerful encryption 🔐 *
***************************************************************************************************
> HELP
AVAILABLE COMMANDS:
- HELP
Display this help menu.
- JOIN #<channel> <key>
Connect to channel #<channel> with the optional key <key>.
- LIST
Display a list of all the channels in this server.
- NAMES #<channel>
Display a list of all the members of the channel #<channel>.
- QUIT
Disconnect from the current server.
***************************************************************************************************
* 🏰 Welcome to EldoriaNet v0.1! 🏰 *
* A mystical gateway built upon the foundations of the original IRC protocol 📜 *
* Every message is sealed with arcane wards and protected by powerful encryption 🔐 *
***************************************************************************************************
> LIST
********** LIST OF AVAILABLE CHANNELS **********
1. #general
2. #secret
************************************************
Nice! We have two channels: #general
and #secret
. However, #secret
requires an access key:
> JOIN #secret
:irc.hackthebox.eu 475 guest secret :Cannot join channel (+k) - bad key
So, we must start with #general
:
> JOIN #general
[23:30] <Doomfang> : 6a61814696a35f034eecb2ca1095
[23:32] <Stormbane> : 6a61814696a348184ef3b9c91f9c2c
[23:34] <Runeblight> : 6a61814696a349194fe4b6c71795210e
[00:00] <Stormbane> : 1c6acf5398a37c0355a1b58b10973e5a0f445d559c30fb8e5fc20239f8a84bb168c5feb8a951527ce5723e3d94bcc72d85ed27ad4e4976f5d359243774c411f6209f
[00:01] <Doomfang> : 1e618c408ff06f034ee5fa8b36933a5a0f4548079872f69e4e8c5630f3f119a763c7fcebe8173e6df930203d8dbcc52fcaf722b6004d3ee7ce1737203ddd05f065dd943f926fed7f586238
[00:02] <Runeblight> : 05609c0584e66f4001e3a1df5ebb6e175b4e45109e39fd954cc2053ef0ed19a164d5e1bee61d3e6af832237c93aa8c7dece472ab064f67b4d41c3a36789205f169918229c622e977467327c3edf0838c4f260bae02e9a044d9733ac5a1ea49221ac32e716154
[00:04] <Doomfang> : 1f678156dde0730d4fefb1c75e9b3a5a154259558e33f29e0b841923bde456ba6d80e6aaeb1a6d37b1192869d8aa822ed2eb26bc060a6afb871621373dc202eb33d08129c63def7f4329
[00:04] <Doomfang> : 036a9a40ddea684c55e9b18b0e933a090b455f148e37b49d4490563ee8fa19a76fc3e7b9e2517d71f03b237893e38278c8eb60b8186256d7f14c321a76d112bf1f858319aa3eef49642168f9
[00:05] <Runeblight> : 0c609c0594f7354c6eefb8d25e81211b09480d1c8972e3925f8a563ee8fa19b965d3e6ebf3036b6ae530293d9eb5ce34c0f17c
[00:06] <Stormbane> : 126a9b0bddcc6e1e01edb5d80ad224150d480d189c2bb4934a941371f1ed5fa02ad4e0aae4146d37b102283d92acd12985e037ff184f6ced871a353778d405ee6b
[00:07] <Doomfang> : 022885059eeb7e0f4ae8bacc5e9d3c085b4142128e72e0940b801371eefd4bb12acefdebf3037f7af475227bdfb6d72f85e331ab074570e7870b31287cdb1ef16b
[00:08] <Runeblight> : 006a8d55ddee7e4c54f1b0ca0a972d545b644b55893af1820b811725fee019bb648cb2bce2567275b13d2c6b9af9d63285e331ab4e4c7fe7d357
[00:09] <Stormbane> : 02288449dde0740151e0a6ce5e86211f5b414c019821e0db4f830230bdff50a06280fdbef5517c78f23e386ddfa9ce3ccbac72880b0a73e1d40d74206fd303e765d09b35c63ce977402770ceacf183de5e6943a818ede0
[00:10] <Doomfang> : 0269c8408be6691555e9bdc519d220095b4e41109c20b8db5c87563cf2fe5cf47ecfb2bfef143e77f42d393d8cadc33ac0ac72901b583ef3c818386574c150f52cc59d25886ff2754f646f85
[00:11] <Runeblight> : 03608441ddec754201c8f3c65e812c1f12434a558e26e69a45851371eee15eba6bcce1ebe1037174b13a38698cb0c6388ba205ba4e4777f3cf0d7427789207e331d29d298261
[00:12] <Stormbane> : 1c6ac8469ced3c1801f5b5c01bd22814020d5f1c8e39e7d50bae1325bafb19b86fc1e4aea7057670e2752e759eb7cc38c9a230ba08456cf1870d3c20649204f024d29e6c933cae
[00:13] <Doomfang> : 0a689a4098e7354c6ceea2ce5e9325165b594c199621b48f44c20239f8a849a663d6f3bfe2516c76fe38633dadaccc38c7ee3bb8065e32b4d71531246ed750e129d4943ec63be8750e6b68ccffa68ec9492c05
[00:14] <Runeblight> : 1e618c408ff06f034ee5fa8b37d5245a1f445e16923cfa9e48961f3ffaa857bb7d8eb282e1516a71f42c6d759eafc77dd6e737b14e5f6db8870e316570c703f665d59c3f873ff0754f7527c2e1eb83c852285fa806f1e0
[00:53] <Doomfang> : 6a638d448be6
[00:53] <Stormbane> : 6a638d448be6
[00:53] <Runeblight> : 6a638d448be6
You must set your channel nickname in your first message at any channel. Format: "!nick <nickname>"
Alright, we have a lot of encrypted messages, but some of them look similar…
[23:30] <Doomfang> : 6a61814696a35f034eecb2ca1095
[23:32] <Stormbane> : 6a61814696a348184ef3b9c91f9c2c
[23:34] <Runeblight> : 6a61814696a349194fe4b6c71795210e
...
[00:53] <Doomfang> : 6a638d448be6
[00:53] <Stormbane> : 6a638d448be6
[00:53] <Runeblight> : 6a638d448be6
These messages correspond to the first and last messages. Therefore, they must correspond to !nick <username>
and !leave
.
Stream cipher
Remember that the server is using AES in CTR mode and using the same counter on each encrypted message. As a result, the encryption method becomes a XOR cipher with a repeated key stream.
Just for a reference, AES in CTR mode works as a stream cipher, where the counter is encrypted using AES blocks, but the actual encryption happens when xoring this result (key stream) with the plaintext:
Stream ciphers are vulnerable to known-plaintext attacks, so if we know part of the plaintext, we can use XOR to find part of the key stream. The problem with this happens when the key stream is reused, which is exactly the scenario presented in this challenge.
For instance, we know that the third message corresponds to !nick Runeblight
, so we can obtain part of the key stream:
$ python3 -q
>>> from pwn import unhex, xor
>>>
>>> key_stream = xor(unhex('6a61814696a349194fe4b6c71795210e'), b'!nick Runeblight')
>>> key_stream
b'K\x0f\xe8%\xfd\x83\x1bl!\x81\xd4\xab~\xf2Iz'
With this key stream, we can now decrypt other messages:
>>> xor(key_stream, unhex('6a61814696a35f034eecb2ca1095'), cut='min')
b'!nick Doomfang'
>>> xor(key_stream, unhex('6a61814696a348184ef3b9c91f9c2c'), cut='min')
b'!nick Stormbane'
>>> xor(key_stream, unhex('6a638d448be6'), cut='min')
b'!leave'
So, we have confirmed that known-plaintext attack works and also that the messages corresponds to !nick
and !leave
commands.
Now, we can decrypt messages partially, only up to the key stream length. For example:
>>> xor(keystream, unhex('1c6acf5398a37c0355a1b58b10973e5a0f445d559c30fb8e5fc20239f8a84bb168c5feb8a951527ce5723e3d94bcc72d85ed27ad4e4976f5d359243774c411f6209f'), cut='min')
b"We've got a new "
>>> xor(keystream, unhex('1e618c408ff06f034ee5fa8b36933a5a0f4548079872f69e4e8c5630f3f119a763c7fcebe8173e6df930203d8dbcc52fcaf722b6004d3ee7ce1737203ddd05f065dd943f926fed7f586238'), cut='min')
b'Understood. Has '
>>> xor(keystream, unhex('05609c0584e66f4001e3a1df5ebb6e175b4e45109e39fd954cc2053ef0ed19a164d5e1bee61d3e6af832237c93aa8c7dece472ab064f67b4d41c3a36789205f169918229c622e977467327c3edf0838c4f260bae02e9a044d9733ac5a1e\
a49221ac32e716154'), cut='min')
b"Not yet, but I'm"
>>> xor(keystream, unhex('1f678156dde0730d4fefb1c75e9b3a5a154259558e33f29e0b841923bde456ba6d80e6aaeb1a6d37b1192869d8aa822ed2eb26bc060a6afb871621373dc202eb33d08129c63def7f4329'), cut='min')
b'This channel is '
Crib dragging
So, we are left with trying to guess some characters and words in order to find the complete key stream. This technique is called crib dragging. For this purpose, We can use the following Python script:
#!/usr/bin/env python3
from pwn import sys, unhex, xor
filename = sys.argv[1]
with open(filename) as f:
encs = [unhex(line.split('> : ')[1]) for line in f if '> : ' in line]
key = unhex(sys.argv[2])
if len(sys.argv) >= 5:
index = int(sys.argv[3])
known = sys.argv[4].encode()
if len(sys.argv) == 6 and sys.argv[5] == 'back':
key_stream = key_stream[:-len(known)]
else:
key_stream += xor(encs[index][len(key_stream):], known, cut='min')
print(key_stream.hex())
for i, enc in enumerate(encs):
print(i, xor(key_stream, enc, cut='min'), file=sys.stderr)
It is coded so that decrypted messages are printed to stderr
and the new key stream is printed to stdout
. This way, we can use environment variables to update the value of the key stream. Then, we must tell the guessed plaintext and the message index.
For instance, we start with an empty key and know that index 2
is !nick Runeblight
:
$ KEY_STREAM=''
$ KEY_STREAM=$(python3 solve.py general.txt "$KEY_STREAM" 2 '!nick Runeblight')
0 b'!nick Doomfang'
1 b'!nick Stormbane'
2 b'!nick Runeblight'
3 b"We've got a new "
4 b'Understood. Has '
5 b"Not yet, but I'm"
6 b'This channel is '
7 b'Here is the pass'
8 b'Got it. Only sha'
9 b'Yes. Our last mo'
10 b"I'm checking our"
11 b'Keep me updated.'
12 b"I'll compare the"
13 b'If everything is'
14 b"Hold on. I'm see"
15 b"We can't take an"
16 b'Agreed. Move all'
17 b"Understood. I'm "
18 b'!leave'
19 b'!leave'
20 b'!leave'
$ echo $KEY_STREAM
a57bec5c7283b9c7c3f090c117bd981e
Now we can start guessing. For example, message 14
might continue with 'ing '
:
$ KEY_STREAM=$(python3 solve.py general.txt "$KEY_STREAM" 14 'ing ')
0 b'!nick Doomfang'
1 b'!nick Stormbane'
2 b'!nick Runeblight'
3 b"We've got a new tip "
4 b'Understood. Has ther'
5 b"Not yet, but I'm che"
6 b'This channel is not '
7 b'Here is the passphra'
8 b'Got it. Only share i'
9 b'Yes. Our last move m'
10 b"I'm checking our log"
11 b'Keep me updated. If '
12 b"I'll compare the lat"
13 b'If everything is cle'
14 b"Hold on. I'm seeing "
15 b"We can't take any ri"
16 b'Agreed. Move all tal'
17 b"Understood. I'm disc"
18 b'!leave'
19 b'!leave'
20 b'!leave'
In case we get a mistake when guessing, we can go back adding another parameter back
:
$ KEY_STREAM=$(python3 solve.py general.txt "$KEY_STREAM" 14 'ing ')
0 b'!nick Doomfang'
1 b'!nick Stormbane'
2 b'!nick Runeblight'
3 b"We've got a new tip i|'i5"
4 b'Understood. Has therm>*y$'
5 b"Not yet, but I'm cheku!r&"
6 b'This channel is not {\x7f.ya'
7 b'Here is the passphra{{hz.'
8 b'Got it. Only share i|>?u5'
9 b'Yes. Our last move might '
10 b"I'm checking our log{><sa"
11 b'Keep me updated. If |v-ea'
12 b"I'll compare the latmm<<%"
13 b'If everything is cleild<6'
14 b"Hold on. I'm seeing {j:}/"
15 b"We can't take any ri{u;2a"
16 b'Agreed. Move all talcmhh.'
17 b'Understood. I\'m discgp&y"'
18 b'!leave'
19 b'!leave'
20 b'!leave'
$ KEY_STREAM=$(python3 solve.py general.txt "$KEY_STREAM" 14 'ing ' back)
0 b'!nick Doomfang'
1 b'!nick Stormbane'
2 b'!nick Runeblight'
3 b"We've got a new tip "
4 b'Understood. Has ther'
5 b"Not yet, but I'm che"
6 b'This channel is not '
7 b'Here is the passphra'
8 b'Got it. Only share i'
9 b'Yes. Our last move m'
10 b"I'm checking our log"
11 b'Keep me updated. If '
12 b"I'll compare the lat"
13 b'If everything is cle'
14 b"Hold on. I'm seeing "
15 b"We can't take any ri"
16 b'Agreed. Move all tal'
17 b"Understood. I'm disc"
18 b'!leave'
19 b'!leave'
20 b'!leave'
We can notice a guess is wrong whenever we see weird characters and words that make no sense.
So… after a lot of guessing and trial and error, we end up with these messages:
!nick Doomfang
!nick Stormbane
!nick Runeblight
We've got a new tip about the rebels. Let's keep our chat private.
Understood. Has there been any sign of them regrouping since our last move?
Not yet, but I'm checking some unusual signals. If they sense us, we might have to change[...]
This channel is not safe for long talks. Let's switch to our private room.
Here is the passphrase for our secure channel: %mi2gvHHCV5f_kcb=Z4vULqoYJ&oR
Got it. Only share it with our most trusted allies.
Yes. Our last move may have left traces. We must be very careful.
I'm checking our logs to be sure no trace of our actions remains.
Keep me updated. If they catch on, we'll have to act fast.
I'll compare the latest data with our backup plan. We must erase any sign we were here.
If everything is clear, we move to the next stage. Our goal is within reach.
Hold on. I'm seeing strange signals from outside. We might be watched.
We can't take any risks. Let's leave this channel before they track us.
Agreed. Move all talks to the private room. Runeblight, please clear the logs here.
Understood. I'm disconnecting now. If they have seen us, we must disappear immediately.
!leave
!leave
!leave
With this, we have the access key to #secret
:
> JOIN #secret %mi2gvHHCV5f_kcb=Z4vULqoYJ&oR
[00:10] <Doomfang> : 380667b22fb275f2f01eb373979d
[00:12] <Stormbane> : 380667b22fb262e9f001b870989476
[00:14] <Runeblight> : 380667b22fb263e8f116b77e909d7b6c
[00:26] <Runeblight> : 4e0d2ea22cfd44f1fb53be779c8a3377c53dbba095a4e7e299c892bdc631ea306a8f743a34f2c6dc64fd45b8453c16ea618139ac0b129969baa511efdb3eff2766c9f8581040e548051ef67aa04873cc6ed5f10a7fe5ca2b110c8286539dfd64d753456d0b84b54e1aec24ea
[01:01] <Doomfang> : 580f7cb421f61fbdcb1bb0329c947675c968e8f08aa6e6f984d5d5fadc3bef7529c0523771a2ccdb63f144ec48330ea832e83efe1a5a927feee603fedb23ad273c8cf7161540e64f034deb7ebc113ccf37dff75828e0db3b104b989d1a9de67cd74445790a89b70012ef21a1d01731b2cbf3d5e0c0dd03ead86bebbca9fb04cc11944a0f7b967f23dde5bb00eca5b98a4f37b468dedc7f15710453ee44f82e7ec1c928ccc3bfe64beaf8c4d259c7983c72d26f
[01:02] <Runeblight> : 504f78b464f054f8f153a6668c9e6a71de28bba491a0a9f882c796f8dd74f43022db003034bac0c774b855e10d320ff432d12abb185b9873bda50be4db2ae3362b9df0591a13bd070b50ff3bbd5e3ecc63d8eb446fa4d82d1c4085ce41cffd7ad51345411697fb4e1af420ab821d76bcc4bdd5e485c41cb7d854efefe4fd12d645da4a0e2fd76026d5fcfa18f7e0a4974f25bf2dd9c134146c534ebe4ff83c3ddac825d7dfbba554a2f6c58017db823c22ce2759fc74ad877d2458af387ef3624036a0acc89efa3c444a1cba7bac51f40f4dd85c77248a0fcd26f5084c36c9b89fe2eb5ebe1bf613f246d0e5a3fc26b55ae21ce52b9480a2d3e9d0aa92914db87fdf60ae7046ed3c43168769b2d6b0283110610d771a2bed04a9334001068e76f0c37b14ea0c460d0f74cbf849499c93bb5ce97b171f48eaf54fd9c13c69f98f085c535c29956604a4093c2b0dd96e5d96294b086f78e1c58e87
[01:03] <Stormbane> : 504f63f125fe43f8fe17ac329a887c6bc362f8b89ca6e2e59ec1d5f2db26b82634ca4c3e26bddbc230f950f9443309f232d530bb4e539965a7e00cfe9839e821259bfd455a40d8414a4af372bd1131cc76d3ed4428f3df3b595c979c429dfd72925c0b2e0c89bf450da032aa931e37bdd6f0c3fa9484508ddf50e2bca2e719dc118a571434913d6ff0e7ef54eda6f0911b64b87e8cce3c0e6a5245be43f22a7295d260dddffef245eaffd0d61c948d7972d32e1be562e0c8281944e02978fc6b4038e29d8096a97f484b0ab37ee056f50f4fde5b6b288a018f61ba0d0d2dd97a7c19f754fa48fb5af24885ffeae82eb15ab14be9388980bcd0fc93ad93914eb27fc370bf2246f7794f0cc26df9d68c212800261c6c41
[01:04] <Doomfang> : 4e0d2eb225fc5ff2eb53b4749f95617c9027fea390b1e8f899c99bb38e1dfe7530c7492171bbda8971b855ea483c19ee3e812cb60b5cd772a6e042c2d12ce5620986ec581709fd00191efd74bc5236da37dde35328e5d23a1c4d929716dff734dd5345611697fb540de13ea8de5613a5c7f386e088cd50b7955de2f0a1fd03985c93560f3a9c766fd1fdee18e0e0b4970029f162d9dd7f1f6d5049ec48bd3e3fd8d669d1d6b0ab009df291cd0cc7987961ce2f1fe075e0c6692410af2474ec274d7ea097d585a97d554608b87aac57f44946d95b7a3ec4008070ff5a0e3ad8f6dceef15faa49fb1bbb5495e9e4
[01:05] <Runeblight> : 5c106fb230fe48b3bf32bb76d99f657dde6ff2b6d9b2ecac82c398fcc73ab8202adc45373ff2cfc662b859f75a715af1778136bb0b56d765a1eb16e3d62ce82c2990b9461801ff54441ed27dee453bcc37f3ed5f66e7d724594a999c42d4f47dd74e45671796fb4d1ee73ea7911a76b1c3efd4fd85da03e8d84bebbca7e102d455da49142892332ed1f1fe07f7e0a4974f30b968c5dd7f0977564ff04af53232d1d52698f5b1a557afb7d9c10fd1cc3822d2241ae669e9877a2910ad2f76e7740477e6d8c599fd6e5e0500b03ff85bf40f53c541722c9611c161fb0e097fd4ebdcfefb53b65ef049
[01:06] <Stormbane> : 400d7dfd64f044e9bf04b032948f606c903be9b598b1a9e584869af3c22db834378f41723db3dadd30ea52eb422f0ea832e83efe1957d767adf10bfcd93fe862239db9421b0fb1540551f537ee46368965d9f14128f6db3e1c4d9a8758dab27dc64e45620c86ba5416ef39ead03f22f3cbee86f881ca15a89d58aefdb7b457f065b85e38299e7110f6e0fa13e3a9be9f3001a97dc0c0360e625049f143c20a37c1ce57f3d4a7da6ea5f9d2c526e6892c71c46004
[01:07] <Doomfang> : 5e0761b56ab27ff2bf01b07196887738df29bbb98de5e4f983d2d5f8d63deb2164c64e7225bacc8967ea5eec593814a666ce35bb1d1cd74feef20be6d46be82c399ceb535401fd4b4a4ae97aad54208976c2e70a6df6df3b1c48dace57d3f634db49457d0b84b74c5fee32b2950476b1c7bdd5e48fc315aad853e8bcabfe12d65d830b5b1291333bdaf7bb11eaa5bd814f21a768de8f331f62564eed0df23b7edcd22498c6bba557a3fbdd8011d59a3c22cf2e59fa62ee89663410a32276e7644136
[01:08] <Runeblight> : 580f7cb421f61fbdcb1bb0329495617d9038fef09dacfaef85d586bdc720b47530c7457236a0ccc864fd45b859351fa660c82bb54012b270abf71baad524e027249db9411140f542065fe237ee453bcc37f3ed5f66e7d724595f829c53d3f560da580b7d438caf535fe432a2951825b6d1b386c385881db18b48aefda7fa57cb5e954b5b39927520c0f7bb1bf1b2f08f062ab562db8f301c234b50ee42ef292bdbcf7cc191bde94fb9f2c28e
[01:09] <Stormbane> : 4e0d2ea22cfd44f1fb53b07c9dda6770d93cbbbd9ca0fde59ec1d5fcc030b8382bd9457225bd89c830f558ea487d09e371d42abb4e419668adf117e7966bc4246a9df1531d12b14a0b59fe68ee5e218964c0eb4f7ba4df3a1c0c958259cefb7ad51d0c604fc5af481af977a9910f76bacce9c3e683cd00b0d853fbeee4f918ca55890b5b0c923322c7e1ef54eaafa4d81b25ba688cdb371b770443f64cf33e3b9b8644ddc5fef148a3e491c21c94983167812d18fa73ad8b6d2343a12d72a96e4a38f490c984a96c4b440ab331
[01:09] <Runeblight> : 38046bb032f7
[01:09] <Stormbane> : 38046bb032f7
[01:09] <Doomfang> : 38046bb032f7
You must set your channel nickname in your first message at any channel. Format: "!nick <nickname>"
And again… we need to use crib dragging to decrypt these messages:
$ KEY_STREAM=''
$ KEY_STREAM=$(python3 solve.py secret.txt "$KEY_STREAM" 2 '!nick Runeblight' back)
0 b'!nick Doomfang'
1 b'!nick Stormbane'
2 b'!nick Runeblight'
3 b'We should keep o'
4 b'Agreed. The enem'
5 b"I've been studyi"
6 b"I'm already cros"
7 b'We cannot afford'
8 b'Exactly. And eve'
9 b'Yes, but we must'
10 b'Good. No record '
11 b'Agreed. The more'
12 b'We should end th'
13 b'!leave'
14 b'!leave'
15 b'!leave'
$ KEY_STREAM=$(python3 solve.py secret.txt "$KEY_STREAM" 5 'ng ' back)
0 b'!nick Doomfang'
1 b'!nick Stormbane'
2 b'!nick Runeblight'
3 b'We should keep our '
4 b"Agreed. The enemy's"
5 b"I've been studying "
6 b"I'm already cross-c"
7 b'We cannot afford he'
8 b'Exactly. And even i'
9 b'Yes, but we must tr'
10 b'Good. No record of '
11 b'Agreed. The more we'
12 b'We should end this '
13 b'!leave'
14 b'!leave'
15 b'!leave'
Flag
Again, after a lot of guessing… We end up decrypting almost all messages and finding the flag in one of them:
!nick Doomfang
!nick Stormbane
!nick Runeblight
We should keep our planning here. The outer halls are not secure, and too many eyes watch the open channels.
Agreed. The enemy's scouts grow more persistent. If they catch even a whisper of our designs, they will move against us. We must not allow their seers or spies to track our steps.
I've been studying the traces left behind by our previous incantations, and something feels wrong. Our network of spells has sent out signals to an unknown beacon-one that none of us authorized. This could be [...]
I'm already cross-checking our spellwork against the ancient records. If this beacon was part of an older enchantment, I'll find proof. But if it is active now, then we have a problem. It could be a concealed [...]
We cannot afford hesitation. If this is a breach, then the High Council's forces may already be on our trail. Even the smallest mistake could doom our entire campaign. We must confirm at once if our arcane def[...]
Exactly. And even if we remain unseen for now, we need contingency plans. If the Council fortifies its magical barriers, we could lose access to their strongholds. Do we have a secondary means of entry if the [...]
Yes, but we must treat it only as a last resort. If we activate it too soon, we risk revealing its location. It is labeled as: HTB{Crib_Dragging_Exploitation_With_Key_Nonce_Reuse!}
Good. No record of it must exist in the written tomes. I will ensure all traces are erased, and it shall never be spoken of openly. If the enemy ever learns of it, we will have no second chance.
Agreed. The more we discuss it, the greater the risk. Every moment we delay, the Council strengthens its defenses. We must act soon before our window of opportunity closes.
We should end this meeting and move to a more secure sanctum. If their mages or spies are closing in, they may intercept our words. We must not take that chance. Let this be the last message in this place.
!leave
!leave
!leave
And the flag is:
HTB{Crib_Dragging_Exploitation_With_Key_Nonce_Reuse!}