Windowsでリモートマシンのコマンドを実行する

なんで先の記事でASP.NETをやり始めたのかというとIIS/HTTP経由でリモートのWindowsサーバのバッチコマンドを実行するいい方法はないかと探っていたらそこに辿り着いたからという理由なのですがそれが以下。メモ書き。


やりたいことは、
サーバAにある以下のようなバッチコマンド
[stopService.bat]


@net stop "Windows Time"
を、サーバBのコマンドプロンプトから以下のように実行

>cscript //nologo remcmd.vbs 192.168.0.1:80 stopService.bat
したいというもの(192.168.0.1はサーバAのアドレス)。


一般にリモートのWindowsサーバ上でコマンドを実行したいときは
1.SysInternal ツールの psexec を使う。
2.Telnetサーバ立ててクライアントからtelnet
3.Services for Unix を導入する。
の方法をよく聞くが、1.と3.は本番稼動のサーバに導入しようとすると通常難色を示されるしtelnetはバッチ的に制御する事が出来ないのでシステム管理を自動化する向きには使えない(マクロ実行できるクライアント買ってサーバに導入できれば別だけど)。


他に、リモートから実行させたいバッチをWindowsタスクに登録して、schtasksコマンドでリモートから実行という方法もなくはないのだけど実行結果の取得や標準出力への出力結果の取得が鬼面倒くさいので実行したいバッチが増えてくると破綻するというかやる気がなくなってくる。ATコマンドを使う方法も無くはないが、より面倒くさい。


そんなわけで最近なるべく手間の少ない(≒WindowsのCDに含まれている以外の機能を使わない)方法でリモートサーバ上でコマンドを実行する方法を考えていたのだけど、やっぱりIIS/HTTP使ってリモートサーバのコマンド実行するのが当たり障りなくていいかなーと思いこの3連休を使って検証。いい方法かどうかはわからないけど、まあ、勉強にはなった。設定する手順は以下。


1.サーバ側の設定
1−1.IISを導入する
Windows Server 2003の、サーバーの役割管理画面からアプリケーションサーバー(IIS)をASP.NETを有効にして導入する。


1−2.アプリケーションプールを作成する
実行ユーザにLocal Systemを割り当てるため、アプリケーションプールは新しく作ったほうが吉。
アプリケーションサーバ管理画面からIISマネージャの下を展開し、新しいアプリケーションプールを作成する。名前は、たとえばSystemとか。
作ったらプロパティー画面を開き識別タブのアプリケーションプールIDを「Local System」にする。


1−3.Webサイトを作る
アプリケーションプールを分離するために、サイトもなるべく新しいものを作ったほうが吉。
アプリケーションサーバ管理画面のWebサイトの下に新しいWebサイトを作成する。名前はSystemとか。
使用するポートは、ほかに使わなければ80でもいいと思うけど8080とか、それも最近メジャーなので8085とか。
ホームディレクトリのパスは後で述べるaspxファイル置くところがhomeでいいか、たとえば C:\System\web とか作ってそこに。
匿名アクセスは使い方次第だけどとりあえず許可するに。
Webサイトのアクセス許可は読み取り+ASPなどのスクリプト


Webサイトを作ったらプロパティ画面を開いて、「ホームディレクトリ」タブのアプリケーションプールを先に作ったSystemとかにする。
ディレクトリセキュリティ」タブを開き「IPアドレスドメイン名の制限」で、規定のアクセス権を「拒否する」に設定し、例外としてリモートからのコマンド実行を許可するマシンのIPアドレスのみを設定しておくのが吉。


1−4.aspxファイルを配置する
以下のaspxファイルを先に作ったwebサイトのホームディレクトリ(C:\System\web)に置く。
[run.aspx]


<?xml version="1.0" encoding="shift_jis"?>
<%@ Page Language="VB" ResponseEncoding="shift_jis" %>
<script runat="server">

' このaspx経由で実行できるバッチの置かれているフォルダパス
' 以下のフォルダにおかれている以外のバッチは実行できない
dim command_path = "C:\SYSTEM\bin"

dim status = "NG - unknown error"
dim command = ""
dim errorlevel = 0
dim stdout = ""
dim stderr = ""

private sub Page_Load()
Response.ContentType = "text/xml"

if Request.QueryString("command") is Nothing then
status = "NG - no command indicated"
exit sub
end if

command = Request.QueryString("command")
if (not instr(command, "\") = 0) or (not instr(command, "|") = 0) _
or (not instr(command, ">") = 0) or (not instr(command, "<") = 0) then
status = "NG - bad command"
exit sub
end if

dim finfo = new System.IO.FileInfo(command_path & "\" & command)
if not finfo.Exists() then
status = "NG - command does not exist"
exit sub
end if


System.IO.Directory.SetCurrentDirectory(command_path)

dim p = new System.Diagnostics.Process()
p.StartInfo.UseShellExecute = False
p.StartInfo.RedirectStandardOutput = True
p.StartInfo.RedirectStandardError = True
p.StartInfo.FileName = command

try
p.Start()
catch ex as Exception
status = "NG - command execute error"
exit sub
end try

do while not p.HasExited
stdout = stdout & p.StandardOutput.ReadToEnd()
stderr = stderr & p.StandardError.ReadToEnd()
p.WaitForExit(100)
loop
stdout = stdout & p.StandardOutput.ReadToEnd()
stderr = stderr & p.StandardError.ReadToEnd()

errorlevel = p.ExitCode
status = "OK"
end sub
</script>
<Envelope>
<Header>
<command><%= Server.HtmlEncode(command) %></command>
<status><%= Server.HtmlEncode(status) %></status>
</Header>
<Body>
<errorlevel><%= errorlevel %></errorlevel>
<stdout><%= Server.HtmlEncode(stdout) %></stdout>
<stderr><%= Server.HtmlEncode(stderr) %></stderr>
</Body>
</Envelope>

aspxはURLオプションの"command"で指定されたバッチファイルを実行し、実行結果の標準出力と標準エラーをxmlに埋め込んで返すというもの。
実行対象のバッチの置き場所に合わせて command_path の値は変更する。


1−5.実行したいバッチを配置する
上のaspxファイル内で指定した command_path(上記だと C:\System\bin)に実行したいバッチファイルなりを配置する。例としてはこの記事の最初のほうに挙げたstopServer.batとか。


2.クライアント側の設定
設定というか。クライアント側のWindows XPなりWindows Server 2003なりに以下のVBScriptを配置する。
[remcmd.vbs]


set w = WScript

' サーバ名とコマンド名の2つの引数が指定されていないときはエラー
if w.Arguments.Count < 2 then
w.StdErr.WriteLine "ERROR: too few arguments"
w.StdErr.WriteLine _
"Usage: cscript //nologo remcmd.vbs <server[:port]> <command>"
w.Quit -1
end if

' リモートサーバの run.asp を使ってコマンドを実行
set httpRequest = CreateObject("MSXML2.XMLHTTP")
httpRequest.open "GET", "http://" & w.Arguments(0) _
& "/run.aspx?command=" & w.Arguments(1), False
on error resume next
httpRequest.send
if not Err.Number = 0 then
w.StdErr.WriteLine "ERROR: cannot to send request"
w.StdErr.WriteLine "Err.Description = " & Err.Description
w.Quit -1
end if
on error goto 0

' HTTPのレスポンスコードが200以外のときはエラー
if not httpRequest.status = 200 then
w.StdErr.WriteLine "ERROR: bad http response"
w.StdErr.WriteLine "http response = " & httpRequest.Status
w.Quit -1
end if

' 結果データの <status> が OK となっていない場合はエラー
' 実行したコマンドのエラーレベルとは関係無い
set xml = httpRequest.responseXML
if not xml.getElementsByTagName("status").item(0).text = "OK" then
w.StdErr.WriteLine "ERROR: bad command status"
w.StdErr.WriteLine "command status = " _
& xml.getElementsByTagName("status").item(0).text
w.Quit -1
end if

' コマンドの実行結果を出力
WScript.StdOut.WriteLine xml.getElementsByTagName("stdout").item(0).text
WScript.StdErr.WriteLine xml.getElementsByTagName("stderr").item(0).text

' 終了コードにリモートコマンドのそれをセット
WScript.Quit CInt(xml.getElementsByTagName("errorlevel").item(0).text)

スクリプトはMSXML2.XMLHTTPコントロールを使ってリモートサーバのaspxに指定したコマンドをURLオプションにしてアクセスするもの。


で、サーバのIPアドレスが192.168.0.1だったとして、以下のように実行すると


cscript //nologo remcmd.vbs 192.168.0.1:8085 stopService.bat
以下のような実行結果が返って来、リモートサーバのWindows Timeサービスが停止できる。

Windows Time サービスを停止中です.
Windows Time サービスは正常に停止されました。



ここまで出来れば、あとはサーバ上に適当にバッチ書いて置いておくだけでサーバに設定したリモートマシンからhttp経由でバッチファイルが実行できる。


ちなみに、上記方法でバッチファイルを実行するときのバッチの実行ユーザーはLocal Systemアカウント。
たとえば


@whoami.exe
というバッチをサーバに置いてリモートから実行したときの実行結果は

nt authority\system
となる。これは、IISへのアクセス時にNTLM認証なりで他のユーザーアカウントで認証を受けて実行しても一緒。