Удаленное управление изолированными системами
Parallax
Опубликовано: dl, 06.11.06 04:38
Введение или disclaimer
Сразу хочу предупредить, что при написании этой статьи преследовались только мирные цели. Средством разработки сознательно был выбран Perl, чтобы уменьшить вероятность использования в качестве трояна на Windows-системах (ну а юниксоиды и сами о себе позаботятся). Да и в мирных целях использовать описываемую программу лучше со всеми возможными предосторожностями.
Задача
Задача довольно обычная: получить возможность выполнить какие-либо команды на рабочем компьютере, находять при этом, например, дома. Причины могут быть самые разные: поставить ночную закачку гигазов варезов, перезагрузить RAS-сервер, да мало ли. Использование популярных средств удаленного управления, таких как PC Anywhere, RAdmin и т.п. при этом очень часто затруднено или из-за политики корпоративного файрволла, или вообще из-за использования в рабочей сети фиктивных адресов типа 192.168.0.1, которые снаружи недоступны.
По уму с этим лучше всего справиться установкой VPN либо хотя бы пробросом портов, но сами понимаете, не всегда и не у всех есть такая возможность. То есть считаем, что все, что у нас есть, так это рабочая машина, умеющая выходить наружу на веб, но изолированная от доступа снаружи, и желание ей воспользоваться. Посмотрим, что с этим можно сделать.
Решение
Самое очевидное, что приходит в голову при такой ситуации, это использовать промежуточный сервер. На самом деле, чтобы что-то заработало, достаточно выкладывать на бесплатный хостинг кусок батч-файла, и настроить на рабочем сервере другой батч-файл, который будет время от времени запускать wget, выкачивать новый батч и запускать его. Но такая схема работы выглядит не очень безопасно, да и не дает возможности узнать, удачно выполнились команды, или нет.
Поэтому, если есть возможность не ограничиваться халявным сервером без скриптов, лучше подойти к делу аккуратно и написать промежуточный CGI-скрипт (назовем его серверным модулем), который будет получать наши команды и запоминать их. Модуль на рабочей машине, назовем его исполнительным модулем, будет периодически обращаться к серверному модулю и получать текст очередной команды, которую надо выполнить. Дальше он вызывает эту команду, перехватывает результат ее работы и пересылает обратно на серверный модуль, который выкладывает его на веб-страницу.
Доступ к серверной программе, конечно, нужно обезопасить - паролем на доступ, шифрованием и т.п. В описанной программе я для простоты ограничился установкой паролей средствами сервера (например, через .htaccess в Apache).
Детали реализации
Серверный модуль должен обрабатывать четыре команды. Выбор между ними делается с помощью параметра скрипта cmd:
- run - запоминает команду в базе (для простоты в программе используются dbm-файлы, ключ - текущее время) и выводит форму для ввода команды.
- getcmd - обрабатывает запрос от исполнительного модуля, выводит текст последней команды и удаляет ее из базы.
- putres - получает результат работы от исполнительного модуля и складывает ее в базу. Для упрощения кода сохраняется только последний результат, а вообще вместо %RES = () можете вставить очистку по таймеру или по количеству.
- show - печатает страницу с результатами, для ее периодического обновления используется тег meta http-equiv=refresh.
Для удобства работы форму для получения команд и страницу с результатами лучше всего объединить в одну страницу с помощью фреймов. В итоге получается что-то похожее на telnet через веб, как на следующем рисунке.
Исполнительный модуль с заданной периодичностью опрашивает серверный, отправляя ему команду 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/</</g;
$res =~ s/>/>/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>
Комментариев нет:
Отправить комментарий