-
Notifications
You must be signed in to change notification settings - Fork 2
/
taintmode.py
569 lines (457 loc) · 16.6 KB
/
taintmode.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
# -*- coding: utf-8 -*-
'''
Taint Mode for Python via a Library
Copyright 2009 Juan José Conti
Copyright 2010 Juan José Conti - Alejandro Russo
This file is part of taintmode.py
taintmode is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
taintmode.py is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with taintmode.py. If not, see <http://www.gnu.org/licenses/>.
'''
import inspect
import sys
from itertools import chain
__version__ = 'trunk-svn-2'
__all__ = ['tainted', 'taint', 'untrusted', 'untrusted_args', 'ssink',
'validator', 'cleaner', 'STR', 'INT', 'FLOAT', 'UNICODE', 'chr',
'ord', 'len', 'ends_execution', 'XSS', 'SQLI', 'OSI', 'II']
ENDS = False
RAISES = False
KEYS = [XSS, SQLI, OSI, II] = range(1, 5)
TAGS = set(KEYS)
class TaintException(Exception):
pass
def ends_execution(b=True):
global ENDS
ENDS = b
# ------------------------- Taint-aware functions -----------------------------
def propagate_func(original):
def inner (*args, **kwargs):
t = set()
for a in args:
collect_tags(a, t)
for v in kwargs.values():
collect_tags(v, t)
r = original(*args, **kwargs)
if t:
r = taint_aware(r, t)
return r
return inner
len = propagate_func(len)
ord = propagate_func(ord)
chr = propagate_func(chr)
# ------------------------- Auxiliaries functions -----------------------------
def mapt(o, f, check=lambda o: type(o) in tclasses):
if check(o):
return f(o)
elif isinstance(o, list):
return [mapt(x, f, check) for x in o]
elif isinstance(o, tuple):
return tuple(mapt(x, f, check) for x in o)
elif isinstance(o, set):
return set(mapt(x, f, check) for x in o)
elif isinstance(o, dict):
klass = type(o) # It's quite common for frameworks to extend dict
# with useful new methdos - i.e. web.py
return klass((k, mapt(v, f, check)) for k, v in o.iteritems())
else:
return o
def remove_taint(v):
def _remove(o):
if hasattr(o, 'taints'):
o.taints.discard(v)
return _remove
def remove_tags(r, v):
mapt(r, remove_taint(v), lambda o: True)
def collect_tags(s, t):
'''Collect tags from a source s into a target t.'''
mapt(s, lambda o: t.update(o.taints), lambda o: hasattr(o, 'taints'))
def update_tags(r, t):
mapt(r, lambda o: o.taints.update(t), lambda o: hasattr(o, 'taints'))
def taint_aware(r, ts=set()):
r = mapt(r, tclass)
update_tags(r, ts)
return r
# ------------------------- Decorators ----------------------------------------
def untrusted_args(nargs=[], nkwargs=[]):
'''
Mark a function or method that would recive untrusted values.
nargs is a list of positions. Positional arguments in that position will be
tainted for all the types of taint.
nkwargs is a list of strings. Keyword arguments for those keys will be
tainted for all the types of taint.
Some frameworks works as follow: they ask the programmer to write certain
function or method in such a way that then the framework itself use it to
pass to the program values received from the outside. Twisted, the framework
for building network applications, gives us an accurate example when you
extend the LineOnlyReceiver class:
.. code-block:: python
class MyProtocol(LineOnlyReceiver):
def lineReceived(self, line):
self.doSomething(line) # line is a tainted value
It's complicated or even impossible use the untrusted decorator in this kind
of situations. Instead of it, you should use the untrusted_args decorator,
which receives as an optional argument a list of non-trusted arguments positions
and a list of keywords. The line parameter in the example can be marked as
untrusted in this way:
.. code-block:: python
class MyProtocol(LineOnlyReceiver):
@untrusted_args([1])
def lineReceived(self, line):
self.doSomething(line)
Example
=======
>>> import taintmode
>>> from taintmode import *
>>> @ssink(OSI)
... def exec_comand(cm):
... print "executing ", cm
...
>>> @untrusted_args([1])
... def rutine(auxvalue, command):
... exec_comand(command)
... print auxvalue
...
>>> rutine(42,"rm -r /")
===============================================================================
Violation in line 3 from file <doctest __main__.untrusted_args[3]>
Tainted value: rm -r /
-------------------------------------------------------------------------------
print auxvalue
<BLANKLINE>
===============================================================================
executing rm -r /
42
'''
def _untrusted_args(f):
def inner(*args, **kwargs):
args = list(args) # args is a tuple - add a test
for n in nargs:
args[n] = mapt(args[n], taint)
for n in nkwargs:
kwargs[n] = mapt(kwargs[n], taint)
r = f(*args, **kwargs)
return r
return inner
return _untrusted_args
def untrusted(f):
'''
Mark a function or method as untrusted.
The returned value will be tainted for all the types of taint.
Examples
========
If you have access to the function or method definition, for example if it's part
of your code-base the decorator can be applied using Python's syntactic sugar:
>>> @untrusted
... def from_outside():
... return 'a string' #this value is untrusted
...
>>> value = from_outside()
>>> value
'a string'
>>> type(value)
<class '__main__.tklass'>
>>> tainted(value)
True
While using third-party modules, we still can apply the decorator. The next
example is from a program writed using the web.py framework:
>>> import web
>>> web.input = untrusted(web.input)
'''
def inner(*args, **kwargs):
r = f(*args, **kwargs)
return taint_aware(r, TAGS)
return inner
def validator(v, cond=True, nargs=[], nkwargs=[]):
'''
Mark a function or method as capable to validate its input.
nargs is a list of positions. Positional arguments in that positions are
the ones validated.
nkwargs is a list of strings. Keyword arguments for those keys are the ones
validated.
If the function returns cond, v will be removed from the the validated
inpunt.
Example:
for a function called invalid_mail, cond is liked to be False. If
invalid_mail returns False, then the mail IS valid and have no craft data
on it.
for a function called valid_mail, cond is liked to be True.
'''
def _validator(f):
def inner(*args, **kwargs):
r = f(*args, **kwargs)
if r == cond:
tovalid = set(args[n] for n in nargs)
tovalid.update(kwargs[n] for n in nkwargs)
for a in tovalid:
remove_tags(a, v)
return r
return inner
return _validator
def cleaner(v):
'''
Mark a function or methos as capable to clean its input.
:param v: taint removed from the returned value.
Example
=======
>>> @cleaner(SQLI) #this make the following function capable to clean objets from SQLI taints
... def clean_string(stri):
... return stri.split(' ')[0]
...
>>> @untrusted #simulates an utrusted value
... def get_id_employee():
... return "21 or 1=1"
...
>>> @ssink(SQLI) #simulate a rutine that will execute an sql query
... def erase_employee(id):
... print "this value this value is placed in the WHERE clause of a sql delete statment:" , id
>>> i=get_id_employee()
>>> erase_employee(i)
===============================================================================
Violation in line 1 from file <doctest __main__.cleaner[4]>
Tainted value: 21 or 1=1
-------------------------------------------------------------------------------
--> erase_employee(i)
<BLANKLINE>
===============================================================================
this value this value is placed in the WHERE clause of a sql delete statment: 21 or 1=1
>>> i = clean_string(i) # the untrusted value is clean now
>>> i
'21'
>>> erase_employee(i)
this value this value is placed in the WHERE clause of a sql delete statment: 21
'''
def _cleaner(f):
def inner(*args, **kwargs):
r = f(*args, **kwargs)
remove_tags(r, v)
return r
return inner
return _cleaner
def reached(t, v=None):
'''
Execute if a tainted value reaches a sensitive sink
for the vulnerability v.
If the module-level variable ENDS is set to True, then the sink is not
executed and the reached function is executed instead. If ENDS is set to False,
the reached function is executed but the program continues its flow.
The provided de facto implementation alerts that the violation happened and
information to find the error.
'''
frame = sys._getframe(3)
filename = inspect.getfile(frame)
lno = frame.f_lineno
print "=" * 79
print "Violation in line %d from file %s" % (lno, filename)
# Localize this message
print "Tainted value: %s" % t
print '-' * 79
lineas = inspect.findsource(frame)[0]
lineas = [' %s' % l for l in lineas]
lno = lno - 1
lineas[lno] = '--> ' + lineas[lno][4:]
lineas = lineas[lno - 3: lno + 3]
print "".join(lineas)
print "=" * 79
def ssink(v=None, reached=reached):
'''
Mark a function or method as sensitive to tainted data.
If it is called with a value with the v tag
(or any tag if v is None),
it's not executed and reached is executed instead.
These sinks are sensitive to a kind of vulnerability, and must be specified when
the decorator is used
Examples
========
The web.py framework offers SQL Injection sensitive sink examples:
>>> import web
>>> db = web.database(dbn="sqlite", db="DB_NAME")
>>> db.delete=ssink(SQLI)(db.delete)
>>> db.select = ssink(SQLI)(db.select)
>>> db.insert = ssink(SQLI)(db.insert)
Like the rest of decorators, if the sensitive sink is defined in our code, we can
use syntactic sugar:
@ssink(OSI):
def list_dir(input)
...
The decorator can also be used without specifying a vulnerability. In this case,
the sink is marked as sensitive to every kind of vulnerability, although this is not
a very common use case:
@ssink():
def very_sensitive(input):
...
'''
def _solve(a, f, args, kwargs):
if ENDS:
if RAISES:
reached(a)
raise TaintException()
else:
return reached(a)
else:
reached(a)
return f(*args, **kwargs)
def _ssink(f):
def inner(*args, **kwargs):
allargs = chain(args, kwargs.itervalues())
if v is None: # sensitive to ALL
for a in allargs:
t = set()
collect_tags(a, t)
if t:
return _solve(a, f, args, kwargs)
else:
for a in allargs:
t = set()
collect_tags(a, t)
if v in t:
return _solve(a, f, args, kwargs)
return f(*args, **kwargs)
return inner
return _ssink
def tainted(o, v=None):
'''
Tells if a value o, a tclass instance, is tainted for the given
vulnerability v.
If v is not provided, checks for all taints. If the value is tainted
with at least one vulnerability, returns True.
Example
=======
>>> t1 = taint(42, OSI)
>>> t2 = taint(42)
>>> tainted(t1)
True
>>> tainted(t1, OSI)
True
>>> tainted(t1, II)
False
>>> tainted(t2, II)
True
>>> tainted(t2, SQLI)
True
>>> tainted(t2)
True
'''
if not hasattr(o, 'taints'):
return False
if v is not None:
return v in o.taints
return bool(o.taints)
def taint(o, v=None):
'''
Helper function for taint the value o with the vulnerability v.
If v is not provided, taint with all types of taints.
Example: tainting with all vulnerabilities
==========================================
>>> t = taint(42)
>>> t.taints
set([1, 2, 3, 4])
>>> tainted(t, XSS)
True
>>> tainted(t, OSI)
True
>>> tainted(t, SQLI)
True
>>> tainted(t, II)
True
Example: tainting with Interpreter Injection vulnerability
==========================================================
>>> taint(42, II)
42
>>> t = taint(42, II)
>>> t.taints
set([4])
>>> tainted(t, II)
True
>>> tainted(t, OSI)
False
'''
ts = set()
if v is not None:
ts.add(v)
else:
ts.update(TAGS)
return taint_aware(o, ts)
# ------------------------- Taint-aware classes -------------------------------
def propagate_method(method):
def inner(self, *args, **kwargs):
r = method(self, *args, **kwargs)
t = set()
for a in args:
collect_tags(a, t)
for v in kwargs.values():
collect_tags(v, t)
t.update(self.taints)
return taint_aware(r, t)
return inner
def taint_class(klass, methods=None):
if not methods:
methods = attributes(klass)
class tklass(klass):
def __new__(cls, *args, **kwargs):
self = super(tklass, cls).__new__(cls, *args, **kwargs) #justificar analizar pq no init
self.taints = set()
# if any of the arguments is tainted, taint the object aswell
for a in args: # this CHUNK of code appears at least 3 times, refactor later
collect_tags(a, self.taints)
for v in kwargs.values():
collect_tags(v, self.taints)
return self
# support for assigment and taint change in classobj
def __setattr__(self, name, value):
if self.__dict__ and name in self.__dict__ and tainted(self.__dict__[name]):
for t in self.__dict__[name].taints:
# if other field had it, keep it
taintsets = [v.taints for k,v in self.__dict__.items() if not callable(v) and tainted(v) and k != name]
if not any([t in x for x in taintsets]):
self.taints.remove(t)
if self.__dict__ is not None:
self.__dict__[name] = value
if tainted(value):
self.taints.update(value.taints)
d = klass.__dict__
for name, attr in [(m, d[m]) for m in methods]:
if inspect.ismethod(attr) or inspect.ismethoddescriptor(attr):
setattr(tklass, name, propagate_method(attr))
# str has no __radd__ method
if '__add__' in methods and '__radd__' not in methods:
setattr(tklass, '__radd__', lambda self, other:
tklass.__add__(tklass(other), self))
# unicode __rmod__ returns NotImplemented
if klass == unicode:
setattr(tklass, '__rmod__', lambda self, other:
tklass.__mod__(tklass(other), self))
return tklass
dont_override = set(['__repr__', '__cmp__', '__getattribute__', '__new__',
'__init__','__nonzero__', '__reduce__', '__reduce_ex__',
'__str__', '__int__', '__float__', '__unicode__'])
# ------- Taint-aware classes for strings, integers, floats, and unicode ------
def attributes(klass):
a = set(klass.__dict__.keys())
return a - dont_override
str_methods = attributes(str)
unicode_methods = attributes(unicode)
int_methods = attributes(int)
float_methods = attributes(float)
STR = taint_class(str, str_methods)
UNICODE = taint_class(unicode, unicode_methods)
INT = taint_class(int, int_methods)
FLOAT = taint_class(float, float_methods)
tclasses = {str: STR, int: INT, float: FLOAT, unicode: UNICODE}
def tclass(o):
'''Tainted instance factory.'''
klass = type(o)
if klass in tclasses.keys():
return tclasses[klass](o)
else:
raise KeyError
if __name__ == "__main__":
import doctest
doctest.testmod()