added official hacktheboo2024 writeups
This commit is contained in:
parent
1f7a9b0566
commit
e3c46450f7
327 changed files with 14303 additions and 0 deletions
|
@ -0,0 +1,18 @@
|
|||
FROM python:3.9-slim-buster
|
||||
|
||||
RUN apt update
|
||||
RUN apt install -y socat
|
||||
RUN pip install pycryptodome
|
||||
|
||||
# Add application
|
||||
WORKDIR /challenge
|
||||
COPY challenge .
|
||||
|
||||
# Expose the port
|
||||
EXPOSE 1337
|
||||
|
||||
# Switch to use a non-root user from here on
|
||||
USER nobody
|
||||
|
||||
# Start the python application
|
||||
CMD ["socat", "-dd", "TCP-LISTEN:1337,reuseaddr,fork", "exec:python -u /challenge/server.py"]
|
35
htb/hacktheboo2024/crypto/[Very Easy] brevi moduli/Makefile
Normal file
35
htb/hacktheboo2024/crypto/[Very Easy] brevi moduli/Makefile
Normal file
|
@ -0,0 +1,35 @@
|
|||
default:
|
||||
ifdef name
|
||||
@cd challenge; \
|
||||
mkdir -p ../release/crypto_$(name); \
|
||||
cp server.py ../release/crypto_$(name); \
|
||||
|
||||
@cd release; \
|
||||
zip -9 -r ./crypto_$(name).zip ./crypto_$(name); \
|
||||
unzip -l ./crypto_$(name).zip;
|
||||
|
||||
@echo [+] Challenge was built successfully.
|
||||
else
|
||||
@echo [-] Please define the challenge name. For example, \"make name=cool_chall_name\"
|
||||
endif
|
||||
|
||||
flag:
|
||||
@echo [+] Flag : $$(cd challenge; python3 -c 'print(open("flag.txt").read())')
|
||||
|
||||
solver:
|
||||
@echo [+] Running solver
|
||||
@cd htb ; \
|
||||
sage -python3 solver.py
|
||||
@find ./ -name "*.sage.py" -type f -delete
|
||||
|
||||
solver_remote:
|
||||
@echo [+] Running remote solver
|
||||
./build-docker.sh
|
||||
@sage -python3 htb/solver.py REMOTE localhost:1337
|
||||
|
||||
test: clean default flag solver solver_remote
|
||||
|
||||
clean:
|
||||
@rm -rf release/*
|
||||
@find . -name "*.sage.py" -type f -delete
|
||||
@echo [+] Challenge release deleted successfully.
|
193
htb/hacktheboo2024/crypto/[Very Easy] brevi moduli/README.md
Normal file
193
htb/hacktheboo2024/crypto/[Very Easy] brevi moduli/README.md
Normal file
|
@ -0,0 +1,193 @@
|
|||

|
||||
|
||||
<img src='../../../../../assets/logo.png' style='zoom: 80%;' align=left /><font size='5'>brevi moduli</font>
|
||||
|
||||
25<sup>th</sup> September 2024 / Document No. D24.102.XX
|
||||
|
||||
Prepared By: `rasti`
|
||||
|
||||
Challenge Author(s): `rasti`
|
||||
|
||||
Difficulty: <font color=lightgreen>Very Easy</font>
|
||||
|
||||
Classification: Official
|
||||
|
||||
|
||||
|
||||
# Synopsis
|
||||
|
||||
- `brevi moduli` is a very easy challenge. The player has to pass five rounds to get the flag. At each round, they will have to submit the prime factors $p, q$ of a 220-bit RSA modulus. Since the modulus is small, it can be factored by most tools, such as SageMath.
|
||||
|
||||
## Description
|
||||
|
||||
- On a cold Halloween night, five adventurers gathered at the entrance of an ancient crypt. The Cryptkeeper appeared from the shadows, his voice a chilling whisper: "Five locks guard the treasure inside. Crack them, and the crypt is yours." One by one, they unlocked the crypt's secrets, but as the final door creaked open, the Cryptkeeper's eerie laughter filled the air. "Beware, for not all who enter leave unchanged."
|
||||
|
||||
|
||||
|
||||
## Skills Required
|
||||
|
||||
- Know how to interact with TCP servers using pwntools.
|
||||
- Very basic knowledge of the RSA cryptosystem and the Integer Factorization problem.
|
||||
|
||||
## Skills Learned
|
||||
|
||||
- Learn how to factor small RSA moduli with SageMath (or any other tool).
|
||||
|
||||
# Enumeration
|
||||
|
||||
In this challenge, we are provided with just a single file:
|
||||
|
||||
- `server.py` : This is the python script that runs when we connect to the challenge instance.
|
||||
|
||||
## Analyzing the source code
|
||||
|
||||
Let us first analyze the server code.
|
||||
|
||||
```python
|
||||
rounds = 5
|
||||
e = 65537
|
||||
|
||||
for i in range(rounds):
|
||||
print('*'*10, f'Round {i+1}/{rounds}', '*'*10)
|
||||
|
||||
p = getPrime(110)
|
||||
q = getPrime(110)
|
||||
n = p * q
|
||||
pubkey = RSA.construct((n, e)).exportKey()
|
||||
print(f'\nCan you crack this RSA public key?\n{pubkey.decode()}\n')
|
||||
|
||||
assert isPrime(_p := int(input('enter p = '))), exit()
|
||||
assert isPrime(_q := int(input('enter q = '))), exit()
|
||||
|
||||
if n != _p * _q:
|
||||
print('wrong! bye...')
|
||||
exit()
|
||||
|
||||
print()
|
||||
|
||||
print(open('flag.txt').read())
|
||||
```
|
||||
|
||||
To get the flag, we have to pass 5 rounds successfully. At each round, we are provided with an RSA public key and the task is to provide the prime factors $p, q$ of the modulus $N$. The value of the public exponent, namely $e$, is fixed to the standardized value $65537$.
|
||||
|
||||
# Solution
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
The security of the RSA cryptosystem, lies entirely on the hardness of solving the Integer Factorization problem. In simple words, this problem can be summarized as:
|
||||
|
||||
*Given $p = 10412581$ and $q = 15559549$, it is trivial and very fast to calculate that $10412581 \cdot 15559549 = 162015064285969 = N$. However, given only $N = 162015064285969$, it is much harder to find which two numbers $p,q$ were multiplied to produce it.*
|
||||
|
||||
For much larger numbers, it becomes infeasible for the modern computers to solve this problem.
|
||||
|
||||
In this challenge, the size of each prime is $110$ bits, so their product is $110 + 110 = 220$ bits in total. It turns out that a $220$-bit RSA modulus is totally insecure to use for cryptography as it can be factored by modern computers very fast. There are many libraries and tools that factor integers but in CTF challenges, the most commonly used is SageMath so we will stick with it. Let us see an example first with dummy numbers.
|
||||
|
||||
```python
|
||||
sage: p = random_prime(2^110)
|
||||
sage: q = random_prime(2^110)
|
||||
sage: n = p * q
|
||||
sage: %time factor(n)
|
||||
CPU times: user 6.46 s, sys: 7.96 ms, total: 6.47 s
|
||||
Wall time: 6.5 s
|
||||
356113038545854871808945806883821 * 1270756668530534635604669619715399
|
||||
sage: p
|
||||
356113038545854871808945806883821
|
||||
sage: q
|
||||
1270756668530534635604669619715399
|
||||
```
|
||||
|
||||
We can see it took only 6.5 seconds for SageMath to factor $n$. Therefore, our task is pretty clear:
|
||||
|
||||
Connect to the server, retrieve the modulus at each round, factor it and send the factors back to the server.
|
||||
|
||||
However, the RSA public key is provided to us in PEM format.
|
||||
|
||||
```python
|
||||
pem_pubkey = RSA.construct((n, e)).exportKey()
|
||||
print(f'\nCan you crack this RSA public key?\n{pubkey.decode()}\n')
|
||||
```
|
||||
|
||||
The PEM format is as follows:
|
||||
|
||||
```
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MDcwDQYJKoZIhvcNAQEBBQADJgAwIwIcBEwL/SBkcv+AmVwzDWtY80vQ4ALwjtUt
|
||||
RgeXuwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
```
|
||||
|
||||
One might wonder why do we have to use such a weird format for sending RSA public keys. The reason is that RSA is widely used in applications such as TLS, which means that the key format must be consistent to avoid errors while parsing the parameters. Conventionally, applications utilize the PEM format. If no such format is used, one could argue which of the following should be used:
|
||||
|
||||
- `(n = value_of_n, e = value_of_e)`
|
||||
- `(n=value_of_n,e=value_of_e)`
|
||||
- `[n=value_of_n, e=value_of_e]`
|
||||
|
||||
and so on.
|
||||
|
||||
The way to read the actual RSA parameters from a PEM formatted key is to use the inverse of `exportKey` which is `importKey`.
|
||||
|
||||
```python
|
||||
>>> from Crypto.PublicKey import RSA
|
||||
>>> n = 452533018482816403250499886919603981486991592917670642633077659579
|
||||
>>> e = 65537
|
||||
>>> pem_pubkey = RSA.construct((n, e)).exportKey()
|
||||
>>> pem_pubkey
|
||||
b'-----BEGIN PUBLIC KEY-----\nMDcwDQYJKoZIhvcNAQEBBQADJgAwIwIcBEwL/SBkcv+AmVwzDWtY80vQ4ALwjtUt\nRgeXuwIDAQAB\n-----END PUBLIC KEY-----'
|
||||
>>> key = RSA.importKey(pem_pubkey)
|
||||
>>> key
|
||||
RsaKey(n=452533018482816403250499886919603981486991592917670642633077659579, e=65537)
|
||||
```
|
||||
|
||||
## Exploitation
|
||||
|
||||
### Connecting to the server
|
||||
|
||||
We can finally move on and write a function that retrieves and factors the five moduli.
|
||||
|
||||
```python
|
||||
from pwn import *
|
||||
from Crypto.PublicKey import RSA
|
||||
from sage.all import *
|
||||
|
||||
def get_flag():
|
||||
e = 65537
|
||||
for _ in range(5):
|
||||
io.recvuntil(b'key?\n')
|
||||
key = RSA.importKey(io.recvuntil(b'-----END PUBLIC KEY-----\n'))
|
||||
n, e = key.n, key.e
|
||||
p, q = list(factor(n))
|
||||
io.sendlineafter(b'p = ', str(p[0]).encode())
|
||||
io.sendlineafter(b'q = ', str(q[0]).encode())
|
||||
io.recvline()
|
||||
flag = io.recvline().strip().decode()
|
||||
return flag
|
||||
```
|
||||
|
||||
### Getting the flag
|
||||
|
||||
A final summary of all that was said above:
|
||||
|
||||
1. Notice that the provided RSA public keys are small so they can be easily factored using SageMath.
|
||||
2. Retrieve the public key of each round, factor it and send back the primes.
|
||||
3. Repeat this five times to get the flag.
|
||||
|
||||
This recap can be represented by code with the `pwn()` function:
|
||||
|
||||
```python
|
||||
def pwn():
|
||||
flag = get_flag()
|
||||
print(flag)
|
||||
|
||||
if __name__ == '__main__':
|
||||
if args.REMOTE:
|
||||
host_port = sys.argv[1].split(':')
|
||||
HOST = host_port[0]
|
||||
PORT = host_port[1]
|
||||
io = remote(HOST, PORT, level='error')
|
||||
else:
|
||||
import os
|
||||
os.chdir('../challenge')
|
||||
io = process(['python3', 'server.py'], level='error')
|
||||
|
||||
pwn()
|
||||
```
|
5
htb/hacktheboo2024/crypto/[Very Easy] brevi moduli/build-docker.sh
Executable file
5
htb/hacktheboo2024/crypto/[Very Easy] brevi moduli/build-docker.sh
Executable file
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
NAME="brevi_moduli"
|
||||
docker rm -f crypto_$NAME
|
||||
docker build --tag=crypto_$NAME . && \
|
||||
docker run -p 1337:1337 --rm --name=crypto_$NAME --detach crypto_$NAME
|
|
@ -0,0 +1 @@
|
|||
HTB{this_was_a_warmup_to_get_you_used_to_integer_factoring_and_parsing_pem_formatted_keys}
|
|
@ -0,0 +1,25 @@
|
|||
from Crypto.Util.number import isPrime, getPrime, bytes_to_long
|
||||
from Crypto.PublicKey import RSA
|
||||
|
||||
rounds = 5
|
||||
e = 65537
|
||||
|
||||
for i in range(rounds):
|
||||
print('*'*10, f'Round {i+1}/{rounds}', '*'*10)
|
||||
|
||||
pumpkin1 = getPrime(110)
|
||||
pumpkin2 = getPrime(110)
|
||||
n = pumpkin1 * pumpkin2
|
||||
large_pumpkin = RSA.construct((n, e)).exportKey()
|
||||
print(f'\n🎃Can you crack this pumpkin🎃?\n{large_pumpkin.decode()}\n')
|
||||
|
||||
assert isPrime(_pmp1 := int(input('enter your first pumpkin = '))), exit()
|
||||
assert isPrime(_pmp2 := int(input('enter your second pumpkin = '))), exit()
|
||||
|
||||
if n != _pmp1 * _pmp2:
|
||||
print('wrong! bye...')
|
||||
exit()
|
||||
|
||||
print()
|
||||
|
||||
print(open('flag.txt').read())
|
|
@ -0,0 +1,38 @@
|
|||
from pwn import *
|
||||
from Crypto.PublicKey import RSA
|
||||
from sage.all import *
|
||||
|
||||
io = None
|
||||
|
||||
def get_flag():
|
||||
e = 65537
|
||||
for _ in range(5):
|
||||
io.recvuntil(b'Round ')
|
||||
print(io.recvline().split()[0].decode())
|
||||
io.recvuntil(b'?\n')
|
||||
key = RSA.importKey(io.recvuntil(b'-----END PUBLIC KEY-----\n'))
|
||||
n, e = key.n, key.e
|
||||
p, q = list(factor(n))
|
||||
io.sendlineafter(b'pumpkin = ', str(p[0]).encode())
|
||||
io.sendlineafter(b'pumpkin = ', str(q[0]).encode())
|
||||
io.recvline()
|
||||
flag = io.recvline().strip().decode()
|
||||
return flag
|
||||
|
||||
|
||||
def pwn():
|
||||
flag = get_flag()
|
||||
print(flag)
|
||||
|
||||
if __name__ == '__main__':
|
||||
if args.REMOTE:
|
||||
host_port = sys.argv[1].split(':')
|
||||
HOST = host_port[0]
|
||||
PORT = host_port[1]
|
||||
io = remote(HOST, PORT, level='error')
|
||||
else:
|
||||
import os
|
||||
os.chdir('../challenge')
|
||||
io = process(['python3', 'server.py'], level='error')
|
||||
|
||||
pwn()
|
Binary file not shown.
|
@ -0,0 +1,25 @@
|
|||
from Crypto.Util.number import isPrime, getPrime, bytes_to_long
|
||||
from Crypto.PublicKey import RSA
|
||||
|
||||
rounds = 5
|
||||
e = 65537
|
||||
|
||||
for i in range(rounds):
|
||||
print('*'*10, f'Round {i+1}/{rounds}', '*'*10)
|
||||
|
||||
pumpkin1 = getPrime(110)
|
||||
pumpkin2 = getPrime(110)
|
||||
n = pumpkin1 * pumpkin2
|
||||
large_pumpkin = RSA.construct((n, e)).exportKey()
|
||||
print(f'\n🎃Can you crack this pumpkin🎃?\n{large_pumpkin.decode()}\n')
|
||||
|
||||
assert isPrime(_pmp1 := int(input('enter your first pumpkin = '))), exit()
|
||||
assert isPrime(_pmp2 := int(input('enter your second pumpkin = '))), exit()
|
||||
|
||||
if n != _pmp1 * _pmp2:
|
||||
print('wrong! bye...')
|
||||
exit()
|
||||
|
||||
print()
|
||||
|
||||
print(open('flag.txt').read())
|
31
htb/hacktheboo2024/crypto/[Very Easy] sekur julius/Makefile
Normal file
31
htb/hacktheboo2024/crypto/[Very Easy] sekur julius/Makefile
Normal file
|
@ -0,0 +1,31 @@
|
|||
default:
|
||||
ifdef name
|
||||
@cd challenge; \
|
||||
python3 source.py; \
|
||||
mkdir crypto_$(name); \
|
||||
cp source.py output.txt ./crypto_$(name); \
|
||||
cp output.txt ../htb/; \
|
||||
mv ./crypto_$(name) ../release/;
|
||||
|
||||
@cd release; \
|
||||
zip -9 -r ./crypto_$(name).zip ./crypto_$(name); \
|
||||
unzip -l ./crypto_$(name).zip;
|
||||
|
||||
@echo [+] Challenge was built successfully.
|
||||
else
|
||||
@echo [-] Please define the challenge name. For example, \"make name=cool_chall_name\"
|
||||
endif
|
||||
|
||||
flag:
|
||||
@echo [+] Flag : $$(cd challenge; python3 -c 'print(open("secret.txt").read())')
|
||||
solver:
|
||||
@echo [+] PoC :
|
||||
@cd htb ; python3 solver.py
|
||||
|
||||
test: clean default flag solver
|
||||
|
||||
clean:
|
||||
@rm -rf release/*
|
||||
@rm -rf htb/output.txt
|
||||
@find . -name "*.sage.py" -type f -delete
|
||||
@echo [+] Challenge release deleted successfully.
|
141
htb/hacktheboo2024/crypto/[Very Easy] sekur julius/README.md
Normal file
141
htb/hacktheboo2024/crypto/[Very Easy] sekur julius/README.md
Normal file
|
@ -0,0 +1,141 @@
|
|||

|
||||
|
||||
<img src='../../../../../assets/logo.png' style='zoom: 80%;' align=left /><font size='5'>sekur julius</font>
|
||||
|
||||
2<sup>nd</sup> October 2024 / Document No. D24.102.XX
|
||||
|
||||
Prepared By: `rasti`
|
||||
|
||||
Challenge Author: `rasti`
|
||||
|
||||
Difficulty: <font color=lightgreen>Very Easy</font>
|
||||
|
||||
Classification: Official
|
||||
|
||||
# Synopsis
|
||||
|
||||
- `sekur julius` is a very easy crypto challenge. The player has to understand that no matter how many times Caesar cipher is applied to a message, the security does not increase as each character is shifted by the same shift offset each time. Therefore, 1337 Caesar encryptions are equivalent to that of a single one. The task is to perform Caesar cipher decryption to obtain the flag string and wrap it with the usual HTB flag format `HTB{}`.
|
||||
|
||||
## Description
|
||||
|
||||
- Hidden deep in the forest was an ancient scroll, rumored to grant immense power to anyone who could read its shifting symbols. On Halloween, a curious traveler found the scroll, its letters strangely out of order. As they deciphered the message, the words slowly rearranged themselves, revealing a dark spell. But with the final shift, the traveler felt a cold presence behind them, whispering, "You were never meant to understand." The forest grew silent, but the spell was already cast.
|
||||
|
||||
## Skills Required
|
||||
|
||||
- Basic knowledge of the Caesar cipher
|
||||
|
||||
## Skills Learned
|
||||
|
||||
- Learn that performing several encryptions with Caesar cipher does not increase the security.
|
||||
|
||||
# Enumeration
|
||||
|
||||
In this challenge we are provided with two files:
|
||||
|
||||
- `source.py` : This is the script that encrypts the secret message and writes the ciphertext to `output.txt`
|
||||
- `output.txt` : This is the output file that contains the encrypted message
|
||||
|
||||
## Analyzing the source code
|
||||
|
||||
Let us first analyze the source of the script.
|
||||
|
||||
```python
|
||||
from random import choices
|
||||
|
||||
def julius_encrypt(msg, shift):
|
||||
ct = ''
|
||||
for p in msg:
|
||||
if p == ' ':
|
||||
ct += '0'
|
||||
elif not ord('A') <= ord(p) <= ord('Z'):
|
||||
ct += p
|
||||
else:
|
||||
o = ord(p) - 65
|
||||
ct += chr(65 + (o + shift) % 26)
|
||||
return ct
|
||||
|
||||
def encrypt(msg, key):
|
||||
for shift in key:
|
||||
msg = julius_encrypt(msg, shift)
|
||||
return msg
|
||||
|
||||
msg = open('secret.txt').read().upper()
|
||||
secure_key = os.urandom(1337)
|
||||
|
||||
with open('output.txt', 'w') as f:
|
||||
f.write(encrypt(msg, secure_key))
|
||||
```
|
||||
|
||||
The flow is very simple to follow. A message is read from `secret.txt` and is encrypted with a key. The key is a random byte string of 1337 bytes.
|
||||
|
||||
The message is then encrypted using each of these bytes as a key. The function that encrypts the actual message is called `julius_encrypt` and we are given its source code. The function iterates over each message character and encrypts it as follows:
|
||||
|
||||
1. If the character is a whitespace, it appends a '$0$' to the ciphertext
|
||||
2. If the character is any other non-uppercase character, it appends it as it is.
|
||||
3. If the character is in uppercase, it is substituted with the character being $\text{shift}$ positions to the right. For example, the letter 'A' with a key of 4 would have been encrypted to 'E'.
|
||||
|
||||
This encryption process should remind us of the Caesar Cipher and in this case the function name is a hint to verify our educated guess.
|
||||
|
||||
# Solution
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
Caesar cipher is known to be vulnerable due to its small key space, which in this challenge consists of a total of 26 characters; the uppercase English alphabet. However, there is a twist in this challenge; the message is not encrypted with a single shift but with 1337 shifts where each shift can be any number in the range $[0, 255]$.
|
||||
|
||||
A standard choice of a shift would be in the range $[0, 25]$ and not in $[0, 255]$. However, the encryption methods adds the shift to the plaintext letter and then reduces the result $\pmod {26}$.
|
||||
|
||||
```python
|
||||
(o + shift) % 26
|
||||
```
|
||||
|
||||
Due to the properties of modular arithmetic, this is equivalent too:
|
||||
|
||||
```python
|
||||
(o % 26 + shift % 26) % 26
|
||||
```
|
||||
|
||||
We observe no matter how large `shift` is, the final number will lie in the range $[0, 25]$. For example, encrypting with $\text{shift} = 250$ is equivalent to $250 \pmod {26} = 10$. As a result, we conclude that the effective keyspace of the cipher remains exactly the same.
|
||||
|
||||
Now, regarding the several rounds of encryption, let us see what happens if we encrypt a message with caesar two times, with two different shifts. Let the message `CRYPTOGRAPHY`. First, we encrypt the plaintext with a shift value of $3$, and then we encrypt the result with a shift value of $5$.
|
||||
$$
|
||||
\text{Plaintext} :& \text{C R Y P T O G R A P H Y}\\
|
||||
\text{Shift by 3} :& \text{F U B S W R J U D S K B}\\
|
||||
\text{Shift by 5} :& \text{K Z G X B W O Z I X P G}
|
||||
$$
|
||||
Notice that we can get the final result by encrypting the initial plaintext with a shift value equal to the sum of the sub-shifts; that is $3 + 5 = 8$.
|
||||
$$
|
||||
\text{Plaintext} :& \text{C R Y P T O G R A P H Y}\\
|
||||
\text{Shift by 8} :& \text{K Z G X B W O Z I X P G}
|
||||
$$
|
||||
This is crucial as we eliminated one round of encryption and yet ended up with the same result. Back to our challenge, the secret message is encrypted with 1337 rounds. Similarly, we can obtain the final ciphertext by encrypting the message with the shift being the sum of all the 1337 sub-shifts, reduced $\pmod {26}$. In the end, the effective shift value is again a number in the range $[0, 25]$.
|
||||
|
||||
The solution plan is trivial. All we have to do is decrypt the ciphertext with all 26 possible shift values and check if the plaintext looks like English language. We could use the index of coincidence technique to find the correct plaintext directly but it is not mandatory in this case.
|
||||
|
||||
Let us write a function that decrypts the ciphertext with all possible 26 keys.
|
||||
|
||||
```python
|
||||
def decrypt():
|
||||
enc = open('output.txt').read()
|
||||
for i in range(1, 26):
|
||||
print(f'{i = } | {julius_decrypt(enc, i)}')
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Getting the flag
|
||||
|
||||
A final summary of all that was said above:
|
||||
|
||||
1. Notice that the provided cipher is identical to the Caesar cipher.
|
||||
2. Figure out that the "twist" of the shift values being in the range $[0, 255]$ does not add something to the security of the cipher and the keyspace remains the same.
|
||||
3. With simple logic and experimentations, one can conclude that the total number of encryption rounds eventually drops down to a single encryption with Caesar cipher.
|
||||
4. Knowing that, we can decrypt the ciphertext with all the possible 26 keys and check which result looks like English language.
|
||||
|
||||
This recap can be represented by code with the `pwn()` function:
|
||||
|
||||
```python
|
||||
def pwn():
|
||||
decrypt()
|
||||
|
||||
pwn()
|
||||
```
|
|
@ -0,0 +1 @@
|
|||
LTARDBT0ID0WPRZIWTQDD0ILDIWDJHPCSILTCINUDJG!0IWXH0XH0P0EGDDU0DU0RDCRTEI0ID0EGDKT0NDJ0IWPI0IWT0RPTHPG0RXEWTG0XH0XCHTRJGT0CD0BPIITG0WDL0BPCN0IXBTH0NDJ0PEEAN0XI.0IWT0HTRJGXIN0DU0P0IWDJHPCS0SXHIXCRI0HWXUIH0XH0TKTCIJPAAN0IWT0HPBT0PH0IWPI0DU0P0HXCVAT0HWXUI.0TCDJVW0BJBQAXCV,0IPZT0NDJG0UAPV0PCS0TCYDN0IWT0GTHI0DU0IWT0RDCITHI.0BPZT0HJGT0NDJ0LGPE0IWT0UDAADLXCV0ITMI0LXIW0IWT0WIQ0UAPV0UDGBPI0HTRJGXINDUPIWDJHPCSDGHTRJGXINDUPHXCVAT.
|
|
@ -0,0 +1 @@
|
|||
Welcome to HackTheBoo TwoThousandTwentyFour! This is a proof of concept to prove you that the Caesar cipher is insecure no matter how many times you apply it. The security of a thousand distinct shifts is eventually the same as that of a single shift. Enough mumbling, take your flag and enjoy the rest of the contest. Make sure you wrap the following text with the HTB flag format SECURITYOFATHOUSANDORSECURITYOFASINGLE.
|
|
@ -0,0 +1,24 @@
|
|||
from random import choices
|
||||
|
||||
def julius_encrypt(msg, shift):
|
||||
ct = ''
|
||||
for p in msg:
|
||||
if p == ' ':
|
||||
ct += '0'
|
||||
elif not ord('A') <= ord(p) <= ord('Z'):
|
||||
ct += p
|
||||
else:
|
||||
o = ord(p) - 65
|
||||
ct += chr(65 + (o + shift) % 26)
|
||||
return ct
|
||||
|
||||
def encrypt(msg, key):
|
||||
for shift in key:
|
||||
msg = julius_encrypt(msg, shift)
|
||||
return msg
|
||||
|
||||
msg = open('secret.txt').read().upper()
|
||||
secure_key = os.urandom(1337)
|
||||
|
||||
with open('output.txt', 'w') as f:
|
||||
f.write(encrypt(msg, secure_key))
|
|
@ -0,0 +1 @@
|
|||
LTARDBT0ID0WPRZIWTQDD0ILDIWDJHPCSILTCINUDJG!0IWXH0XH0P0EGDDU0DU0RDCRTEI0ID0EGDKT0NDJ0IWPI0IWT0RPTHPG0RXEWTG0XH0XCHTRJGT0CD0BPIITG0WDL0BPCN0IXBTH0NDJ0PEEAN0XI.0IWT0HTRJGXIN0DU0P0IWDJHPCS0SXHIXCRI0HWXUIH0XH0TKTCIJPAAN0IWT0HPBT0PH0IWPI0DU0P0HXCVAT0HWXUI.0TCDJVW0BJBQAXCV,0IPZT0NDJG0UAPV0PCS0TCYDN0IWT0GTHI0DU0IWT0RDCITHI.0BPZT0HJGT0NDJ0LGPE0IWT0UDAADLXCV0ITMI0LXIW0IWT0WIQ0UAPV0UDGBPI0HTRJGXINDUPIWDJHPCSDGHTRJGXINDUPHXCVAT.
|
|
@ -0,0 +1,16 @@
|
|||
def julius_decrypt(enc, shift):
|
||||
pt = ''
|
||||
for c in enc:
|
||||
if c == '0':
|
||||
pt += ' '
|
||||
elif not ord('A') <= ord(c) <= ord('Z'):
|
||||
pt += c
|
||||
else:
|
||||
o = ord(c) - 65
|
||||
pt += chr(65 + (o - shift) % 26)
|
||||
return pt
|
||||
|
||||
enc = open('output.txt').read()
|
||||
|
||||
for i in range(1, 26):
|
||||
print(f'{i = } | {julius_decrypt(enc, i)}')
|
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
LTARDBT0ID0WPRZIWTQDD0ILDIWDJHPCSILTCINUDJG!0IWXH0XH0P0EGDDU0DU0RDCRTEI0ID0EGDKT0NDJ0IWPI0IWT0RPTHPG0RXEWTG0XH0XCHTRJGT0CD0BPIITG0WDL0BPCN0IXBTH0NDJ0PEEAN0XI.0IWT0HTRJGXIN0DU0P0IWDJHPCS0SXHIXCRI0HWXUIH0XH0TKTCIJPAAN0IWT0HPBT0PH0IWPI0DU0P0HXCVAT0HWXUI.0TCDJVW0BJBQAXCV,0IPZT0NDJG0UAPV0PCS0TCYDN0IWT0GTHI0DU0IWT0RDCITHI.0BPZT0HJGT0NDJ0LGPE0IWT0UDAADLXCV0ITMI0LXIW0IWT0WIQ0UAPV0UDGBPI0HTRJGXINDUPIWDJHPCSDGHTRJGXINDUPHXCVAT.
|
|
@ -0,0 +1,24 @@
|
|||
from random import choices
|
||||
|
||||
def julius_encrypt(msg, shift):
|
||||
ct = ''
|
||||
for p in msg:
|
||||
if p == ' ':
|
||||
ct += '0'
|
||||
elif not ord('A') <= ord(p) <= ord('Z'):
|
||||
ct += p
|
||||
else:
|
||||
o = ord(p) - 65
|
||||
ct += chr(65 + (o + shift) % 26)
|
||||
return ct
|
||||
|
||||
def encrypt(msg, key):
|
||||
for shift in key:
|
||||
msg = julius_encrypt(msg, shift)
|
||||
return msg
|
||||
|
||||
msg = open('secret.txt').read().upper()
|
||||
secure_key = os.urandom(1337)
|
||||
|
||||
with open('output.txt', 'w') as f:
|
||||
f.write(encrypt(msg, secure_key))
|
|
@ -0,0 +1,31 @@
|
|||
default:
|
||||
ifdef name
|
||||
@cd challenge; \
|
||||
python3 source.py; \
|
||||
mkdir crypto_$(name); \
|
||||
cp source.py output.txt ./crypto_$(name); \
|
||||
cp output.txt ../htb/; \
|
||||
mv ./crypto_$(name) ../release/;
|
||||
|
||||
@cd release; \
|
||||
zip -9 -r ./crypto_$(name).zip ./crypto_$(name); \
|
||||
unzip -l ./crypto_$(name).zip;
|
||||
|
||||
@echo [+] Challenge was built successfully.
|
||||
else
|
||||
@echo [-] Please define the challenge name. For example, \"make name=cool_chall_name\"
|
||||
endif
|
||||
|
||||
flag:
|
||||
@echo [+] Flag : $$(cd challenge; python3 -c 'print(open("flag.txt").read())')
|
||||
solver:
|
||||
@echo [+] PoC : $$(cd htb ; sage -python3 solver.py)
|
||||
@find . -name "*.sage.py" -type f -delete
|
||||
|
||||
test: clean default flag solver
|
||||
|
||||
clean:
|
||||
@rm -rf release/*
|
||||
@rm -rf htb/output.txt
|
||||
@find . -name "*.sage.py" -type f -delete
|
||||
@echo [+] Challenge release deleted successfully.
|
|
@ -0,0 +1,143 @@
|
|||

|
||||
|
||||
<img src="assets/images/htb.png" style="zoom: 80%;" align=left /><font size="5">sugar free candies</font>
|
||||
|
||||
22<sup>th</sup> September 2023 / Document No. D23.102.XX
|
||||
|
||||
Prepared by : `rasti`
|
||||
|
||||
Challenge Author(s): `0x50r4`
|
||||
|
||||
Difficulty: <font color=lightgreen>Very Easy</font>
|
||||
|
||||
Classification: Official
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Synopsis
|
||||
|
||||
- `sugar free candies` is a very easy crypto challege. The flag of this challenge is splitted into three parts. Four relations between these splitted parts are calculated and outputted to an output text file. The players will have to solve a system of three unknowns, given four equations with SageMath, or any other tool of their choice, and recover the flag.
|
||||
|
||||
## Description
|
||||
|
||||
- For years, strange signals pulsed through the air on the eve of October 31st. Some said it was the voice of an ancient witch, others believed it was a message from something far darker. A cryptic message, scattered in three parts, was intercepted by a daring group of villagers. Legend spoke of a deal made between the witch and a shadowy figure, but the true intent of their secret could only be revealed by those brave enough to decipher it before midnight, when the veil between worlds would thin.
|
||||
|
||||
|
||||
|
||||
## Skills Required
|
||||
|
||||
- Basic Python source code analysis.
|
||||
- Know how to work with equations with multiple variables programmatically.
|
||||
|
||||
## Skills Learned
|
||||
|
||||
- Solve equations with SageMath.
|
||||
|
||||
# Enumeration
|
||||
|
||||
In this challenge we are provided with two files.
|
||||
|
||||
- `source.py` : It is the main python script that produces the output file.
|
||||
- `output.txt` : Contains the data outputted from the source script.
|
||||
|
||||
## Analyzing the challenge
|
||||
|
||||
The code of the script is not lengthy so let us take a look.
|
||||
|
||||
```python
|
||||
from Crypto.Util.number import bytes_to_long
|
||||
|
||||
FLAG = open("flag.txt", "rb").read()
|
||||
|
||||
step = len(FLAG) // 3
|
||||
candies = [bytes_to_long(FLAG[i:i+step]) for i in range(0, len(FLAG), step)]
|
||||
|
||||
cnd1, cnd2, cnd3 = candies
|
||||
|
||||
with open('output.tcnd1t', 'w') as f:
|
||||
f.write(f'v1 = {cnd1**3 + cnd3**2 + cnd2}\n')
|
||||
f.write(f'v2 = {cnd2**3 + cnd1**2 + cnd3}\n')
|
||||
f.write(f'v3 = {cnd3**3 + cnd2**2 + cnd1}\n')
|
||||
f.write(f'v4 = {cnd1 + cnd2 + cnd3}\n')
|
||||
```
|
||||
|
||||
The steps can be summarised as:
|
||||
|
||||
- The challenge reads the flag from `flag.txt`, which is secret and not provided to us
|
||||
- It is splitted in three parts, which are denoted as `cnd1`, `cnd2`, and `cnd3` respectively. Each part is then converted into an integer representation.
|
||||
- Four relations between the splitted parts are calculated and finally outputted to the output file `output.txt`.
|
||||
|
||||
# Solution
|
||||
|
||||
Our task is straight forward. We have to solve a system of three unknowns given four equations. Since the number of unknowns is less than the number of equations we are given, it is guaranteed that a solution $(x, y, z) = (x_0, y_0, z_0)$ exists. It should be noted that three equations would also guarantee a unique solution.
|
||||
|
||||
However, the problem is that the numbers are large and the equations non-linear so it would be very hard to solve with just pen and paper. We have to use some tool for this purpose. By researching the keywords `python solve equations` with the search engine of your choice, one should stumble upon the `SymPy` library. However, as the SageMath library is more common in CTFs, we will prefer that one. Note that there are also online equation solvers that should work too.
|
||||
|
||||
# Exploitation
|
||||
|
||||
Let us first load the data from the output file.
|
||||
|
||||
```python
|
||||
exec(open('output.txt').read())
|
||||
```
|
||||
|
||||
Let us first write a function that creates and returns three variables, one for each part of the flag. Due to use of `bytes_to_long`, we know they must be integers so we care only about solutions in the set of integers $\mathbb{Z}$. In SageMath, we can define variables using the `var()` function.
|
||||
|
||||
```python
|
||||
from sage.all import *
|
||||
|
||||
def create_variables():
|
||||
x,y,z = var('x,y,z', domain=ZZ)
|
||||
return x,y,z
|
||||
```
|
||||
|
||||
Then, we move on solving the system. For this purpose, we will utilize SageMath's [function](https://doc.sagemath.org/html/en/reference/calculus/sage/symbolic/relation.html#sage.symbolic.relation.solve) `solve()`. This functions receives a list of equations and the variables we want to solve for. Let us write a function that returns the solutions of the equations.
|
||||
|
||||
```python
|
||||
def solve_system(x, y, z, v1, v2, v3, v4):
|
||||
return solve([
|
||||
x**3 + z**2 + y == v1,
|
||||
y**3 + x**2 + z == v2,
|
||||
z**3 + y**2 + x == v3,
|
||||
x + y + z == v4
|
||||
], x, y, z, solution_dict=True)[0]
|
||||
```
|
||||
|
||||
We also choose to retrieve the solutions in a dictionary format, to make the solution parsing more convenient.
|
||||
|
||||
Having the solutions, we can reconstruct the flag as:
|
||||
|
||||
```python
|
||||
from Crypto.Util.number import long_to_bytes
|
||||
|
||||
def get_flag(sols):
|
||||
return b''.join([long_to_bytes(int(n)) for n in [sols[x], sols[y], sols[z]]])
|
||||
```
|
||||
|
||||
## Getting the flag
|
||||
|
||||
Let us recap the steps for getting the flag.
|
||||
|
||||
- We noticed the flag is splitted into three parts.
|
||||
- We noticed that we are given four relations of these parts so the system must have a unique solution.
|
||||
- By minimal research, we see that we can solve systems using SageMath, SymPy or any online tool.
|
||||
- Solving for the three unknown parts, we reconstruct the flag.
|
||||
|
||||
This recap can be represented with code as below:
|
||||
|
||||
```python
|
||||
exec(open('output.txt').read())
|
||||
|
||||
def pwn():
|
||||
x, y, z = create_variables()
|
||||
sols = solve_system(x, y, z)
|
||||
flag = get_flag(sols)
|
||||
print(flag)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pwn()
|
||||
```
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
|
@ -0,0 +1 @@
|
|||
HTB{__protecting_the_secret_in_equations_is_not_secure__}
|
|
@ -0,0 +1,4 @@
|
|||
v1 = 4196604293528562019178729176959696479940189487937638820300425092623669070870963842968690664766177268414970591786532318240478088400508536
|
||||
v2 = 11553755018372917030893247277947844502733193007054515695939193023629350385471097895533448484666684220755712537476486600303519342608532236
|
||||
v3 = 14943875659428467087081841480998474044007665197104764079769879270204055794811591927815227928936527971132575961879124968229204795457570030
|
||||
v4 = 6336816260107995932250378492551290960420748628
|
|
@ -0,0 +1,14 @@
|
|||
from Crypto.Util.number import bytes_to_long
|
||||
|
||||
FLAG = open("flag.txt", "rb").read()
|
||||
|
||||
step = len(FLAG) // 3
|
||||
candies = [bytes_to_long(FLAG[i:i+step]) for i in range(0, len(FLAG), step)]
|
||||
|
||||
cnd1, cnd2, cnd3 = candies
|
||||
|
||||
with open('output.txt', 'w') as f:
|
||||
f.write(f'v1 = {cnd1**3 + cnd3**2 + cnd2}\n')
|
||||
f.write(f'v2 = {cnd2**3 + cnd1**2 + cnd3}\n')
|
||||
f.write(f'v3 = {cnd3**3 + cnd2**2 + cnd1}\n')
|
||||
f.write(f'v4 = {cnd1 + cnd2 + cnd3}\n')
|
|
@ -0,0 +1,4 @@
|
|||
v1 = 4196604293528562019178729176959696479940189487937638820300425092623669070870963842968690664766177268414970591786532318240478088400508536
|
||||
v2 = 11553755018372917030893247277947844502733193007054515695939193023629350385471097895533448484666684220755712537476486600303519342608532236
|
||||
v3 = 14943875659428467087081841480998474044007665197104764079769879270204055794811591927815227928936527971132575961879124968229204795457570030
|
||||
v4 = 6336816260107995932250378492551290960420748628
|
|
@ -0,0 +1,29 @@
|
|||
from sage.all import *
|
||||
from Crypto.Util.number import long_to_bytes
|
||||
|
||||
exec(open('output.txt').read())
|
||||
|
||||
def create_variables():
|
||||
x,y,z = var('x,y,z', domain=ZZ)
|
||||
return x,y,z
|
||||
|
||||
def solve_system(x, y, z):
|
||||
return solve([
|
||||
x**3 + z**2 + y == v1,
|
||||
y**3 + x**2 + z == v2,
|
||||
z**3 + y**2 + x == v3,
|
||||
x + y + z == v4
|
||||
], x, y, z, solution_dict=True)[0]
|
||||
|
||||
def get_flag(sols):
|
||||
return b''.join([long_to_bytes(int(n)) for n in [sols[x], sols[y], sols[z]]])
|
||||
|
||||
def pwn():
|
||||
x, y, z = create_variables()
|
||||
sols = solve_system(x, y, z)
|
||||
flag = get_flag(sols)
|
||||
print(flag)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pwn()
|
Binary file not shown.
|
@ -0,0 +1,4 @@
|
|||
v1 = 4196604293528562019178729176959696479940189487937638820300425092623669070870963842968690664766177268414970591786532318240478088400508536
|
||||
v2 = 11553755018372917030893247277947844502733193007054515695939193023629350385471097895533448484666684220755712537476486600303519342608532236
|
||||
v3 = 14943875659428467087081841480998474044007665197104764079769879270204055794811591927815227928936527971132575961879124968229204795457570030
|
||||
v4 = 6336816260107995932250378492551290960420748628
|
|
@ -0,0 +1,14 @@
|
|||
from Crypto.Util.number import bytes_to_long
|
||||
|
||||
FLAG = open("flag.txt", "rb").read()
|
||||
|
||||
step = len(FLAG) // 3
|
||||
candies = [bytes_to_long(FLAG[i:i+step]) for i in range(0, len(FLAG), step)]
|
||||
|
||||
cnd1, cnd2, cnd3 = candies
|
||||
|
||||
with open('output.txt', 'w') as f:
|
||||
f.write(f'v1 = {cnd1**3 + cnd3**2 + cnd2}\n')
|
||||
f.write(f'v2 = {cnd2**3 + cnd1**2 + cnd3}\n')
|
||||
f.write(f'v3 = {cnd3**3 + cnd2**2 + cnd1}\n')
|
||||
f.write(f'v4 = {cnd1 + cnd2 + cnd3}\n')
|
Loading…
Add table
Add a link
Reference in a new issue