Блоґ одного кібера

Історія хвороби контуженого інформаційним вибухом

Елементарний захист від експлойтів у Python

leave a comment »

Кусочок перекладу Dive into Python 3 від Марка Пілгрима. Ліцензія cc-by-sa 3.0. Робота над перекладом проходить на wikibooks – можете приєднуватись. Поїхали!

…Після всіх тих незвичних маніпуляцій, ми отримуємо рядок на зразок '9567 + 1085 == 10652'. Але це рядок, а що доброго в рядку? Введіть eval() – універсальний інструмент обчислень в Python.

>>> eval('1 + 1 == 2')
True
>>> eval('1 + 1 == 3')
False
>>> eval('9567 + 1085 == 10652')
True

Але зачекайте, є ще! eval() не обмежується булевими виразами. Вона може обробляти будь-який вираз, та повертати будь-який тип даних.

>>> eval('"A" + "B"')
'AB'
>>> eval('"MARK".translate({65: 79})')
'MORK'
>>> eval('"AAAAA".count("A")')
5
>>> eval('["*"] * 5')
['*', '*', '*', '*', '*']

Але зачекайте, це ще не все!

>>> x = 5
>>> eval("x * 5") ① 
25 
>>> eval("pow(x, 2)") ② 
25 
>>> import math 
>>> eval("math.sqrt(x)") ③ 
2.2360679774997898

① Вираз який показує, що eval() може використовувати глобальні змінні описані за його межами. А також локальні змінні, якщо виклик відбувається з функції.

② Та функції.

③ Та модулі.

Ей, зачекайте хвилинку…

>>> import subprocess 
>>> eval("subprocess.getoutput('ls ~')") ① 
'Desktop         Library         Pictures \
 Documents       Movies          Public   \
 Music           Sites' 
>>> eval("subprocess.getoutput('rm /some/random/file')") ②

① Модуль subprocess дозволяє запускати консольні команди, та отримувати результат в вигляді рядка.

② Деякі консольні команди можуть мати незворотні наслідки.

Ситуація стає ще гіршою від того, що існує глобальна функція __import__(), яка отримує ім’я модуля як рядок, імпортує модуль, та повертає посилання на нього. Якщо це поєднати з силою eval() можна створити один єдиний вираз що знищить всі ваші файли.

>>> eval("__import__('subprocess').getoutput('rm /some/random/file')") ①

① Тепер уявіть вивід 'rm -rf ~'. Хоча насправді не буде ніякого виводу, але також у вас більше не буде ніяких файлів.

eval() – зло

Емм, злою частиною є обчислення довільних виразів з ненадійних джерел. Ви маєте використовувати eval() тільки на довірених джерелах. Звичайно, фокус в тому щоб визначити що є “довіреним”. Але є щось, що я знаю точно: ви НЕ маєте брати цей зручний калькулятор, та розміщувати його в інтернет як милий маленький веб-сервіс. Не робіть помилку думаючи, “Ох, функція робить багато маніпуляцій з рядками перед тим як почати їх обчислення; Важко уявити, як хтось може проексплоїтити це.” Хтось ВИЯСНИТЬ як протягнути небезпечний код крізь всі маніпуляції з рядками (ставались і дивніші речі), а після цього ви можете попрощатись з сервером.

Чи існує хоч якийсь спосіб безпечного обчислення виразів? Щоб поставити eval() в певні обмеження, за які він не зможе вийти, щоб зашкодити зовнішньому світу? Ну, насправді і так, і ні.

>>> x = 5 
>>> eval("x * 5", {}, {}) ① 
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "<string>", line 1, in <module> 
NameError: name 'x' is not defined 
>>> eval("x * 5", {"x": x}, {}) ② 
>>> import math 
>>> eval("math.sqrt(x)", {"x": x}, {}) ③ 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module> 
NameError: name 'math' is not defined

① Другий та третій параметри, що передаються функції eval(), стають глобальним та локальним просторами імен для виразу. В даному випадку вони обидва порожні. Це означає, що при обчисленні рядка "x * 5" немає посилання на змінну x ні в глобальному ні в локальному просторі імен, тому eval() вилітає з виключенням.

② Можна вибірково включати певні змінні в глобальний простір імен, перечислюючи їх індивідуально. Тоді ті, і тільки ті змінні будуть доступними під час обчислення.

③ Незважаючи на те, що ви імпортували модуль math, ви не додали його в простір імен, що передається функції eval(), тому обчислення не вдається.

Ех, це ж просто! Давайте я тепер зроблю криптоарифметичний веб-сервіс!

>>> eval("pow(5, 2)", {}, {}) ① 
25 
>>> eval("__import__('math').sqrt(5)", {}, {}) ② 
2.2360679774997898

① Незважаючи на те, що ви передали порожні словники, як глобальний та локальний простори імен, всі вбудовані функції Python досі доступні в процесі обчислень. Тому pow(5,2), працює, тому що 5 та 2 – літерали, а pow() – вбудована функція. ② На нещастя (і якщо ви не зрозуміли чому “на нещастя” – прочитайте розділ спочатку), функція __import__() – також є вбудованою, та також працює.

Ага, це означає, що ви все ще можете нашкодити, навіть якщо будете передавати в eval() порожні простори імен.

>>> eval("__import__('subprocess').getoutput('rm /some/random/file')", {}, {})

Ойойой, який я радий що ще не зробив веб-сервіс. Так чи існує якийсь спосіб безпечного використання eval()?

>>> eval("__import__('math').sqrt(5)", ...     {"__builtins__":None}, {}) ① 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module> 
NameError: name '__import__' is not defined 
>>> eval("__import__('subprocess').getoutput('rm -rf /')", ...     {"__builtins__":None}, {}) ② 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module> 
NameError: name '__import__' is not defined

① Щоб безпечно обчислити вирази яким не довіряєте, ви маєте описати глобальний простір імен, що присвоює "__builtins__" None, тобто відсутність будь-яких значень. Загалом всі вбудовані функції зберігаються всередині псевдо-модуля названого "__builtins__". Цей псевдо-модуль (набір вбудованих функцій) доступний функції eval() за замовчуванням, якщо ви не заміните його явно.

② Переконайтесь що ви перегрузили __builtins__. Не __builtin__, __built-ins__, чи ще якийсь варіант, що буде працювати прекрасно, але зовсім не зачепить набору вбудованих функцій.

То eval() тепер безпечний? Ну, і так і ні.

>>> eval("2 ** 2147483647",
...     {"__builtins__":None}, {})          ①

Навіть без доступу до __builtins__, все ще можна запустити DoS атаку. Наприклад спроба піднести 2 до великого степеня, повністю завантажить процесор вашого комп’ютера на деякий час. (Якщо ви пробуєте це в консолі інтерпритатора, натисніть Ctrl-C кілька разів, щоб перервати це). Технічно цей вираз швидко поверне значення, проте в той час ваш сервер не зможе робити нічого іншого.

В кінці кінців, можливо безпечно виконувати невідомі вирази Python, для певного визначення безпеки, яке виявляється не сильно корисним в реальному житті. Це нормально, якщо ви просто експериментуєте, і нормально, якщо ви передаєте на обчислення текст якому довіряєте. Але все інше – лиш напрошується на неприємності.

Advertisements

Written by bunyk

Квітень 9, 2010 at 12:17

Оприлюднено в Кодерство

Tagged with , , ,

Залишити відповідь

Заповніть поля нижче або авторизуйтесь клікнувши по іконці

Лого WordPress.com

Ви коментуєте, використовуючи свій обліковий запис WordPress.com. Log Out / Змінити )

Twitter picture

Ви коментуєте, використовуючи свій обліковий запис Twitter. Log Out / Змінити )

Facebook photo

Ви коментуєте, використовуючи свій обліковий запис Facebook. Log Out / Змінити )

Google+ photo

Ви коментуєте, використовуючи свій обліковий запис Google+. Log Out / Змінити )

З’єднання з %s

%d блогерам подобається це: