четверг, 10 мая 2007 г.

Удаленное управление изолированными системами


Parallax

Опубликовано: dl, 06.11.06 04:38


Введение или disclaimer


Сразу хочу предупредить, что при написании этой статьи преследовались только мирные цели. Средством разработки сознательно был выбран Perl, чтобы уменьшить вероятность использования в качестве трояна на Windows-системах (ну а юниксоиды и сами о себе позаботятся). Да и в мирных целях использовать описываемую программу лучше со всеми возможными предосторожностями.


Задача


Задача довольно обычная: получить возможность выполнить какие-либо команды на рабочем компьютере, находять при этом, например, дома. Причины могут быть самые разные: поставить ночную закачку гигазов варезов, перезагрузить RAS-сервер, да мало ли. Использование популярных средств удаленного управления, таких как PC Anywhere, RAdmin и т.п. при этом очень часто затруднено или из-за политики корпоративного файрволла, или вообще из-за использования в рабочей сети фиктивных адресов типа 192.168.0.1, которые снаружи недоступны.


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


Решение


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


Поэтому, если есть возможность не ограничиваться халявным сервером без скриптов, лучше подойти к делу аккуратно и написать промежуточный CGI-скрипт (назовем его серверным модулем), который будет получать наши команды и запоминать их. Модуль на рабочей машине, назовем его исполнительным модулем, будет периодически обращаться к серверному модулю и получать текст очередной команды, которую надо выполнить. Дальше он вызывает эту команду, перехватывает результат ее работы и пересылает обратно на серверный модуль, который выкладывает его на веб-страницу.


Доступ к серверной программе, конечно, нужно обезопасить - паролем на доступ, шифрованием и т.п. В описанной программе я для простоты ограничился установкой паролей средствами сервера (например, через .htaccess в Apache).


Детали реализации


Серверный модуль должен обрабатывать четыре команды. Выбор между ними делается с помощью параметра скрипта cmd:



  1. run - запоминает команду в базе (для простоты в программе используются dbm-файлы, ключ - текущее время) и выводит форму для ввода команды.

  2. getcmd - обрабатывает запрос от исполнительного модуля, выводит текст последней команды и удаляет ее из базы.

  3. putres - получает результат работы от исполнительного модуля и складывает ее в базу. Для упрощения кода сохраняется только последний результат, а вообще вместо %RES = () можете вставить очистку по таймеру или по количеству.

  4. show - печатает страницу с результатами, для ее периодического обновления используется тег meta http-equiv=refresh.


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


scr


Исполнительный модуль с заданной периодичностью опрашивает серверный, отправляя ему команду getcmd. При этом используется библиотека LWP, для доступа к запароленному каталогу добавляется заголовок Authorization.


Полученная команда отправляется на выполнение с помощью функции open(APP, "$cmd |"), позволяющей перехватить вывод консольной программы. Тут есть одна сложность. Запущенная команда может зависнуть из-за какой-нибудь ошибки, или если это оказалась оконная программа, дождаться завершения которой мы тоже не сможем. Чтобы не остаться из-за этого с повисшим сервером, выполнение команды запускается из дочернего процесса, запущенного функцией fork, а основной процесс исполнительного модуля сразу возвращается в цикл опроса. Результат работы команды опять пересылается на серверный модуль с помощью команды putres.


То, что у нас получилось - основной каркас, который можно наращивать по своему вкусу. Как видно из скриншота, в систему не мешает добавить конвертирование из DOS-кодировки (cp866), в которой выводятся результаты команд, в Windows-кодировку (cp1251), хотя для большинства задач это не так уж и важно. Для повышения безопасности стоит добавить шифрование пересылаемых данных. Кроме того, может понадобиться отдельная обработка команд типа set и cd, которые сейчас запускаются во внешнем процессе и никак не влияют на переменные окружения и текущий каталог исполнительного модуля. Все это, как говорится, выходит за рамки этой статьи :)


Исходные коды


Серверный модуль


#!/usr/bin/perl

use CGI;

$datapath = $ENV{'DOCUMENT_ROOT'}."/data/";
$selfurl = $ENV{'REQUEST_URI'};
$refresh=10;

$query = new CGI;
$cmd = $query->param("cmd");

%MENU = (
show => \&show,
run => \&run,
getcmd => \&getcmd,
putres => \&putres
);

print $query->header();

dbmopen (%CMD, $datapath."cmd", 0666);
dbmopen (%RES, $datapath."res", 0666);

$MENU{$cmd}->() if(defined($MENU{$cmd}));

dbmclose %RES;
dbmclose %CMD;

sub show
{
print "<html><head><meta http-equiv=refresh content=\"$refresh;url=$selfurl\"></head><body><pre>";
foreach my $t (sort {$b<=>$a} keys %RES)
{
my $res = $RES{$t};
$res =~ s/</&lt;/g;
$res =~ s/>/&gt;/g;
print $res;
}
print "</pre></body></html>";
}

sub run
{
$CMD{time()} = $query->param("command") if (length($query->param("command")));
print <<FORM;
<form action=$selfurl method=POST>
<input type=hidden name=cmd value=run>
<input type=text name=command size=80>
<input type=submit value=Enter>
</form>
FORM
}

sub getcmd
{
my $cmdcount = keys %CMD;
if($cmdcount)
{
my $k = (sort {$a<=>$b} keys %CMD)[0];
print $CMD{$k};
delete $CMD{$k};
}
}

sub putres
{
%RES = ();
$RES{time()} = $query->param("res");
}

Исполнительный модуль


#!/usr/bin/perl

use LWP::UserAgent;
use HTTP::Request;
use HTTP::Response;
use HTTP::Headers;
use MIME::Base64;

$url = 'http://yourserver.ru/cgi-bin/protected/server.cgi';
$name = 'user';
$pass = 'password';
$timeout=10;

while(1)
{
my $cmd = getcmd();
run($cmd) if(length($cmd));
sleep($timeout);
}

sub getcmd
{
my $text = '';
my $ua = new LWP::UserAgent;
$ua->agent("ProxyZilla 1.0");
my $req = new HTTP::Request(GET => "$url?cmd=getcmd");

my $author = MIME::Base64::encode_base64("$name:$pass");
$req->header(Authorization => "BASIC $author");

my $res = $ua->request($req);
$text = $res->content if($res->is_success);

return $text;
}

sub putcmd
{
my $res = shift;
$res =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg;

my $ua = new LWP::UserAgent;
$ua->agent("ProxyZilla 1.0");

my $req = new HTTP::Request(POST => $url);
$req->content_type('application/x-www-form-urlencoded');
$req->content("cmd=putres&res=$res");

my $author = MIME::Base64::encode_base64("$name:$pass");
$req->header(Authorization => "BASIC $author");

$ua->request($req);
}

sub run
{
my($cmd) = shift;
my $res = "";
unless(fork)
{
open(APP, "$cmd |");
$res .= $_ while(<APP>);
close APP;
print "$res\n";
putcmd ($res);
exit;
}
}

Рабочий фреймсет


<frameset rows=*,50>

<frame src="http://yourserver.ru/cgi-bin/protected/server.cgi?cmd=show">

<frame src="http://yourserver.ru/cgi-bin/protected/server.cgi?cmd=run">

</frameset>

Комментариев нет: