1313
1414from bbot .core .helpers .url import add_get_params
1515from bbot .modules .base import BaseModule
16+ from bbot .core .helpers .nowafpls import BypassResult
1617from bbot .modules .lightfuzz .lightfuzz import lightfuzz
1718from bbot .modules .lightfuzz .submodules .base import BaseLightfuzz
1819
@@ -3823,6 +3824,95 @@ def check(self, module_test, events):
38233824 pass
38243825
38253826
3827+ class Test_Lightfuzz_filter_event_try_bypasses (ModuleTestBase ):
3828+ """Under try_bypasses the nowafpls verdict decides, not the "waf" tag. The tag reflects the
3829+ CDN/WAF provider's identity, so a host that isn't gating payloads must still be fuzzed."""
3830+
3831+ targets = [HTTPSERVER_URL ]
3832+ modules_overrides = ["http" , "lightfuzz" ]
3833+ config_overrides = {
3834+ "interactsh_disable" : True ,
3835+ "modules" : {
3836+ "lightfuzz" : {
3837+ "enabled_submodules" : ["xss" ],
3838+ "avoid_wafs" : "try_bypasses" ,
3839+ }
3840+ },
3841+ }
3842+
3843+ def _web_param (self , module_test , param_type ):
3844+ return module_test .scan .make_event (
3845+ {
3846+ "host" : "127.0.0.1" ,
3847+ "type" : param_type ,
3848+ "name" : "test" ,
3849+ "original_value" : "value" ,
3850+ "url" : f"{ HTTPSERVER_URL } /" ,
3851+ "description" : "Test parameter" ,
3852+ },
3853+ "WEB_PARAMETER" ,
3854+ module_test .scan .root_event ,
3855+ module = "excavate" ,
3856+ tags = ["distance-0" , "waf" ],
3857+ )
3858+
3859+ async def setup_after_prep (self , module_test ):
3860+ self .url_event = module_test .scan .make_event (
3861+ f"{ HTTPSERVER_URL } /" ,
3862+ "URL" ,
3863+ module_test .scan .root_event ,
3864+ module = "http" ,
3865+ tags = ["status-200" , "distance-0" , "waf" ],
3866+ )
3867+ self .getparam_event = self ._web_param (module_test , "GETPARAM" )
3868+ self .postparam_event = self ._web_param (module_test , "POSTPARAM" )
3869+
3870+ @staticmethod
3871+ def _accepted (result ):
3872+ # filter_event returns True to accept, or False / (False, reason) to reject
3873+ return result is True
3874+
3875+ def _set_verdict (self , module_test , status ):
3876+ async def _stub (event , * args , ** kwargs ):
3877+ return BypassResult (status = status )
3878+
3879+ module_test .scan .helpers .nowafpls .is_bypassable = _stub
3880+
3881+ async def test_filter_event (self , module_test ):
3882+ module = module_test .scan .modules ["lightfuzz" ]
3883+ all_events = (self .url_event , self .getparam_event , self .postparam_event )
3884+
3885+ # nothing is gating the payload, so there is no WAF to work around: fuzz every event type
3886+ self ._set_verdict (module_test , BypassResult .STATUS_NO_INTERFERENCE )
3887+ for event in all_events :
3888+ result = await module .filter_event (event )
3889+ assert self ._accepted (result ), (
3890+ f"{ event .type } should be fuzzed when the probe reports no interference, got { result } "
3891+ )
3892+
3893+ # padding is body-only, so a confirmed bypass only helps events that can fire a POST probe
3894+ self ._set_verdict (module_test , BypassResult .STATUS_BYPASSED )
3895+ assert self ._accepted (await module .filter_event (self .postparam_event )), (
3896+ "POSTPARAM should be accepted when the WAF is bypassable via body padding"
3897+ )
3898+ for event in (self .url_event , self .getparam_event ):
3899+ result = await module .filter_event (event )
3900+ assert not self ._accepted (result ), (
3901+ f"{ event .type } has no POST-style probe to pad and should be rejected, got { result } "
3902+ )
3903+
3904+ # the gate held, or we never got a verdict: reject everything
3905+ for status in (BypassResult .STATUS_BLOCKED , BypassResult .STATUS_ERROR ):
3906+ self ._set_verdict (module_test , status )
3907+ for event in all_events :
3908+ result = await module .filter_event (event )
3909+ assert not self ._accepted (result ), f"{ event .type } should be rejected on status={ status } , got { result } "
3910+
3911+ def check (self , module_test , events ):
3912+ # assertions live in test_filter_event
3913+ pass
3914+
3915+
38263916class _NowafplsFuzzTestBase (ModuleTestBase ):
38273917 """Shared setup: dummy module emits a WAF-tagged POSTPARAM WEB_PARAMETER, and the mocked
38283918 endpoint's callback records every POST body so tests can assert on what actually fired.
@@ -3833,6 +3923,8 @@ class _NowafplsFuzzTestBase(ModuleTestBase):
38333923
38343924 targets = ["nowafpls-fuzz.test" ]
38353925 bypass_works = True
3926+ # when False the endpoint answers the malicious payload normally, i.e. nothing is gating it
3927+ waf_blocks = True
38363928 avoid_wafs = "try_bypasses"
38373929
38383930 class DummyModule (BaseModule ):
@@ -3890,6 +3982,7 @@ async def setup_after_prep(self, module_test):
38903982 await module_test .mock_dns ({"nowafpls-fuzz.test" : {"A" : ["127.0.0.1" ]}})
38913983 self .post_bodies : list [bytes ] = []
38923984 bypass = self .bypass_works
3985+ blocks = self .waf_blocks
38933986
38943987 def cb (request ):
38953988 body = request .content or b""
@@ -3902,7 +3995,7 @@ def cb(request):
39023995 # Padded malicious is accepted iff bypass_works.
39033996 has_pad = body .startswith (b"__nowafpls_pad=" )
39043997 has_malicious = b"%3Cscript" in body or b"<script" in body
3905- if has_malicious and not has_pad :
3998+ if has_malicious and not has_pad and blocks :
39063999 return MockResponse (status_code = 403 , text = "Attention Required! | Cloudflare\n Ray ID: abcd" )
39074000 if has_malicious and has_pad and not bypass :
39084001 return MockResponse (status_code = 403 , text = "Attention Required! | Cloudflare\n Ray ID: abcd" )
@@ -3953,6 +4046,31 @@ def check(self, module_test, events):
39534046 )
39544047
39554048
4049+ class Test_Nowafpls_try_bypasses_no_interference (_NowafplsFuzzTestBase ):
4050+ """try_bypasses + the host isn't gating the payload: the probe reports no interference, so
4051+ lightfuzz fuzzes normally and unpadded. A verdict of "not bypassed" must not be read as
4052+ "skip this host" -- only an observed block should suppress fuzzing."""
4053+
4054+ waf_blocks = False
4055+ avoid_wafs = "try_bypasses"
4056+
4057+ # the only POST bodies nowafpls's own probe ever sends: two benign baselines and one
4058+ # unpadded malicious payload (it returns before testing padding when there's no interference)
4059+ PROBE_BODIES = {b"q=hello" , b"q=%3Cscript%3Ealert%281%29%3C%2Fscript%3E" }
4060+
4061+ def check (self , module_test , events ):
4062+ fuzz_bodies = [b for b in self .post_bodies if b not in self .PROBE_BODIES ]
4063+ padded = [b for b in self .post_bodies if b .startswith (b"__nowafpls_pad=" )]
4064+ # anything the probe didn't send is lightfuzz, which only reaches the wire if
4065+ # filter_event accepted the WEB_PARAMETER
4066+ assert fuzz_bodies , (
4067+ f"Expected lightfuzz to fuzz a host with no interference, but the only POST traffic "
4068+ f"was nowafpls's own probe: { self .post_bodies [:5 ]} "
4069+ )
4070+ # nothing is gating the payload, so there is nothing to pad around
4071+ assert not padded , f"No padding should be applied when nothing is gating the payload. Got: { padded [:3 ]} "
4072+
4073+
39564074class Test_Nowafpls_never_still_pads (_NowafplsFuzzTestBase ):
39574075 """avoid_wafs=never: no filter-time probe, but prepare_request still opportunistically pads POST
39584076 when the helper reports bypassable. Padded fuzz bodies should still land on the wire."""
0 commit comments