During a security-focused week in August, I had the perfect environment to take a look at the security of the libraries bundled with one of my favorite languages, Erlang. The language itself follows a functional paradigm, so the connection with the outside world is made possible with so-called port drivers implemented in C. Suprisingly, very little code is written that way, and even that codebase is elegant, and seemed secure.
So I started poking at the libraries implemented in pure Erlang, priorizing network-facing modules, which are mostly concentrated in the inets OTP application. Latter contains clients and servers for some of the most used internet protocols, such as FTP, TFTP and HTTP. As they are written in pure Erlang, traditional binary vulnerabilities (such as buffer overflows) are almost impossible to find, so I tried looking for logical errors, using my previous experience with such services.
After some time, I started poking the HTTP server called inets:httpd. One
of the most common vulnerabilities with HTTP daemons serving static content is
the ability to access files outside the so-called document root, which is
called directory traversal. It's called that way, because most of the time,
these issues are exploited using repeated ../
or ..\
to move higher and
higher in the directory tree of the file system, slowly traversing out of the
“sandbox” called document root.
I started following the flow of control in the code, and found an elegant way of handling these issues, and at first sight, everything seemed OK with it. In the November 2010 version of lib/inets/src/http_server/httpd_request.erl the following check is made at line 316 (I whitespace-formatted the code a little bit for the sake of readability).
Path2 = [X || X <- string:tokens(Path, "/"), X =/= "."], %% OTP-5938
validate_path(Path2, 0, RequestURI)
The first line uses a construct called list comprehension, which is
syntactic sugar for generating and filtering a list, thus avoiding boilerplate
code to create a new list, some kind of loop, and a bunch of if
statements.
The string:tokens
splits the requested path at slashes, =/=
expresses
nonequivalence, so Path2
contains a list (array) of strings that represent
the parts of the URL except the ones that contain a single dot ("."
).
For example, a request for /foo/bar/./qux
results in Path2
being a list
with three strings in it: ["foo", "bar", "qux"]
.
The second line passes this list, a zero, and the requested URI to a
function called validate_path
, which can be found next to it at line 320.
validate_path([], _, _) ->
ok;
validate_path([".." | _], 0, RequestURI) ->
{error, {bad_request, {forbidden, RequestURI}}};
validate_path([".." | Rest], N, RequestURI) ->
validate_path(Rest, N - 1, RequestURI);
validate_path([_ | Rest], N, RequestURI) ->
validate_path(Rest, N + 1, RequestURI).
In Erlang, a function can be declared as many times as needed, and the runtime tries to match the arguments in order of declaration. Being a functional language, Erlang contains no such thing as a loop, but solves most of those problems with recursion. The first two declarations are the two possible exits of the function.
- If there are no (more) items in the list, the request is OK.
- If the second argument is zero, and the first item of the list is
".."
, the request is denied with an HTTP 403 Forbidden message.
The last two declarations are the ones processing the items, one by one.
- If the item is
".."
, the second argument is decremented. - In any other case, the second argument is incremented.
As you can see, it accepts URLs that contain ".."
parts, as long as they
don't lead outside the document root, by simply counting how deep the path
goes inside the document root. The first line has a comment, referring to a
ticket number (or similar), and by doing a little web search, I found the
Erlang OTP R10B release 10 README from 2006, which implies, that this
code was designed with directory traversal attacks in mind.
There's only one problem with this solution, and it's called Windows. Erlang runs on a number of platforms, and Windows is one of them – with the exception that it uses backslash to separate paths (although most APIs accept slashes too), which leads to the following ugly result.
$ curl -silent -D - 'http://192.168.56.3:8080/../boot.ini' | head -n 1
HTTP/1.1 403 Forbidden
$ curl -D - 'http://192.168.56.3:8080/..\boot.ini'
HTTP/1.1 200 OK
Server: inets/5.6
Date: Fri, 21 Oct 2011 17:11:45 GMT
Content-Type: text/plain
Etag: dDGWXY211
Content-Length: 211
Last-Modified: Thu, 06 Mar 2008 21:23:24 GMT
[boot loader]
timeout=30
default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional" /noexecute=optin /fastdetect
Erlang/OTP R14B04 has been released on October 5, 2011, and it contains the simple fix I wrote that closes the vulnerability. I'd like to thank everyone at hekkcamp 2011 for their help, especially Buherátor who helped me with testing.