§6.4. Скачиваем интернет

Иногда нам нужно автоматизировать и сетевые запросы. Когда речь идёт об обычных GET-запросах, с этим могут справиться различные расширения для браузера, но в любой более сложной ситуации без программирования не обойтись.

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

В этом параграфе мы изучим библиотеку requests — она позволяет совершать HTTP-запросы. В предыдущем параграфе мы её уже установили. Если нет, то это можно сделать с помощью pip: pip3 install requests.

Простые запросы

Давайте вернёмся в интерактивную консоль IPython и попробуем сделать простые запросы. Сначала импортируем библиотеку:

In [1]: import requests

В ответ ничего не вывелось, но теперь мы можем обращаться к функциям этой библиотеки. Попробуем сделать простой GET-запрос: для этого есть функция requests.get(). Эта функция принимает URL первым параметром.

In [2]: requests.get?
Signature: requests.get(url, params=None, **kwargs)
<...>

In [3]: requests.get("google.com")
<...>
MissingSchema: Invalid URL 'google.com': No schema supplied. Perhaps you meant http://google.com?

Ничего не получилось. Это произошло, поскольку мы не указали, какой протокол нужно использовать для выполнения запроса. Из текста ошибки мы можем понять, как её исправить — указать протокол.

Давайте попробуем узнать погоду за окном. Для этого воспользуемся сервисом wttr.in:

In [4]: requests.get("http://wttr.in/Khanty-Mansiysk?format=4")
Out[4]: <Response [200]>

Получили код ответа 200. Значит, страница загрузилась, но где же ответ? На самом деле, функция вернула специальный объект Response, содержащий все данные ответа — их нужно просто правильно достать. Для этого нам нужно сохранить его в переменную, а потом посмотреть его атрибуты.

In [5]: r = requests.get("http://wttr.in/Khanty-Mansiysk?format=4")

In [6]: r.status_code
Out[6]: 200

In [7]: r.headers
Out[7]: {'Server': 'nginx/1.10.3', 'Date': 'Thu, 30 Apr 2020 18:37:39 GMT', 'Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '54', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*'}

In [8]: r.text
Out[8]: 'Khanty-Mansiysk: ⛅️ 🌡️+6°C 🌬️↙7 km/h\n'

Мы можем передавать произвольные заголовки или куки. Для этого нам понадобятся именованные аргументы — такие аргументы у функций, у которых есть имя. Перед аргументом мы напишем его имя и знак равно: func(a=2) вызовет функцию func с аргументом a, равным 2. Для заголовков используется аргумент headers, а для кук — cookies. Например, таким образом мы можем получить условие любого задания с борды:

In [9]: r = requests.get("https://board.course.ugractf.ru/tasks/allfields/flag_
   ...: iframe", cookies={"session": "eyJfZnJlc2gi0mIabCDeFGHij3JmX..."})

Чтобы этот фрагмент кода заработал, необходимо подставить свою сессионную куку в поле session. Она будет немного длиннее, чем приведённая в примере. Узнать её можно из консоли разработчика в браузере.

Создание мемов

Перейдём к тому, ради чего вообще создан весь этот курс — сгенерируем мем. Для этого воспользуемся бесплатным сервисом сайта Imgflip. Нам понадобится аккаунт на этом сайте. После регистрации мы сможем делать запросы на генерацию мемов. Данный сайт принимает POST-запросы. В качестве параметров передадим номер шаблона из галереи, наши логин и пароль, а также тексты. Параметры POST-запроса передаются в аргументе data. Давайте попробуем сделать такой запрос:

In [10]: r = requests.post("https://api.imgflip.com/caption_image", data={
    ...:     "template_id": "124822590",
    ...:     "username": "user",
    ...:     "password": "pass",
    ...:     "text0": "писать полезные программы",
    ...:     "text1": "генерировать мемы"
    ...: })

In [11]: r.text
Out[11]: '{"success":true,"data":{"url":"https:\\/\\/i.imgflip.com\\/3zd25d.jpg","page_url":"https:\\/\\/imgflip.com\\/i\\/3zd25d"}}'

Ответ пришёл довольно нечитаемый. По обилию фиругных скобок и кавычек можно понять, что это формат JSON. Чтобы показать его в немного более читаемом виде, вместо атрибута .text воспользуемся методом .json():

In [12]: r.json()
Out[12]:
{'success': True,
 'data': {'url': 'https://i.imgflip.com/3zd25d.jpg',
  'page_url': 'https://imgflip.com/i/3zd25d'}}

Если мы перейдем по полученной ссылке в браузере, то увидим мем. Его можно даже скачать:

In [13]: r = requests.get("https://i.imgflip.com/3zd25d.jpg")

In [14]: f = open("image.jpg", "wb")

In [15]: f.write(r.content)
Out[15]: 47554

In [16]: f.close()

Мы сделали запрос к файлу с картинкой, открыли файл для записи и записали туда ответ сервера. Теперь наш мем сохранён в файле image.jpg.

Автоматические запросы

Все эти трюки можно было сделать и вручную. Теперь рассмотрим случаи, когда без программирования не обойтись. Предположим, нам нужно сделать 2020 запросов на одну и ту же страницу. Это задание доступно по ссылке refresh.course.ugractf.ru.

Если мы перейдем в браузере по этому адресу и обновим страницу пару раз, то счётчик уменьшится. Дело за малым — сделать запрос 2020 раз. Однако, если мы попробуем повторить всё в IPython, то ничего не выйдет:

In [17]: requests.get("https://refresh.course.ugractf.ru").text
Out[17]: '<meta charset="utf-8"/><p>Чтобы найти флаг, тебе нужно открыть эту страницу ещё <b>2020</b> раз.</p>'

In [18]: requests.get("https://refresh.course.ugractf.ru").text
Out[18]: '<meta charset="utf-8"/><p>Чтобы найти флаг, тебе нужно открыть эту страницу ещё <b>2020</b> раз.</p>'

Счётчик не уменьшается. Дело в том, что сервер, чтобы идентифицировать нас, передаёт куки с каждым запросом — если мы их не запоминаем, то о прогрессе не может быть и речи.

In [19]: requests.get("https://refresh.course.ugractf.ru").headers
Out[19]: {'Content-Type': 'text/html; charset=utf-8', 'Content-Length': '147', 'Vary': 'Cookie', 'Set-Cookie': 'session=eyJjb3VudCI6MjAxOX0.Xqs4Xg.qlWUINhJZlbLHKox335VMX-RK84; HttpOnly; Path=/', 'Server': 'nginx', 'Date': 'Fri, 1 May 2020 12:00:00 GMT'}

Чтобы не запоминать куки вручную, в requests есть специальный объект Session — он сам запоминает все куки от сервера и передаёт их в запросах. Пользоваться им очень просто:

In [20]: s = requests.Session()

In [21]: s.get("https://refresh.course.ugractf.ru").text
Out[21]: '<meta charset="utf-8"/><p>Чтобы найти флаг, тебе нужно открыть эту страницу ещё <b>2020</b> раз.</p>'

In [22]: s.get("https://refresh.course.ugractf.ru").text
Out[22]: '<meta charset="utf-8"/><p>Чтобы найти флаг, тебе нужно открыть эту страницу ещё <b>2019</b> раз.</p>'

Осталось немного — сделать запрос 2020 раз. Для этого воспользуемся циклом:

In [23]: s = requests.Session()

In [24]: for i in range(2020):
    ...:     r = s.get("https://refresh.course.ugractf.ru")
    ...:

In [25]: r.text
Out[25]: '<meta charset="utf-8"/><p>Чтобы найти флаг, тебе нужно открыть эту страницу ещё <b>1</b> раз.</p><p><i>Ты не думаешь, что нажимать <b>F5</b> много раз &ndash; не лучшая идея?</i></p>'

In [26]: s.get("https://refresh.course.ugractf.ru").text
Out[26]: '<meta charset="utf-8" /><p><b>Успех!</b> Держи флаг: <b>ugra_i_can_do_many_requests</b>.</p>'

Таким образом, мы решили этот таск, написав всего пять строк кода.

TCP-сервисы

Кроме HTTP, иногда встречаются и TCP-сервисы. В CTF вы их часто можете встретить по слову nc:

Команда nc (network cat) предназначена для общения с сервером по протоколу TCP. С помощью этого протокола клиент и сервер могут обмениваться обычным текстом. Попробуйте в консоли сделать HTTP-запрос вручную:

$ nc course.ugractf.ru 80
GET / HTTP/1.1
Host: course.ugractf.ru

В конце нужно нажать Enter дважды, чтобы увидеть ответ. Мы увидим примерно то же самое, что получает curl. Однако, чаще nc нам встретится в сервисах, которые общаются не по HTTP:

Чтобы удобнее работать с такими запросами, можно использовать библиотеку pwntools. Её мы также скачаем из pip: pip3 install pwntools. Давайте попробуем сделать такой же запрос, как и в прошлом примере. Нам понадобится объект remote для поддержки удалённого соединения. Метод .sendline() отправляет строку на сервер, а метод .recvuntil() — получает данные с сервера, пока не придёт определенная строка.

In [27]: import pwn
[*] Checking for new versions of pwntools
    To disable this functionality, set the contents of /home/upml/.pwntools-cache-3.6/update to 'never'.
[*] You have the latest version of Pwntools (4.0.1)

In [28]: r = pwn.remote('course.ugractf.ru', 80)
[x] Opening connection to course.ugractf.ru on port 80
[x] Opening connection to course.ugractf.ru on port 80: Trying 95.217.155.244
[+] Opening connection to course.ugractf.ru on port 80: Done

In [29]: r.sendline('GET / HTTP/1.1')

In [30]: r.sendline('Host: course.ugractf.ru')

In [31]: r.sendline('')

In [32]: r.recvuntil('</html>')
Out[32]: b'HTTP/1.1 301 Moved Permanently\r\nServer: nginx\r\nDate: Thu, 30 Apr 2020 21:11:01 GMT\r\nContent-Type: text/html\r\nContent-Length: 162\r\nConnection: keep-alive\r\nLocation: https://course.ugractf.ru/\r\n\r\n<html>\r\n<head><title>301 Moved Permanently</title></head>\r\n<body>\r\n<center><h1>301 Moved Permanently</h1></center>\r\n<hr><center>nginx</center>\r\n</body>\r\n</html>\r\n'

Примечание

  • Существует и метод .recv(). Однако, он возвращает только те данные, которые сервер уже успел прислать. Если сервер немного задерживается с ответом, метод ничего не вернёт. В отличие от него, .recvuntil() будет ждать данные с сервера, пока не получит нужную строку.

У объектов соединения remote есть и много других методов, которые облегчают работу с TCP-соединениями. Используя их, мы можем легко общаться с любым сервером.

Выводы

  1. Программирование можно применять и для того, чтобы автоматизировать интернет-запросы.

  2. В языке Python для этого применяется библиотека requests.

  3. Чтобы не думать о хранении и обработке кук, можно использовать объект Session.

  4. Не все сервисы работают по HTTP. Общаться с любым TCP-сервисом можно с помощью библиотеки pwntools.

§6.5. Картинки ⟶