Dominoes was a fun challenge. It was an introduction challenge to two harder ones if someone had only connected the dots :) The idea of the challenge comes from these two hard ones actually. I was glad that many teams solved it because that was my intention all along.
The player had to assemble a given set of puzzles into a meaningful sentence. Each domino had three letters on it. I didn't want the challenge to be too guessy so I included a hint in the challenge's description:
For example, from DOM, OMI, MIN, INO, NOE, OES you create:
DOM
OMI
MIN
INO
NOE
OES
After overlapping, you see the word DOMINOES
.
I used the backtracking
algorithm to find all the possible solutions where the correctness function is the existence of each word in the english top 10000
dictionary.
After running the algorithm, it produced 7296
correct solutions, which is a lot. Impossible to bruteforce all combinations when submitting the flag.
From reading the first lines of the produced file we can notice that each sentence consists of the same words but in the permutated order.
there_the_player_like_you_shall_solve_doubt_great_in_my_mind_that_puzzle_was_no_single
there_the_player_like_you_shall_solve_doubt_in_my_mind_that_great_puzzle_was_no_single
there_the_player_like_you_shall_solve_was_no_single_doubt_great_in_my_mind_that_puzzle
there_the_player_like_you_shall_solve_was_no_single_doubt_in_my_mind_that_great_puzzle
there_the_player_like_you_shall_single_was_no_solve_doubt_great_in_my_mind_that_puzzle
there_the_player_like_you_shall_single_was_no_solve_doubt_in_my_mind_that_great_puzzle
there_the_player_like_you_solve_doubt_great_in_my_mind_that_puzzle_was_no_shall_single
The idea of the challenge is to find the correct flag from these 7296
lines. From the challenge description
I suspect the key to be a semantically correct and meaningful sentence consisted of lowercase english words only. Can you help me uncover the key?
it was clear that the flag is in the unusual format, and that it consists of English words which when combined produce a grammatically correct meaningful sentence.
The approach I had in mind, was to reduce the number of lines via filtering out all "strange" sentences.
For example, I manually regex-removed the lines in the form:
Start: 7296
^.*(the_was).*$\n - 1632
^.*(the_you).*$\n - 1632
^.*(the_that).*$\n - 528
^.*(was_no_shall).*$\n - 1752
^.*(was_no_solve).*$\n - 875
^.*(solve_doubt_great).*$\n - 90
^the_there.*$\n - 40
^that_in_my_mind.*$\n - 86
^.*there_doubt_great.*$\n - 36
^.*that_great_in_my_mind.*$\n - 192
^.*player_like_there.*$\n - 28
^.*puzzle_the_doubt.*$\n - 12
^.*great_puzzle_player.*$\n - 13
^.*player_like_the_doubt.*$\n - 19
^.*player_like_doubt.*$\n - 65
^.*shall_solve_doubt.*$\n - 39
^.*player_like_the_puzzle.*$\n - 5
^.*there_doubt_in_my_mind.*$\n - 21
^.*was_no_single$\n - 116
^.*the_doubt_great.*$\n - 48
Left: 67
The above approach reduced the number of solutions to 67, from where it was relatively easy to find the best candidate for the flag, which is justCTF{there_was_no_single_doubt_in_my_mind_that_great_player_like_you_shall_solve_the_puzzle}
I've got a nice idea of combining Server-Side Template Injection (SSTI) and Cross-site Search (XS-Search) when solving a Safe Ninja
challenge from hack.cert.pl. In the challenge, you had a clear SSTI
in Jinja2
parser, but you couldn't use any of the '"()|
characters. I won't reveal the solution to the challenge here since it's quite different than what I will be presenting but rather present my idea. What I noticed was that I only needed a-z[space].
characters to solve the challenge. The idea was to use XS-Search and do injection like {{ O.o if title in request.cookies else 0 }}
which throws HTTP 500
(O is undefined) if title
was found in cookies
, and returns HTTP 200
otherwise.
My challenge was a bit more complex - SSTI wasn't that obvious and the charset was very limited (originally [\w\.\ ]
. You had a field in the form of scam.scammer.full_name
which was a hint for a possible SSTI. If you tried scam.scammer
you would notice an object displayed. During the CTF, when there were still 0 solves, I released a hint that the server runs on Python:2.7
which was helpful information to solve the challenge.
As mentioned already, the solution for the challenge was supposed to be a XS-Search. You could easily throw an error in Jinja2
by providing O.o
for instance. This would normally result in HTTP 500
but in my challenge instead of throwing the error, a message Hacking detected!
was displayed. The only difference in the response was the header X-Content-Type: nosniff
. This header means that strict content-type checking should be enabled and therefore scripts included in the page cannot arrive with text/html
MIME type.
Browsers do not turn the nosniff
feature on by default, so including a and html page Hacking detected
as a script will result in the following error Uncaught SyntaxError: Unexpected identifier
which can be caught using window.onerror
listener. Otherwise, the resource will be blocked by CORB
mechanism and will not throw any errors.
As for the SSTI, the player was supposed to first discover standard context variables such as config
where I hide PROMO_CODE
that gives a player more time of bot usage. Then, sending the payload O.o if request.args.p in request.cookies.session else g
as victim's type should give the URL like http://scam-generator.web.jctf.pro/gen_scam?scammer=5fea79a4-d1f2-464b-bac1-66c7808962ff&victim=49fb91c4-0e33-4304-89e5-8eb76e800da2. Now appending a parameter p=smth
will either return Hacking detected
if smth
was found in session
cookie or the SCAM message will be displayed otherwise.
The intended solution involved taking advantage of Dominoes
challenge, and requesting all triples from the range [a-f0-f-]
and then recover the session cookie from it. The character-by-character approach was supposed to be too slow, and the cookie was changing with each new admin's session (that guaranteed you could use multiple instances of the bot to solve the challenge in parts).
Then, after recovering the cookie, all you had to do was to log in as admin, copy scammer identity and display the template in gen_scam
, where the flag was hidden in scammer's phone number. justCTF{Oh_I_didnt_realize__GLOBAL__was_a_thing_without_parentheses_Hope_this_time_you_found_the_expected_XS-Search?!}
During the CTF, we got a message from a player that they are confused because a flag says something about xs-search
but they found the flag in globals… using a payload like scam.scammer.__class__.to_dict.__globals__.FLAG
. I acted very quickly and managed to clone the challenge before any other team solved it. In the original challenge, I disabled the intended solution by removing the flag from DB (instead it stated that the same approach should provide a correct flag in the harder challenge). This created two different challenges, which you couldn't both solve using the same payload. The patch in the FIXED version was to remove _
from allowed characters. The original flag was justCTF{XS-Search_is_s0o_fr3aKING_Aw3s0m3!!!11}
The idea for this challenge was this one-liner shellcode that I wrote when solving a challenge from one CTF.
read x;y=`md5sum $x`;echo $y;
The original challenge was about Local File Inclusion (LFI) and I used similar code to retrieve files from the server. I then tried wildcarded /etc/passwd
in the form of /etc/*wd
and to my surprise, it worked. I wasted some time before I realized that it was my script that was expanding the path locally and sending the expanded path to the server… I think it's a very funny bug so I decided to create a challenge that takes advantage of this funny hole in the code. Although you cannot achieve Remote Code Execution (RCE) by abusing the bug, you can pass wildcarded paths and flags to the command and with that, discover hidden files on the server. In the challenge, I created a vulnerable MD5
command that was calculating hashsum of the file from the provided path and safe READ
command that was just reading the file when the correct path was provided.
The idea of the challenge was to read md5service.py
file, from there read vulnerable code and discover a hint The flag is hidden somewhere on the server, it contains `flag` in its name
. Then running the following will display the prefix location of the flag.
Welcome to md5service!
I have two commands:
MD5 <file> -- will return a md5 for a file
READ <file> -- will read a file
Cmd: MD5 --tag /*/*flag*
Executing MD5 on '--tag /*/*flag*'
Result:
b'MD5 (/0c8702194e16f006e61f45d5fa\n'
With that, it's easy to bruteforce character after character and discover the full path of the flag which is /0c8702194e16f006e61f45d5fa0cd511/flag_a6214417905b7d091f00ff59b51d5d78.txt
More detailed write-up by Cr0w team.
I will not lie, this is not a challenge fully invented by me. The idea of the challenge comes from Dragon Sector team and was used during DS Finals in 2018, but I've done further math and extended the challenge to be more challenging and fun.
In the original challenge we could give any p
, q
and cipher
in the RSA algorithm and the server would respond whether it was deciphered to the chosen plaintext or not (after stripping trailing spaces). The intended solution was to use any p=hex(chosen_text+spaces)
, q=3
and cipher=p
. Without any math, you could basically solve the challenge with 75% chances just by trying this combination. Let's write some math:
This is how you decipher ciphertext in RSA algorithm. When using the combination mentioned above, we get:
Let say message=m
, the above eqation can be written as:
k
.
We can notice that when moduling the equation by mod(p)
, we get:
which implies m = lp
for some l
. When doing mod(3)
we get:
If p mod(3) = 1
, and we control that, it implies that m = lp + 1
for some l
and every d
, because:
We can quickly notice that p
fulfils both conditions m = 1p
and m mod(3) = 1
. From the Chinese Reminder Theorem, this is also the only solution in the adequate range.
If p mod(3) = 2
it is not true for every d
. That is because:
has two different solutions, depending on the d
. With 50% chances though we will hit the interesting number.
As we could notice, we would want to have
For q = 3
it was relatively easy, but I wanted to generalize the solutions to more numbers. Let's see what happens when q = p-1
. We get:
We see that p mod(p-1) = 1
and therefore we achieved exactly what we wanted:
It would be nice, but it's impossible for p
and p-1
to be both primes.
Let's see what happens for q= (p-1)/2
:
which we can write as:
m
. By multplying by 2
we get:
By doing mod(p-1)
we get:
While l
is smaller than p/2
it implies that l = 1
, and therefore we got once again:
So the solution is p
, q = (p-1)/2
, cipher = p
, while preserving the condition that q
is prime.
I believe that the challenge created by me is significantly different than the original one, hence knowing the solution for the original would not give a significant advantage to other teams. Instead of trailing spaces I used unsafe padding and created strict requirements for p
and q
to both be very strong primes. A well-explained solution (without math proof) can be found here. After solving the challenge, the flag would pop out justCTF{pure_math_is_better_than_insecure_crypto_:)_shout_out_to_DS_for_giving_me_the_idea!}