Detection Exercise: D-Link DIR-513 (CVEs: 2025-8184, 8169, and 8168)
We’ve recently had an intern join the ET team (say hello to @kraghu). I went over how I turned a github repo with some PoC exploit code into a rule that covered all three exploits presented in the repo. We went over rule structure, what the different options and modifiers do, the differences between the Suricata and Snort rule and why they exist, how to archive proof-of-concept code (or articles, blog posts, etc.) so that they aren’t lost when code repos get deleted, or blogs get removed, etc.
I figured this might be something nice for the community, and doesn’t deserve to be walls of text in a Discord channel, so here I am. Let’s get started!
The Official Documentation
Being a member of the ET team means that, as of August 2025 we support Snort 2.9.x, Suricata 5, and Suricata 7.0.3. So, first and foremost, that means being familiar with sources of documentation for all three engines.
The OISF has an expansive “Read the Docs” style wiki for all the major features available with every release of Suricata
The Suricata 5 documentation is here, While Suricata 7.0.3 is here.
Meanwhile Cisco maintains the website snort.org, that has documentation for Snort 2.9.x, and Snort 3. To that effect, here’s a PDF, and here’s the HTML version of the documentation for snort 2.9.x.
Why do we only support certain Suricata/Snort rule versions?
While Suricata 8 is the current release, we standardize on the 5.x and the 7.0.3 rule Syntax. You, as an independent researcher can use all of the new, cool stuff in newer versions, but we can’t, because we have to maintain some form of consistency in the ruleset we ship. So we ship the 5.x ruleset, because it’s forward-compatible all the way up to the latest 8.x releases of Suricata, and we also ship the 7.0.3 ruleset, so that we would introduce newer features and efficiencies into the ruleset to everyone’s benefit.
When we decide to upgrade the syntax we support, or drop support for an older version of Snort/Suricata (like, say, Suricata 4.x) that is called a “ruleset fork”, and its a major endeavor to comb through the ruleset and try and update it as much as possible to make better use of the new keywords and features.
Do you support Snort3 yet?
We don’t officially support it, but if you’re interested in trying to run the ET ruleset on a Snort3 sensor, I did a very in-depth blog post on that already, about how to use snort2lua
to convert existing ET Snort2.9 rules to work with Snort3.
The Subject at Hand
So far, we’ve covered where to find the official documentation, the versions of Snort and Suricata supported by Emerging Threats, and why we handle version coverage the way we do. Now, let’s talk about the rule I wrote to cover some IoT vulnerabilities in D-Link’s DIR-513 model router. A very reliable colleague linked me to a github repo that contains details and proof-of-concept exploits.
Just kidding, now you get to play archivist
Okay, I lied. Before we actually get down to business and do anything, let me give you some friendly advice: While I’m thankful for free proof-of-concept code, I’ve noticed that it’s a somewhat frequent occurence, especially for research pertaining to IoT devices, that many of the blog posts and/or github repos I’ve observed that contain proof-of-concept code and detailed write-ups disappear quite frequently.
Whether its the result of the researcher deleting their code or blogs arbitrarily, or other causes, it’s very detrimental to be in the middle of investigating other research, get a tip-up from friends and colleagues in information security about a new proof-of-concept exploit, put a pin in it to come back to it later when there is more time for me to examine it, only to find the blog post, code, or write-up is just gone. Even worse is cve.org will often include reference links to said blog posts or github repositories, leading to dead links. Several other threat intel and vulnerability management sites also mirror/scrape data from cve.org, and will also gladly archive dead links, unaware the data is no longer available.
How does one prevent this? Take matters into your own hands, and archive the data immeidately. There are a variety of archival tools available out there.
- archive.ph
- archive.is
- web.archive.org
wget -m
git clone
the code repo in question- making screen captures of the page/blog, wget and/or other archival sources are blocked
- Do you use a solution not listed here for archiving research? List them in the comments below.
Use these tools to take snapshots of the page before the research you want to look at gets taken down. Out of the tools above, web.archive.org (aka “The Wayback Machine”) is probably my favorite. The Internet Archive team has provided very helpful web browser extensions available for chrome and firefox. If you create an account on archive.org, you can use the extension to snapshot the current page, take a screenshot of the page AND it will archive documents that the current page links to (outlinks). I very highly recommend installing this extension to your browser of choice.Chrome users, go here. Firefox users, go here.
Be aware that some of the archive/mirroring solutions (even the internet archive) are imperfect solutions. Several websites feature anti-scraping countermeasures that will prevent these tools from working, such as cloudflare “turnstiles”, “proof-of-work” things the website will require a web browser to do to prove they are not a bot or an AI scraper, and/or utilizing the robots.txt and/or the robots http metatag using either the noindex
, noarchive
, and/or nofollow
options. If one solution doesn’t work (web archive), try another (wget -m
, git clone
, screenshots, etc.). Because while the famous quote is “Once it’s on the internet, it’s there forever”, that’s not always true.
Here is an example of this issue occurring: CVE-2024-42757. The CVE advisory contains a link to a github repo that contains a Proof of concept/writeup. The link is dead. However, someone thought ahead, and archived the write-up, making it available on the wayback machine.
The good news is that the write-up contains enough of the details to write a rule for this vulnerability, the bad news is, because the exp.py
in the github repo wasn’t also archived (it’s considered an “outlink”), the actual proof of concept code was lost.
I can’t understate the value of archiving the data you wish to research for later. With that covered, let’ continue on to the vulnerabilities we’ll be looking at today.
The vulnerabilities and the exploits
Going back to the repo for the D-Link DIR-513, If you look inside of the DIR-513 directory (the model of the vulnerable router), you’ll see three markdown files.
All three markdown files in the github repo.
Here is a rundown of the contents of all three markdown files:
All three vulnerabilities are buffer overflows.
Vuln 1: buffer overflow in curTime
POST body parameter in /goform/formSetWanPPPoE
URI endpoint
Vuln 2: buffer overflow in curTime
POST body parameter in /goform/formSetWanL2TP
URI endpoint
Vuln 3: buffer overflow in curTime
POST body parameter in /goform/formSetWanPPTP
URI endpoint
and for those who like details, here are the detailed proof of concept HTTP requests included in the write-ups:
POST /goform/formSetWanPPPoE HTTP/1.1
Host: 192.168.0.1
Content-Length: 6928
Cache-Control: max-age=0
Origin: http://192.168.0.1
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.0.1/Basic/wan_pppoe.asp?t=1746450588613
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Connection: keep-alive
settingsChanged=1&curTime=111111111[truncated]111111111111&dsl_mode=2&config.wan_force_static_dns_servers=false&config.wan_ip_mode=2&config.pppoe_use_dynamic_address=true&config.pppoe_reconnect_mode=1&config.wan_mtu_use_default=true&config.mac_cloning_enabled=false&config.mac_cloning_address=00%3A00%3A00%3A00%3A00%3A00&mac_clone=000000000000&pppoe_use_dynamic_dns=true&config.pppoe_netsniper=false&config.pppoe_xkjs=false&config.xkjs_mode=0&webpage=%2FBasic%2Fwan_pppoe.asp&pppoe_use_dynamic_address_radio=true&config.pppoe_username=admin&config.pppoe_password=WDB8WvbXdHtZyM8Ms2RENgHlacJghQy&config.pppoe_service_name=&mac1=00&mac2=00&mac3=00&mac4=00&mac5=00&mac6=00&pppoe_use_dynamic_dns_radio=true&config.pppoe_max_idle_time=5&config.wan_mtu=1492&ppp_schedule_control_0=Always&pppoe_reconnect_mode_radio=1
--------------------------------------------------------
POST /goform/formSetWanPPTP HTTP/1.1
Host: 192.168.0.1
Content-Length: 6804
Cache-Control: max-age=0
Origin: http://192.168.0.1
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.0.1/Basic/wan_pptp.asp?t=1746450737117
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Connection: keep-alive
settingsChanged=1&curTime=11111111111[truncated]111111111111111&dsl_mode=3&config.wan_force_static_dns_servers=false&config.wan_ip_mode=3&config.wan_mtu_use_default=true&config.mac_cloning_enabled=false&mac_clone=&config.wan_pptp_reconnect_mode=1&config.wan_pptp_use_dynamic_carrier=true&webpage=%2FBasic%2Fwan_pptp.asp&config.mac_cloning_address=00%3A00%3A00%3A00%3A00%3A00&wan_pptp_use_dynamic_carrier_radio=true&mac1=00&mac2=00&mac3=00&mac4=00&mac5=00&mac6=00&config.wan_pptp_server=1.1.1.1&config.wan_pptp_username=admin&config.wan_pptp_password=WDB8WvbXdHtZyM8Ms2RENgHlacJghQy&config.wan_pptp_max_idle_time=5&config.wan_mtu=1400&ppp_schedule_control_0=Always&pptp_reconnect_mode_radio=1
--------------------------------------------------------
POST /goform/formSetWanL2TP HTTP/1.1
Host: 192.168.0.1
Content-Length: 6816
Cache-Control: max-age=0
Origin: http://192.168.0.1
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.0.1/Basic/wan_l2tp.asp?t=1746450855638
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Connection: keep-alive
settingsChanged=1&curTime=1111111111[truncated]1111111111111111&dsl_mode=4&config.wan_force_static_dns_servers=false&config.wan_ip_mode=4&config.wan_mtu_use_default=true&config.mac_cloning_enabled=false&mac_clone=000000000000&config.wan_l2tp_use_dynamic_carrier=true&config.wan_l2tp_reconnect_mode=1&config.mac_cloning_address=00%3A00%3A00%3A00%3A00%3A00&webpage=%2FBasic%2Fwan_l2tp.asp&wan_l2tp_use_dynamic_carrier_radio=true&mac1=00&mac2=00&mac3=00&mac4=00&mac5=00&mac6=00&config.wan_l2tp_server=1.1.1.1&config.wan_l2tp_username=admin&config.wan_l2tp_password=WDB8WvbXdHtZyM8Ms2RENgHlacJghQy&config.wan_l2tp_max_idle_time=5&config.wan_mtu=1400&ppp_schedule_control_0=Always&l2tp_reconnect_mode_radio=1
The first thing I did, was turn all three proof-of-concept HTTP requests into pcap files through the tool flowsynth. It’s tool that is provided with Dalton, an IDS analysis and testing framework that I’ve gushed about plenty in this blog. If you need a rundown on how to utilize flowsynth for creating a packet capture, I have a blog post about how to do that here (Check out the “Forging pcaps with Flowsynth” section).
I recommend getting familiar with flowsynth, as its pretty easy to use, and makes testing for IDS/IPS rules considerably easier.
The Results: Suricata
You’d probably think that you would need to create three rules to cover three vulns, but they’re all similar enough to where, with a little simple pcre
work, we can cover all this in a single rule. Here is what I came up with:
alert http any any -> $HOME_NET any (msg:"ET WEB_SPECIFIC_APPS D-Link formSetWAN Multiple Endpoints curTime Parameter Buffer Overflow Attempt (CVE-2025-8184, CVE-2025-8169, CVE,2025-8168)"; flow:established,to_server; http.method; content:"POST"; http.uri; content:"/goform/formSetWan"; fast_pattern; startswith; pcre:"/^(?:L2TP|PPTP|PPPoE)$/R"; http.request_body; content:"curTime|3d|"; pcre:"/^[^&]{100,}(?:&|$)/R"; reference:url,github.com/InfiniteLin/Lin-s-CVEdb; reference:cve,2025-8184; reference:cve,2025-8169; reference:cve,2025-8168; classtype:web-application-attack; sid:1; rev:1;)
so, let’s talk about how we got here. For generic HTTP exploit rules, I have a template that I use for creating my rules:
alert http any any -> $HOME_NET any (msg:""; flow:established,to_server; reference: classtype:attempted-admin; sid:1; rev:1;)
I usually start with this, and fill in the blanks as necessary. So let’s start with the rule header:
alert http any any -> $HOME_NET any
This rule header is looking for http traffic from any source IP and port combo, heading towards our HOME_NET on any destination port. The nice thing about Suricata is that there is dynamic detection of HTTP traffic. It doesn’t matter what port the traffic is on, so long as Suricata can get the packets and recognize it as HTTP, we don’t need to know specific port numbers. Next up, we have:
(msg:"ET WEB_SPECIFIC_APPS D-Link formSetWAN Multiple Endpoints curTime Parameter Buffer Overflow Attempt (CVE-2025-8184, CVE-2025-8169, CVE,2025-8168)";
The msg
field is a metadata tag in our rule body. This is the message that gets displayed when an alert for your rule triggers, so you want it to be as descriptive as you can. In this case, we denote the rule covers multiple URI endpoints, the curTime parameter, and include the individual CVE numbers assigned to all three vulnerabilities.
How did I find the CVE numbers? While this isn’t always a foolproof exercise, rule writers can visit cve.org, and try searching to see if the vulnerability has a registered CVE number. Sometimes I’ll try searching for the vulnerable parameter (curTime
), other times, I’ll search for the URI endpoint (formSetWan
), and other times, I’ll search for the vendor (D-Link
) and see what pops up in the list of CVE numbers. While we’re talking about rule metadata, I want to talk about this chunk towards the end of the rule body:
reference:url,github.com/InfiniteLin/Lin-s-CVEdb; reference:cve,2025-8184; reference:cve,2025-8169; reference:cve,2025-8168; classtype:web-application-attack; sid:1; rev:1;)
All of these keywords are metadata tags, and have absolutely no bearing on how the rule operates. At least in a vanilla Suricata installation. Your rule management platform, MSSP, or enterprise solution might have its own metadata tags that are used to manage your rules, or signals that certain actions should be performed, but that’s outside of the scope of this lesson. This is additional information that describes where users can get more information about the rule (reference
tag – there can be more than one reference
tag per rule), under what category should this rule be placed (classification:web-application-attack
), and finally, the sid
and rev
numbers. As far as sid numbers are concerned, the website sidallocation.org has a mapping of sid ranges and what organizations/rulesets they belong to. For researchers at home, the sid range 1000000 - 1999999
is reserved for locally developed rules. The rev
number should be incremented each time the rule is modified to denote it has been changed.
Not pictured in the rule I’ve created is the actual metadata
keyword. As the name implies, this keyword contains more metadata about the rule in question that may not be covered by the other metadata keyword. In the rules I write, I don’t usually include the metadat keyword, because our internal rule management platform handles filling that out for us. Here is what the metadata
keyword looked like for this rule once it was pushed to the ETOPEN/ETPRO rulesets:
metadata:affected_product D_Link, attack_target Networking_Equipment, tls_state plaintext, created_at 2025_08_01, cve CVE_2025_8184_CVE_2025_8169, deployment Perimeter, deployment Internal, performance_impact Low, confidence High, signature_severity Major, tag Exploit, updated_at 2025_08_01, mitre_tactic_id TA0001, mitre_tactic_name Initial_Access, mitre_technique_id T1190, mitre_technique_name Exploit_Public_Facing_Application; target:dest_ip;
The format of the metadata
tag is just comma separated key-value pairs, that themselves are separated by spaces.
With that out of the way, let’s hop back towards the beginning of this rule:
flow:established,to_server; http.method; content:"POST";
The flow
keyword is used to establish whether or not the content of this rule should be a part of an established session, just an individual packet on its own, as well as the direction associated with the traffic, is it going to the client, or to the server? Let me explain this in terms of TCP three-way handshake: established
means that we’re looking for this content in a TCP stream in which the full three-way handshake has occurred. to_server
indicates we’re looking for this traffic coming from a client in the TCP stream, (session iniator) going to the server (session responder). In non-tcp terms, suricata usually defines the client side of the conversation as the first host to send a packet (initiator), and the host that responds to the request as the server (responder). typically, the flow
keyword isn’t used often at all with non-tcp traffic.
The next keyword, http.method
is our first http sticky buffer. Suricata 4 onwards utilize a concept called sticky buffers. These so-called sticky buffers allow you to define a specific portion of network traffic in which you’re looking for a content match, an independent keyword (like bsize
, isdataat
, byte_jump
, etc.) a regular expression (pcre
) etc. should apply. In this case, we’re just looking for the string “POST” in the http method portion of an HTTP request. The are two ways to escape a sticky buffer:
- Call another sticky buffer (in our case,
http.uri
) - Use
pkt_data
(nobody usespkt_data
, because it’s not a perfect solution).
If you have a content match or keyword that must be run outside of a sticky buffer, declare those keywords before declaring any sticky buffers. Very rarely will you ever need to do this, but just remember that you can do this if Suricata is acting up and the sticky buffers aren’t working like you think they should.
So that brings us to:
http.uri; content:"/goform/formSetWan"; fast_pattern; startswith;
So, we’ve jumped from the http.method
sticky buffer to http.uri
, and we’re looking for the content string “/goform/formSetWan”. The modifier startswith
allows rule writers to say “I’m expecting this content match to be at the start of the sticky buffer (or the start of the TCP payload, if you don’t define a sticky buffer)”. For those who are familiar with snort rule syntax, thing of this as: content:"/goform/formSetWan"; http_uri; depth:18
. Now, the fast_pattern
keyword is a doozy.
Setting a good, unique fast_pattern
for Snort and Suricata rules is very important. But why is that? I’m going to give you the cliffnotes version:
-
When Snort/Suricata starts up, the IDS engine sorts the rules into buckets (based on criteria in the header of the rule – IP addresses and Ports, Protocols,
flow
keyword direction, and a couple of other criterion). -
Inside those buckets, similar rules are grouped into “trees”. without getting too deep in the weeds, the fast_pattern is very important for establishing linked rule trees, and which rules might potentially be scanning different types of traffic that make it to that bucket.
-
When network traffic being analyzed by Suricata/Snort, and the traffic makes it to a certain bucket, the IDS engine will then compare the network traffic against the
fast_pattern
strings inside that bucket. -
If there is a match in the traffic against the fast pattern defined in a rule in the current bucket, that’s called a “Check”. Whenever a Check is found, the rest of the rule criteria is loaded, and evaluated against the network traffic that Snort/Suricata is analyzing.
-
If the rest of the rule matches against the network traffic, it’s a “Match”, and the rule performs the action configured in the rule header (alert, pass, drop, reject, etc.).
-
Rules that do not have a
fast_pattern
defined, Snort/Suricata default to setting the longest content match in the rule as thefast_pattern
. This isn’t always desirable. -
If the
fast_pattern
is very common, the then rule gets “checked” frequently, and the rest of the rule gets evaluated frequently. -
If there are many “checks”, but not so many matches, your fast_pattern isn’t unique enough, and your rule is causing wasted CPU cycles.
-
If there are many “checks” and many “matches”, you’re probably get a ton of alerts in the form of either alert fatigue, or false positives.
-
If there are no content matches (you decided to just vomit the
pcre
keyword everywhere), then no matter what bucket the rule ends up in, its going to get checked, loaded, and evaluated constantly, calling thepcre
engine, and wasting a TON of cpu cycles.
Next up, we have:
pcre:"/^(?:L2TP|PPTP|PPPoE)$/R";
This is a regular expression, using the pcre
(Perl Compatible Regular Expression) keyword (also sometimes called “regex”). In plain english, this regular expression would roughly translate to:
“Relative to the last content match (\R
), beginning IMMEDIATELY AFTER (^
), I need to find the string “L2TP”, or “PPTP”, or “PPPoE”, followed by the end of the line ($
)”.
If you need to brush up on your regex, I highly recommend using regex101 to test and debug your regular expressions. Let’s move on to the last keywords in this rule:
http.request_body; content:"curTime|3d|"; pcre:"/^[^&]{100,}(?:&|$)/R";
We immediately jump from the http.uri
buffer, to the http.request_body
. buffer. and we’re looking for the content match curTime|3d|
. Something important to note is that both Snort and Suricata support using the pipe characters to denote hex characters. In this case, |3d|
translates to the equal sign (“=
”). While hex escaping the equal sign in this rule isn’t strictly necessary, its good practice to escape special characters in Snort/Suricata rules. Also, its good practice to keep an ascii hex chart on-hand. There are certain special characters that Snort/Suricata will require you to escape such as double quotes ("
, |22|
), semicolons (;
, |3b|
), open parenthesis ((
, |28|
), close parenthesis ()
, |29|
), pipe characters (|
, |7c|
), etc.
That brings us to another regular expression via the pcre
keyword:
pcre:"/^[^&]{100,}(?:&|$)/R";
Which roughly translates to "Relative to the last content match (/R
), beginning IMMEDIATELY after (^
), I need to find at least 100 characters ({100,}
) that are NOT the ampersand character ([^&]
), followed by either an ampersand, or the end of line ((?:&|$)
).”
Now, here’s the rule in the Suricata job web page on my local Dalton instance:
He shoots..
and the results:
He scores! In this instance, I used one pcap with all three Proof of Concept exploit attemps. Three separate POST requests, three separate alerts.
The Results: Snort
Just like with Suricata, let’s start with the end-product:
alert tcp any any -> $HOME_NET $HTTP_PORTS (msg:"ET WEB_SPECIFIC_APPS D-Link formSetWAN Multiple Endpoints curTime Parameter Buffer Overflow Attempt"; flow:established,to_server; content:"POST"; http_method; content:"/goform/formSetWan"; depth:18; http_uri; fast_pattern; pcre:"/\x2fgoform\x2fformSetWan(?:L2TP|PPTP|PPPoE)$/U"; content:"curTime|3d|"; http_client_body; pcre:"/curTime\x3d[^&]{100,}(?:&|$)/P"; reference:url,github.com/InfiniteLin/Lin-s-CVEdb; classtype:attempted-admin; sid:1; rev:1;)
Let’s break this rule down! Staring with the very beginning of the rule, the header portion:
alert tcp any any -> $HOME_NET $HTTP_PORTS
Unlike Suricata, snort 2.9 does not have dynamic protocol detection, and you can’t define http
as a protocol in the rule header. For this rule, we have to use tcp
as the protocol, and in the destination, we have to use the portvar
HTTP_PORTS
. This variable is defined in the snort.conf and contains a myriad of ports that http servers are typically (or not very typically) seen on:
Snort 2.9.x doesn’t have dynamic protocol detection. Instead, you have a large array of port numbers specified in the
snort.conf
config file for different protocols.
Let’s say you encounter HTTP traffic on a port that isn’t in the HTTP_PORTS variable. You would have to modify the snort.conf to add that port to your config. On top of that, let’s say you want to use the http_inspect
preprocessor, (which is necessary to use snort’s variety of http modifier keywords) and take advantage of automatic http normalization/decoding the snort does automatically for HTTP traffic, for your rule. You would also need to:
- Add the port to the stream_5
ports_both
value
Because one messy array of port numbers…
- Add the port to the
http_inspect
preprocessor’s port array.
Just isn’t enough.
Why? Because the stream_5 tcp stream reassembly preprocessor, the preprocessor that handles reassembling TCP segments in a singular, cohesive stream requires manual definition of the TCP ports it will perform session reassembly on. And on top of that, the HTTP preprocessor (http_inspect
) requires stream_5’s TCP stream reassembly before HTTP normalization, and rule modifiers can be used. If your adversary uses dynamic HTTP ports for the C2, then quite frankly, rule writers can’t use http modifiers, http normalization, and if the payload spans across multiple packets, you can’t detect it, because Snort isn’t reassembling the TCP streams on those ports.
Fun fact: in environments where TLS decryption tools are in place (polarproxy, mitmproxy, etc.), this process of adding port 443 to the HTTP_PORTS
portvar, the stream5 ports_both
list, and the http_inspect
ports value has to be done for decrypted TLS traffic on port 443, or for decrypted pcaps you wish to analyze. Please note in the illustrations above, that port 443 is NOT a default value in any of those locations, that those are customizations I’ve added because I have access to TLS decrypted pcaps.
With that out of the way, I’m not going to cover the msg
, flow
, reference
, classtype
, sid
or rev
keywords again. These keywords are practically identical in both Suricata and Snort. Let’s start with:
content:"POST"; http_method;
Just like with the Suricata rule, we want to establish that we’re only interested in HTTP POST requests. With very few exceptions (file_data
, base64_decode
and base64_data
), Snort doesn’t use the concept of sticky buffers. Instead, for our HTTP rule we’re using content modifiers provided via http_inspect
. In this case, we’re modifying the content keyword, and telling Snort we only want this content where the HTTP method would be located in an HTTP request. Next up:
content:"/goform/formSetWan"; depth:18; http_uri; fast_pattern;
We’ve already established that Snort doesn’t have the startswith
modifier and we need to use depth:18
to emulate that feature. Note that the depth
keyword is used to specify how many bytes “deep” into the payload (or in this case, the http_uri
) that Snort should search for a content match. Notice that the length of this content match is exactly 18 bytes.
We’ve already established the purpose of the fast_pattern
keyword, but I just want to cover some special notes on how fast_pattern
is slightly different for Snort users. By default, Snort truncates the fast_pattern
string to 20 bytes. This behavior can be modified in snort.conf via the line:
config detection: search-method ac-split search-optimize max-pattern-len 20
This is the reason why when you look at rules in the Emerging Threats ruleset, why you’ll sometimes see fast_pattern:x,y
, x defines how far into the content match string to “slice” the content string, while y is the number of remaining bytes we want to grab (up to 20) from that sliced string.
Moving on with the rest of the rule, http_uri
is a modifier to instructs snort to search out our content match in the URI portion of an HTTP payload. Next up, we have a regular expression via the pcre
keyword:
pcre:"/\x2fgoform\x2fformSetWan(?:L2TP|PPTP|PPPoE)$/U";
Suricata and Snort both have a myriad of custom modifiers for the pcre
keyword that determine where the regular expression applies. Without going too deep into the weeds, we can’t use the /R
modifier here to say relative to string we found in the http_uri
field. But we still need that content match because it serves as the rule’s fast_pattern
. What we do with this regular expression is to instead use the /U
modifier to denote that this regex should match in the URI field, and try to be as exact as possible with our regex. The regex above roughly reads out to:
“From the beginning of the URI field (^
, /U
), I need to find the string “/goform/formSetWan” (\x2f
, /
), followed IMMEDATELY by the string “L2TP”, or “PPTP”, or “PPPoE” ((?:L2TP|PPTP|PPPoE)
), followed by the end of the http uri field ($
).”
Finally, that leaves us with:
content:"curTime|3d|"; http_client_body; pcre:"/curTime\x3d[^&]{100,}(?:&|$)/P";
we’re looking for the curTime=
string, with the “=” hex escaped to “|3d|”, in the http_client_body
, the snort modifier that is the rough equivalent of http.request_body
. That leaves us with a final regular expression. That roughly reads as:
“In the http_client_body portion of the data (/P
), locate the string “/curTime=” (\x3d
, =
), then immediately after that find at least 100 characters ({100,0}
) that are NOT the ampersand character ([^&]
), followed immediately by either an ampersand, or the end of the line ((?:&|$)
).”
As before, here is our rule in a Snort job on my Dalton instance:
From downtown…
And the results. 3 POST requests, 3 alerts:
Nothin’ but net.
Conclusions
This concludes our lesson on exploit archiving and feature parity in the ET ruleset between Snort and Suricata. Please note that this rule has been added to the Emerging Threats Open (ETOPEN) and Emerging Threats Pro (ETPRO) rulesets on 8/1/2025:
2063869 - ET WEB_SPECIFIC_APPS D-Link formSetWAN Multiple Endpoints curTime Parameter Buffer Overflow Attempt (CVE-2025-8184, CVE-2025-8169, CVE,2025-8168) (web_specific_apps.rules)
Happy Hunting,
-Tony