I do all my ciphering electronically. https://electronical.chall.pwnoh.io/
When going to the linked site, you get told to encrypt any message or view the site’s source code. After submitting a message to encrypt, it returns some hex string.
The source is:
from Crypto.Cipher import AES
from flask import Flask, request, abort, send_file
import math
import os
app = Flask(__name__)
key = os.urandom(32)
flag = os.environ.get('FLAG', 'bctf{fake_flag_fake_flag_fake_flag_fake_flag}')
cipher = AES.new(key, AES.MODE_ECB)
def encrypt(message: str) -> bytes:
length = math.ceil(len(message) / 16) * 16
padded = message.encode().ljust(length, b'\0')
return cipher.encrypt(padded)
def decrypt(msg: str) -> bytes:
return cipher.decrypt(msg)
@app.get('/encrypt')
def handle_encrypt():
param = request.args.get('message')
if not param:
return abort(400, "Bad")
if not isinstance(param, str):
return abort(400, "Bad")
print(encrypt(param + flag))
return encrypt(param + flag).hex()
@app.get('/source')
def handle_source():
return send_file(__file__, "text/plain")
@app.get('/')
def handle_home():
return """
<style>
form {
display: flex;
flex-direction: column;
max-width: 20em;
gap: .5em;
}
input {
padding: .4em;
}
</style>
<form action="/encrypt">
<h2><i>ELECTRONICAL</i></h2>
<label for="message">Message to encrypt:</label>
<input id="message" name="message"></label>
<input type="submit" value="Submit">
<a href="/source">Source code</a>
</form>
"""
if __name__ == "__main__":
app.run()
It seems that the flag is appended to the user’s message and then encrypted with AES-ECB. The total message is also padded to be a multiple of 16 bytes.
According to Wikipedia, ECB (electronic codebook) works by dividing a message into blocks of a certain size (like 16 bytes). The problem however is that ECB doesn’t attempt to make any encrypted block unique like by adding a salt or nonce, so any blocks of data that are identical would also be identical when encrypted. Wikipedia also has an interesting example of encrypting an image of Tux and a mountain (on French Wikipedia) with AES.
Through some more searching online, it seems a way to exploit this is with something called a Chosen Plaintext Attack. Since the message before the flag is controlled by us the user (attacker?) and the flag is appended to the end, the provided message can be made in a way that only one byte of the flag needs to be bruteforced at a time.
Let’s say that this is our message: thischallengesucksFLAG{5om3_!mp0r74nt_$3cr37}
This string is 45 characters, so the server would pad this with 3 \0 characters to make it evenly divisible by 16 characters.
b'thischallengesucksFLAG{5om3_!mp0r74nt_$3cr37}\x00\x00\x00'
We know what “thischallengesucks” is, but FLAG and anything else after is appended by the server and is what we’re trying to find.
“thischallengesucks” is 18 characters, but if we send a 15 character string, then the first block to be encrypted would be “thischallengesu?”, where ? is the mystery character.
For readability purposes, I’m going to use repeated “0” characters needed instead of “thischallengesucks”.
When passing “000000000000000?” to the server, a certain hex string would be returned (newlines every 32 characters not included in original):
b57189530dacbb9c5707c1cb0b044a34
5377049685bb9553a73e4408565505dd
0c614f69c4749b10f8cbc9c735fd7314
5a9ae527825603a8eb0dba6a0347a4e5
Replacing the ? with any other character would result in only the first row being changed, like with “000000000000000A”:
b2457a857e82a1d5ad919a4bdaf9133a
7835c84bc75d836fad8ca5fbcec086ff
937cf83a682fa26162a65f2295b2b119
6b398dd6f75e212b1633c5189bdb5689
Since the other three blocks remained the same, the last character in the message being sent simply needs to bruteforced with every printable character until it results in the same block from ?. In this case, that character would be F:
b57189530dacbb9c5707c1cb0b044a34
5377049685bb9553a73e4408565505dd
0c614f69c4749b10f8cbc9c735fd7314
5a9ae527825603a8eb0dba6a0347a4e5
(0000000000000, 14 characters long)
5ec61f1209adfeff202edbba28339f83
4f7cc7a4c0c553380874383e93408678
ca91d95b091956edb162da583b51051b
33b91ab14b8807348fc98bf223b4b3a5
(0000000000000FL, 16 characters long)
5ec61f1209adfeff202edbba28339f83
4f7cc7a4c0c553380874383e93408678
ca91d95b091956edb162da583b51051b
33b91ab14b8807348fc98bf223b4b3a5
Then the 0 left pad would be decreased by one character and the process repeats until the whole block is done. However, a flag usually won’t be just 16 characters long. I had some difficulty trying to bruteforce the 17th character and above because I was prepending and appending the zeros within a single block (between 0 and 15 padded 0s), but that was among a few other issues I had that were the result of the message I was sending being in the format of “pad + known_flag + brute_single_char + pad” where this only worked for the first block. This did not work later because those messages would have the pad bytes in the middle of the message, which did not go well.
In the end, I realized that I can check the target block hexstring by sending only the padded 0 bytes (or anything else of that length) without other characters and then append my known bytes of the flag and a single other character to fill that block to brute force that last character.
A visual representation is this:
Block size: 8 characters
7 pad, 0 known
XXXXXXX?
XXXXXXXF
6 pad, 1 known
XXXXXX??
XXXXXXFL
5 pad, 2 known
XXXXX???
XXXXXFLA
...
0 pad, 7 known
FLAG{5o?
8 known
FLAG{5om
This only decrypts the first block, so how I decrypted each additional block was by prepending another block of pad characters (blocksize - 1) and repeating the process.
7 pad, 7 known
XXXXXXXFLAG{5om?
XXXXXXXFLAG{5om3
6 pad, 8 known
XXXXXXFLAG{5om3?
XXXXXXFLAG{5om3_
...
5 pad, 26 known
XXXXXFLAG{5om3_!mp0r74nt_$3cr37?
XXXXXFLAG{5om3_!mp0r74nt_$3cr37}
...
0 pad, 31 known
FLAG{5om3_!mp0r74nt_$3cr37}\0\0?
FLAG{5om3_!mp0r74nt_$3cr37}\0\0\0
After some automating help with python, I was able to finally get the flag.
Flag: bctf{1_c4n7_b3l13v3_u_f0und_my_c0d3b00k}
My python file to solve this was:
from requests import get
from requests.utils import quote
# list of characters that will be bruteforced, these are the printable chars
chars = [chr(i) for i in range(ord(' '), ord('~') + 1)]
# nul character is also checked because that's the pad character
chars += '\0'
def encrypt(msg):
#url = "https://electronical.chall.pwnoh.io/encrypt?message="
url = "http://localhost:5000/encrypt?message="
return get(url + quote(msg)).content;
def calc_padding_for_known():
# divided by 2 because each hex byte is 2 characters long
cur = len(encrypt("0")) // 2
# 16 chosen because that's the padding chosen in app.py on the server
for i in range(2, 16):
tmp = len(encrypt("0" * i)) // 2
if tmp > cur:
return tmp, tmp - cur, i - 1
# shouldn't come here
return 0,0,0
totalblocks, bs, pad = calc_padding_for_known()
print(f"Block size of padding: {bs}, {pad}")
# first block are known to not be the flag (is all 0 being encrypted)
# second block is what's being bruteforced
# third block's last character is unknown and being compared with second block
#msg = "0" * (bs + bs - 1) + "a" * 1 + "0" * (bs - 1)
#cur = encrypt(msg)
#print(cur)
flag = ""
curflag = ""
tbs = bs * 2
for j in range(totalblocks // bs):
for i in range(1, bs + 1):
known = "0" * (bs * (1 + j) - len(flag) - 1)
msg = known
target = encrypt(msg).decode("utf-8")
print(f"\nNew target message: {msg}")
print("New target message return:")
print('\n'.join([target[A:A + tbs] for A in range(0, len(target), tbs)]))
target = target[tbs * (0 + j):tbs * (1 + j)]
print(f"New target block: {target}")
for c in chars:
msg = known + flag + c
print(f"Current character: {c}")
print(f"Current message: {msg}")
print(f"Target block: {target}")
cur = encrypt(msg).decode("utf-8")
print('\n'.join([cur[A:A + tbs] for A in range(0, len(cur), tbs)]))
print(f"Current block: {cur[:tbs]}")
if (cur[tbs * (0 + j):tbs * (1 + j)] == target):
flag += c
print(flag)
break
print("\n")
curflag += flag
print(flag)