ASM Advanced WAF
23 TopicsExample OWASP Top 10-compliant declarative WAF policy
Problem this snippet solves: This is an example of a basic declarative BIG-IP WAF policy that is OWASP Top 10-compliant. This policy can be used as a starting point for a production-ready version. A companion Dev Central article discussing this policy in depth can be found here. How to use this snippet: This policy can be deployed through an AS3 declaration to the BIG-IP. Code : { "policy": { "name": "Complete_OWASP_Top_Ten", "description": "A basic, OWASP Top 10 protection items v1.0", "template": { "name": "POLICY_TEMPLATE_RAPID_DEPLOYMENT" }, "enforcementMode":"transparent", "protocolIndependent": true, "caseInsensitive": true, "general": { "trustXff": true }, "signature-settings":{ "signatureStaging": false, "minimumAccuracyForAutoAddedSignatures": "high" }, "blocking-settings": { "violations": [ { "alarm": true, "block": true, "description": "ASM Cookie Hijacking", "learn": false, "name": "VIOL_ASM_COOKIE_HIJACKING" }, { "alarm": true, "block": true, "description": "Access from disallowed User/Session/IP/Device ID", "name": "VIOL_SESSION_AWARENESS" }, { "alarm": true, "block": true, "description": "Modified ASM cookie", "learn": true, "name": "VIOL_ASM_COOKIE_MODIFIED" }, { "name": "VIOL_LOGIN_URL_BYPASSED", "alarm": true, "block": true, "learn": false }, { "alarm": true, "block": true, "description": "XML data does not comply with format settings", "learn": true, "name": "VIOL_XML_FORMAT" }, { "name": "VIOL_FILETYPE", "alarm": true, "block": true, "learn": true }, { "name": "VIOL_URL", "alarm": true, "block": true, "learn": true }, { "name": "VIOL_URL_METACHAR", "alarm": true, "block": true, "learn": true }, { "name": "VIOL_PARAMETER_VALUE_METACHAR", "alarm": true, "block": true, "learn": true }, { "name": "VIOL_PARAMETER_NAME_METACHAR", "alarm": true, "block": true, "learn": true } ], "evasions": [ { "description": "Bad unescape", "enabled": true, "learn": true }, { "description": "Apache whitespace", "enabled": true, "learn": true }, { "description": "Bare byte decoding", "enabled": true, "learn": true }, { "description": "IIS Unicode codepoints", "enabled": true, "learn": true }, { "description": "IIS backslashes", "enabled": true, "learn": true }, { "description": "%u decoding", "enabled": true, "learn": true }, { "description": "Multiple decoding", "enabled": true, "learn": true, "maxDecodingPasses": 3 }, { "description": "Directory traversals", "enabled": true, "learn": true } ] }, "session-tracking": { "sessionTrackingConfiguration": { "enableTrackingSessionHijackingByDeviceId": true } }, "urls": [ { "name": "/trading/auth.php", "method": "POST", "protocol": "https", "type": "explicit" }, { "name": "/internal/test.php", "method": "GET", "protocol": "https", "type": "explicit", "isAllowed": false }, { "name": "/trading/rest/portfolio.php", "method": "GET", "protocol": "https", "type": "explicit", "urlContentProfiles": [ { "headerName": "Content-Type", "headerValue": "text/html", "type": "json", "contentProfile": { "name": "portfolio" } }, { "headerName": "*", "headerValue": "*", "type": "do-nothing" } ] } ], "login-pages": [ { "accessValidation": { "headerContains": "302 Found" }, "authenticationType": "form", "passwordParameterName": "password", "usernameParameterName": "username", "url": { "name": "/trading/auth.php", "method": "POST", "protocol": "https", "type": "explicit" } } ], "login-enforcement": { "authenticatedUrls": [ "/trading/index.php" ] }, "brute-force-attack-preventions": [ { "bruteForceProtectionForAllLoginPages": true, "leakedCredentialsCriteria": { "action": "alarm-and-blocking-page", "enabled": true } } ], "csrf-protection": { "enabled": true }, "csrf-urls": [ { "enforcementAction": "verify-csrf-token", "method": "GET", "url": "/trading/index.php" } ], "data-guard": { "enabled": true }, "xml-profiles": [ { "name": "Default", "defenseAttributes": { "allowDTDs": false, "allowExternalReferences": false } } ], "json-profiles": [ { "name": "portfolio" } ], "policy-builder-server-technologies": { "enableServerTechnologiesDetection": true } } } Tested this on version: 16.01.7KViews4likes4CommentsiRule for Brute Force Password Guessing Attacks
Problem this snippet solves: Here I'm introducing an iRule for use as Brute Force Password Guessing Protection. If you are running on 12.1.x you could solve this kind of attacks using ASM https://support.f5.com/csp/article/K54335130 But this release it's also affected for one bug that could lead in some unwanted behavior. The idea beneath this iRule is to workaround this bug and give a solution for those users that still run an ASM module on 12.1.x. How to use this snippet: This iRule requires a Data-Group to reference all the login pages in your infrastructure. ltm data-group internal LOGIN_URI_DG { records { /app1/login { } /app2/login { } } type string } In the STREAM expression you should identify how a login failure attempt is represented in your HTTP response. In my case, all login failures are represented by one JSON structure with the word "ACCESS_FAIL" in some of their fields. STREAM::expression {=ACCESS_FAIL=} You should also customize these global variables. set static::maxtry 5 ; # Maximum failed login attempts set static::time 900 ; # Time window for failed login attempts evaluation [seconds] set static::bantime 3600 ; # How long the user is banned [seconds] Code : when RULE_INIT { # Variable Definition set static::maxtry 5 ; # Maximum failed login attempts set static::time 900 ; # Time window for failed login attempts evaluation [seconds] set static::bantime 3600 ; # How long the user is banned [seconds] } when CLIENT_ACCEPTED priority 100 { # Get client IP set source_ip [IP::remote_addr] # Reject user if exists in blacklist if { [table lookup -subtable "blacklist" $source_ip] != "" } { reject } } when HTTP_REQUEST priority 100 { event HTTP_RESPONSE enable event STREAM_MATCHED enable STREAM::disable set login_access 0 # Enable login access flag if { [class match [string tolower [HTTP::uri] ] contains LOGIN_URI_DG ] } { set login_access 1 } } when HTTP_RESPONSE priority 100 { if { $login_access } { # Configure stream expression when response is JSON if {[HTTP::header value Content-Type] starts_with "application/json"} { STREAM::expression {=ACCESS_FAIL=} STREAM::enable } event HTTP_RESPONSE disable } } when STREAM_MATCHED priority 100 { if { $login_access } { # Increase variable of login attempts set key "attempts:$source_ip" table add $key 0 indefinite $static::time set count [table incr $key] # If maximum login attempts value is exceeded, then include user in blacklist if { $count >= $static::maxtry } { table add -subtable "blacklist" $source_ip "blocked" indefinite $static::bantime table delete $key log local1. "User Rejected: $source_ip" } event STREAM_MATCHED disable } } Tested this on version: 12.11.2KViews2likes1CommentExporting and importing ASM/AWAF security policies with Ansible and Terraform
Problem this snippet solves: This ansible playbook and Terraform TF file can be ised to copy the test ASM policy from the dev/preproduction environment to the production environment as this is Continuous integration and continuous delivery. Ansible You use the playbook by replacing the vars with "xxx" with your F5 device values for the connection. Also with the "vars_prompt:" you add policy name during execution as the preprod policy name is "{{ asm_policy }}_preprod" and the prod policy name is "{{ asm_policy }}_prod". For example if we enter "test" during the policy execution the name will be test_prod and test_preprod. If using Ansible Tower with the payed version you can use Jenkins or bamboo to push variables (I still have not tested this). Also there is a task that deletes the old asm policy file saved on the server as I saw that the ansible modules have issues overwriting existing files when doing the export and the task name is "Ansible delete file example" and in the group "internal" I have added the localhost. https://docs.ansible.com/ansible/latest/collections/f5networks/f5_modules/index.html Also after importing the policy file the bug https://support.f5.com/csp/article/K25733314 is hit, so the last 2 tasks deactivate and and activate the production policy. A nice example that I based my own is: https://support.f5.com/csp/article/K42420223 You can also write the connections vars in the hosts file as per K42420223 vars: provider: password: "{{ bigip_password }}" server: "{{ ansible_host }}" user: "{{ bigip_username }}" validate_certs: no server_port: 443 Example hosts: [bigip] f5.com [bigip:vars] bigip_password=xxx bigip_username=xxx ansible_host=xxx The policy is exported in binary format otherwize there is an issue importing it after that "binary: yes". Also when importing the option "force: yes" provides an overwrite if there is a policy with the same name. See the comments for my example about using host groups with this way your dev environment can be on one F5 device and the exported policy from it will be imported on another F5 device that is for production. When not using ''all'' for hosts you need to use set_facts to only be propmpted once for the policy name and then this to be shared between plays. Code : --- - name: Exporting and importing the ASM policy hosts: all connection: local become: yes vars: provider: password: xxx server: xxxx user: xxxx validate_certs: no server_port: 443 vars_prompt: - name: asm_policy prompt: What is the name of the ASM policy? private: no tasks: - name: Ansible delete file example file: path: "/home/niki/asm_policy/{{ asm_policy }}" state: absent when: inventory_hostname in groups['internal'] - name: Export policy in XML format bigip_asm_policy_fetch: name: "{{ asm_policy }}_preprod" file: "{{ asm_policy }}" dest: /home/niki/asm_policy/ binary: yes provider: "{{ provider }}" - name: Override existing ASM policy bigip_asm_policy_import: name: "{{ asm_policy }}_prod" source: "/home/niki/asm_policy/{{ asm_policy }}" force: yes provider: "{{ provider }}" notify: - Save the running configuration to disk - name: Task - deactivate policy bigip_asm_policy_manage: name: "{{ asm_policy }}_prod" state: present provider: "{{ provider }}" active: no - name: Task - activate policy bigip_asm_policy_manage: name: "{{ asm_policy }}_prod" state: present provider: "{{ provider }}" active: yes handlers: - name: Save the running configuration to disk bigip_config: save: yes provider: "{{ provider }}" Tested this on version: 13.1 Edit: -------------- When I made this code there was no official documentation but now I see F5 has provided examples for exporting and importing ASM/AWAF policies and even APM policies: https://clouddocs.f5.com/products/orchestration/ansible/devel/modules/bigip_asm_policy_fetch_module.html https://clouddocs.f5.com/products/orchestration/ansible/devel/modules/bigip_apm_policy_fetch_module.html -------------- Terraform Nowadays Terraform also provides the option to export and import AWAF policies (for APM Ansible is still the only way) as there is an F5 provider for terraform. I usedVisual Studio as Visual Studio wil even open for you the teminal, where you can select the folder where the terraform code will be saved after you have added the code run terraform init, terraform plan, terraform apply. VS even has a plugin for writting F5 irules. The terraform data type is not a resource and it is used to get the existing policy data. Data sources allow Terraform to use information defined outside of Terraform, defined by another separate Terraform configuration, or modified by functions. Usefull links for Visual Studio and Terraform: https://registry.terraform.io/providers/F5Networks/bigip/1.16.0/docs/resources/bigip_ltm_datagroup Usefull links for Visual Studio and Terraform: https://registry.terraform.io/providers/F5Networks/bigip/latest/docs/resources/bigip_waf_policy#policy_import_json https://www.youtube.com/watch?v=Z5xG8HLwIh4 The big issue is that Terraform not like Ansible needs you first find the aWAF policy "ID" that is not the name but a random generated identifier and this is no small task. I suggest looking at the link below: https://community.f5.com/t5/technical-articles/manage-f5-big-ip-advanced-waf-policies-with-terraform-part-2/ta-p/300839 Code: You may need to add also this resource below as to save the config and with "depends_on" it wil run after the date group is created. This is like the handler in Ansible that is started after the task is done and also terraform sometimes creates resources at the same time not like Ansible task after task, resource "bigip_command" "save-config" { commands = ["save sys config"] depends_on = [ bigip_waf_policy.test-awaf ] } Tested this on version: 16.11.1KViews1like1CommentProactive Bot Defense Bypass by Bot Signature
Problem this snippet solves: This code enables you to bypass Proactive Bot Defense for a specific bot signature. Caution: If the signature is simple, it may be easy for an attacker to guess it and craft a response to match the signature and thus bypass Proactive Bot Defense with this in place. For this reason, another bypass solution is recommended where possible. You can bypass Proactive Bot Defense without this iRule by setting a benign category to "Report" and ensuring that the signature has a reverse DNS lookup in place. This will validate the source in addition to other factors such as the User-Agent. How to use this snippet: Add to the virtual server that is protected by Proactive Bot Defense and Bot Signatures. Enter the signature you want to bypass in the code where the example "curl" is placed currently. The signature's category must be set to report or block for this to take effect. Tested on v13.1. Code : when BOTDEFENSE_ACTION { #log local0. "signature: [BOTDEFENSE::bot_signature]" if { [BOTDEFENSE::bot_signature] ends_with "curl"} { BOTDEFENSE::action allow } }763Views1like1CommentBlock IP Addresses With Data Group And Log Requests On ASM Event Log
Problem this snippet solves: This is Irule which will block IP Addresses that are not allowed in your organization. instead of adding each IP Address in Security ›› Application Security : IP Addresses : IP Address Exceptions you can create a data group and use a simple IRULE to block hundreds of Addressess. Also,createing a unique signature to specify the request of the illigile IP Address. First, You will need to create Data Group under Local Traffic ›› iRules : Data Group List and add your illigile IP Addresses to the list. If you have hundreds of IP's that you want to block, you can to it in TMSH using this command: TMSH/modify ltm data-group internal <Data-Group-Name> { records add {IP-ADDRESS} } Now, We are ready to create the IRULE under Local Traffic ›› iRules : iRule List Last, Create violation list under Security ›› Options : Application Security : Advanced Configuration : Violations List Create -> Name:Illegal_IP_Address -> Type:Access Violation -> Severity:Critical -> Update Don't forgat to enable trigger ASM IRULE events with "Normal Mode" How to use this snippet: Code : when HTTP_REQUEST { set reqBlock 0 if { [class match [IP::remote_addr] equals ] } { set reqBlock 1 # log local0. "HTTP_REQUEST [IP::client_addr]" } } when ASM_REQUEST_DONE { if { $reqBlock == 1} { ASM::raise "Illegal_IP_Address" # log local0. "ASM_REQUEST_DONE [IP::client_addr]" } } Tested this on version: 13.01.6KViews1like5Comments