Почти OCR для получения пароля VPNBook. PHP + Mikrotik

    Недавно VPNBook стал публиковать пароль вместо прямого текста в виде изображения. «Ну как же так» — подумал я и начал искать пути решения этой проблемы. Распознаем «картиночный» пароль VPNBook на PHP. И, конечно, скрипт для Mikrotik.

    Уже давно я у себя на роутере (Mikrotik) настроил автоматический бесплатный PPTP VPN туннель от VPNBook.com и успешно до недавнего времени им пользовался. Не буду вдаваться в подробности, они описаны в статье "Настраиваем автоматическое получение пароля для VPN на Mikrotik". До проблемы пароль для VPNBook можно было просто извлечь из html страницы, например, так:

    preg_match('/Password: <strong>([^<]+)/', $homepage, $matches);
    print($matches[1])

    А с недавнего времени пароль стал «картинкой». И первая мысль была воспользоваться оптическим распознаванием текста. Я стал пробовать онлайн и оффлайн сервисы OCR, которыми можно было бы распознать пароль. Передаю привет хабражителю Winand, с которым у нас на эту тему состоялась переписка. В общем последний OCR, с которым я возился, был Tesseract, который «из коробки» определял пароль, но с ошибками. Но его можно обучить новым шрифтам, чем я и собирался заняться. Когда я подбирал шрифт, похожий на «картиночный», возникла мысль, что это что-то простое, хотя и похоже на teminal из Windows или terminus font из Linux. И voila — это оказался просто встроенный шрифт PHP под номером (размер) 5. Далее я забросил OCR и написал скрипт на PHP, который ищет символы «картиночного» пароля по сгенерированному словарю. Словарь — это набор изображений возможных символов пароля того же цвета и размера. Поиск делается по совпадению изображений. Вот такой нехитрый реверс-инжиниринг. Предполагаю, что текущий вариант изображения у VPNBook продержится недолго, учитывая его примитивность.

    Скрипт vpnbook.php


    Сприпт возвращает строку-пароль.

    <?php
    // размер символа
    $wchar = 9;
    $hchar = 13;
    $strDict = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 ';
    $imgDict = imagecreatetruecolor(2 + strlen($strDict)* $wchar, $hchar);
    $bg = imagecolorallocate($imgDict, 0xF6, 0xF6, 0xF6);
    $textcolor = imagecolorallocate($imgDict, 0x4C, 0x4C, 0x4C);
    imagefill($imgDict, 0, 0, $bg);
    imagestring($imgDict, 5, 2, 0, $strDict, $textcolor);
    
    
    // инициализируем cURL
    $ch = curl_init();
    // устанавливаем url, с которого будем получать данные
    curl_setopt($ch, CURLOPT_URL, 'https://www.vpnbook.com/password.php');
    // устанавливаем опцию, чтобы содержимое вернулось нам в string
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1); // also, this seems wise considering output is image.
    // выполняем запрос
    $output = curl_exec($ch);
    // закрываем cURL
    curl_close($ch);
    
    $imgOCR = imagecreatefromstring($output);
    // $imgOCR = imageCreateFromPng('password.png');
    // в текущее изображение может поместиться 10 полных символов. 2 + 10*9 = 92 < 100
    $maxchar = floor((imagesx($imgOCR) - 2) / 9);
    $imgBox = imagecreatetruecolor($wchar, $hchar);
    $hashDict = Array();
    
    // генерируем словарь
    for ($k = 0; $k < strlen($strDict) ; $k++) {
    	imagecopy($imgBox, $imgDict, 0, 0, 2 + $k * $wchar, 0, $wchar, $hchar);
    	$hashStr = "";
    	for($y = 0; $y < $hchar ; $y++)
    		for($x = 0; $x < $wchar; $x++) $hashStr .= (imagecolorat($imgBox, $x, $y) != 0xF6F6F6)? '1': '0';
    	$hashDict[$hashStr] = $strDict[$k];
    }
    
    // ищем символы по словарю
    for ($k = 0; $k < $maxchar ; $k++) {
    	imagecopy($imgBox, $imgOCR, 0, 0, 2 + $k * $wchar, 0, $wchar, $hchar);
    	$hashStr = "";
    	for($y = 0; $y < $hchar ; $y++)
    		for($x = 0; $x < $wchar; $x++) $hashStr .= (imagecolorat($imgBox, $x, $y) != 0xF6F6F6)? '1': '0';
    	$tempchar = $hashDict[$hashStr];
    	if ($tempchar==' ') break;
    	print($tempchar);
    }
    
    /*header('Content-type: image/png');
    imagepng($imgOCR);
    */
    //var_dump($hashDict);
    imagedestroy($imgDict);
    imagedestroy($imgOCR);
    imagedestroy($imgBox);
    ?>


    План Б. Пароль из twitter


    С подсказки vvsvic привожу простую реализацию альтернативного скрипта, для извлечения пароля из твиттера VPNBook (https://twitter.com/vpnbook/)
    <?php
    function url_get_html($url) {
    	// инициализируем cURL
    	$ch = curl_init();
    	// устанавливаем url с которого будем получать данные
    	curl_setopt($ch, CURLOPT_URL, $url);
    	// устанавливаем опцию чтобы содержимое вернулось нам в string
    	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    	// выполняем запрос
    	$output = curl_exec($ch);
    	// закрываем cURL
    	curl_close($ch);
    	// возвращаем содержимое
    	return $output;
    }
    
    $homepage = url_get_html('https://twitter.com/vpnbook');
    preg_match('/Password: ([^<]+)/', $homepage, $matches);
    
    // Print the entire match result
    // var_dump($matches);
    print($matches[1])
    // print_r($matches);
    ?>
    


    Скрипт Mikrotik VPNBook


    Скрипт нужно вызывать каждую минуту из шедулера. Скрипт мониторит статус PPTP соединения и при разрыве связи вызывает всю процедуру запроса нового пароля, таким образом микротик не«флудит попытками открытия соединение в течении нескольких часов с неправильным паролем, а переподключение делается за 1 минуту. Также тут отслеживаются ошибки on-error для fetch и file get, чтобы точнее определить, что пароль получен.

    # VPNBookScript v2
    :global VPNBookpIfName "pptp-out1"
    :global VPNBookServerAddresses {"euro217.vpnbook.com";"euro214.vpnbook.com";"us1.vpnbook.com";"us2.vpnbook.com";"ca1.vpnbook.com";"de233.vpnbook.com";"fr1.vpnbook.com"}
    #:if ([:typeof $VPNBookServerAddresses] != "array") do={
    #  :set VPNBookServerAddresses {"euro217.vpnbook.com";"euro214.vpnbook.com";"us1.vpnbook.com";"us2.vpnbook.com";"ca1.vpnbook.com";"de233.vpnbook.com"}
    #}
    
    :global VPNBookErr false
    :global VPNBookPassFile "VPNBookPass.txt"
    :global VPNBookPass
    :global VPNBookRun
    #:global TToken "4.....................2"
    #:global TChatId "2342432...9"
    
    
    :global VPNBookServerIndex
    :if ([:typeof $VPNBookServerIndex] != "num") do={:set VPNBookServerIndex 0}
    
    :if ([/interface pptp-client get $VPNBookpIfName running]) do={
      :set VPNBookRun true
    } else {
      :if (!$VPNBookRun) do={
        :set VPNBookServerIndex ($VPNBookServerIndex + 1)
        :if ($VPNBookServerIndex>=[:len $VPNBookServerAddresses]) do={:set VPNBookServerIndex 0}
      } else {
        :set VPNBookRun false
      }
      :if (![/interface pptp-client get $VPNBookpIfName disabled]) do={/interface pptp-client set $VPNBookpIfName disabled=yes}
      :do {/tool fetch url="http://server/vpnbookpass.php" dst-path=$VPNBookPassFile} on-error={:set VPNBookErr true}
      :delay 2
      :do {:set VPNBookPass [/file get $VPNBookPassFile contents]} on-error={:set VPNBookErr true}
      :if (!$VPNBookErr) do={
        :if ([/interface pptp-client get $VPNBookpIfName password] != $VPNBookPass) do={/interface pptp-client set $VPNBookpIfName password=$VPNBookPass}
        :if ([/interface pptp-client get $VPNBookpIfName connect-to] != $VPNBookServerAddresses->$VPNBookServerIndex) do={/interface pptp-client set $VPNBookpIfName connect-to=($VPNBookServerAddresses->$VPNBookServerIndex)}
        :log info ("VPNBook: Attempt to connect to: ".($VPNBookServerAddresses->$VPNBookServerIndex).". Password: $VPNBookPass")
    #    /tool fetch url=("https://api.telegram.org/bot$TToken/sendmessage\?chat_id=$TChatId&text=VPNBook: Attempt to connect to: ".($VPNBookServerAddresses->$VPNBookServerIndex).". Password: $VPNBookPass") keep-result=no
        /interface pptp-client set $VPNBookpIfName disabled=no
      }
    }

    Еще рекомендую добавить отключение PPTP интерфейса по разрыву связи (событие on-down) в PPP-профиле, чтобы переподключение вообще не флудило даже в течении 1 минуты.

    Соответственно, основной скрипт в течении 1 минуты в случае удачного получения нового пароля поднимет pptp-out1 соединение.

    add change-tcp-mss=yes name=VPNBook on-down=\
        ":if (![/interface pptp-client get pptp-out1 disabled]) do={\r\
        \n  /interface pptp-client set pptp-out1 disabled=yes\r\
        \n}" only-one=yes use-compression=yes use-encryption=required use-ipv6=no use-mpls=no use-upnp=no
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 11

      0
      интересно, но в Китае оно не работает
        0
        Дай тебе Бог здоровья, добрый человек :)
          –3
          Я так в первом классе писал
            0
            А зачем такие сложности?
            Они же в Твиттере есть. Там все по старому осталось.
              0
              Эту возможность я как-то упустил. Но, вопрос: оперативно ли появляется твит с новый паролем? В любом случае я добавлю в заметку план Б, спасибо.
                0
                В этот раз Твиттер не обновился.
                0
                Спасибо, не знал что у них твиттер есть. Тоже уже хотел научить свой роутер распознавать картинки. :)
                0
                Может кто посоветует аналогичные сервисы? Имею ввиду VPNBook
                  0
                  Автор капчи (VPNBook) не учел, что «u» и «y» у него сольются в идентичный символ. Пользователи просто не смогут распознать «y» в обрезанном символе «u».
                  Костыль для обхода проблемы, вставить в блок "// ищем символы по словарю":
                    $tempchar = $hashDict[$hashStr];
                    if ($tempchar=='u' || $tempchar=='y') // проблема совпадения символов
                      $tempchar = (mt_rand(0, 1))? 'u': 'y';
                      //$tempchar = (time() / 60 % 60 % 2)? 'u': 'y';
                    elseif ($tempchar==' ') break;
                    0
                    Недавно заблокировали сайт vpnbook, теперь нельзя автоматически получить пароль, но vpn сервера работают. Обход проблемы: парсить fb или twitter
                    PHP парсер пароля из fb:
                    <?php
                    function url_get_html($url) {
                    	// инициализируем cURL
                    	$ch = curl_init();
                    	// устанавливаем url с которого будем получать данные
                    	curl_setopt($ch, CURLOPT_URL, $url);
                    	// curl_setopt($ch, CURLOPT_VERBOSE, true);
                    	curl_setopt($ch, CURLOPT_USERAGENT, 'cURL');
                    	// устанавливаем опцию чтобы содержимое вернулось нам в string
                    	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
                    	// выполняем запрос
                    	$output = curl_exec($ch);
                    	// закрываем cURL
                    	curl_close($ch);
                    	// возвращаем содержимое
                    	return $output;
                    }
                    
                    $homepage = url_get_html('https://mbasic.facebook.com/VPNBookNews/');
                    preg_match('/Password: ([^<]+)/', $homepage, $matches);
                    
                    // var_dump($matches);
                    print($matches[1])
                    // print_r($matches);
                    ?>
                      0

                      Автору спасибо за идею!
                      Немного модифицировал скрипт, теперь он работает как сервис, не делает запись в файл, а только в переменные.


                      Необходимо добавить в шедулер запуск скрипта VPNBook_watchdog_script


                      Скрипт для MikroTik
                      # VPNBookScript v2.5
                      local scriptName "VPNBook_watchdog_script";
                      
                      :global VPNBookpIfName "pptp-VPNBook"
                      :global VPNBookServerAddresses {"PL226.vpnbook.com";"de4.vpnbook.com";"us1.vpnbook.com";"us2.vpnbook.com";"fr1.vpnbook.com";"fr8.vpnbook.com";"ca222.vpnbook.com";"ca198.vpnbook.com"}
                      #:if ([:typeof $VPNBookServerAddresses] != "array") do={
                      #  :set VPNBookServerAddresses {"euro217.vpnbook.com";"euro214.vpnbook.com";"us1.vpnbook.com";"us2.vpnbook.com";"ca1.vpnbook.com";"de233.vpnbook.com"}
                      #}
                      
                      :global VPNBookErr false
                      :global VPNBookPass
                      :global VPNBookRun
                      :global VPNBookURL https://myservername/vpnbook
                      #:global TToken "4.....................2"
                      #:global TChatId "2342432...9"
                      
                      if ( [len [/system script job find where script=$scriptName]] > 1) do= { error "single instance" };
                      delay 15;
                      
                      while (true) do {
                      :delay 60s;
                      
                      :global VPNBookServerIndex
                      :if ([:typeof $VPNBookServerIndex] != "num") do={:set VPNBookServerIndex 0}
                      
                      :if ([/interface pptp-client get $VPNBookpIfName running]) do={
                        :set VPNBookRun true
                      } else {
                        :if (!$VPNBookRun) do={
                          :set VPNBookServerIndex ($VPNBookServerIndex + 1)
                          :if ($VPNBookServerIndex>=[:len $VPNBookServerAddresses]) do={:set VPNBookServerIndex 0}
                        } else {
                          :set VPNBookRun false
                        }
                        :if (![/interface pptp-client get $VPNBookpIfName disabled]) do={/interface pptp-client set $VPNBookpIfName disabled=yes}
                        :delay 2
                        :do {:set VPNBookPass ([/tool fetch url="$VPNBookURL" output=user as-value]->"data")} on-error={:set VPNBookErr true}
                        :if (!$VPNBookErr) do={
                          :if ([/interface pptp-client get $VPNBookpIfName password] != $VPNBookPass) do={/interface pptp-client set $VPNBookpIfName password=$VPNBookPass}
                          :if ([/interface pptp-client get $VPNBookpIfName connect-to] != $VPNBookServerAddresses->$VPNBookServerIndex) do={/interface pptp-client set $VPNBookpIfName connect-to=($VPNBookServerAddresses->$VPNBookServerIndex)}
                          :log info ("VPNBook: Attempt to connect to: ".($VPNBookServerAddresses->$VPNBookServerIndex).". Password: $VPNBookPass")
                      #    /tool fetch url=("https://api.telegram.org/bot$TToken/sendmessage\?chat_id=$TChatId&text=VPNBook: Attempt to connect to: ".($VPNBookServerAddresses->$VPNBookServerIndex).". Password: $VPNBookPass") keep-result=no
                          /interface pptp-client set $VPNBookpIfName disabled=no
                        }
                      }
                      
                      }

                      На своём ресурсе запускаю bash скрипт как cgi приложение

                      нашёл скрипт здесь и немного модифицировал (в Apache конфиг домена добавил вот эту строку:


                      ScriptAlias /vpnbook /var/www/html/vpnbook/password_extractor.sh

                      )


                      #!/bin/sh
                      
                      # Begin functions
                      # Print out the help
                      __usage(){
                          echo "usage: password_extractor [-o output_file | [-h]]"
                      }
                      
                      __extract_pwd(){
                          local file="$@"
                          local pwd=$(cat ${output_file} \
                              | grep -Eo '"WordText":.*?[^\\]",' \
                              | awk -F':' '{print $2}' \
                              | awk -F',' '{print $1}' \
                              | awk '{ gsub(/^[ \t]+|[ \t]+$/, ""); print }' \
                              | tr -d \")
                          echo ${pwd##*|}
                      }
                      # End functions
                      
                      debug_flag=0
                      vpnbook_folder=$HOME/.vpnbook
                      vpnbook_base_url=https://www.vpnbook.com
                      vpnbook_url=${vpnbook_base_url}/freevpn
                      tesseract_service_url=https://api.ocr.space/parse/image
                      timestamp=$(date +%s)
                      output_file=/tmp/vpnbok_pwd_${timestamp}.json
                      log_file=/tmp/vpnbok_pwd_${timestamp}.log
                      
                      # Start Script
                      while [ "$1" != "" ]; do
                          case $1 in
                              -o | --output )         shift
                                                      output_file=$1
                                                      ;;
                              -d | --debug )          shift
                                                      debug_flag=1
                                                      ;;
                              -h | --help )           usage
                                                      exit
                                                      ;;
                              * )                     usage
                                                      exit 1
                          esac
                          shift
                      done
                      
                      echo "Content-type:text/plain"
                      echo ""
                      # Retrieve the Password URL from the official webpage
                      pwd_url=$(curl -s ${vpnbook_url} | grep -m2 "Password:" | tail -n1 | cut -d \" -f2)
                      #echo "Retrieving Password at the following URL: ${vpnbook_base_url}/${pwd_url}"
                      
                      curl -X POST --header "apikey: 5a64d478-9c89-43d8-88e3-c65de9999580" \
                      -F "url=${vpnbook_base_url}/${pwd_url}" \
                      -F 'language=eng' \
                      -F 'isOverlayRequired=true' \
                      -F 'FileType=.Auto' \
                      -F 'IsCreateSearchablePDF=false' \
                      -F 'isSearchablePdfHideTextLayer=true' \
                      -F 'scale=true' \
                      -F 'detectOrientation=false' \
                      -F 'isTable=false' \
                      ${tesseract_service_url} -o ${output_file}
                      
                      pwd=$(__extract_pwd ${output_file})
                      echo ${pwd} > ${output_file}
                      
                      #echo 'Retrieved password:---'${pwd}'---'
                      #echo "${pwd}"
                      tr -d "\n\r" < ${output_file}
                      
                      exit 0;

                      Only users with full accounts can post comments. Log in, please.