🧑‍💻

【セキュリティ/ハッキング】セキュリティ脆弱性の実践と防御策: 任意ファイル読み取りとコード実行の攻撃

に公開

1. はじめに

業務を実施していると、エンジニア誰しも敏感になりがちな脆弱性の話。実際に攻撃者はどのように脆弱性を見つけて、攻撃したり情報を盗んだりしているのでしょうか。今回は、任意ファイル読み取り任意コード実行(RCE)といった攻撃手法を実際にデモンストレーションし、それに対する効果的な防御策について解説します。
*あくまで自分で立てたサーバに対して、実施しています。ので、実施する場合は、自分のローカルホストに対してやってみてください。


2. 脆弱性の概要

今回、脆弱性としては、下記のようなものをあえて埋め込みました。

任意ファイル読み取り(パストラバーサル)

任意ファイル読み取りとは、攻撃者がアプリケーションに対して不正なファイルパスを入力し、システムの重要なファイル(例えば /etc/passwd)を読み取る攻撃です。これを防ぐためには、ファイルパスの適切なバリデーションが必要です。

任意コード実行(RCE)

任意コード実行(Remote Code Execution, RCE)は、攻撃者がアプリケーションの脆弱性を利用してサーバ上で任意のコードを実行できる攻撃です。これにより、攻撃者はシステムを完全に制御できる可能性があります。


3. 攻撃デモ

脆弱なサーバのセットアップ

以下のPythonコードを使って、脆弱なHTTPサーバを立ち上げます。このサーバは、file パラメータを利用して、任意のファイルを読み取ることができます。ここに、適当なファイルも置いておきましょう。

week_sever.py
from http.server import BaseHTTPRequestHandler, HTTPServer
import urllib.parse
import os

class SimpleVulnServer(BaseHTTPRequestHandler):
    def do_GET(self):
        parsed_path = urllib.parse.urlparse(self.path)
        query = urllib.parse.parse_qs(parsed_path.query)

        filename = query.get("file", [None])[0]
        if filename:
            try:
                with open(filename, "r") as f:
                    content = f.read()
                    self.send_response(200)
                    self.end_headers()
                    self.wfile.write(content.encode())
            except Exception as e:
                self.send_response(404)
                self.end_headers()
                self.wfile.write(str(e).encode())
        else:
            self.send_response(400)
            self.end_headers()
            self.wfile.write(b"Please provide a ?file=... parameter.")

def run():
    server_address = ("127.0.0.1", 8080)
    httpd = HTTPServer(server_address, SimpleVulnServer)
    print("Serving on http://127.0.0.1:8080 ...")
    httpd.serve_forever()

if __name__ == "__main__":
    run()

こちらを実行して、実際にアドレスに入ってみると下記のように表示されます。

では、同じ階層に、secret.txtを配置して、http://127.0.0.1:8080/?file=secret.txtにアクセスしてみましょう。すると、fileの中身が見れてしまいます。*すでにガバガバな脆弱性。。笑

ここからハッキングして、他のファイルを除けないか考えていきましょう!!
(あくまで自身が立てたサーバとローカルです。)

ハッカーが考えること

ハッカーはどのように、サイトの脆弱性を見つけるのでしょうか。まず、ハッカーもやることは情報収集です。どのポートでどんなプロトコルで、何が使われているかなどなどです。
その後、攻撃できそうな部分を詳しく調べ、攻撃するのが一般的です。

①情報収集

まずは、情報収集をしましょう。下記のコマンドで使用されているポートをスキャンしまうす。

*必ずローカルに対して行なってください。(ローカル以外の実施で、問題が起こっても責任は取れません)

nmap -sV 127.0.0.1

nmap:

nmap はネットワーク探索ツールで、主にネットワークに接続されたデバイスの情報収集、セキュリティスキャン、サービスのバージョン検出などに使用されます。ポートスキャンやOS検出など、さまざまな診断を行うことができます。

-sV:

-sV オプションは「サービスバージョン検出」を意味します。これを使用すると、nmapは開いているポートに対して、そのサービスのバージョン情報も調べます。

例えば、HTTPサーバーが稼働している場合、そのバージョン(例えば、Apache 2.4.7など)も調べることができます。これはセキュリティの観点から非常に重要で、特定のバージョンに脆弱性がある場合、その情報を元に攻撃が行われることがあります。

127.0.0.1:

127.0.0.1 は「ローカルホスト」のIPアドレスです。これは自分自身のコンピュータを指し、他のネットワーク機器ではなく、自分のマシンに対してスキャンを実行することになります。

②情報収集(結果)

すると、下記のような結果が出力されました。

Starting Nmap 7.95 ( https://nmap.org ) at 2025-04-19 15:37 JST
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000031s latency).
Not shown: 997 closed tcp ports (conn-refused)
PORT     STATE SERVICE VERSION
XXXX/tcp open  XXXX    YYYYYYY service1 123.45.6
XXXX/tcp open  XXXX    YYYYYYY service1 123.45.6
8080/tcp open  http    BaseHTTPServer 0.6 (Python 3.XX.XX)

Service detection performed. Please report any incorrect results at XX
Nmap done: 1 IP address (1 host up) scanned in 6.16 seconds

先ほど建てた脆弱なサーバがありました。8080/tcp open http BaseHTTPServer 0.6 (Python 3.XX.XX) httpで何か狙えそう。。。このようにハッカーは考えます。
実際は、使っているBaseHTTPServerに対して、コマンドなどで脆弱性をより詳しく調べます。
*今回は省略

searchsploit BaseHTTPServer

今回は、本当に脆弱なサーバを立てたので、情報収集はここまでにして、実際に攻撃していきます。

🔥③攻撃開始

今わかった情報としては、127.0.0.1のローカルホストに対して、8080が空いててしかもhttpで暗号化していない。一旦、fileを盗めるかcurlで実施します。

curl 'http://127.0.0.1:8080/?file=../../../../etc/passwd'

🧩 コマンドの各パーツと意味

パーツ 意味
curl 指定したURLにHTTPリクエストを送り、レスポンスを表示するコマンドラインツール
http://127.0.0.1:8080/ アクセス先のURL。ローカルホスト(自分のPC)で動いているWebサーバ(ポート8080)にアクセスしている
?file=../../../../etc/passwd URLのクエリパラメータ。file という名前で「相対パス」を指定して、システム内の /etc/passwd ファイルを読み込もうとしている

これを実行すると、なんともう盗めます。結果は、下記のような形で出力されます。
一瞬ですね。一連の流れがわかって実に面白い

nobody:*:XXXXXXXXX
root:*:YYYYYYY
etc....

では、せっかくですので、脆弱なサーバのコードの解説等を行なっていきましょう。

🔓 攻撃が成功する理由とコードの解説

1. 入力値のバリデーションがない

ユーザーが指定する file パラメータに対して、以下のような検証処理が一切行われていません:

filename = query.get("file", [None])[0]
  • ../ などのパストラバーサル(ディレクトリを遡る)を防ぐチェック
  • 絶対パスや特殊文字の禁止
  • アクセス対象のファイル名制限(ホワイトリストや拡張子制限など)

そのため、悪意のあるユーザーがシステム内の任意のファイルを読み取ることが可能になります。


2. ユーザー入力をそのままファイルパスに使用している

以下のコードで、filename にユーザーが入力した値をそのまま open() に渡しています:

with open(filename, "r") as f:
    content = f.read()

このように、信頼できない入力を直接ファイル操作に使うと、任意ファイルの読み取りが可能になってしまいます。

4. セキュリティ対策

🛡 セキュリティ対策:任意ファイル読み取りへの防御

脆弱なWebアプリは、ユーザーの入力を適切に検証せずに使ってしまうことで、重大な情報漏洩を招きます。このセクションでは、今回のような「ファイルパスをユーザー入力に任せてしまう脆弱性」に対する対策をまとめます。


✅ 1. 入力値のバリデーションを行う

ユーザーから受け取ったファイル名をそのまま使うのではなく、想定された形式かどうかをチェックします。

  • ../ を含むパストラバーサルを拒否
  • 絶対パス(例:/etc/passwd)の使用を禁止
  • 使用可能なファイルをホワイトリスト方式で限定
if ".." in filename or filename.startswith("/"):
    self.send_response(400)
    self.end_headers()
    self.wfile.write(b"Invalid file path")
    return

✅ 2. アクセス可能なディレクトリを制限する

たとえば safe_dir の中だけを読み取り可能とし、それ以外はブロックします。

SAFE_DIR = os.path.abspath("./public")

requested_path = os.path.abspath(os.path.join(SAFE_DIR, filename))

# SAFE_DIR の外を指している場合は拒否
if not requested_path.startswith(SAFE_DIR):
    self.send_response(403)
    self.end_headers()
    self.wfile.write(b"Access denied")
    return

全体のコードを見ていきましょう。

secure_server.py
from http.server import BaseHTTPRequestHandler, HTTPServer
import urllib.parse
import os
import re

# 安全なディレクトリ(ここにあるファイルのみアクセス可能)
SAFE_DIR = os.path.abspath("./public")

class SecureFileServer(BaseHTTPRequestHandler):
    def do_GET(self):
        parsed_path = urllib.parse.urlparse(self.path)
        query = urllib.parse.parse_qs(parsed_path.query)

        filename = query.get("file", [None])[0]
        if filename is None:
            self.send_response(400)
            self.end_headers()
            self.wfile.write(b"Please provide a ?file=... parameter.")
            return

        # パストラバーサル防止 & 絶対パスの拒否
        if ".." in filename or filename.startswith("/"):
            self.send_response(400)
            self.end_headers()
            self.wfile.write(b"Invalid file path")
            return

        # ファイル名のフォーマット制限(例:英数字と .txt のみ)
        if not re.fullmatch(r"[a-zA-Z0-9_\-]+\.txt", filename):
            self.send_response(400)
            self.end_headers()
            self.wfile.write(b"Invalid filename")
            return

        # 安全なパスの構築とチェック
        requested_path = os.path.abspath(os.path.join(SAFE_DIR, filename))
        if not requested_path.startswith(SAFE_DIR):
            self.send_response(403)
            self.end_headers()
            self.wfile.write(b"Access denied")
            return

        # ファイルの読み込みとレスポンス
        try:
            with open(requested_path, "r") as f:
                content = f.read()
                self.send_response(200)
                self.send_header("Content-Type", "text/plain")
                self.send_header("Server", "SecureFileServer/1.0")
                self.end_headers()
                self.wfile.write(content.encode())
        except Exception as e:
            self.send_response(404)
            self.end_headers()
            self.wfile.write(b"File not found")

def run():
    server_address = ("127.0.0.1", 8080)
    httpd = HTTPServer(server_address, SecureFileServer)
    print("Serving securely on http://127.0.0.1:8080 ...")
    httpd.serve_forever()

if __name__ == "__main__":
    run()

こちらを実行して、実際にアドレスに入ってみると下記のように表示されます。

では、先ほどと同様に、file名を指定してhttp://127.0.0.1:8080/?file=secret.txtにアクセスしてみましょう。すると、下記のように表示されfileが見れなくなっています。

🔥再度攻撃開始

再度、curlを実行して攻撃をしてみましょう。

curl 'http://127.0.0.1:8080/?file=../../../../etc/passwd'

すると、下記のように、ターミナル上で表示され盗むことができなくなりました👏

curl: (28) Failed to connect to 127.0.0.1 port 8080 after 25913 ms: Couldn't connect to server

5. まとめ

今回は、「セキュリティ脆弱性の実践と防御策: 任意ファイル読み取りとコード実行の攻撃」という題材で、脆弱性をもったサーバを立てて自身で攻撃し、セキュリティを向上させた上で再度攻撃してみました。実際にコーディングしながら学習すると、得る量が違いますね。次何か試して欲しいことなどあったら、リクエストください🙌

Discussion