Testing AppSec rules with POST body contents

I'm testing whether my AppSec component blocks in case one of the rules of the virtual patching collection is being hit. I'm using CVE-2024-29824 as an example: https://app.crowdsec.net/hub/author/crowdsecurity/appsec-rules/vpatch-CVE-2024-29824. According to the rule a POST request to a URL ending in /wsstatusevents/eventhandler.asmx and containing xp_cmdshell should trigger the rule. When I'm simulating such a request, it's not blocked, though. As far as I know this is an in-band rule, so it should react immediately. I have successfully tested with GET /rpc2, so the AppSec component itself seems to be functioning. POST requests that only consider headers being set seem to be result in the expected response too. When I check the (debug) logs I see this:
crowdsec-1 | time="2024-11-21T16:04:21Z" level=debug msg="Evaluating operator: NO MATCH" arg= band=inband name=myAppSecComponent operator_data=xp_cmdshell operator_function=@contains rule_id=3229511417 runner_uuid=144d5fd6-7851-4691-a791-23152b3e2164 tx_id=9e1d267f-1ea2-4de8-be71-e9b9c4bfe286 type=appsec
crowdsec-1 | time="2024-11-21T16:04:21Z" level=debug msg="Evaluating operator: NO MATCH" arg= band=inband name=myAppSecComponent operator_data=xp_cmdshell operator_function=@contains rule_id=3229511417 runner_uuid=144d5fd6-7851-4691-a791-23152b3e2164 tx_id=9e1d267f-1ea2-4de8-be71-e9b9c4bfe286 type=appsec
It seems the arg= indicates the body that is evaluated is empty, but I haven't set disable_body_inspection: true. Is there something I'm missing?
CrowdSec Console
Hub Appsec rule
Use CrowdSec Console to visualize security data, manage dynamic blocklists, and gain real-time intelligence on IPs. Enhance your threat response capabilities.
16 Replies
CrowdSec
CrowdSec11mo ago
Important Information
Thank you for getting in touch with your support request. To expedite a swift resolution, could you kindly provide the following information? Rest assured, we will respond promptly, and we greatly appreciate your patience. While you wait, please check the links below to see if this issue has been previously addressed. If you have managed to resolve it, please use run the command /resolve or press the green resolve button below.
Log Files
If you possess any log files that you believe could be beneficial, please include them at this time. By default, CrowdSec logs to /var/log/, where you will discover a corresponding log file for each component.
Guide Followed (CrowdSec Official)
If you have diligently followed one of our guides and hit a roadblock, please share the guide with us. This will help us assess if any adjustments are necessary to assist you further.
Screenshots
Please forward any screenshots depicting errors you encounter. Your visuals will provide us with a clear view of the issues you are facing.
© Created By WhyAydan for CrowdSec ❤️
blotus
blotus11mo ago
Hey, Do you forward all the request headers to crowdsec (on top of the specific appsec headers) ? We use coraza behind the scenes, and unfortunately, to properly parse some request body, it needs the content-type header (we've made some modification to expose the body as a giant string if coraza cannot parse it, ) (although, the rule matches on RAW_BODY so that should not be an issue :/) I've just tested with our nginx bouncer, and it seems to work:
~ ᐅ curl -s http://xxxxx/wsstatusevents/eventhandler.asmx -d 'xp_cmdshell' -i | head -n1
HTTP/1.1 403 Forbidden
~ ᐅ curl -s http://xxxxx/wsstatusevents/eventhandler.asmx -d 'xp_cmdshell' -i | head -n1
HTTP/1.1 403 Forbidden
I had a quick look at your PR in the bouncer, I don't see anything obviously wrong with the way you pass the request to crowdsec the only thing I see right now to improve would be to add some configuration to prevent forwarding huge bodies to crowdsec, as it can consume tons of memory and will be very slow: the idea would be to allow the user to configure a max body size to forward and what to do if the body is bigger than the limit: - drop the request - only send query string + headers (we don't do it currently in the nginx bouncer, but it's planned, and I think we'll document this in the protocol reference for the appsec to make use everybody does it)
hslatman
hslatmanOP11mo ago
Yeah, that's a sensible configuration to add. I'll likely iterate on what I currently have, and the nice thing is that Caddyfile parsing makes adding things fairly simple and clean. This is an example dumping the request, using your curl example:
POST / HTTP/1.1
Host: localhost:7422
Accept: */*
Content-Length: 11
Content-Type: application/x-www-form-urlencoded
User-Agent: caddy-cs-bouncer
X-Crowdsec-Appsec-Api-Key: <api-key>
X-Crowdsec-Appsec-Host: localhost:8443
X-Crowdsec-Appsec-Ip: ::1
X-Crowdsec-Appsec-Uri: /wsstatusevents/eventhandler.asmx
X-Crowdsec-Appsec-User-Agent: curl/8.7.1
X-Crowdsec-Appsec-Verb: POST

xp_cmdshell
POST / HTTP/1.1
Host: localhost:7422
Accept: */*
Content-Length: 11
Content-Type: application/x-www-form-urlencoded
User-Agent: caddy-cs-bouncer
X-Crowdsec-Appsec-Api-Key: <api-key>
X-Crowdsec-Appsec-Host: localhost:8443
X-Crowdsec-Appsec-Ip: ::1
X-Crowdsec-Appsec-Uri: /wsstatusevents/eventhandler.asmx
X-Crowdsec-Appsec-User-Agent: curl/8.7.1
X-Crowdsec-Appsec-Verb: POST

xp_cmdshell
It does seem to match on the path, but the body doesn't match:
crowdsec-1 | time="2024-11-21T20:39:53Z" level=debug msg="Evaluating operator: NO MATCH" arg= band=inband name=myAppSecComponent operator_data=xp_cmdshell operator_function=@contains rule_id=3229511417 runner_uuid=144d5fd6-7851-4691-a791-23152b3e2164 tx_id=3444d926-aea9-45d7-a7b2-47d3f87d0b58 type=appsec

crowdsec-1 | time="2024-11-21T20:39:53Z" level=debug msg="Evaluating operator: MATCH" arg=/wsstatusevents/eventhandler.asmx band=inband name=myAppSecComponent operator_data=/wsstatusevents/eventhandler.asmx operator_function=@endsWith rule_id=3165748236 runner_uuid=144d5fd6-7851-4691-a791-23152b3e2164 tx_id=3444d926-aea9-45d7-a7b2-47d3f87d0b58 type=appsec
crowdsec-1 | time="2024-11-21T20:39:53Z" level=debug msg="Evaluating operator: NO MATCH" arg= band=inband name=myAppSecComponent operator_data=xp_cmdshell operator_function=@contains rule_id=3229511417 runner_uuid=144d5fd6-7851-4691-a791-23152b3e2164 tx_id=3444d926-aea9-45d7-a7b2-47d3f87d0b58 type=appsec

crowdsec-1 | time="2024-11-21T20:39:53Z" level=debug msg="Evaluating operator: MATCH" arg=/wsstatusevents/eventhandler.asmx band=inband name=myAppSecComponent operator_data=/wsstatusevents/eventhandler.asmx operator_function=@endsWith rule_id=3165748236 runner_uuid=144d5fd6-7851-4691-a791-23152b3e2164 tx_id=3444d926-aea9-45d7-a7b2-47d3f87d0b58 type=appsec
Still looks empty in that args=, which I think is where that xp_cmdshell should be Does that turn up in your AppSec (debug) logs when you do it with Nginx?
blotus
blotus11mo ago
I do see the body in the log:
time="2024-11-21T21:09:41Z" level=debug msg="Evaluating operator: MATCH" arg=blabla_xp_cmdshell_blabla band=inband operator_data=xp_cmdshell operator_function=@contains rule_id=3229511417 runner_uuid=9a6ef547-1ea3-4f4c-91dc-83d0099f95d2 tx_id=94bebebb-93e5-4637-9fff-f5450fcf8e79 type=appsec
time="2024-11-21T21:09:41Z" level=debug msg="Evaluating operator: MATCH" arg=blabla_xp_cmdshell_blabla band=inband operator_data=xp_cmdshell operator_function=@contains rule_id=3229511417 runner_uuid=9a6ef547-1ea3-4f4c-91dc-83d0099f95d2 tx_id=94bebebb-93e5-4637-9fff-f5450fcf8e79 type=appsec
do you see the body if you add some debug in the bouncer ? some webserver might treat the body a bit differently and not always make it available straightaway
hslatman
hslatmanOP11mo ago
This is what the curl request looks like with httputils.DumpRequestOut:
POST / HTTP/1.1
Host: localhost:7422
User-Agent: caddy-cs-bouncer
Transfer-Encoding: chunked
Accept: */*
Content-Type: application/x-www-form-urlencoded
X-Crowdsec-Appsec-Api-Key: <api-key>
X-Crowdsec-Appsec-Host: localhost:8443
X-Crowdsec-Appsec-Ip: ::1
X-Crowdsec-Appsec-Uri: /wsstatusevents/eventhandler.asmx
X-Crowdsec-Appsec-User-Agent: curl/8.7.1
X-Crowdsec-Appsec-Verb: POST
Accept-Encoding: gzip

b
xp_cmdshell
0
POST / HTTP/1.1
Host: localhost:7422
User-Agent: caddy-cs-bouncer
Transfer-Encoding: chunked
Accept: */*
Content-Type: application/x-www-form-urlencoded
X-Crowdsec-Appsec-Api-Key: <api-key>
X-Crowdsec-Appsec-Host: localhost:8443
X-Crowdsec-Appsec-Ip: ::1
X-Crowdsec-Appsec-Uri: /wsstatusevents/eventhandler.asmx
X-Crowdsec-Appsec-User-Agent: curl/8.7.1
X-Crowdsec-Appsec-Verb: POST
Accept-Encoding: gzip

b
xp_cmdshell
0
iiamloz
iiamloz11mo ago
From this I don't see a content length header so I guess go (crowdsec side) is ignoring the body, oh wait it chunked so it doesn't need it
blotus
blotus11mo ago
good catch @iiamloz I've just tried using chunked encoding, and nginx did not block anything
curl -s http://xxxxx/wsstatusevents/eventhandler.asmx -X POST --data 'xp_cmdshell' -i -H "Transfer-Encoding: chunked" | head -n1
HTTP/1.1 404 Not Found
curl -s http://xxxxx/wsstatusevents/eventhandler.asmx -X POST --data 'xp_cmdshell' -i -H "Transfer-Encoding: chunked" | head -n1
HTTP/1.1 404 Not Found
So we need to find out if it's an issue on the bouncers side or something in crowdsec the nginx bouncer seems to read the body just fine, so this seems to be an issue in crowdsec but that's weird, AFAIK, go should handle this automatically for us
blotus
blotus11mo ago
@hslatman can you try with this PR: https://github.com/crowdsecurity/crowdsec/pull/3342 ? Should fix the issue (I still need to test it a bit more thoroughly to make sure it did not break something else)
GitHub
appsec: better handle chunked requests by blotus · Pull Request #33...
We were relying on the content-length header to compute the size of the buffer we need to allocate to store the body, but in the case of chunked requests, the content length is not set, thus we wer...
hslatman
hslatmanOP11mo ago
Hey @blotus, didn't have time over the weekend due to some personal stuff, but I'll have a look at it. When I set the content length it does pass the POST tests I made, so it's indeed something related to that. I suppose the Docker images from the build of that branch aren't published and pullable like the regular ones, right?
blotus
blotus11mo ago
no we don't build images on PRs You'll need to checkout the branch and build it yourself
hslatman
hslatmanOP11mo ago
Hey @blotus, I tested your patch last week, and that did the trick. For now I've changed it on the client side by explicitly setting the content length. There's already the need to read the incoming request, so it's not that big of an issue to have to set it. I could change that in the future to default to not setting it, but before I do so I think it would be nice if there was a way to determine which version of the LAPI / CrowdSec is running from the bouncer's viewpoint. But maybe I'm overthinking, and people should just update their CrowdSec.
blotus
blotus11mo ago
I don't think we expose the version of LAPI to the bouncers But i guess we could return it as a header in all (authenticated) LAPI/appsec responses (I'll discuss with the team, but I don't see any drawback)
hslatman
hslatmanOP11mo ago
Yes, I was thinking about something like that 🙂 Alternatively it could go (just) in /heartbeat, I suppose Or is that not intended to be used from a bouncer? It's not in the OpenAPI spec, so maybe not accessible?
blotus
blotus11mo ago
the heartbeat endpoint only supports credentials authentication (ie, log processors) (as we recommend to run bouncers in stream mode, we have a heartbeat for "free")
hslatman
hslatmanOP11mo ago
Right, yeah; with streaming it's fine, and I've basically been using it a bit like that with support for "failling hard" when the connection fails, and otherwise returning/logging an error. But my bouncer also supports live mode, and for that it would be nice to see if the connection details for the LAPI are OK. I suppose a HEAD request to some endpoint, for example the decisions endpoint, would be OK?
blotus
blotus11mo ago
yes, a HEAD request on /decisions/stream will work fine (we won't update anything in the DB)

Did you find this page helpful?