One challenge from PlaidCTF 2023, and the only one I wrote up: CSS-crimes, worth 200 points. It is a flag checker with no <script> tag anywhere in the page, which sounds impossible until you realise the checking is being done by the layout engine itself.
Rev/CSS-crimes - 200pts §
Description:
I found this locked chest at the bottom o’ the ocean, but the lock seems downright… criminal. Think you can open it? We recommend chrome at 100% zoom. Other browsers may be broken.
The challenge link here
First look §
At the first look, it seems like a simple flag checker, but after I inspect this page, there is no <script> tag here.11 With no script, the page cannot compute anything in the usual sense. Everything that follows is layout doing the work a function would normally do: sizes, positions and stacking order. That why this challenge little hard.
Approach §
After I did a quick check, I found the message “Correct” here, but this z-index:0. That mean it’s covered by some things.
Also, there is no <button> tag here, I was figout what how we can choose the letter, I found this:
There are 27*3 <details> tags and followed by 4 svgReference: the details element on MDN. An open box is taller than a closed one, and that height difference is the only signal the page has to work with.
So, when we choose letter, those <details> tag will open, it was check 3 characters 1 time. This mean, if we choosed 3 character _, all details tag will open:
So what does opening these tags mean to check flags?
When these details have different size, so, when we open one of these, the page size was changed, that mean it affect on the svg position (top position):
But what was that svg background image?
I changed the fill from #fff to #111(black). As we can see, it’s a image with a part missing in the middle.
I changed z-index of the correct message to 1, and it matches the missing part of our svg image.
We know we can move the image up and down, so how did it move?
Let me example with the first image, this is base positon of this image:
and the calculated top position was -300:
I changed the first letter from a to b, and here is the result:
It was changed from -300 to -160, call first letter as x we now coefficient of x is 140
Reset that letter to a, do the same with the 2nd character (y), we can see the top position was change fromm -300 to -140, that mean coefficient of y is 160
and coefficient of z is 20
And we know exactly the pixel was change of the first image:
d = 140x + 160y + 20z
but when the top position >= 80, they will reset to base top positon (-300 as example), that mean we need modulo right side by 80 - (-300) = 380px.22 The image wraps back to its base position once it passes the top, so the offsets are modular. That is what turns each image into one linear equation with its own modulus.
But how we know what is the goal of top position in that case? I was do it by hand but it little hard, so I created the formula :
As we can see 62 was the first position the missing part was appear. And it same with the correct message.
So the correct top position of this image was (60 - 60) = 0
And the formula for this goal was: goal = 60 - start_postition
Do the same thing with 3 orther image we have this equations:
####################------------------------- test, first brute
start = -300
goal = 0
mod = (goal - start) + 100
import string
m = string.ascii_lowercase + '_'
for x in range(27):
for y in range(27):
for z in range(27):
if (140*x+160*y+20*z)%380 == (0 - (-300))\
and (60*x +100*y+20*z)%220 == (20 - (-140))\
and (320*x+80*y+ 20*z)%460 == (380 - 380)\
and ( 300*x+200*y+20*z)%340 == (-60 - (-260)):
print_chr(x,y,z)
#output : "you"
Solve this stuff §
So we confirm that the 3 letter was correct, all you need to do is find the remaining characters
Each 3 letter, we need to find the following number:
4 start top position of 4 image
start = [-180,-300,-140,-260]
The top position of image when we change the first letter from a to b is present in p1[0], p2[0],… in order to caculate coefficient of x later.
And similar with the second letter. Also, cofficient of z alway 20.
p1,p2,p3,p4 = [
[-160,-160,20],
[-160,-140,20],
[-80,-40,20],
[40,-60,20],
]
And the goal are the first position the missing part was appear. (rounded)
goal = [80,300,200,160]
Put things together:
def brute(start,p1,p2,p3,p4,goal):
p1[0],p1[1] = p1[0] - start[0], p1[1] - start[0] # calc the coefficient of x, y of equation 1
p2[0],p2[1] = p2[0] - start[1], p2[1] - start[1]
p3[0],p3[1] = p3[0] - start[2], p3[1] - start[2]
p4[0],p4[1] = p4[0] - start[3], p4[1] - start[3]
goal = [60 - i for i in goal] # calc goal of top positio
mod1,mod2,mod3,mod4 = [0 - start[i]+80 for i in range(4)] # find the mod
dis1,dis2,dis3,dis4 = [goal[i] - start[i] for i in range(4)] #find the correct distance
for x in range(27):
for y in range(27):
for z in range(27):
if (p1[0]*x+p1[1]*y+p1[2]*z)%mod1 == dis1\
and (p2[0]*x+p2[1]*y+p2[2]*z)%mod2 == dis2\
and (p3[0]*x+p3[1]*y+p3[2]*z)%mod3 == dis3\
and (p4[0]*x+p4[1]*y+p4[2]*z)%mod4 == dis4:
print_chr(x,y,z)
Do the same with the remaining characters we will get flag.
Full script here: https://github.com/lephuduc/CTFs-Honors/blob/main/The%20Plaid%20Flag/solve.py















