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 :
print(arg1, arg2, ...);
Print arguments to stdout, separated by space.load('file.js', obj);
Execute filefile.js
.obj
parameter is optional.obj
is a global namespace object. If not specified, a current global namespace is passed to the script, which allowsfile.js
to modify the current namespace.die(message);
Exit interpreter with the given error message.let value = JSON.parse(str);
Parse JSON string and return parsed value.let str = JSON.stringify(value);
Get string representation of the mJS value.let proto = {foo: 1}; let o = Object.create(proto);
Create an object with the provided prototype.'some_string'.slice(start, end);
Return a substring between two indices. Example:'abcdef'.slice(1,3) === 'bc';
'abc'.at(0);
Return numeric byte value at given string index. Example:'abc'.at(0) === 0x61;
'abc'.indexOf(substr[, fromIndex]);
Return index of first occurrence of substr within the string or-1
if not found. Example:'abc'.indexOf('bc') === 1;
chr(n);
Return 1-byte string whose ASCII code is the integern
. Ifn
is not numeric or outside of0-255
range,null
is returned. Example:chr(0x61) === 'a';
let a = [1,2,3,4,5]; a.splice(start, deleteCount, ...);
Change the contents of an array by removing existing elements and/or adding new elements. Example:let a = [1,2,3,4,5]; a.splice(1, 2, 100, 101, 102); a === [1,100,101,102,4,5];
let s = mkstr(ptrVar, length);
Create a string backed by a C memory chunk. A strings
starts at memory locationptrVar
, and islength
bytes long.let s = mkstr(ptrVar, offset, length, copy = false);
Likemkstr(ptrVar, length)
, but strings
starts at memory locationptrVar + offset
, and the caller can specify whether the string needs to be copied to the internal mjs buffer. By default, it’s not copied.let f = ffi('int foo(int)');
Import C function into mJS. See next section.gc(full);
Perform garbage collection. Iffull
istrue
, reclaim RAM to OS.
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 :
ffi()
mkstr()
s2o()
These function is used to call C function from JS. So we can’t use it to get RCE.
|
|
Write What Where
By doing simple issue search, we can find a Buffer Overflow Issue in the repository.
|
|
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.
I tried to simplified the payload and the simple version to trigger bug is
|
|
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.
We got the base binary address at -0xfe3f. Lets verify by using the following code.
|
|
Its seem we need to add 1 byte to the offset for the correct address.
|
|
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.
0x2c018 is offset from the base address.
Lets validate by leaking those address.
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.
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
|
|
|
|
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
|
|
|
|