1#!/usr/bin/env python
2
3#
4#
5#
6
7import datetime
8import sys
9import logging
10import urllib
11import urlparse
12try:
13    import libinjection
14except:
15    pass
16
17from tornado import template
18import tornado.httpserver
19import tornado.ioloop
20import tornado.web
21import tornado.wsgi
22import tornado.escape
23import tornado.options
24
25def breakapart(s):
26    """ attempts to add spaces in a SQLi so it renders nicely on the webpage
27    """
28    return s.replace(',', ', ').replace('/*',' /*')
29
30# http://stackoverflow.com/questions/312443/how-do-you-split-a-list-into-evenly-sized-chunks-in-python
31def chunks(l, n):
32    """ Yield successive n-sized chunks from l.
33    """
34    for i in xrange(0, len(l), n):
35        yield l[i:i+n]
36
37def breakify(s):
38    output = ""
39    for c in chunks(s, 20):
40        output += c
41        if ' ' not in c:
42            output += ' '
43    return output
44
45def print_token_string(tok):
46    """
47    returns the value of token, handling opening and closing quote characters
48    """
49    out = ''
50    if tok.str_open != '\0':
51        out += tok.str_open
52    out += tok.val
53    if tok.str_close != '\0':
54        out += tok.str_close
55    return out
56
57def print_token(tok):
58    """
59    prints a token for use in unit testing
60    """
61    out = ''
62    if tok.type == 's':
63        out += print_token_string(tok)
64    elif tok.type == 'v':
65        vc = tok.count;
66        if vc == 1:
67            out += '@'
68        elif vc == 2:
69            out += '@@'
70        out += print_token_string(tok)
71    else:
72        out += tok.val
73    return (tok.type, out)
74
75def alltokens(val, flags):
76
77    if flags & libinjection.FLAG_QUOTE_SINGLE:
78        contextstr = 'single'
79    elif flags & libinjection.FLAG_QUOTE_DOUBLE:
80        contextstr = 'double'
81    else:
82        contextstr = 'none'
83
84    if flags & libinjection.FLAG_SQL_ANSI:
85        commentstr = 'ansi'
86    elif flags & libinjection.FLAG_SQL_MYSQL:
87        commentstr = 'mysql'
88    else:
89        raise RuntimeException("bad quote context")
90
91    parse = {
92        'comment': commentstr,
93        'quote': contextstr
94    }
95    args = []
96    sqlstate = libinjection.sqli_state()
97    libinjection.sqli_init(sqlstate, val, flags)
98    count = 0
99    while count < 25:
100        count += 1
101        ok = libinjection.sqli_tokenize(sqlstate)
102        if ok == 0:
103            break
104        args.append(print_token(sqlstate.current))
105
106
107    parse['tokens'] = args
108
109    args = []
110    fingerprint = libinjection.sqli_fingerprint(sqlstate, flags)
111    for i in range(len(sqlstate.fingerprint)):
112        args.append(print_token(libinjection.sqli_get_token(sqlstate,i)))
113    parse['folds'] = args
114    parse['sqli'] = bool(libinjection.sqli_blacklist(sqlstate) and libinjection.sqli_not_whitelist(sqlstate))
115    parse['fingerprint'] = fingerprint
116    # todo add stats
117
118    return parse
119
120class PageHandler(tornado.web.RequestHandler):
121    def get(self, pagename):
122        if pagename == '':
123            pagename = 'home'
124
125        self.add_header('X-Content-Type-Options', 'nosniff')
126        self.add_header('X-XSS-Protection', '0')
127
128        self.render(
129            pagename + '.html',
130            title = pagename.replace('-',' '),
131            ssl_protocol=self.request.headers.get('X-SSL-Protocol', ''),
132            ssl_cipher=self.request.headers.get('X-SSL-Cipher', '')
133        )
134
135class XssTestHandler(tornado.web.RequestHandler):
136    def get(self):
137        settings = self.application.settings
138
139        ldr = template.Loader(".")
140
141        args = ['', '', '', '', '', '', '', '', '', '']
142
143        qsl = [ x.split('=', 1) for x in self.request.query.split('&') ]
144        for kv in qsl:
145            print kv
146            try:
147                index = int(kv[0])
148                val = tornado.escape.url_unescape(kv[1])
149                print "XXX", index, val
150                args[index] = val
151            except Exception,e:
152                print e
153
154        self.add_header('Cache-Control', 'no-cache, no-store, must-revalidate')
155        self.add_header('Pragma', 'no-cache')
156        self.add_header('Expires', '0')
157        self.add_header('X-Content-Type-Options', 'nosniff')
158        self.add_header('X-XSS-Protection', '0')
159
160        self.write(ldr.load('xsstest.html').generate(args=args))
161
162class DaysSinceHandler(tornado.web.RequestHandler):
163    def get(self):
164        lastevasion = datetime.date(2013, 9, 12)
165        today       = datetime.date.today()
166        daynum = (today - lastevasion).days
167        if daynum < 10:
168            days = "00" + str(daynum)
169        elif daynum < 100:
170            days = "0" + str(daynum)
171        else:
172            days = str(daynum)
173
174        self.render(
175            "days-since-last-bypass.html",
176            title='libinjection: Days Since Last Bypass',
177            days=days,
178            ssl_protocol=self.request.headers.get('X-SSL-Protocol', ''),
179            ssl_cipher=self.request.headers.get('X-SSL-Cipher', '')
180        )
181
182class NullHandler(tornado.web.RequestHandler):
183    def get(self):
184        arg = self.request.arguments.get('type', [])
185        if len(arg) > 0 and arg[0] == 'tokens':
186            return self.get_tokens()
187        else:
188            return self.get_fingerprints()
189
190    def get_tokens(self):
191        ids = self.request.arguments.get('id', [])
192
193        if len(ids) == 1:
194            formvalue = ids[0]
195        else:
196            formvalue = ''
197
198        val = urllib.unquote(formvalue)
199        parsed = []
200        parsed.append(alltokens(val, libinjection.FLAG_QUOTE_NONE | libinjection.FLAG_SQL_ANSI))
201        parsed.append(alltokens(val, libinjection.FLAG_QUOTE_NONE | libinjection.FLAG_SQL_MYSQL))
202        parsed.append(alltokens(val, libinjection.FLAG_QUOTE_SINGLE | libinjection.FLAG_SQL_ANSI))
203        parsed.append(alltokens(val, libinjection.FLAG_QUOTE_SINGLE | libinjection.FLAG_SQL_MYSQL))
204        parsed.append(alltokens(val, libinjection.FLAG_QUOTE_DOUBLE | libinjection.FLAG_SQL_MYSQL))
205
206        self.add_header('Cache-Control', 'no-cache, no-store, must-revalidate')
207        self.add_header('Pragma', 'no-cache')
208        self.add_header('Expires', '0')
209        self.add_header('X-Content-Type-Options', 'nosniff')
210        self.add_header('X-XSS-Protection', '0')
211
212        self.render("tokens.html",
213                    title='libjection sqli token parsing diagnostics',
214                    version = libinjection.version(),
215                    parsed=parsed,
216                    formvalue=val,
217                    ssl_protocol=self.request.headers.get('X-SSL-Protocol', ''),
218                    ssl_cipher=self.request.headers.get('X-SSL-Cipher', '')
219                    )
220
221    def get_fingerprints(self):
222        #unquote = urllib.unquote
223        #detectsqli = libinjection.detectsqli
224
225        ids = self.request.arguments.get('id', [])
226        if len(ids) == 1:
227            formvalue = ids[0]
228        else:
229            formvalue = ''
230
231        args = []
232        extra = {}
233        qssqli = False
234
235        sqlstate = libinjection.sqli_state()
236
237        allfp = {}
238        for name,values in self.request.arguments.iteritems():
239            if name == 'type':
240                continue
241
242            fps = []
243
244            val = values[0]
245            val = urllib.unquote(val)
246            if len(val) == 0:
247                continue
248            libinjection.sqli_init(sqlstate, val, 0)
249            pat = libinjection.sqli_fingerprint(sqlstate, libinjection.FLAG_QUOTE_NONE | libinjection.FLAG_SQL_ANSI)
250            issqli = bool(libinjection.sqli_blacklist(sqlstate) and libinjection.sqli_not_whitelist(sqlstate))
251            fps.append(['unquoted', 'ansi', issqli, pat])
252
253            pat = libinjection.sqli_fingerprint(sqlstate, libinjection.FLAG_QUOTE_NONE | libinjection.FLAG_SQL_MYSQL)
254            issqli = bool(libinjection.sqli_blacklist(sqlstate) and libinjection.sqli_not_whitelist(sqlstate))
255            fps.append(['unquoted', 'mysql', issqli, pat])
256
257            pat = libinjection.sqli_fingerprint(sqlstate, libinjection.FLAG_QUOTE_SINGLE | libinjection.FLAG_SQL_ANSI)
258            issqli = bool(libinjection.sqli_blacklist(sqlstate) and libinjection.sqli_not_whitelist(sqlstate))
259            fps.append(['single', 'ansi', issqli, pat])
260
261            pat = libinjection.sqli_fingerprint(sqlstate, libinjection.FLAG_QUOTE_SINGLE | libinjection.FLAG_SQL_MYSQL)
262            issqli = bool(libinjection.sqli_blacklist(sqlstate) and libinjection.sqli_not_whitelist(sqlstate))
263            fps.append(['single', 'mysql', issqli, pat])
264
265            pat = libinjection.sqli_fingerprint(sqlstate, libinjection.FLAG_QUOTE_DOUBLE | libinjection.FLAG_SQL_MYSQL)
266            issqli = bool(libinjection.sqli_blacklist(sqlstate) and libinjection.sqli_not_whitelist(sqlstate))
267            fps.append(['double', 'mysql', issqli, pat])
268
269            allfp[name] = {
270                'value': breakify(breakapart(val)),
271                'fingerprints': fps
272            }
273
274        for name,values in self.request.arguments.iteritems():
275            if name == 'type':
276                continue
277            for val in values:
278                # do it one more time include cut-n-paste was already url-encoded
279                val = urllib.unquote(val)
280                if len(val) == 0:
281                    continue
282
283                # swig returns 1/0, convert to True False
284                libinjection.sqli_init(sqlstate, val, 0)
285                issqli = bool(libinjection.is_sqli(sqlstate))
286
287                # True if any issqli values are true
288                qssqli = qssqli or issqli
289                val = breakapart(val)
290
291                pat = sqlstate.fingerprint
292                if not issqli:
293                    pat = 'see below'
294                args.append([name, val, issqli, pat])
295
296        self.add_header('Cache-Control', 'no-cache, no-store, must-revalidate')
297        self.add_header('Pragma', 'no-cache')
298        self.add_header('Expires', '0')
299        self.add_header('X-Content-Type-Options', 'nosniff')
300        self.add_header('X-XSS-Protection', '0')
301
302        self.render("form.html",
303                    title='libjection sqli diagnostic',
304                    version = libinjection.version(),
305                    is_sqli=qssqli,
306                    args=args,
307                    allfp = allfp,
308                    formvalue=formvalue,
309                    ssl_protocol=self.request.headers.get('X-SSL-Protocol', ''),
310                    ssl_cipher=self.request.headers.get('X-SSL-Cipher', '')
311                    )
312
313import os
314settings = {
315    "static_path": os.path.join(os.path.dirname(__file__), "static"),
316    "template_path": os.path.join(os.path.dirname(__file__), "."),
317    "xsrf_cookies": False,
318    "gzip": False
319}
320
321application = tornado.web.Application([
322    (r"/diagnostics", NullHandler),
323    (r'/xsstest', XssTestHandler),
324    (r'/bootstrap/(.*)', tornado.web.StaticFileHandler, {'path': '/opt/bootstrap' }),
325    (r'/jquery/(.*)', tornado.web.StaticFileHandler, {'path': '/opt/jquery' }),
326    (r'/robots.txt', tornado.web.StaticFileHandler, {'path': os.path.join(os.path.dirname(__file__), "static")}),
327    (r'/favicon.ico', tornado.web.StaticFileHandler, {'path': os.path.join(os.path.dirname(__file__), "static")}),
328    (r"/([a-z-]*)", PageHandler)
329    ], **settings)
330
331
332if __name__ == "__main__":
333    tornado.options.parse_command_line()
334
335    logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(process)d %(message)s")
336
337    application.listen(8888)
338    tornado.ioloop.IOLoop.instance().start()
339
340