0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Metasploitのガイドに従って、HTTP LoginScannerを書いてみる

Posted at

タイトル通り、MetasploitのサイトにあるWriting a HTTP LoginScannerに従って、HTTP LoginScannerを書いてみました。
ガイドでは、Symantec Web Gatewayのトライアル版を使用することになっていますが、見つけることができませんでしたので、テスト用にログインするだけのサイトを作りました。

サイトの準備

Ubuntuサーバーで、LAMP環境を作ります。
テスト用なので、UFW等の設定は行いません。
UbuntuサーバーのIPアドレス:192.168.56.133

Apacheのインストール

sudo apt update
sudo apt upgrade
sudo apt install apache2

ブラウザで、以下のURLにアクセスして、デフォルトページが表示されるか、確認します。

http://192.168.56.133

MySQLのインストール

sudo apt install mysql-server

セキュリティの設定をします。

sudo mysql_secure_installation

VALIDATE PASSWORD PLUGINは、Nで回答し、以降は、Yで回答します。
完了したら、MySQLコンソールにログインできるか、確認します。

sudo mysql

ログイン出来たら、以下のコマンドで、終了します。

exit

PHPのインストール

sudo apt install php libapache2-mod-php php-mysql

PHPの動作確認をします。

php -v

PHPとApacheの連携を確認します。
以下のファイルを作成します。

sudo nano /var/www/html/info.php
info.php
<?php phpinfo(); ?>

ブラウザで、以下のURLにアクセスして、PHPのインフォメーションが表示されるか、確認します。

http://192.168.56.133/info.php

確認ができたら、ファイルを削除します。

sudo rm /var/www/html/info.php

データベースの準備

MySQLにログインします。

sudo mysql

データベース(example_database)を作成します。

CREATE DATABASE example_database;

ユーザー(example_user)を作成して、パスワード(password)を設定します。

CREATE USER 'example_user'@'%' IDENTIFIED WITH mysql_native_password BY 'password';

example_userにexample_databaseの権限を付与します。

GRANT ALL ON example_database.* TO 'example_user'@'%';

一旦、MySQLから、ログアウトします。

exit

example_userで、MySQLにログインします。

mysql -u example_user -p

example_databaseに、認証で使用するusernameとpasswordのテーブル(users)を作成します。

CREATE TABLE example_database.users (
user_id INT AUTO_INCREMENT,
username VARCHAR(255),
password VARCHAR(255),
PRIMARY KEY(user_id)
);

認証用のデータを作成します。

INSERT INTO example_database.users (username,password) VALUES ("leia_organa","help_me_obiwan");

確認します。

select * from example_database.users;
+---------+-------------+----------------+
| user_id | username    | password       |
+---------+-------------+----------------+
|       1 | leia_organa | help_me_obiwan |
+---------+-------------+----------------+

MySQLから、ログアウトします。

exit

サイトの作成

/var/www/htmltestsiteを作成します。

sudo mkdir /var/www/html/testsite

以下の3つのPHPファイルを作成します。

dbconfig.php
<?php
$dsn = 'mysql:host=localhost;dbname=example_database;charset=utf8';
$user = 'example_user';
$password = 'password';

try {
    $pdo = new PDO($dsn, $user, $password);
} catch (PDOException $e) {
    echo 'Connection failed: ' . $e->getMessage();
}
?>
login.php
<?php
session_start();
require_once 'dbconfig.php';

if(isset($_POST['username']) && isset($_POST['password'])) {

	$username = $_POST['username'];
	$password = $_POST['password'];
	
	$sql = 'SELECT * FROM users WHERE username = :username';
	$stmt = $pdo->prepare($sql);
	$stmt->execute(['username' => $username]);
	$user = $stmt->fetch();
	if(isset($user['username']) && $password === $user['password']) {
		$_SESSION['username'] = $user['username'];
		session_regenerate_id();
		header('Location: ./index.php');
	} else {
		echo 'ログイン失敗';
?>
<br><a href="./login.php">login.php</a>
<?php
	}
} else {
?>
<form action="login.php" method="post">
    <label for="username">ユーザー名:</label>
    <input type="text" id="username" name="username"><br>
    <label for="password">パスワード:</label>
    <input type="password" id="password" name="password"><br>
    <input type="submit" value="ログイン">
</form>
<?php
}
?>
index.php
<?php
session_start();

if(isset($_POST['logout'])) {
	session_destroy();
	header('Location:./login.php');
}
if(isset($_SESSION['username'])) {
	echo 'ログイン成功';
?>
<form action="index.php" method="post">
    <input type="hidden" id="logout" name="logout">
    <input type="submit" value="ログアウト">
</form>
<?php
} else {
	header('Location:./login.php');
}
?>

動作

ブラウザで、http://192.168.56.133/testsite/にアクセスすると、http://192.168.56.133/testsite/login.phpにリダイレクトします。
ユーザー名、パスワードで認証すると、http://192.168.56.133/testsite/index.phpにリダイレクトします。
認証に失敗した時は、「ログイン失敗」とlogin.phpに戻るリンクが表示されますので、リンクで戻ります。
index.phpのログアウトボタンをクリックすると、login.phpに戻ります。
ユーザー名:leia_organa
パスワード:help_me_obiwan

HTTP LoginScannerの作成

ログイン時の動作の確認

ログイン成功時のリクエストとレスポンス
ログインに成功した時は、302でレスポンスして、Locationヘッダーにindex.phpがあります。

POST /testsite/login.php HTTP/1.1
Host: 192.168.56.133
Content-Length: 44
Cache-Control: max-age=0
Accept-Language: en-US,en;q=0.9
Origin: http://192.168.56.133
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.56.133/testsite/login.php
Accept-Encoding: gzip, deflate, br
Cookie: PHPSESSID=k55vh6i6neboo7istaeag3krin
Connection: keep-alive

username=leia_organa&password=help_me_obiwan
HTTP/1.1 302 Found
Date: Tue, 06 May 2025 06:30:44 GMT
Server: Apache/2.4.58 (Ubuntu)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Set-Cookie: PHPSESSID=32qos1p3mfdidluaa8021d06p0; path=/
Location: ./index.php
Content-Length: 0
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8
GET /testsite/index.php HTTP/1.1
Host: 192.168.56.133
Cache-Control: max-age=0
Accept-Language: en-US,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.56.133/testsite/login.php
Accept-Encoding: gzip, deflate, br
Cookie: PHPSESSID=32qos1p3mfdidluaa8021d06p0
Connection: keep-alive
HTTP/1.1 200 OK
Date: Tue, 06 May 2025 06:30:44 GMT
Server: Apache/2.4.58 (Ubuntu)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Vary: Accept-Encoding
Content-Length: 168
Keep-Alive: timeout=5, max=99
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8

ログイン成功<form action="index.php" method="post">
    <input type="hidden" id="logout" name="logout">
    <input type="submit" value="ログアウト">
</form>

ログイン失敗時のリクエストとレスポンス
ログインに失敗した時は、200でレスポンスします。

POST /testsite/login.php HTTP/1.1
Host: 192.168.56.133
Content-Length: 53
Cache-Control: max-age=0
Accept-Language: en-US,en;q=0.9
Origin: http://192.168.56.133
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.56.133/testsite/login.php
Accept-Encoding: gzip, deflate, br
Cookie: PHPSESSID=32qos1p3mfdidluaa8021d06p0
Connection: keep-alive

username=leia_organa&password=like_my_father_beforeme
HTTP/1.1 200 OK
Date: Tue, 06 May 2025 06:45:13 GMT
Server: Apache/2.4.58 (Ubuntu)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 58
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8

ログイン失敗<br><a href="./login.php">login.php</a>

HTTP LoginScannerの作成

HTTP LoginScanner テンプレートは次のようになります。
空のattempt_loginメソッドしかありません。
attempt_loginが自動的に呼び出されます。

require 'metasploit/framework/login_scanner/http'

module Metasploit
  module Framework
    module LoginScanner
      class SymantecWebGateway < HTTP


        # Attempts to login to the server.
        #
        # @param [Metasploit::Framework::Credential] credential The credential information.
        # @return [Result] A Result object indicating success or failure
        def attempt_login(credential)

        end

      end
    end
  end
end

クラス名を変更して、標準的なattempt_loginを記入します。

require 'metasploit/framework/login_scanner/http'

module Metasploit
  module Framework
    module LoginScanner
      class TestSite < HTTP

        def attempt_login(credential)
          result_opts = {
            credential: credential,
            status: Metasploit::Model::Login::Status::INCORRECT,
            proof: nil,
            host: host,
            port: port,
            protocol: 'tcp'
          }
          result_opts.merge!(do_login(credential.public, credential.private))
          Result.new(result_opts)
        end

      end
    end
  end
end

PHPSESSIDを取得するget_session_idメソッドを追加します。

require 'metasploit/framework/login_scanner/http'

module Metasploit
  module Framework
    module LoginScanner
      class TestSite < HTTP

        def get_session_id
          login_uri = normalize_uri("#{uri}/testsite/login.php")
          res = send_request({'uri' => login_uri})
          sid = res.get_cookies.scan(/(PHPSESSID=\w+);*/).flatten[0] || ''
          return sid
        end

        def attempt_login(credential)
          result_opts = {
            credential: credential,
            status: Metasploit::Model::Login::Status::INCORRECT,
            proof: nil,
            host: host,
            port: port,
            protocol: 'tcp'
          }
          result_opts.merge!(do_login(credential.public, credential.private))
          Result.new(result_opts)
        end

      end
    end
  end
end

実際にリクエストの送信とレスポンスの確認を行うdo_loginを追加します。
レスポンスの確認は、ロケーションヘッダーでtestsite/index.phpページにリダイレクトされることを利用しています。

test_site.rb
require 'metasploit/framework/login_scanner/http'

module Metasploit
  module Framework
    module LoginScanner
      class TestSite < HTTP

        LOGIN_STATUS  = Metasploit::Model::Login::Status

        def get_session_id
          login_uri = normalize_uri("#{uri}/testsite/login.php")
          res = send_request({'uri' => login_uri})
          sid = res.get_cookies.scan(/(PHPSESSID=\w+);*/).flatten[0] || ''
          return sid
        end

        def do_login(username, password)
          protocol  = ssl ? 'https' : 'http'
          peer      = "#{host}:#{port}"
          login_uri = normalize_uri("#{uri}/testsite/login.php")

          res = send_request({
            'uri' => login_uri,
            'method' => 'POST',
            'cookie' => get_session_id,
            'headers' => {
              'Referer' => "#{protocol}://#{peer}/#{login_uri}"
            },
            'vars_post' => {
              'username' => username,
              'password' => password
            }
          })
          if res && res.headers['Location'].include?('index.php')
            return {:status => LOGIN_STATUS::SUCCESSFUL, :proof => res.to_s}
          end

          {:proof => res.to_s}
        end

        def attempt_login(credential)
          result_opts = {
            credential: credential,
            status: Metasploit::Model::Login::Status::INCORRECT,
            proof: nil,
            host: host,
            port: port,
            protocol: 'tcp'
          }
          result_opts.merge!(do_login(credential.public, credential.private))
          Result.new(result_opts)
        end

      end
    end
  end
end

保存先

/usr/share/metasploit-framework/lib/metasploit/framework/login_scanner/

auxiliary moduleの作成

auxiliary moduleはユーザーインターフェースのような役割を果たします。
モジュールの動作を記述し、オプションの処理、オブジェクトの初期化、レポート作成などを行います。
auxiliary moduleのテンプレートは次のようになります。
initializeメソッドと空のrun_hostメソッドがあります。
initializeメソッドは、show infoで表示される情報になると思います。
run_hostメソッドが、メインのメソッドになります。

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'metasploit/framework/login_scanner/symantec_web_gateway'
require 'metasploit/framework/credential_collection'

class MetasploitModule < Msf::Auxiliary

  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::AuthBrute
  include Msf::Auxiliary::Report
  include Msf::Auxiliary::Scanner

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Symantec Web Gateway Login Utility',
        'Description' => %q{
          This module will attempt to authenticate to a Symantec Web Gateway.
        },
        'Author' => [ 'sinn3r' ],
        'License' => MSF_LICENSE,
        'DefaultOptions' => {
          'RPORT' => 443,
          'SSL' => true,
          'SSLVersion' => 'TLS1'
        }
      )
    )
  end

  def run_host(ip)
  end

end

initializeメソッドを修正して、LoginScannerオブジェクトを初期化するscannerメソッドを追加します。

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'metasploit/framework/login_scanner/test_site'
require 'metasploit/framework/credential_collection'

class MetasploitModule < Msf::Auxiliary

  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::AuthBrute
  include Msf::Auxiliary::Report
  include Msf::Auxiliary::Scanner

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Test Site Login Utility',
        'Description' => %q{
          This module will attempt to authenticate to a Test Site.
        },
        'Author' => [ 'tsuko5963' ],
        'License' => MSF_LICENSE,
        'DefaultOptions' => {
          'RPORT' => 80,
          'SSL' => false,
        }
      )
    )
  end

  def scanner(ip)
    @scanner ||= lambda {
      cred_collection = Metasploit::Framework::CredentialCollection.new(
        password:        datastore['PASSWORD'],
        username:        datastore['USERNAME'],
      )

      return Metasploit::Framework::LoginScanner::TestSite.new(
        configure_http_login_scanner(
          host: ip,
          port: datastore['RPORT'],
          cred_details:       cred_collection,
          stop_on_success:    datastore['STOP_ON_SUCCESS'],
          bruteforce_speed:   datastore['BRUTEFORCE_SPEED'],
          connection_timeout: 5
        ))
      }.call
  end

  def run_host(ip)
  end

end

scannerメソッドの呼び出しを行うbruteforceメソッドと
ログイン成功時のレポートを行うreport_good_credメソッドと
ログイン失敗時のレポートを行うreport_bad_credメソッドを追加します。

test_site_login.rb
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'metasploit/framework/login_scanner/test_site'
require 'metasploit/framework/credential_collection'

class MetasploitModule < Msf::Auxiliary

  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::AuthBrute
  include Msf::Auxiliary::Report
  include Msf::Auxiliary::Scanner

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Test Site Login Utility',
        'Description' => %q{
          This module will attempt to authenticate to a Test Site.
        },
        'Author' => [ 'tsuko5963' ],
        'License' => MSF_LICENSE,
        'DefaultOptions' => {
          'RPORT' => 80,
          'SSL' => false,
        }
      )
    )
  end

  def scanner(ip)
    @scanner ||= lambda {
      cred_collection = Metasploit::Framework::CredentialCollection.new(
        password:        datastore['PASSWORD'],
        username:        datastore['USERNAME'],
      )

      return Metasploit::Framework::LoginScanner::TestSite.new(
        configure_http_login_scanner(
          host: ip,
          port: datastore['RPORT'],
          cred_details:       cred_collection,
          stop_on_success:    datastore['STOP_ON_SUCCESS'],
          bruteforce_speed:   datastore['BRUTEFORCE_SPEED'],
          connection_timeout: 5
        ))
      }.call
  end

  def report_good_cred(ip, port, result)
    service_data = {
      address: ip,
      port: port,
      service_name: 'http',
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }

    credential_data = {
      module_fullname: self.fullname,
      origin_type: :service,
      private_data: result.credential.private,
      private_type: :password,
      username: result.credential.public,
    }.merge(service_data)

    login_data = {
      core: create_credential(credential_data),
      last_attempted_at: DateTime.now,
      status: result.status,
      proof: result.proof
    }.merge(service_data)

    create_credential_login(login_data)
  end

  def report_bad_cred(ip, rport, result)
    invalidate_login(
      address: ip,
      port: rport,
      protocol: 'tcp',
      public: result.credential.public,
      private: result.credential.private,
      realm_key: result.credential.realm_key,
      realm_value: result.credential.realm,
      status: result.status,
      proof: result.proof
    )
  end

  def bruteforce(ip)
    scanner(ip).scan! do |result|
      case result.status
      when Metasploit::Model::Login::Status::SUCCESSFUL
        print_brute(:level => :good, :ip => ip, :msg => "Success: '#{result.credential}'")
        report_good_cred(ip, rport, result)
      when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
        vprint_brute(:level => :verror, :ip => ip, :msg => result.proof)
        report_bad_cred(ip, rport, result)
      when Metasploit::Model::Login::Status::INCORRECT
        vprint_brute(:level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'")
        report_bad_cred(ip, rport, result)
      end
    end
  end

  def run_host(ip)
    bruteforce(ip)
  end

end

保存先

~/.msf4/modules/auxiliary/scanner/http/

実行

Metasploitを起動します。

msfconsole

モジュールを読み込ませます。

[msf](Jobs:0 Agents:0) >> use auxiliary/scanner/http/test_site_login

RHOSTS,USERNAME,PASSWORDを設定します。

[msf](Jobs:0 Agents:0) auxiliary(scanner/http/test_site_login) >> set rhosts 192.168.56.133
rhosts => 192.168.56.133
[msf](Jobs:0 Agents:0) auxiliary(scanner/http/test_site_login) >> set username leia_organa
username => leia_organa
[msf](Jobs:0 Agents:0) auxiliary(scanner/http/test_site_login) >> set password help_me_obiwan
password => help_me_obiwan

実行します。

[msf](Jobs:0 Agents:0) auxiliary(scanner/http/test_site_login) >> run
[+] 192.168.56.133:80 - Success: 'leia_organa:help_me_obiwan'
[!] No active DB -- Credential data will not be saved!
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed

ログインに成功しました。
WireSharkで通信を確認すると、PHPSESSIDを取得するために、GETでリクエストして、その後、POSTでリクエストしています。

GET /testsite/login.php HTTP/1.1
Host: 192.168.56.133
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36


HTTP/1.1 200 OK
Date: Tue, 06 May 2025 07:50:07 GMT
Server: Apache/2.4.58 (Ubuntu)
Set-Cookie: PHPSESSID=51ms1fhvkn5s34l0hrsvsl54qu; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Vary: Accept-Encoding
Content-Length: 317
Content-Type: text/html; charset=UTF-8

<form action="login.php" method="post">
    <label for="username">...............:</label>
    <input type="text" id="username" name="username"><br>
    <label for="password">...............:</label>
    <input type="password" id="password" name="password"><br>
    <input type="submit" value="............">
</form>


POST /testsite/login.php HTTP/1.1
Host: 192.168.56.133
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Cookie: PHPSESSID=51ms1fhvkn5s34l0hrsvsl54qu
Referer: http://192.168.56.133:80//testsite/login.php
Content-Type: application/x-www-form-urlencoded
Content-Length: 44

username=leia_organa&password=help_me_obiwan


HTTP/1.1 302 Found
Date: Tue, 06 May 2025 07:50:07 GMT
Server: Apache/2.4.58 (Ubuntu)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Set-Cookie: PHPSESSID=seesrcj3lrdka9vkuighdgm1gd; path=/
Location: ./index.php
Content-Length: 0
Content-Type: text/html; charset=UTF-8
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?