From f3138ee2d183824f18313a0aa53a9b72e814eda1 Mon Sep 17 00:00:00 2001 From: Iman Khosravi Date: Sun, 5 Nov 2023 00:11:40 +0000 Subject: [PATCH] UDP 427 SLP Protocol (#2) * SLP protocol information and honeypot service implementation * SLP updated protocol information --- .github/workflows/publish.yml | 29 +++++ UDP-389-cLDAP-DDoS-Amplification/README.md | 3 - UDP-427-SLP-DDoS-Amplification/Dockerfile | 18 +++ UDP-427-SLP-DDoS-Amplification/README.md | 119 ++++++++++++++++++ .../docker/creator.py | 55 ++++++++ .../docker/parse.py | 68 ++++++++++ .../docker/requirements.txt | 1 + UDP-427-SLP-DDoS-Amplification/docker/slp.py | 97 ++++++++++++++ docker-compose.yml | 15 ++- rate_limit.sh | 15 --- 10 files changed, 401 insertions(+), 19 deletions(-) create mode 100644 UDP-427-SLP-DDoS-Amplification/Dockerfile create mode 100644 UDP-427-SLP-DDoS-Amplification/README.md create mode 100644 UDP-427-SLP-DDoS-Amplification/docker/creator.py create mode 100644 UDP-427-SLP-DDoS-Amplification/docker/parse.py create mode 100644 UDP-427-SLP-DDoS-Amplification/docker/requirements.txt create mode 100644 UDP-427-SLP-DDoS-Amplification/docker/slp.py delete mode 100644 rate_limit.sh diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 650505b..7dc45f0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -32,3 +32,32 @@ jobs: cd UDP-389-cLDAP-DDoS-Amplification docker build . --tag ghcr.io/im4kv/malicious-traffic-research:cldap.latest docker push ghcr.io/im4kv/malicious-traffic-research:cldap.latest + +publish-slp-docker-image: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dorny/paths-filter@v2 + id: changes + with: + filters: | + slp: + - 'UDP-427-SLP-DDoS-Amplification/**' + + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v1 + with: + registry: https://ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # run only if some file in 'cldap' folder was changed + - name: Build the SLP Honeypot Docker image + if: steps.changes.outputs.slp == 'true' + run: | + cd UDP-427-SLP-DDoS-Amplification + docker build . --tag ghcr.io/im4kv/malicious-traffic-research:slp.latest + docker push ghcr.io/im4kv/malicious-traffic-research:slp.latest diff --git a/UDP-389-cLDAP-DDoS-Amplification/README.md b/UDP-389-cLDAP-DDoS-Amplification/README.md index adaef29..96529a9 100644 --- a/UDP-389-cLDAP-DDoS-Amplification/README.md +++ b/UDP-389-cLDAP-DDoS-Amplification/README.md @@ -1,8 +1,5 @@ # cLDAP (Connectionless Lightweight Directory Access Protocol) UDP 389 ---- - - ### protocol flaw diff --git a/UDP-427-SLP-DDoS-Amplification/Dockerfile b/UDP-427-SLP-DDoS-Amplification/Dockerfile new file mode 100644 index 0000000..fd0bdc8 --- /dev/null +++ b/UDP-427-SLP-DDoS-Amplification/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.10-alpine3.17 + +# Copy application files & Install packages +COPY docker/ /opt/dist/ +RUN python3 -m pip install --no-cache-dir -r /opt/dist/requirements.txt && \ +# Setup user, groups and configs + addgroup --gid 2000 mtrapp && \ + adduser -D -u 2000 -G mtrapp mtrapp && \ + chown mtrapp:mtrapp -R /opt/dist + +STOPSIGNAL SIGINT +USER mtrapp:mtrapp + +WORKDIR /opt/dist + +# Start Service +CMD ["python3","slp.py"] + diff --git a/UDP-427-SLP-DDoS-Amplification/README.md b/UDP-427-SLP-DDoS-Amplification/README.md new file mode 100644 index 0000000..c3b9998 --- /dev/null +++ b/UDP-427-SLP-DDoS-Amplification/README.md @@ -0,0 +1,119 @@ +# SLP (Service Location Protocol) UDP 427 + +### protocol flaw + +The Service Location Protocol ([SLP](https://en.wikipedia.org/wiki/Service_Location_Protocol)) [RFC 2165](https://www.rfc-editor.org/rfc/rfc2165.html) is a legacy "service discovery" protocol that dates back to 1997 and was meant to be used on local networks for automated service discovery and dynamic configuration between applications. The SLP daemon on a system will maintain a directory of available services such as printers, file servers, and other network resources. It will listen to requests on UDP port 427. +SLP is a relatively obsolete protocol and has mostly been supplanted by more modern alternatives like UPnP, mDNS/Zeroconf, and WS-Discovery. Nevertheless, many commercial products still offer support for SLP. Since SLP has no method for authentication, it should never be exposed to the public Internet. + +It should be mentioned that the SLP protocols uses both UDP and TCP for the transmission of data (most packets are transmitted using UDP, but TCP can also be used for the transmission of longer packets). Researchers have discovered that the UDP version of this protocol has an amplification factor of up to 2,200x. + +As specified in the protocol [RFC](https://www.rfc-editor.org/rfc/rfc2165.html#page-56) it has three main components: + +#### User-Agents (UA) - Clients: +- Description: User-Agents are the clients or end-user applications that utilize SLP to discover and access services available on the network. +- Functionality: UAs send service requests to the network in order to find specific services, such as printers, file servers, or other resources. +- Example: A web browser acting as a User-Agent can use SLP to locate available web servers on the network. + +#### Service Agents (SA) - Register/DeRegister Services: +- Description: Service Agents are responsible for registering services on the network and informing the Directory Agents of their availability. +- Functionality: SAs announce their services to the network so that User-Agents can locate them when needed. They also handle the process of unregistering services when they become unavailable. +- Example: A printer with an SLP-enabled component can register itself on the network, allowing User-Agents to discover and print documents. + +#### Directory Agents (DA) - Advertise Services: +- Description: Directory Agents maintain a directory of available services within a network domain. +- Functionality: DAs collect and store service advertisements from Service Agents. They respond to service requests from User-Agents by providing information about available services. +- Example: A Directory Agent can maintain a list of all the printers, file servers, and other services available in a local network and respond to User-Agent queries with this information. + +Malicious actors may craft User-Agent requests with victim's IP address that trigger excessive responses from Directory Agents. This amplifies the traffic directed at the victim, making the attack more potent. + + +The protocol has two main versions.The original version of SLP (Version 1), defined in [RFC 2165](https://www.rfc-editor.org/rfc/rfc2165.html). It was published in June 1997. SLPv2 is an enhanced and more widely adopted version of the protocol. It addressed some of the limitations of SLPv1 and introduced additional features. SLPv2 is defined in [RFC 2608](https://www.ietf.org/rfc/rfc2608.txt) (published in June 1999) and later updated by [RFC 3111](https://datatracker.ietf.org/doc/html/rfc3111) for IPv6 (published in May 2001). + +SLPv2 is the version most commonly used in practice due to its improved capabilities and broader support in networking environments + + +### payload Info + +SLP defines its own custom message format with specific fields, as outlined in the SLP RFC documents (such as page 17 of [RFC 2608](https://www.ietf.org/rfc/rfc2608.txt) for SLPv2). + +Here are general structure of the SLP requests: +
+
+ 0                   1                   2                   3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|    Version    |  Function-ID  |            Length             |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| Length, contd.|O|F|R|       reserved          |Next Ext Offset|
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|  Next Extension Offset, contd.|              XID              |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|      Language Tag Length      |         Language Tag          \
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+Information about fields:
+
+Version (2 bytes): This field indicates the version of the SLP protocol being used.
+
+Function ID (2 bytes): Specifies the function of the message (e.g., Service Request, Service Reply, Service Registration).
+    Service Request          SrvRqst              1             # Absused in DDoS Attacks
+    Service Reply            SrvRply              2
+    Service Registration     SrvReg               3             # Abused in DDoS Attacks
+    Service Deregister       SrvDeReg             4
+    Service Acknowledge      SrvAck               5
+    Attribute Request        AttrRqst             6
+    Attribute Reply          AttrRply             7
+    DA Advertisement         DAAdvert             8
+    Service Type Request     SrvTypeRqst          9             #  Abused in DDoS Attacks
+    Service Type Reply       SrvTypeRply          10
+    SA Advertisement         SAAdvert             11
+
+Length (2 bytes): Indicates the length of the message in bytes.
+
+Flags (1 byte): Contains flags that provide additional information about the message.
+
+Next Ext Offset (1 byte): Points to the offset of the next extension, allowing for variable-length extension blocks.
+
+XID (2 bytes): Transaction identifier used to match replies to requests.
+
+Language Tag (variable length): Indicates the natural language used in string fields.
+
+Payload (variable length): Contains the specific information related to the function of the message (e.g., URL for service location, service attributes, etc.).
+
+Extensions (variable length): May be present in some messages to carry additional information.
+
+ +Here is an example of a binary SLP request used to to check vulnerable servers:
\x02\t\x00\x00\x1d\x00\x00\x00\x00\x00s_\x00\x02en\x00\x00\xff\xff\x00\x07default
+ +To successfully decode this, we can use custom parsers to decode the binary protocol based on the specification available in the SLP RFCs: +
+{'version': 2, 'function_id': 9, 'length': 29, 'xid': 29535, 'language_tag_length': 2, 'language_tag': 'en'}
+
+The decoded payload shows it is a SLP version two packet which contains a `Service Type Request (SrvTypeRqst)` as the `function_id` field. the `xid` contain the transaction ID which will be the same for the response of this request. `Service Type Request (SrvTypeRqst)` allows a User-Agent (Could be Malicious Actor) to discover all types of service on a network. This is useful for general purpose service browsers. as a note `function_id` of 1 for `Service Request (SrvRqst)` will only be used to get specific services that matches the type specified in the request, hence it covers smaller scope of services and usually it has smaller responses. + +for testing purposes, we can send the payload to a SLP server and check the size of the response via wireshark or tcpdump: +
echo "\x02\t\x00\x00\x1d\x00\x00\x00\x00\x00s_\x00\x02en\x00\x00\xff\xff\x00\x07default" | nc -4u -w1 SERVER-ADDRESS-HERE 427
+ +A greatly amplified attack using SLP would have the following attack design: +1) Find a publicly available SLP service - Reconnaissance +2) Verify it allows registration of new services - Setup Phase +3) Register services, until SLP denies more entries - Setup Phase +4) Check response size - Finalize +5) Create spoofed packet-stream with the victim as the origin - Launch an attack + + +### [Honeypot Service Info](docker) + + - Honeypot service will accept three `function_id`: Service Request, Service Registration and Service Type Request. + - It will parse the protocol and drop the coming datagram in case the packet is malformed. + - In the case of Service Request and Service Type Requests, the server will respond with two predefined services (The URI of an API and VMware ESXi service) + - For Service Registration requests, it will not store the Incoming data but acknowledge the requests. + - The default rate limiting threshold is five datagrams (5 requests) per 24 hours. The server will not respond to the requests when the rate limiting threshold is exceeded. + + + +#### References +1) CVE-2023-29552 Service Location Protocol-Denial of Service Amplification Attack: + - https://curesec.com/blog/article/CVE-2023-29552-Service-Location-Protocol-Denial-of-Service-Amplification-Attack-212.html + - https://www.bitsight.com/blog/new-high-severity-vulnerability-cve-2023-29552-discovered-service-location-protocol-slp + diff --git a/UDP-427-SLP-DDoS-Amplification/docker/creator.py b/UDP-427-SLP-DDoS-Amplification/docker/creator.py new file mode 100644 index 0000000..55aef39 --- /dev/null +++ b/UDP-427-SLP-DDoS-Amplification/docker/creator.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +import uuid + + +def create_header(function_id, data_length, ofr, version=2, xid=None, language_tag='en'): + header = b'' + for b in [version, function_id]: + header += bytes([b]) + + language_tag_length = len(language_tag.encode()) + + header += (14 + language_tag_length + data_length).to_bytes(3, byteorder='big') + + u = uuid.uuid1() + for b in [ofr, 0, 0, 0, 0]: + header += bytes([b]) + + if xid is None: + for b in [u.clock_seq_hi_variant, u.clock_seq_low]: + header += bytes([b]) + else: + header += xid.to_bytes(2, byteorder='big') + + header += language_tag_length.to_bytes(2, byteorder='big') + header += language_tag.encode() + + return header + + +def create_acknowledge(xid, error_code=0): + data = error_code.to_bytes(length=2, byteorder='big') + header = create_header(function_id=5, data_length=len(data), xid=xid, ofr=0) + return header + data + + +def create_url_entry(lifetime, url): + data = bytes([0]) + data += lifetime.to_bytes(length=2, byteorder='big') + data += (len(url.encode())).to_bytes(length=2, byteorder='big') + data += url.encode() + data += bytes([0]) + return data + + +def create_reply(xid, url_entries, error_code=0): + data = error_code.to_bytes(length=2, byteorder='big') + data += len(url_entries).to_bytes(length=2, byteorder='big') + for entry in url_entries: + data += create_url_entry(**entry) + header = create_header(function_id=2, data_length=len(data), xid=xid, ofr=0) + return header + data + + + diff --git a/UDP-427-SLP-DDoS-Amplification/docker/parse.py b/UDP-427-SLP-DDoS-Amplification/docker/parse.py new file mode 100644 index 0000000..dfbc9db --- /dev/null +++ b/UDP-427-SLP-DDoS-Amplification/docker/parse.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + + +def convert_to_int(data): + return int.from_bytes(data, byteorder='big') + + +def _parse(data, count): + p = 0 + result = list() + for _ in range(count): + length = convert_to_int(data[p:p + 2]) + p += 2 + value = data[p:p + length] + p += length + result.append( + dict( + value=value, + length=length + ) + ) + + return [info['value'] for info in result], 7 + sum([info['length'] for info in result]) + + +def parse_header(data): + language_tag_length = convert_to_int(data[12:14]) + header_length = 14 + language_tag_length + return dict( + version=data[0], + function_id=data[1], + length=convert_to_int(data[2:5]), + xid=convert_to_int(data[10:12]), + language_tag_length=language_tag_length, + language_tag=data[14:header_length].decode() + ), header_length + + +def parse_url_entry(data): + url_length = convert_to_int(data[3:5]) + auth_length = data[5 + url_length] + length = 6 + url_length + auth_length + return dict( + lifetime=convert_to_int(data[1:3]), + url=data[5:5 + url_length].decode() + ), length + + +def parse_registration(data): + header, header_length = parse_header(data) + url_entry, url_entries_length = parse_url_entry(data[header_length:]) + + result, length = _parse(data[header_length + url_entries_length:], 3) + + return header, url_entry, dict( + service_type=result[0].decode(), + scope_list=result[1].decode(), + attr_list=result[2].decode() + ) + + +def parse_request(data): + header, header_length = parse_header(data) + result, length = _parse(data[header_length:], 5) + return header, dict( + service_type=result[1].decode(), + scope_list=result[2].decode() + ) diff --git a/UDP-427-SLP-DDoS-Amplification/docker/requirements.txt b/UDP-427-SLP-DDoS-Amplification/docker/requirements.txt new file mode 100644 index 0000000..522b0e8 --- /dev/null +++ b/UDP-427-SLP-DDoS-Amplification/docker/requirements.txt @@ -0,0 +1 @@ +twisted~=23.8.0 \ No newline at end of file diff --git a/UDP-427-SLP-DDoS-Amplification/docker/slp.py b/UDP-427-SLP-DDoS-Amplification/docker/slp.py new file mode 100644 index 0000000..207ceeb --- /dev/null +++ b/UDP-427-SLP-DDoS-Amplification/docker/slp.py @@ -0,0 +1,97 @@ +import sys +from twisted.internet import reactor, task +from twisted.internet.protocol import DatagramProtocol +from twisted.python import log +import binascii +import parse +import creator +from collections import defaultdict + + +APP_NAME = 'udp-slp' +RATE_LIMITING_THRESHOLD = 3 + +class MockSLP(DatagramProtocol): + rate_limit_counter = defaultdict(int) + def _service_request_response(self,datagram,addr): + '''Handling function ID 1: Service Request (SrvRqst) and 9: Service Type Request (SrvTypeRqst)''' + header, _ = parse.parse_header(datagram) + response = creator.create_reply( + xid=header['xid'], + url_entries=self.url_entries + ) + self.transport.write(response, addr) + + def _service_reg_response(self,datagram,addr): + + ''' Service Registration (SrvReg) + We are not accepting any service registration requests, let's just create fake acknowledgement response + ''' + try: + header, url_entries, msg = parse.parse_registration(datagram) + log.msg(f"app:{APP_NAME} source_ip:{addr[0]} source_port:{addr[1]} decoded_header:{header} url_entries:{url_entries}, msg:{msg}") + response = creator.create_acknowledge(xid=header['xid']) + self.transport.write(response, addr) + except Exception as err: + log.err(f"app:{APP_NAME} source_ip:{addr[0]} source_port:{addr[1]} payload_parse_error: {err}") + return None + def _default_request_hanlder(self,datagram,addr): + ''' Unsupported function IDs will not be processed''' + log.msg(f"app:{APP_NAME} source_ip:{addr[0]} source_port:{addr[1]} msg: Unsupported function ID") + return None + + + + def __init__(self) -> None: + super().__init__() + self.url_entries = [dict( + url='service:VMwareInfrastructure://10.10.125.10:427/MyVMwareService', + lifetime=15 + ), + dict( + url='service:api:https://10.10.125.11', + lifetime=15 + ), + ] + self.function_handlers = { + 1 : self._service_request_response, + 3: self._service_reg_response, + 9: self._service_request_response + } + + def reset_rate_limit_counter(): + MockSLP.rate_limit_counter = defaultdict(int) + + def datagramReceived(self, datagram, addr): + source_ip = addr[0] + source_port = addr[1] + MockSLP.rate_limit_counter[source_ip] += 1 + log.msg(f"app:{APP_NAME} source_ip:{source_ip} source_port:{source_port} raw_message_hex:" + \ + f"{binascii.b2a_hex(datagram, b' ')}") + try: + header, _ = parse.parse_header(datagram) + log.msg(f"app:{APP_NAME} source_ip:{addr[0]} source_port:{addr[1]} decoded_header:{header}") + except Exception as err: + log.err(f"app:{APP_NAME} source_ip:{addr[0]} source_port:{addr[1]} payload_parse_error: {err}") + return None + + function_id_handler = self.function_handlers.get(header['function_id'], self._default_request_hanlder) + if MockSLP.rate_limit_counter[source_ip] < RATE_LIMITING_THRESHOLD: + function_id_handler(datagram,addr) + else: + log.msg(f"app:{APP_NAME} source_ip:{addr[0]} source_port:{addr[1]} msg:rate limit exceeded { self.rate_limit_counter[source_ip]} requests received") + + + +def main(): + log.startLogging(sys.stdout) + # Schedule reset_counter to be called every 24 hours + rate_limit_reset_task = task.LoopingCall(MockSLP.reset_rate_limit_counter) + rate_limit_reset_task.start(24 * 60 * 60) # 24 hours in seconds + + reactor.listenUDP(427, MockSLP()) + reactor.run() + +if __name__ == '__main__': + main() + diff --git a/docker-compose.yml b/docker-compose.yml index 414d7ec..3dbccaf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,20 @@ services: - mtr_local ports: - "389:389/udp" - image: "hcr.io/im4kv/malicious-traffic-research:cldap.latest" + image: "ghcr.io/im4kv/malicious-traffic-research:cldap.latest" + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "5" + slp: + container_name: mtr-slp + restart: always + networks: + - mtr_local + ports: + - "427:427/udp" + image: "ghcr.io/im4kv/malicious-traffic-research:cldap.latest" logging: driver: "json-file" options: diff --git a/rate_limit.sh b/rate_limit.sh deleted file mode 100644 index 6aaa578..0000000 --- a/rate_limit.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -# OBJECTIVE: rate limit the cLDAP inboud traffic to evade cLDAP DDoS amplification attack -# add this to your initialization iptables script - -#iptables -F -#iptables -X - -iptables -N CLDAP_RATE_LIMIT - iptables -A CLDAP_RATE_LIMIT -m hashlimit --hashlimit-above 5/minute --hashlimit-mode srcip --hashlimit-name cldap_rate_limit -j DROP - iptables -A CLDAP_RATE_LIMIT -m hashlimit --hashlimit-above 10/hour --hashlimit-mode srcip --hashlimit-name cldap_rate_limit -j DROP - iptables -A CLDAP_RATE_LIMIT -j ACCEPT -# optionally jump to your custom LOG_DROP or LOG_ACCEPT chains - -# cLDAP honeypot -iptables -I INPUT -p udp --dport 389 -j CLDAP_RATE_LIMIT