- Event: Plaid CTF 2021
- Category: pwn
- Solves: 33 + 28
- Points: 400 + 150
This challenge consisted of 2 sub-challenges.
The second one was protected where password was a flag from the pevious one.
Files for the first part are here.
sudo docker run -v $(pwd)/input:/input -it name runs the challenge.
In this challenge we had to read the flag by uploading OCaml script to the server.
The path to the fflag file is known from a Docker file.
It is sandboxed in a way that
open! Sos is added at the very beginning of our script.
It means that we load a module which filename is
It looked like this:
Here is a source code for this module - it blacklists a lot of functions.
In OCaml, similarly to Rust, functions are split into safe and unsafe ones.
Unsafe functions can lead to memory corruption bugs or other problems and usually they have
unsafe word in their name.
But grep checks if our code contains this word.
At the beginning I was trying to find an “soundness”/memory corruption bug in OCaml interpreter by reviewing github issues but my teammates told me that this is an old language so the probability of this is pretty low.
So I started looking for not blacklisted functions which could give a flag.
https://ocaml.org/api/ lists libraries and their functions for OCaml. We reviewed all of them and found 2 interesting functions:
- Digest.file which takes a filename as an argument and returns md5 of it.
- Stdlib.input_value - “This function is identical to Marshal.from_channel; see the description of module Marshal for more information, in particular concerning the lack of type safety.”
Digest.file the process should contain a flag in its memory, maybe somewhere on the heap.
Stdlib.input_value receives our payload which is a marshalled big buffer which has size and capacity to big values allowing us to read process memory.
Here is the OCaml script which we send to the server:
This is just a script that sends our script and a payload:
Now it’s time to think about the payload which will be sent to the server. At the beginning let’s create a binary blob of the normal buffer:
This blob can be found here
We got a binary blob with
01 06 66 01 06 66 04 01 At the end.
06 66 is an equivalent to our buffer size, these values are probably the size and the capacity.
01 must mean 2-bytes integer.
Playing with it we realized that these integers are represented in big endian mode.
03 means 8 bytes integer.
By changing it to very big values - like
03 01 00 00 00 00 00 00 00 03 01 00 00 00 00 00 00 00 04 01,
we got a Buffer which pointer points to a string with the length of
0x666 bytes and size and capacity has set to
The blob after modifications is here
The exploit worked for both sub-challenges: