--root / on the first compilation implies that we can read the flag from
/flag.txt, but sadly the main.pdf that would contain it is gone before the
second compilation.
The only generated file that persist is the timing json!
On the first compilation, we can read the flag and encode it somehow on the
timings json. Later, on the second compilation, as we have --root /tmp/, we
can read the “encoded” timings json. Finally we receive main.pdf.
A problem for the future is that we have to do all of this with a single Typst
document… but we’ll cover that later on.
In order to encode the flag into the timings json, there are probably infinite
ways. I took a very simple (and probably inefficient) way: I read one character
of the flag, then cast it to an integer, and generate as many PDF pages as the
casted integer. On the timings json, then we see N calls to handle page,
corresponding to the integer value of the char of the flag.
Now that we know what we want to do in each step:
Read the flag and generate N pages encoding the char of the flag into the timings json
Dump the previously generated timings json (the one holding our encoded char flag)
Let’s see how we do this in one single document.
Personally I used the touch I artificially added (I couldn’t find any other
way, but I would be happy to hear if there’s something else).
1
touch "$(echo${TMPDIR:-/tmp} 2>/dev/null)/0.json"
With this touch, on the document we can do:
1
2
3
4
5
6
7
#letn=read("0.json").len()#ifn<=0[// Case 1]else[// Case 2]
This is because the timings json is initially empty, and it gets initialized
for the first time after the first compilation, giving us a distinguiser
between “compilation 1” and “compilation 2”.
The final exploit file more or less looks like this: