Secure OCaml Sandbox
- Event: Plaid CTF 2021
- Category: pwn
- Solves: 33 + 28
- Points: 400 + 150
Overview
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 sos.ml
It looked like this:
Here is a source code for this module - it blacklists a lot of functions.
Solution
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.”
After calling 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.
So 01
must mean 2-bytes integer.
Playing with it we realized that these integers are represented in big endian mode.
And 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 0x0100000000000000
.
The blob after modifications is here
The exploit worked for both sub-challenges:
PCTF{d0nt_f0rget_t0_t3rminate_y0ur_l1nes}
PCTF{trY1ng_To_get_c4mlS_In_a_Lin3_is_a_r3cipe_f0r_cOrruPtion}
¯\(ツ)/¯