Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use sslscan in place of sslyze #602

Merged
merged 1 commit into from
Jun 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install php8.1-cli php8.1-xml -y --no-install-recommends
sudo apt-get install php8.1-cli php8.1-xml sslscan -y --no-install-recommends
python -m pip install --upgrade pip
pip install -U setuptools
pip3 install .[test]
Expand Down
4 changes: 1 addition & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ See `INSTALL.md <https://github.com/wapiti-scanner/wapiti/blob/master/INSTALL.md

Running Wapiti on Windows can be accomplished through the use of `WSL <https://learn.microsoft.com/en-us/training/modules/get-started-with-windows-subsystem-for-linux/>`__.

The `ssl` module used to scan TLS/SSL misconfiguration won't work on ARM processors (see `SSLyze documentation <https://nabla-c0d3.github.io/sslyze/documentation/installation.html>`__).

How it works
============

Expand Down Expand Up @@ -145,7 +143,7 @@ The aforementioned attacks are tied to the following module names :
+ shellshock (Test Shellshock attack, see `Wikipedia <https://en.wikipedia.org/wiki/Shellshock_%28software_bug%29>`__)
+ spring4shell (Detects websites vulnerable to CVE-2020-5398)
+ sql (Error-based and boolean-based SQL injection detection)
+ ssl (Evaluate the security of SSL/TLS certificate configuration, requires `SSLyze <https://github.com/nabla-c0d3/sslyze>`__)
+ ssl (Evaluate the security of SSL/TLS certificate configuration, requires `sslscan <https://github.com/rbsec/sslscan>`__)
+ ssrf (Server Side Request Forgery)
+ takeover (Subdomain takeover)
+ timesql (SQL injection vulnerabilities detected with time-based methodology)
Expand Down
1 change: 1 addition & 0 deletions doc/ChangeLog_Wapiti
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Unrelease
Core : fix headless explorer method
Core : fix max-scan-time and missing timeout
Python : update dependencies and pip configurations
mod_ssl: Move to sslscan for the ssl module instead of sslyze

09/08/2023
Wapiti 3.1.8
Expand Down
4 changes: 2 additions & 2 deletions doc/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ If you are really lost, feel free to contact me.

### I have a warning about the ssl module not working ! ###

The `ssl` module requires the Python libraries [SSLyze](https://github.com/nabla-c0d3/sslyze) and [humanize](https://github.com/python-humanize/humanize).
The module is optional therefore you need to install those dependencies to use the `ssl` module.
The `ssl` module requires the [sslscan](binary) to be present in your PATH.
Check if the software is available with your package manager.

### I have some UnicodeDecodeError as soon as I launch Wapiti ! ###

Expand Down
6 changes: 1 addition & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ dependencies = [
"httpcore==1.0.4",
"httpx[brotli, socks]==0.27.0",
"httpx-ntlm==1.4.0",
"humanize==4.9.0",
"loguru==0.7.2",
"mako==1.3.2",
"markupsafe==2.1.5",
Expand All @@ -64,17 +65,12 @@ wapiti = "wapitiCore.main.wapiti:wapiti_asyncio_wrapper"
wapiti-getcookie = "wapitiCore.main.getcookie:getcookie_asyncio_wrapper"

[project.optional-dependencies]
ssl = [
"humanize==4.9.0",
"sslyze==5.2.0"
]
test = [
"humanize==4.9.0",
"pytest==8.0.2",
"pytest-cov==4.1.0",
"pytest-asyncio==0.23.5",
"respx==0.20.2",
"sslyze==5.2.0"
]

[tool.setuptools.packages]
Expand Down
130 changes: 124 additions & 6 deletions tests/attack/test_mod_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,22 @@
import http.server
import ssl
from unittest.mock import AsyncMock
from datetime import datetime, timedelta

import httpx
import pytest
import respx
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa

from wapitiCore.net.classes import CrawlerConfiguration
from wapitiCore.net import Request
from wapitiCore.language.vulnerability import CRITICAL_LEVEL, HIGH_LEVEL, INFO_LEVEL
from wapitiCore.language.vulnerability import CRITICAL_LEVEL, HIGH_LEVEL, INFO_LEVEL, MEDIUM_LEVEL
from wapitiCore.net.crawler import AsyncCrawler
from wapitiCore.attack.mod_ssl import ModuleSsl, NAME
from wapitiCore.attack.mod_ssl import ModuleSsl, NAME, extract_altnames, match_address, check_ocsp_must_staple, \
check_ev_certificate, process_vulnerabilities, process_bad_protocols


def https_server(cert_directory: str):
Expand Down Expand Up @@ -88,11 +96,11 @@ async def test_ssl_scanner():
payload_type="vulnerability",
module="ssl",
category=NAME,
level=CRITICAL_LEVEL,
level=HIGH_LEVEL,
request=request,
parameter='',
wstg=["WSTG-CRYP-01"],
info="Certificate is invalid for Mozilla trust store: self-signed certificate",
info="Strict Transport Security (HSTS) is not set",
response=None
)

Expand All @@ -101,10 +109,120 @@ async def test_ssl_scanner():
payload_type="vulnerability",
module="ssl",
category=NAME,
level=HIGH_LEVEL,
level=MEDIUM_LEVEL,
request=request,
parameter='',
wstg=["WSTG-CRYP-01"],
info="Strict Transport Security (HSTS) is not set",
info="Self-signed certificate detected: The certificate is not signed by a trusted Certificate Authority",
response=None
)


def test_extract_alt_names():
assert ["perdu.com", "test.fr"] == extract_altnames("DNS:perdu.com, DNS:test.fr, whatever, ")


def test_match_address():
assert match_address("sub.domain.com", "domain.com", ["*.domain.com", "yolo"])
assert match_address("sub.domain.com", "*.domain.com", ["yolo"])
assert not match_address("sub.domain.com", "google.com", ["*.truc.com"])


def generate_cert(include_organization_name: bool = True, include_ocsp_must_staple: bool = True):
# Generate a private key
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)

# Build the subject name
subject_name = [
x509.NameAttribute(x509.NameOID.COUNTRY_NAME, u"US"),
x509.NameAttribute(x509.NameOID.STATE_OR_PROVINCE_NAME, u"California"),
x509.NameAttribute(x509.NameOID.LOCALITY_NAME, u"San Francisco"),
x509.NameAttribute(x509.NameOID.COMMON_NAME, u"mysite.com"),
]

if include_organization_name:
subject_name.append(x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, u"My Company"))

# Generate a certificate
subject = issuer = x509.Name(subject_name)

cert_builder = x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
).public_key(
private_key.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.utcnow()
).not_valid_after(
datetime.utcnow() + timedelta(days=10)
).add_extension(
x509.SubjectAlternativeName([x509.DNSName(u"localhost")]),
critical=False,
)

if include_ocsp_must_staple:
cert_builder = cert_builder.add_extension(
x509.TLSFeature([x509.TLSFeatureType.status_request]),
critical=False
)

cert = cert_builder.sign(private_key, hashes.SHA256(), default_backend())
return cert


@pytest.mark.asyncio
@respx.mock
async def test_certificate_transparency():
cert = generate_cert()
respx.get(f'https://crt.sh/?q={cert.serial_number}').mock(
# Method GET that serve as a reference
return_value=httpx.Response(200, text="Success")
)

persister = AsyncMock()
request = Request("https://127.0.0.1:4443/")
request.path_id = 42
crawler_configuration = CrawlerConfiguration(Request("https://127.0.0.1:4443/"), timeout=1)
async with AsyncCrawler.with_configuration(crawler_configuration) as crawler:
options = {"timeout": 10, "level": 2}

module = ModuleSsl(crawler, persister, options, Event(), crawler_configuration)
assert 1 == await module.check_certificate_transparency(cert)


def test_ocsp():
assert 0 == check_ocsp_must_staple(generate_cert(include_ocsp_must_staple=False))
assert 1 == check_ocsp_must_staple(generate_cert())


def test_extended_validation():
assert 0 == check_ev_certificate(generate_cert(include_organization_name=False))
assert 1 == check_ev_certificate(generate_cert())


@pytest.mark.asyncio
async def test_process_vulnerabilities():
base_dir = os.path.dirname(sys.modules["wapitiCore"].__file__)
xml_file = os.path.join(base_dir, "..", "tests/data/ssl/broken_ssl.xml")
results = [info async for info in process_vulnerabilities(xml_file)]
assert [
(4, 'Server is vulnerable to Heartbleed attack via TLSv1.0'),
(3, 'Server honors client-initiated renegotiations (vulnerable to DoS attacks)')
] == results


@pytest.mark.asyncio
async def test_process_bad_protocols():
base_dir = os.path.dirname(sys.modules["wapitiCore"].__file__)
xml_file = os.path.join(base_dir, "..", "tests/data/ssl/broken_ssl.xml")
results = [info async for info in process_bad_protocols(xml_file)]
assert [
(4, 'The following protocols are deprecated and/or insecure and should be deactivated: SSLv2, TLSv1.0')
] == results
42 changes: 42 additions & 0 deletions tests/data/ssl/broken_ssl.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<document title="SSLScan Results" version="2.1.3" web="http://github.com/rbsec/sslscan">
<ssltest host="broken.ssl" sniname="broken.ssl" port="443">
<protocol type="ssl" version="2" enabled="1" />
<protocol type="ssl" version="3" enabled="0" />
<protocol type="tls" version="1.0" enabled="1" />
<protocol type="tls" version="1.1" enabled="0" />
<protocol type="tls" version="1.2" enabled="0" />
<protocol type="tls" version="1.3" enabled="0" />
<renegotiation supported="1" secure="0" />
<compression supported="1" />
<heartbleed sslversion="TLSv1.0" vulnerable="1" />
<cipher status="preferred" sslversion="TLSv1.0" bits="256" cipher="TLS_DHE_RSA_WITH_AES_256_CBC_SHA" id="0x0039" strength="acceptable" dhebits="1024" />
<cipher status="accepted" sslversion="TLSv1.0" bits="128" cipher="TLS_DHE_RSA_WITH_AES_128_CBC_SHA" id="0x0033" strength="acceptable" dhebits="1024" />
<cipher status="accepted" sslversion="TLSv1.0" bits="256" cipher="TLS_RSA_WITH_AES_256_CBC_SHA" id="0x0035" strength="acceptable" />
<cipher status="accepted" sslversion="TLSv1.0" bits="128" cipher="TLS_RSA_WITH_AES_128_CBC_SHA" id="0x002F" strength="acceptable" />
<cipher status="accepted" sslversion="TLSv1.0" bits="40" cipher="TLS_RSA_EXPORT_WITH_RC4_40_MD5" id="0x0003" strength="weak" />
<cipher status="accepted" sslversion="TLSv1.0" bits="128" cipher="TLS_RSA_WITH_RC4_128_MD5" id="0x0004" strength="medium" />
<cipher status="accepted" sslversion="TLSv1.0" bits="128" cipher="TLS_RSA_WITH_RC4_128_SHA" id="0x0005" strength="medium" />
<cipher status="accepted" sslversion="TLSv1.0" bits="40" cipher="TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5" id="0x0006" strength="weak" />
<cipher status="accepted" sslversion="TLSv1.0" bits="40" cipher="TLS_RSA_EXPORT_WITH_DES40_CBC_SHA" id="0x0008" strength="weak" />
<cipher status="accepted" sslversion="TLSv1.0" bits="56" cipher="TLS_RSA_WITH_DES_CBC_SHA" id="0x0009" strength="medium" />
<cipher status="accepted" sslversion="TLSv1.0" bits="112" cipher="TLS_RSA_WITH_3DES_EDE_CBC_SHA" id="0x000A" strength="medium" />
<cipher status="accepted" sslversion="TLSv1.0" bits="40" cipher="TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA" id="0x0014" strength="weak" />
<cipher status="accepted" sslversion="TLSv1.0" bits="56" cipher="TLS_DHE_RSA_WITH_DES_CBC_SHA" id="0x0015" strength="medium" />
<cipher status="accepted" sslversion="TLSv1.0" bits="112" cipher="TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA" id="0x0016" strength="medium" />
<certificates>
<certificate type="short">
<signature-algorithm>sha256WithRSAEncryption</signature-algorithm>
<pk error="false" type="RSA" bits="2048" />
<subject><![CDATA[broken.ssl]]></subject>
<altnames><![CDATA[DNS:whatever.ssl]]></altnames>
<issuer><![CDATA[Thawte TLS RSA CA G1]]></issuer>
<self-signed>false</self-signed>
<not-valid-before>Jan 19 00:00:00 2024 GMT</not-valid-before>
<not-yet-valid>false</not-yet-valid>
<not-valid-after>Feb 22 23:59:59 2024 GMT</not-valid-after>
<expired>true</expired>
</certificate>
</certificates>
</ssltest>
</document>
Loading