【セキュリティ/ハッキング】セキュリティ脆弱性の実践と防御策: 任意ファイル読み取りとコード実行の攻撃
1. はじめに
業務を実施していると、エンジニア誰しも敏感になりがちな脆弱性の話。実際に攻撃者はどのように脆弱性を見つけて、攻撃したり情報を盗んだりしているのでしょうか。今回は、任意ファイル読み取りや任意コード実行(RCE)といった攻撃手法を実際にデモンストレーションし、それに対する効果的な防御策について解説します。
*あくまで自分で立てたサーバに対して、実施しています。ので、実施する場合は、自分のローカルホストに対してやってみてください。
2. 脆弱性の概要
今回、脆弱性としては、下記のようなものをあえて埋め込みました。
任意ファイル読み取り(パストラバーサル)
任意ファイル読み取りとは、攻撃者がアプリケーションに対して不正なファイルパスを入力し、システムの重要なファイル(例えば /etc/passwd
)を読み取る攻撃です。これを防ぐためには、ファイルパスの適切なバリデーションが必要です。
任意コード実行(RCE)
任意コード実行(Remote Code Execution, RCE)は、攻撃者がアプリケーションの脆弱性を利用してサーバ上で任意のコードを実行できる攻撃です。これにより、攻撃者はシステムを完全に制御できる可能性があります。
3. 攻撃デモ
脆弱なサーバのセットアップ
以下のPythonコードを使って、脆弱なHTTPサーバを立ち上げます。このサーバは、file
パラメータを利用して、任意のファイルを読み取ることができます。ここに、適当なファイルも置いておきましょう。
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
全体のコードを見ていきましょう。
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