Се­год­ня мы порабо­таем с фай­ловой сис­темой ОС — научим­ся ходить по катало­гам, откры­вать и изме­нять фай­лы. Затем осво­им могущес­твен­ные зак­линания под наз­вани­ем «регуляр­ные выраже­ния», изу­чим тон­кости соз­дания и вызова фун­кций и под конец напишем прос­тень­кий ска­нер SQL-уяз­вимос­тей. И все это в одном нед­линном уро­ке!

От редакции

Эта статья — часть цик­ла «Python с абсо­лют­ного нуля», где мы рас­ска­зыва­ем об азах Python в нашем фир­менном нес­кучном сти­ле. Ты можешь читать их по поряд­ку или выбирать какие‑то области, которые хотел бы под­тянуть.

Пер­вые два уро­ка дос­тупны целиком без плат­ной под­писки. Этот — поч­ти целиком: за исклю­чени­ем пос­ледне­го при­мера и домаш­него задания.

 

Работаем с файлами

Нач­нем, как всег­да, с нес­ложных вещей. В Python есть модуль с лаконич­ным наз­вани­ем os, который (ты не поверишь!) пред­назна­чен для вза­имо­дей­ствия прог­раммы с опе­раци­онной сис­темой, в том чис­ле для управле­ния фай­лами.

Пер­вым делом, конеч­но, нуж­но импорти­ровать его в начале нашего скрип­та:

import os

И теперь нам откры­вают­ся раз­ные инте­рес­ные воз­можнос­ти. К при­меру, мы можем получить путь к текущей пап­ке. Сна­чала она сов­пада­ет с той, в которой ты был при запус­ке скрип­та (даже если сам скрипт находит­ся где‑то в дру­гом мес­те), но по ходу исполне­ния прог­раммы мы можем менять это зна­чение при помощи фун­кции os.chdir().

# Возвращает путь к текущей рабочей папке
pth=os.getcwd()
print(pth)
# Устанавливает путь к текущей рабочей папке, в данном случае это диск D:/
os.chdir(r'D:/')

info

Ес­ли ты работа­ешь в Windows, то в пути к фай­лу или пап­ке перед откры­вающей кавыч­кой ука­зывай бук­ву r (что озна­чает raw) или вмес­то одной косой чер­ты в пути ставь две.

Поп­робу­ем получить спи­сок фай­лов с рас­ширени­ем .py, находя­щих­ся в текущей дирек­тории. Для это­го исполь­зуем модули os и fnmatch.

import os
import fnmatch
# В цикле, с помощью os.listdir('.') получим список файлов
# в текущей директории (точка в скобках как раз ее и обозначает)
for fname in os.listdir('.'):
# Если у текущего имени файла расширение .py, то печатаем его
if fnmatch.fnmatch(fname, '*.py'):
print(fname)

Мо­дуль fnmatch поз­воля­ет искать в стро­ках опре­делен­ный текст, под­ходящий по мас­ке к задан­ному шаб­лону:

  • * заменя­ет любое количес­тво любых сим­волов;
  • ? заменя­ет один любой сим­вол;
  • [seq] заменя­ет любые сим­волы из пос­ледова­тель­нос­ти в квад­ратных скоб­ках;
  • [!seq] заменя­ет любые сим­волы, кро­ме тех, что при­сутс­тву­ют в квад­ратных скоб­ках.

Да­вай без­жалос­тно уда­лим какой‑нибудь файл:

import os
(os.remove(r'D:\allmypasswords.txt'))

Пе­реиме­нуем файл:

import os
os.rename('lamer.txt','xakep.txt')

А теперь соз­дадим пап­ку по ука­зан­ному пути и сра­зу же уда­лим ее. Для это­го при­годит­ся модуль shutil, где есть фун­кция rmtree(), которая уда­ляет пап­ку вмес­те с содер­жимым.

import os
import shutil
os.makedirs(r'D:\secret\beer\photo') # Создает все папки по указанному пути
shutil.rmtree(r'D:\secret\beer\photo') # Удаляет папку вместе с ее содержимым

До­пус­тим, ты хочешь получить спи­сок всех фай­лов, содер­жащих­ся в пап­ках по ука­зан­ному пути (учи­тывая вло­жен­ные пап­ки тоже), что­бы най­ти что‑то инте­рес­ное. Скрипт будет выг­лядеть сле­дующим обра­зом:

warning

Будь осто­рожен — скрипт в таком виде обша­рит весь диск D. Если он у тебя есть и там мно­го хла­ма, то про­цесс может затянуть­ся.

import os
for root, dirs, files in os.walk(r'D:'):
for name in files:
fullname = os.path.join(root, name)
print(fullname)
if('pass' in fullname):
print('Бинго!!!')

Фун­кция walk() модуля os при­нима­ет один обя­затель­ный аргу­мент — имя катало­га. Она пос­ледова­тель­но про­ходит все вло­жен­ные катало­ги и воз­вра­щает объ­ект‑генера­тор, из которо­го получа­ют:

  • ад­рес оче­ред­ного катало­га в виде стро­ки;
  • спи­сок имен под­катало­гов пер­вого уров­ня вло­жен­ности для дан­ного катало­га;
  • спи­сок имен фай­лов дан­ного катало­га.

info

Ге­нера­тор — это объ­ект, который сра­зу при соз­дании не вычис­ляет зна­чения всех сво­их эле­мен­тов. Этим генера­торы отли­чают­ся от спис­ков — те хра­нят в памяти все свои эле­мен­ты, и уда­лить их мож­но толь­ко прог­рам­мно. Вычис­ления с помощью генера­торов называ­ются ленивы­ми, они эко­номят память. Под­робнее мы рас­смот­рим генера­торы в сле­дующих уро­ках.

Сей­час покажу, как узнать раз­мер любого фай­ла, а так­же дату его модифи­кации.

import os.path
# Модуль для преобразования даты в приемлемый формат
from datetime import datetime
path = r'C:\Windows\notepad.exe'
# Получим размер файла в байтах
size = os.path.getsize(path)
# А теперь в килобайтах
# Две косые черты это целочисленное деление
ksize = size // 1024
atime = os.path.getatime(path)
# Дата последнего доступа в секундах с начала эпохи
mtime = os.path.getmtime(path)
# Дата последней модификации в секундах с начала эпохи
print ('Размер: ', ksize, ' KB')
print ('Дата последнего использования: ', datetime.fromtimestamp(atime))
print ('Дата последнего редактирования: ', datetime.fromtimestamp(mtime))

info

Для опе­раци­онных сис­тем Unix 1 янва­ря 1970, 00:00:00 (UTC) — точ­ка отсче­та вре­мени, или «начало эпо­хи». Чаще все­го вре­мя в компь­юте­ре вычис­ляет­ся в виде про­шед­ших с это­го момен­та секунд и лишь затем перево­дит­ся в удоб­ный для челове­ка вид.

Да­вай пошутим над юзе­ром: соз­дадим какой‑нибудь файл и будем пос­тоян­но его откры­вать с помощью той прог­раммы, которой этот файл обыч­но откры­вает­ся в сис­теме:

import os
# Модуль time понадобится для паузы, чтобы не слишком часто открывалось
import time
# Создаем текстовый файл
f=open('beer.txt','w',encoding='UTF-8')
f.write('СРОЧНО НАЛЕЙТЕ ХАКЕРУ ПИВА, ИНАЧЕ ЭТО НЕ ЗАКОНЧИТСЯ!!')
f.close()
while True:
# Открываем файл программой по умолчанию
os.startfile('beer.txt')
# Делаем паузу в одну секунду
time.sleep(1)

Ни­же при­веден спи­сок еще некото­рых полез­ных команд:

  • os.path.basename('путь') — воз­вра­щает наз­вание фай­ла или пап­ки в кон­це пути;
  • os.path.dirname('путь') — воз­вра­щает родитель­ский путь к объ­екту пути;
  • os.path.splitext('путь') — раз­деля­ет путь на путь и рас­ширение фай­ла;
  • os.path.exists('путь') — сущес­тву­ет ли путь до фай­ла или пап­ки;
  • os.path.isfile('путь') — явля­ется ли объ­ект пути фай­лом (сущес­тву­ющим);
  • os.path.isdir('путь') — явля­ется ли объ­ект пути пап­кой (сущес­тву­ющей).
 

Регулярные выражения

Ре­гуляр­ные выраже­ния — это спе­циаль­ные шаб­лоны для поис­ка и замены строк в тек­сте. Вооб­ще говоря, их впол­не мож­но счи­тать самос­тоятель­ным язы­ком, и его изу­чение выходит за рам­ки это­го цик­ла. Мы прой­дем­ся по самым осно­вам и по исполь­зованию регуля­рок в Python.

www

Под­робнее о регэк­спах ты можешь почитать в до­кумен­тации Python, в Ви­кипе­дии или в кни­ге Джеф­фри Фрид­ла, которая так и называ­ется — «Ре­гуляр­ные выраже­ния».

Мы не раз писали о полез­ных сай­тах, которые помога­ют работать с регуляр­ными выраже­ниями: CyberChef, RegExr, txt2re. Помимо это­го, можешь обра­тить вни­мание на сер­вис regex101.com и сайт RegexOne с инте­рак­тивным тре­наже­ром.

За работу с регуляр­ными выраже­ниями в Python отве­чает модуль re. Пер­вым делом импорти­руем его.

import re

В качес­тве прос­тей­шего пат­терна мы можем исполь­зовать какое‑нибудь сло­во. Пусть по тра­диции это будет «пиво»:

import re
pattern = r"пиво"
string = "Хакер знает, что пиво играет во взломе решающую роль. Свежее пиво ключ к сисадмину. Пока сисадмин ходит писать, можно сесть за его комп и внедрить троян."
result = re.search(pattern, string)
print(result.group(0))

Ко­ман­да re.search(pattern,string) ищет в тек­сте string пер­вое вхож­дение шаб­лона pattern и воз­вра­щает груп­пу строк, дос­туп к которым мож­но получить через метод .group(). Но коман­да search ищет толь­ко пер­вое вхож­дение шаб­лона. Поэто­му в нашем слу­чае вер­нется все­го один резуль­тат — сло­во «пиво», нес­мотря на то что в нашем тек­сте оно при­сутс­тву­ет дваж­ды.

Что­бы вер­нуть все вхож­дения шаб­лона в текст, исполь­зует­ся коман­да re.findall(pattern, string). Эта коман­да вер­нет спи­сок строк, которые при­сутс­тву­ют в тек­сте и сов­пада­ют с шаб­лоном.

import re
pattern = r"пиво"
string = "Хакер знает, что пиво играет во взломе решающую роль. Свежее пиво ключ к сисадмину. Пока сисадмин ходит писать, можно сесть за его комп и внедрить троян."
result = re.findall(pattern, string)
print(result)

info

Об­рати вни­мание, что шаб­лоны в регуляр­ных выраже­ниях име­ют буков­ку r перед началом стро­ки. Это так называ­емые сырые стро­ки, в которых не работа­ет сим­вол экра­ниро­вания с помощью обратно­го сле­ша \. При этом «сырая» стро­ка не может закан­чивать­ся этим сим­волом.

В пре­дыду­щих двух при­мерах прог­рамм в качес­тве шаб­лона pattern для поис­ка строк ты исполь­зовал прос­то какое‑то сло­во. Но мощь регуляр­ных выраже­ний не в этом. Ты можешь заменять час­ти шаб­лона спе­циаль­ными сим­волами, что­бы под шаб­лон под­ходили не толь­ко кон­крет­ные сло­ва, но и самые раз­ные стро­ки.

Да­вай, нап­ример, поп­робу­ем най­ти в тек­сте все сло­ва, которые начина­ются с «пи». Для это­го исполь­зуем спе­циаль­ный сим­вол \b — он озна­чает «начало сло­ва». Сра­зу пос­ле него ука­зыва­ем, с чего дол­жно начинать­ся сло­во, и напишем спе­циаль­ный сим­вол w, который озна­чает, что даль­ше в шаб­лоне дол­жны идти какие‑то бук­вы (плюс озна­чает, что их может быть одна или боль­ше) до тех пор, пока не встре­тит­ся небук­венный сим­вол (нап­ример, про­бел или знак пре­пина­ния). Шаб­лон будет выг­лядеть так: r"\bпи\w+".

import re
pattern = r"\bпи\w+"
string = "Хакер знает, что пиво играет во взломе решающую роль. Свежее пиво ключ к сисадмину. Пока сисадмин ходит писать, можно сесть за его комп и внедрить троян."
result = re.findall(pattern, string)
print(result)
Краткая справка по специальным символам
Крат­кая справ­ка по спе­циаль­ным сим­волам

Да­вай поп­робу­ем выпол­нить чуть более слож­ную задачу. Най­дем в тек­сте все email с доменом mail.ru, если они там есть.

import re
pattern = r"\b\w+@mail\.ru"
string = "Если вы хотите связаться с админом, пишите на почту admin@mail.ru. По другим вопросам обращайтесь на support@mail.ru."
result = re.findall(pattern, string)
print(result)

Как видишь, мы исполь­зовали тот же трюк, что и в прош­лый раз, — написа­ли спе­циаль­ный сим­вол \b, что­бы обоз­начить начало сло­ва, потом \w+, что зна­чит «одна или боль­ше букв», а затем @mail.ru, заэк­раниро­вав точ­ку, пос­коль­ку ина­че она будет озна­чать «любой сим­вол».

Час­то быва­ет нуж­но най­ти какой‑то эле­мент стро­ки, окру­жен­ный дву­мя дру­гими эле­мен­тами. Нап­ример, это может быть URL. Что­бы выделить ту часть шаб­лона, которую нуж­но вер­нуть, исполь­зуют­ся скоб­ки. При­веду при­мер, в котором ты получишь все адре­са ссы­лок из какого‑то кусоч­ка кода на HTML.

import re
string = 'Вы можете посмотреть карту сайта <a href="map.php">тут</a>. Посетите также <a href="best.php"раздел</a>'
pattern = r'href="(.+?)"'
result = re.findall(pattern,string)
print(result)

В коде выше исполь­зовал­ся пат­терн r'href="(.+?)" — в этом шаб­лоне иско­мая стро­ка начина­ется с href=" и закан­чива­ется еще одной двой­ной кавыч­кой. Скоб­ки нуж­ны для того, что­бы ука­зать, какую часть под­ходящей под шаб­лон стро­ки ты хочешь получить в перемен­ную result. Точ­ка и плюс внут­ри ско­бок ука­зыва­ют, что внут­ри кавычек могут быть любые сим­волы (кро­ме сим­вола новой стро­ки). Знак воп­роса озна­чает, что нуж­но оста­новить­ся перед пер­вой же встре­чен­ной кавыч­кой.

info

Знак воп­роса в регуляр­ных выраже­ниях исполь­зует­ся в двух нем­ного раз­ных смыс­лах. Если он идет пос­ле одно­го сим­вола, это зна­чит, что сим­вол может при­сутс­тво­вать или не при­сутс­тво­вать в стро­ке. Если же воп­роситель­ный знак идет пос­ле груп­пы сим­волов, это озна­чает «нежад­ный» (non-greedy) режим: такая регуляр­ка будет ста­рать­ся зах­ватить как мож­но мень­ше сим­волов.

Мы можем не толь­ко искать стро­ки, но и заменять их чем‑то дру­гим. Нап­ример, давай поп­робу­ем уда­лить из HTML-кода все теги. Для это­го исполь­зует­ся коман­да re.sub(pattern,'чем заменять',string).

import re
string = 'Вы можете посмотреть карту сайта <a href="map.php">тут</a>. Посетите также <a href="best.php"раздел</a>'
pattern = r'<(.+?)>'
result = re.sub(pattern,'',string)
print(result)

Прог­рамма напеча­тает стро­ку уже без тегов, так как мы замени­ли их пус­той стро­кой.

Ре­гуляр­ные выраже­ния — очень мощ­ная шту­ка. Осво­ив их, ты смо­жешь делать со стро­ками поч­ти все, что угод­но, а в сочета­нии с кодом на Python — бук­валь­но что угод­но. Для начала же можешь поэк­спе­римен­тировать и изме­нить какие‑то из при­веден­ных рецеп­тов.

 

Функции

Приш­ла пора под­робнее погово­рить о фун­кци­ях. Мы уже неод­нократ­но вызыва­ли раз­ные фун­кции — как встро­енные в Python (нап­ример, print()), так и из под­клю­чаемых модулей (нап­ример, urllib.request()). Но что такое фун­кция изнутри и как их делать самос­тоятель­но?

Пред­ставь, что у тебя есть какой‑то набор команд, которые нуж­но выпол­нять нес­коль­ко раз, изме­няя лишь вход­ные дан­ные. Такие бло­ки команд обыч­но выносят в отдель­ные кусоч­ки прог­раммы.

info

В объ­ектно ори­енти­рован­ном прог­рамми­рова­нии фун­кции явля­ются метода­ми какого‑либо клас­са и пишут­ся через точ­ку от его наз­вания.

s='Hello, xakep!'
print(s) # Функция
s.lower() # Метод

У фун­кции могут быть вход­ные парамет­ры — это одна или нес­коль­ко перемен­ных, которые пишут­ся в скоб­ках пос­ле име­ни фун­кции. При вызове фун­кции ты можешь передать ей аргу­мен­ты для этих парамет­ров. Некото­рые из парамет­ров могут быть необя­затель­ными или иметь зна­чение по умол­чанию — на слу­чай, если его не переда­дут.

Объ­явле­ние фун­кции начина­ется с клю­чево­го сло­ва def, далее сле­дует имя фун­кции, парамет­ры в скоб­ках и прог­рам­мный код, отде­лен­ный четырь­мя про­бела­ми. Фун­кция может воз­вра­щать одно или нес­коль­ко зна­чений с помощью клю­чево­го сло­ва return. Оно, кста­ти, прек­раща­ет работу фун­кции, и, если за ним идут какие‑то коман­ды, они будут про­пуще­ны.

Для при­мера раз­берем прос­тей­шую фун­кцию, которая будет при­нимать в качес­тве аргу­мен­тов два любых чис­ла и перем­ножать их, воз­вра­щая резуль­тат умно­жения. Назовем ее umn.

def umn(a, b):
c = a * b
return c

Те­перь, ког­да ты опи­сал фун­кцию, далее в этой же прог­рамме мож­но ее вызывать.

a = int(input('Введите первое число: '))
b = int(input('Введите второе число: '))
с = umn(a, b)
print(c)

Иног­да надо задать один из парамет­ров как необя­затель­ный, уста­новив для него зна­чение по умол­чанию.

def umn(a, b=10):
c = a * b
return c

Те­перь если ты вызовешь фун­кцию и не передашь ей вто­рой аргу­мент, то она прос­то будет счи­тать его рав­ным десяти, то есть будет умно­жать любое передан­ное чис­ло на десять.

с=umn(5)
print(c)

Нес­мотря на то что параметр b в дан­ном слу­чае равен по умол­чанию 10 и необя­зате­лен для переда­чи в качес­тве вто­рого аргу­мен­та, ты по‑преж­нему можешь переда­вать вто­рой аргу­мент, если это будет нуж­но, и тог­да в качес­тве b будет исполь­зовано не 10, а передан­ное зна­чение.

с=umn(5, b=20)
print(c)

Внут­ри прог­раммы мы можем вызывать соз­данную нами фун­кцию сколь­ко угод­но раз.

Да­вай соз­дадим прог­рамму, которая будет счи­тать при­бав­ку к зар­пла­те за каж­дую уяз­вимость, которую хакер нашел на работе. У каж­дого хакера будет своя зар­пла­та, в зависи­мос­ти от его ран­га, но начис­ление при­бав­ки для всех работа­ет по прин­ципу «+2% к базовой зар­пла­те за уяз­вимость, если таких уяз­вимос­тей най­дено боль­ше чем три».

Сде­лаем фун­кцию, которая при­нима­ет в качес­тве аргу­мен­тов раз­мер зар­пла­ты сот­рудни­ка и количес­тво най­ден­ных уяз­вимос­тей. Для округле­ния резуль­тата исполь­зуем фун­кцию round(), которая округлит при­бав­ку до целого чис­ла.

def pribavka(zarplata, bugs):
k = 0
if bugs > 3:
k = round((bugs - 3) * 0.02 * zarplata)
return k
a = int(input('Введите зарплату сотрудника: '))
b = int(input('Введите количество найденных им уязвимостей за месяц: '))
c = pribavka(a, b)
print(этом месяце прибавка к зарплате составит: ' + str(c))

Ес­ли фун­кция дол­жна воз­вра­щать боль­ше одно­го зна­чения, то мож­но перечис­лить их через запятую.

def myfunc(x):
a = x + 1
b = x * 2
return a, b

Фун­кция будет воз­вра­щать спи­сок, но мы можем сра­зу прис­воить воз­вра­щаемые зна­чения каким‑нибудь перемен­ным:

plusone, sum = myfunc(5)

Внут­ри фун­кций впол­не мож­но исполь­зовать перемен­ные, которые встре­чались в коде прог­раммы до вызова фун­кции. Но если внут­ри кода фун­кции задать перемен­ную с таким же име­нем, то эта перемен­ная авто­мати­чес­ки ста­нет локаль­ной и все даль­нейшие изме­нения будут про­исхо­дить с ней толь­ко в пре­делах фун­кции.

По­ясню на при­мере:

def boom(a, b):
z = 15
c = a * b * z
return c
z = 1
c = boom(15, 20)
print(z)

В резуль­тате выпол­нения прог­раммы ты уви­дишь еди­ницу. Почему? Внут­ри кода фун­кции мы прис­воили перемен­ной z зна­чение 15, и она ста­ла локаль­ной, и все изме­нения с ней будут про­исхо­дить внут­ри фун­кции, тог­да как в основной прог­рамме ее зна­чение будет по‑преж­нему рав­но еди­нице.

Это нем­ного труд­но понять, но на самом деле удоб­но. Если ты пишешь нес­коль­ко похожих фун­кций, то впол­не можешь исполь­зовать внут­ри них оди­нако­вые наз­вания локаль­ных перемен­ных, не опа­саясь, что они будут вли­ять как‑то друг на дру­га.

Пе­ремен­ные, объ­явленные вне фун­кций, называ­ются гло­баль­ными. Если ты хочешь изнутри фун­кции изме­нить одну из них, то объ­яви ее внут­ри фун­кции, исполь­зовав клю­чевое сло­во global.

def addfive(num):
global a
a += num
a = 5
addfive(3)
print(a)

Эта прог­рамма напеча­тает 8. Обра­ти вни­мание, что мы ничего не воз­вра­щали через return, а прос­то изме­нили гло­баль­ную перемен­ную. Кста­ти, фун­кция, в которой ничего не воз­вра­щает­ся через return, будет воз­вра­щать зна­чение None.

a = 5
print(addfive(3))

На экра­не выведет­ся сло­во None. Это быва­ет полез­но, если фун­кция воз­вра­щает что‑то толь­ко при выпол­нении каких‑то усло­вий, а если они не выпол­нены, то выпол­нение не доходит до return. Тог­да мож­но про­верить, не вер­нула ли она None.

def isoneortwo(num):
if(num==1):
return 'Один'
if(num==2):
return 'Два'
print(isoneortwo(1))
print(isoneortwo(2))
print(isoneortwo(3))

Эта фун­кция про­веря­ет, рав­но ли зна­чение еди­нице или двой­ке, и если не рав­но, то вер­нет None. Это мож­но даль­ше про­верить при помощи if:

if isoneortwo(3) is None:
print("Не 1 и не 2!")

Итак, мы научи­лись соз­давать фун­кции, вызывать их и воз­вра­щать из них парамет­ры, а так­же исполь­зовать внут­ри фун­кций гло­баль­ные перемен­ные. С это­го момен­та мы уже можем брать­ся за отно­ситель­но слож­ные при­меры!

 

Практика: проверка SQL-уязвимостей

На этот раз мы соз­дадим скрипт, который будет искать SQL-уяз­вимос­ти по раз­ным URL. Заранее соз­дадим файл urls.txt, в каж­дой строч­ке которо­го будут адре­са сай­тов, содер­жащие GET-парамет­ры. Нап­ример:

http://www.taanilinna.com/index.php?id=325
https://www.925jewellery.co.uk/productlist.php?Group=3&pr=0
http://www.isbtweb.org/index.php?id=1493

На­пишем скрипт, который получа­ет спи­сок подоб­ных URL из нашего фай­ла и добав­ляет в каж­дый из GET-парамет­ров знак кавыч­ки, пыта­ясь выз­вать ошиб­ки SQL баз дан­ных.

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    15 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии