Intro challenges
Smölkkey
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
from gmpy2 import powmod as pow
from Crypto.PublicKey import RSA
class Smolkkey:
def __init__(self):
k = RSA.generate(2048, e = 3)
self.pk = (k.n, k.e)
self.sk = (k.n, k.d)
def encrypt(self, m):
n, e = self.pk
c = pow(m, e, n)
return int(c)
def decrypt(self, c):
n, d = self.sk
m = pow(c, d, n)
return int(m)
# Generate a key
E = Smolkkey()
# Encrypt the fag
flag = open("flag.txt", "rb").read()
flag = int.from_bytes(flag, "little")
c = E.encrypt(flag)
# Test decryption
assert flag == E.decrypt(c)
# Output public values
n, e = E.pk
print(f"{n = }")
print(f"{e = }")
print(f"{c = }")
n = 20828609401338794038836680655046788059251524928933537772275737490132096798900518851229365799426251400151127719543434160180496659560792762761336988343332946920310984844136554433346165529108260963140451576722579583104830933409454682160084747257400706214980238995436388944310800852033141986598424966358149711167942491331040747300866718813771865768701794983365111208518863175847678947437360554933091347604616653687980177405805542214635577758515398014710929022135522835744517328114492844837858920033569071591971676487452812830920525469634367387593309067794509735740140745616085618489218115494716811261406227449967579233657
e = 3
c = 6317668510138686569655374990729607736156413707292408158720036346854309670467296052918552527575331589363290061240725095262980389263184520673983411112154423282089471021996509038472493779143273789325774414352608726252566350689111876373836913240644190951995980896093509379920452743478551321978067299216590452459233562642920123055978471365092000347562228787318105538018723376505390423730687522026043802357456368003656219942603097205774742385485995835519133581552096067468551713114231926639878045212204590071768
This challenge is used vanila RSA with some weird values. In this case, we can clearly see that:
\[c << n\]In such cases, it can happen that:
\[c^e < n\]and thus we can just take the root e
straight away using regular math.
Solve
1
2
3
4
5
from gmpy2 import iroot
c = 6317668510138686569655374990729607736156413707292408158720036346854309670467296052918552527575331589363290061240725095262980389263184520673983411112154423282089471021996509038472493779143273789325774414352608726252566350689111876373836913240644190951995980896093509379920452743478551321978067299216590452459233562642920123055978471365092000347562228787318105538018723376505390423730687522026043802357456368003656219942603097205774742385485995835519133581552096067468551713114231926639878045212204590071768
print(bytes.fromhex(hex(iroot(c, 3)[0])[2:])[::-1])
# FCSC{30f7c4b2fa7f0fb48bfbd9bbd413491c0a6da660764961b862fe38a83b4bc00f}
Carrote radis tomate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
key = os.urandom(32)
print("carotte = ", int.from_bytes(key) % 17488856370348678479)
print("radis = ", int.from_bytes(key) % 16548497022403653709)
print("tomate = ", int.from_bytes(key) % 17646308379662286151)
print("pomme = ", int.from_bytes(key) % 14933475126425703583)
print("banane = ", int.from_bytes(key) % 17256641469715966189)
flag = open("flag.txt", "rb").read()
E = AES.new(key, AES.MODE_ECB)
enc = E.encrypt(pad(flag, 16))
print(f"enc = {enc.hex()}")
For this challenge, the name is a clear hint to use the Chinese Reminder Theorem (CRT). Using sage math we can find that the key is 58537804506201655097879135670024677446002384165465965481902712238119765745741
The rest is trivial
1
2
3
4
5
6
7
8
9
10
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Util.number import long_to_bytes
key = 58537804506201655097879135670024677446002384165465965481902712238119765745741
E = AES.new(long_to_bytes(key, 32), AES.MODE_ECB)
enc = bytes.fromhex("2da1dbe8c3a739d9c4a0dc29a27377fe8abc1c0feacc9475019c5954bbbf74dcedce7ed3dc3ba34fa14a9181d4d7ec0133ca96012b0a9f4aa93c42c61acbeae7640dd101a6d2db9ad4f3b8ccfe285e0d")
print(E.decrypt(enc).decode())
# FCSC{2c4c4b3be7d86e1642ce6a8bf1bd75f33b9736e5943f51a49fb9327e248c3b6a}
Meme generator (not solved)
For this one, the challenge is to use a bot to recover the file from local storage.
After some look through the code we can see that there is 2 entries possible:
1
2
3
4
5
6
<?php if (isset($_GET['image']) && isset($_GET['text'])): ?>
<div class="meme-container">
<img src="img/<?php echo $_GET['image']; ?>" class="img-fluid">
<div class="meme-text"><?php echo strtoupper($_GET['text']); ?></div>
</div>
<?php endif; ?>
Using the image
and text
entries gives you the possibility to inject some JS code that can run and recover the flag from localstorage
Red herring
While both are possible, I focused mainly on the text one. Realized pretty soon that it had a glaring issue: echo strtoupper($_GET['text'])
. Unfortunately JS is sensitive to case and in this case we would need some way of encoding everything to bypass the strtoupper
. One possibility is JS Fuck but unfortunately the payload we need would be too big for the bot and just fail.
Actual solution
Instead of using the text
field you can use the image
field and force JS code to execute since there is no formatting for that part of the code.
We know that we have access to all the console.log
from the code of the bot so our goal is to execute the following payload: console.log(localStorage.getItem('flag'))
Since there is no formatting we can just add the payload right after the image source and just add "
to get out of the <img>
tag giving us:
http://meme-generator/?image=grumpy.jpeg"><script>console.log(localStorage.getItem('flag'))</script>&text=oops
This will get you the flag
Touillette
1
2
3
4
5
6
7
8
9
10
11
12
13
14
flag = open("flag.txt").read()
assert len(flag) == 64
x = "".join([
flag[-8::-8],
flag[-7::-8],
flag[-6::-8],
flag[-5::-8],
flag[-4::-8],
flag[-3::-8],
flag[-2::-8],
flag[-1::-8],
])
print(x[1::2] + x[0::2])
My solve
Here is actually tried to understand what was done and reverse it part by part. Pretty long to do
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
flag = """Z2yFm7bCjR6SMWOCSiw{wqKWoJxTtxP4Hf74mQZ4qmghcu1mdX9HND7u8oxF}JsR"""
x = "".join([
flag[-8::-8],
flag[-7::-8],
flag[-6::-8],
flag[-5::-8],
flag[-4::-8],
flag[-3::-8],
flag[-2::-8],
flag[-1::-8],
])
print(x[1::2] + x[0::2])
flag1 = flag[:32]
flag2 = flag[32:]
trueFlag = ""
for x, y in zip(flag1, flag2):
trueFlag += y + x
print(trueFlag)
partition = 64/8
flag1 = trueFlag[0:partition]
flag2 = trueFlag[partition:partition*2]
flag3 = trueFlag[partition*2:partition*3]
flag4 = trueFlag[partition*3:partition*4]
flag5 = trueFlag[partition*4:partition*5]
flag6 = trueFlag[partition*5:partition*6]
flag7 = trueFlag[partition*6:partition*7]
flag8 = trueFlag[partition*7:64]
print(flag1)
ret = ""
for a, b, c, d, e, f, g, h in zip(flag1,flag2, flag3, flag4, flag5, flag6, flag7, flag8):
ret += h + g +f +e + d +c +b +a
print(ret[::-1])
Better solve
A friend after the end made me realize that this is technically a basic substitution cipher so you can just go and find the index from the original alphabet and tada you get the reversed flag
1
2
3
4
5
6
7
8
9
10
11
12
etalon = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUWXYZ-_="
touilled_etalon = "Mwg0Nxh1Oyi2Pzj3QAk4RBl5SCm6TDn7UEo8WFp9XGqaYHrbZIsc-Jtd_Kue=Lvf"
touilled_flag="Z2yFm7bCjR6SMWOCSiw{wqKWoJxTtxP4Hf74mQZ4qmghcu1mdX9HND7u8oxF}JsR"
flag=["/"]*64
for i in range(64):
curr_char = touilled_etalon[i]
i_prime = etalon.find(curr_char)
flag[i_prime] = touilled_flag[i]
print("".join(flag))
#FCSC{WT444hmHuFRyb6OwKxP7Zg197xs27RWiqJxfQmuXDoJZmjMSwotHmqcdN8}
Crypto challenges
La qu6ete de l’anneau
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
#!/usr/bin/env python3
import json
import os
from Crypto.Util.number import *
from Crypto.Random.random import randrange
from math import gcd
class Cipher:
def __init__(self, size = 512):
self.s = 2**size + randrange(2**size)
self.bs = size // 8
def encrypt(self, m, data = False):
assert len(m) % self.bs == 0, f"Error: wrong length ({len(m)})"
C = []
for i in range(0, len(m), self.bs):
iv = randrange(self.s)
while gcd(iv,self.s) != 1:
iv = randrange(self.s)
b = int.from_bytes(m[i:i + self.bs],"big")
if not data:
C.append({
"iv" : iv,
"c" : (b * iv) % self.s,
})
else:
C.append({
"m" : b,
"iv" : iv,
"c" : (b * iv) % self.s,
})
return C
def decrypt(self, c):
r = b""
for d in c:
m = d["c"] * pow(d["iv"], -1, self.s) % self.s
r += int.to_bytes(m, self.bs,"big")
return r
if __name__ == "__main__":
flag = open("flag.txt", "rb").read().strip()
assert len(flag) == 64, "Error: wrong flag length."
E = Cipher()
m = os.urandom(64).hex()
data = E.encrypt(m.encode(), data = True)
C = E.encrypt(flag)
assert flag == E.decrypt(C), "Error: decryption test failed."
print(json.dumps({
"data" : data,
"C" : C
}, indent = 4))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"data": [
{
"m": 5300924613498218875069386136478898074006475860605696653663766789647491408111939951962787562515179231001576088241821257224379937942480792836286291708950580,
"iv": 990478785346720639288503860820230515465392521155988529067551628642203679040736067705099478569806691727869997283427937397835972477847915536798446362176571,
"c": 14608971898840483258729047186216694135901081092160417249500736213435666342621773315139756301160520793786438865525865528889455866097651720255856961635805732
},
{
"m": 5100397453901829001001274202132069470845583300596122363556250272408483548979629877688935275217404969058564324787967275472360630765503385955263068863095345,
"iv": 7475569552781515571825018823124971469978831837925573660203231293669196922174669333904855142066157502493624218193454674776997566021222129832543051527639435,
"c": 7362760529141371598441777437669466021221530082696679574829167632394833775523119009628310509508552230962414538323390642388879749988667246066507482829109785
}
],
"C": [
{
"iv": 14065646473510331612652599339352010982436820046928032638519239485603018733626860766641543018671779202005925711911356379944986562755350855828351678555098061,
"c": 8973029504100997651890859382562887986006499283456026995696294203177046779401655025542878703573157181581548171480134446908730999381542131074327319465448895
}
]
}
For this challenge, the key resides in the fact that in cases where the message is too long we get more info:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for i in range(0, len(m), self.bs):
iv = randrange(self.s)
while gcd(iv,self.s) != 1:
iv = randrange(self.s)
b = int.from_bytes(m[i:i + self.bs],"big")
if not data:
C.append({
"iv" : iv,
"c" : (b * iv) % self.s,
})
else:
C.append({
"m" : b,
"iv" : iv,
"c" : (b * iv) % self.s,
})
Thankfully for us, while the original message is short and only gives us one point of data, the rest is long and gives us 2 points. Thus we now have a way to solve this for the modulus:
We thus have:
\[b_1 * iv_1 \cong c_1\ [n] \newline \Leftrightarrow b_1 * iv_1 - c_1 \cong 0\ [n] \newline \Leftrightarrow b_1 * iv_1 - c_1 = n * k_1\]For some random k
. We have that for 2 points and we can assume that k will be significantly smaller than n
. Thus finding the GCD will give us n
. We can then use that to find what the original message m
is by:
Cocorico
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
import os
import json
from zlib import crc32 as le_mac
from Crypto.Cipher import AES
class CocoRiCo_Chiffrement_AEAD:
def __init__(self, la_clef):
self.la_clef = la_clef
def le_chiffrement(self):
return AES.new(self.la_clef, AES.MODE_OFB, iv = b"\x00" * 16)
def chiffrer_integre(self, le_message):
le_tag = int.to_bytes(le_mac(le_message), 4)
return self.le_chiffrement().encrypt(le_message + le_tag)
def dechiffrer(self, le_chiffre):
x = self.le_chiffrement().decrypt(le_chiffre)
le_message, t = x[:-4], x[-4:]
le_tag = int.to_bytes(le_mac(le_message), 4)
if le_tag == t:
return le_message
else:
return b""
try:
la_clef = os.urandom(32)
E = CocoRiCo_Chiffrement_AEAD(la_clef)
for _ in "FCSC":
print("0. Quit")
print("1. Login")
print("2. Logout")
print("3. TODO")
choice = int(input(">>> "))
if choice == 0:
break
elif choice == 1:
new = input("Are you new ? (y/n) ")
if new == "y":
name = input("Name: ")
if name == "toto":
print("Toto is one of our admin! Do not try to outsmart the system!")
exit(1)
d = json.dumps({
"name": name,
"admin": False,
}).encode()
c = E.chiffrer_integre(d)
print(f"Welcome {name}. Here is your token:")
print(c.hex())
logged = 1
print("This challenge is still under active developement, please come back in a few weeks to try it out!")
# TODO: Add vulnerable code here
elif new == "n":
token = bytes.fromhex(input("Token: "))
x = E.dechiffrer(token)
d = json.loads(x)
if d["name"] == "toto" and d["admin"]:
print("Congrats! Here is your flag:")
print(open("flag.txt").read().strip())
else:
print(f"Weclome back {d['name']}!")
elif choice == 2:
logged = 0
elif choice == 3:
print("This challenge is still under active developement, please come back in a few weeks to try it out!")
# TODO: Add another vuln here
except:
print("Please check your inputs.")
This challenge asks us to forge a token. Thankfully we can create our own. The 2 function that interest us are the following:
1
2
3
4
5
6
def le_chiffrement(self):
return AES.new(self.la_clef, AES.MODE_OFB, iv = b"\x00" * 16)
def chiffrer_integre(self, le_message):
le_tag = int.to_bytes(le_mac(le_message), 4)
return self.le_chiffrement().encrypt(le_message + le_tag)
We can see that the iv is constant and the key as well. AES OFB only XORs the plain text with the iv after the later went through a round of AES. We just need to get that output in order to forge our message. The tag is trivial to find as it is just the CRC32 checksum of the message.
My solve is as follows:
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
#!/usr/bin/env python3
# Install using pip:
# pip install pwntools --break-system-packages
from pwn import *
import json
from zlib import crc32
'''
To use this template, execute the script with python3:
$ python3 template.py
If you want to run it with a different server and port, use
$ python3 template.py HOST=localhost PORT=4000
If you want to debug and see the data going exchanged, add "DEBUG":
$ python3 template.py DEBUG
'''
# Define the remote connexion details: server address and port
HOST = args.HOST or "chall.fcsc.fr"
PORT = args.PORT or 2150
# Connect to the remote server
# The variable "io" will be used to send and receive text to the remove server
io = remote(HOST, PORT)
io.recvuntil(b">>>")
admin = "toto"
adminMsg = json.dumps({"name": "toto","admin": True,}).encode()
adminTag = int.to_bytes(crc32(adminMsg), 4, "big")
user = "test"
userMessage = json.dumps({"name": user,"admin": False,}).encode()
tag = int.to_bytes(crc32(userMessage), 4, "big")
io.sendline("1".encode())
io.recvuntil(b"Are you new ? (y/n) ")
io.sendline("y".encode())
io.recvuntil(b"Name: ")
io.sendline(userMessage)
io.recvline()
token = bytes.fromhex((io.recvline().decode()))
iv = xor(userMessage+tag, token)
print(token.hex())
print(iv.hex())
io.recvuntil(b">>> ")
forged = xor(adminMsg+adminTag, iv)[:-1]
io.sendline("1".encode())
io.recvuntil(b"Are you new ? (y/n) ")
io.sendline("n".encode())
io.recvuntil(b"Token: ")
io.sendline(forged.hex().encode())
io.recvuntil(b">>> ")
io.close()