[Bounty] Matters Lab XSS - HITCON ZeroDay

Vulnerability Detail Report

Vulnerability Overview

  • ZDID: ZD-2021-00237
  •  發信 Vendor: Matters Lab
  • Title: [Bounty] Matters Lab XSS
  • Introduction: XSS

處理狀態

目前狀態

公開
Last Update : 2022/02/24
  • 新提交
  • 已審核
  • 已通報
  • 已修補
  • 未複測
  • 公開

處理歷程

  • 2021/05/04 22:06:35 : 新提交 (由 huli 更新此狀態)
  • 2022/02/18 14:13:02 : 新提交 (由 huli 更新此狀態)
  • 2022/02/18 14:14:41 : 新提交 (由 huli 更新此狀態)
  • 2022/02/23 07:43:30 : 已修補 (由 組織帳號 更新此狀態)
  • 2022/02/23 07:45:26 : 公開 (由 組織帳號 更新此狀態)
  • 2022/02/24 03:00:01 : 公開 (由 HITCON ZeroDay 平台自動更新)

詳細資料

  • ZDID:ZD-2021-00237
  • 通報者:aszx87410 (huli)
  • 風險:低
  • 類型:預存式跨站腳本攻擊 (Stored Cross-Site Scripting)

參考資料

攻擊者可經由該漏洞竊取使用者身份,或進行掛碼、轉址等攻擊行為。

漏洞說明: OWASP - Cross-site Scripting (XSS)
https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)

防護原則:
https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet

XSS 防禦繞過方式:
https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
(本欄位資訊由系統根據漏洞類別自動產生,做為漏洞參考資料。)

相關網址

https://matters.news/@fbmbrtrhdbfy/%E6%90%AC%E5%AE%B6%E6%B8%AC%E8%A9%A6-777-bafyreicszqnnaf7ddf67utaeumg4efzcitqnsyozc2kpl4os65npusint4

敘述

在文章顯示部份有 XSS 漏洞,可以讓瀏覽文章的人都觸發 XSS。

POC 網址:https://matters.news/@fbmbrtrhdbfy/%E6%90%AC%E5%AE%B6%E6%B8%AC%E8%A9%A6-777-bafyreicszqnnaf7ddf67utaeumg4efzcitqnsyozc2kpl4os65npusint4

這邊為了給出 POC 但不影響一般使用者,改用 console.log 取代 alert,打開 devtool 可看到 log 1337 即為 XSS 觸發

漏洞發現過程如下:

對於文章的顯示,在 server 有先做過 sanitize(src/common/utils/xss.ts

import xss from 'xss'

const CUSTOM_WHITE_LISTS = {
  a: [...(xss.whiteList.a || []), 'class'],
  figure: [],
  figcaption: [],
  source: ['src', 'type'],
  iframe: ['src', 'frameborder', 'allowfullscreen', 'sandbox'],
}

const onIgnoreTagAttr = (tag: string, name: string, value: string) => {
  /**
   * Allow attributes of whitelist tags start with "data-" or "class"
   *
   * @see https://github.com/leizongmin/js-xss#allow-attributes-of-whitelist-tags-start-with-data-
   */
  if (name.substr(0, 5) === 'data-' || name.substr(0, 5) === 'class') {
    // escape its value using built-in escapeAttrValue function
    return name + '="' + xss.escapeAttrValue(value) + '"'
  }
}

const ignoreTagProcessor = (
  tag: string,
  html: string,
  options: { [key: string]: any }
) => {
  if (tag === 'input' || tag === 'textarea') {
    return ''
  }
}

const xssOptions = {
  whiteList: { ...xss.whiteList, ...CUSTOM_WHITE_LISTS },
  onIgnoreTagAttr,
  onIgnoreTag: ignoreTagProcessor,
}
const customXSS = new xss.FilterXSS(xssOptions)

export const sanitize = (string: string) => customXSS.process(string)

可是在 client 的時候,render 文章時會再傳進一個 optimizeEmbed 的 function( src/views/ArticleDetail/Content/index.tsx

return (
  <>
    <div
      className={classNames({ 'u-content': true, translating })}
      dangerouslySetInnerHTML=
        __html: optimizeEmbed(translation || article.content),

      onClick={captureClicks}
      ref={contentContainer}
    />

    <style jsx>{styles}</style>
  </>
)

繼續往下追,看到 src/common/utils/text.ts

export const optimizeEmbed = (content: string) => {
  return content
    .replace(/\<iframe /g, '<iframe loading="lazy"')
    .replace(
      /<img\s[^>]*?src\s*=\s*['\"]([^'\"]*?)['\"][^>]*?>/g,
      (match, src, offset) => {
        return /* html */ `
      <picture>
        <source
          type="image/webp"
          media="(min-width: 768px)"
          srcSet=${toSizedImageURL({ url: src, size: '1080w', ext: 'webp' })}
          onerror="this.srcset='${src}'"
        />
        <source
          media="(min-width: 768px)"
          srcSet=${toSizedImageURL({ url: src, size: '1080w' })}
          onerror="this.srcset='${src}'"
        />
        <source
          type="image/webp"
          srcSet=${toSizedImageURL({ url: src, size: '540w', ext: 'webp' })}
        />
        <img
          src=${src}
          srcSet=${toSizedImageURL({ url: src, size: '540w' })}
          loading="lazy"
        />
      </picture>
    `
      }
    )
}

這邊採用 RegExp 把 img src 拿出來,然後用字串拼接的方式直接拼成 HTML,看起來不太安全,再往下看 toSizedImageURL

export const toSizedImageURL = ({ url, size, ext }: ToSizedImageURLProps) => {
  const assetDomain = process.env.NEXT_PUBLIC_ASSET_DOMAIN
    ? `https://${process.env.NEXT_PUBLIC_ASSET_DOMAIN}`
    : ''
  const isOutsideLink = url.indexOf(assetDomain) < 0
  const isGIF = /gif/i.test(url)

  if (!assetDomain || isOutsideLink || isGIF) {
    return url
  }

  const key = url.replace(assetDomain, ``)
  const extedUrl = changeExt({ key, ext })
  const prefix = size ? '/' + PROCESSED_PREFIX + '/' + size : ''

  return assetDomain + prefix + extedUrl
}

只要 domain 是 assets 的 domain 並符合其他條件,就會經過一些字串處理之後回傳。

重點可以放回剛剛 source 那邊,其中一段程式碼為:

<source
  type="image/webp"
  media="(min-width: 768px)"
  srcSet=${toSizedImageURL({ url: src, size: '1080w', ext: 'webp' })}
  onerror="this.srcset='${src}'"
/>

如果 ${toSizedImageURL({ url: src, size: '1080w', ext: 'webp' })} 這段我們可以控制的話,就可以用空格插入新屬性,然後 onerror 其實是無效的,這邊可以用 onanimationstart 搭配網站上找到的 keyframe: spinning

因此如果 img src 為:https://assets.matters.news/processed/1080w/embed/test style=animation-name:spinning onanimationstart=console.log(1337)

結合後的程式碼就是:

<source
  type="image/webp"
  media="(min-width: 768px)"   
  srcSet=https://assets.matters.news/processed/1080w/embed/test 
  style=animation-name:spinning 
  onanimationstart=console.log(1337)
  onerror="this.srcset='${src}'"
/>

就製造了一個 XSS 漏洞。

修補建議

建議不要採用字串拼接搭配 dangerouslySetInnerHTML,一率使用 xss 相關 library 來防護

擷圖

留言討論

聯絡組織

 發送私人訊息
您也可以透過私人訊息的方式與組織聯繫,討論有關於這個漏洞的相關資訊。
;