タイトル通り、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
<?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/html
にtestsite
を作成します。
sudo mkdir /var/www/html/testsite
以下の3つの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();
}
?>
<?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
}
?>
<?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ページにリダイレクトされることを利用しています。
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メソッドを追加します。
##
# 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