Skip to content

Commit

Permalink
mod_ssl: Use sslscan in place of sslyze
Browse files Browse the repository at this point in the history
  • Loading branch information
devl00p committed Jun 30, 2024
1 parent 3a0f685 commit b591702
Show file tree
Hide file tree
Showing 8 changed files with 472 additions and 260 deletions.
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

0 comments on commit b591702

Please sign in to comment.