從 WTForm 的 URLXSS 談開源元件的安全性

wyzsk發表於2020-08-19
作者: phith0n · 2016/02/25 13:19

0x00 開源元件與開源應用


開源元件是我們大家平時開發的時候必不可少的工具,所謂『不要重複造輪子』的原因也是因為,大量封裝好的元件我們在開發中可以直接呼叫,減少了重複開發的工作量。
開源元件和開源程式也有一些區別,開源元件面向的使用者是開發者,而開源程式就可以直接面向使用者。開源元件,如JavaScript裡的uploadify,php裡的PHPExcel等;開源程式,如php寫的wordpress、joomla,node.js寫的ghost等。
就安全而言,毋庸置疑,開源元件的漏洞影響面遠比開源軟體要大。但大量開源元件的漏洞卻很少出現在我們眼中,我總結了幾條原因:

  1. 開源程式的漏洞具有通用性,很多可以透過一個通用的poc來測試全網,更具『商業價值』;而開源元件由於開發者使用方法不同,導致測試方法不統一,利用門檻也相對較高
  2. 大眾更熟悉開源軟體,如wordpress,而很少有人知道wordpress內部使用了哪些開源元件。相應的,當出現漏洞的時候人們也只會認為這個漏洞是wordpress的漏洞。
  3. 慣性思維讓人們認為:『庫』裡應該不會有漏洞,在程式碼審計的時候也很少會關注import進來的第三方庫的程式碼缺陷。所以,開源元件爆出的漏洞也較少。
  4. 能夠開發開源元件的開發者本身素質相對較高,程式碼質量較高,也使開源元件出漏洞的可能性較小。
  5. 元件漏洞多半有爭議性,很多鍋分不清是元件自身的還是其使用者的,很多問題我們也只能稱其為『特性』,但實際上這些特性反而比某些漏洞更可怕。

特別是現在國內浮躁的安全氛圍,可以明顯感受到第一條原因。就前段時間出現的幾個影響較大的漏洞:Java反序列化漏洞、joomla的程式碼執行、redis的寫ssh key,可以明顯感覺到後兩者炒的比前者要響,而前者不慍不火的,曝光了近一年才受到廣泛關注。
Java反序列化漏洞,恰好就是典型的『元件』特性造成的問題。早在2015年的1月28號,就有白帽子報告了利用Apache Commons Collections這個常用的Java庫來實現任意程式碼執行的方法,但並沒有太多關注(原來國外也是這樣)。直到11月有人提出了用這個方法攻擊WebLogic、WebSphere、JBoss、Jenkins、OpenNMS等應用的時候,才被突然炒起來。
這種對比明顯反應出『開源元件』和『開源應用』在安全漏洞關注度上的差距。
我個人在烏雲上發過幾個元件漏洞,從前年發的ThinkPHP框架注入,到後面的Tornado檔案讀取,到slimphp的XXE,基本都是我自己在使用完這些元件後,對整體程式碼做code review的時候發現的。
這篇文章以一個例子,簡單地談談如何對第三方庫進行code review,與如何正確使用第三方庫。

0x01 WTForm中的弱validator


WTForms是python web開發中重要的一個元件,它提供了簡單的表單生成、驗證、轉換等功能,是眾多python web框架(特別是flask)不可缺少的輔助庫之一。
WTForms中有一個重要的功能就是對使用者輸入進行檢查,在文件中被稱為validator:
http://wtforms.readthedocs.org/en/latest/validators.html

A validator simply takes an input, verifies it fulfills some criterion, such as a maximum length for a string and returns. Or, if the validation fails, raises a ValidationError. This system is very simple and flexible, and allows you to chain any number of validators on fields.

我們可以簡單地使用其內建validator對資料進行檢查,比如我們需要使用者輸入一個『不為空』、『最短10個字元』、『最長64個字元』的『URL地址』,那麼我們就可以編寫如下class:

#!py
class MyForm(Form):
    url = StringField("Link", validators=[DataRequired(), Length(min=10, max=64), URL()])

以flask為例,在view檢視中只需呼叫validate()函式即可檢查使用者的輸入是否合法:

#!py
@app.route('/', methods=['POST'])
def check():
    form = MyForm(flask.request.form)
    if form.validate():
        pass # right input
    else:
        pass # bad input

典型的敏捷開發手段,減少了大量開發工作量。
但我自己在做code review的過程中發現,WTForms的內建validators並不可信,與其說是不可信,不如說在安全性上部分validator完全不起任何作用。
就拿上訴程式碼為例子,這段程式碼真的可以檢查使用者輸入的資料是否是一個『URL』麼?我們看到wtforms.validators.URL()類:

#!py
class URL(Regexp):
    def __init__(self, require_tld=True, message=None):
        regex = r'^[a-z]+://(?P<host>[^/:]+)(?P<port>:[0-9]+)?(?P<path>\/.*)?$'
        super(URL, self).__init__(regex, re.IGNORECASE, message)
        self.validate_hostname = HostnameValidation(
            require_tld=require_tld,
            allow_ip=True,
        )

    def __call__(self, form, field):
        message = self.message
        if message is None:
            message = field.gettext('Invalid URL.')

        match = super(URL, self).__call__(form, field, message)
        if not self.validate_hostname(match.group('host')):
            raise ValidationError(message)

其繼承了Rexexp類,實際上就是對使用者輸入進行正則匹配。我們看到它的正則:

regex = r'^[a-z]+://(?P<host>[^/:]+)(?P<port>:[0-9]+)?(?P<path>\/.*)?$'

可見,這個正則與開發者理解的URL嚴重的不匹配。大部分的開發者希望獲得的URL是一個『HTTP網址』,但這個正則匹配到的卻寬泛的太多了,最大特點就是其可匹配任意protocol。
最容易想到的一個攻擊方式就是利用Javascript協議觸發的XSS,比如我傳入的url是

javascript://...xss code

WTForms將認為這是一個合法的URL,並存入資料庫。而在業務邏輯中URL通常是輸出在超連結的href屬性中,而href屬性支援利用Javascript偽協議執行JavaScript程式碼。那麼,這裡就有極大的可能構造一個XSS攻擊。
另一個草草編寫的validator是wtforms.validators.Email()類,檢視其程式碼:

#!py
class Email(Regexp):
    def __init__(self, message=None):
        self.validate_hostname = HostnameValidation(
            require_tld=True,
        )
        super(Email, self).__init__(r'^.+@([^.@][^@]+)$', re.IGNORECASE, message)

    def __call__(self, form, field):
        message = self.message
        if message is None:
            message = field.gettext('Invalid email address.')

        match = super(Email, self).__call__(form, field, message)
        if not self.validate_hostname(match.group(1)):
            raise ValidationError(message)

看看他的正則^.+@([^.@][^@]+)$,這個正則根本無法檢測使用者的輸入是否是Email。最前面的.+就讓一切壞字元全進入了資料庫。
所以我私下稱URL()和Email()為URL Finder和Email Finder,而非validator,因為他們根本無法驗證使用者輸入,倒是更適合作為爬蟲查詢目標的finder。

0x02 利用弱validator構造XSS


這個漏洞實際上是出現在我寫的某個網站中。這個網站允許訪客輸入其部落格地址,而後臺使用URL()對地址的合法性進行驗證,在使用者主頁其他使用者可以點選其頭像訪問部落格。
整個過程如下: https://gist.github.com/phith0n/807869afbe1365015627

#!py
#(๑¯ω¯๑) coding:utf8 (๑¯ω¯๑)
import os
import flask
from flask import Flask
from wtforms.form import Form
from wtforms.validators import DataRequired, URL
from wtforms import StringField
app = Flask(__name__)

class UrlForm(Form):
    url = StringField("Link", validators=[DataRequired(), URL()])

@app.route('/', methods=['GET', 'POST'])
def show_data():
    form = UrlForm(flask.request.form)
    if flask.request.method == "POST" and form.validate():
        url = form.url.data
    else:
        url = flask.request.url
    return flask.render_template('form.html', url=url, form=form)

if __name__ == '__main__':
    app.debug = False
    app.run(os.getenv('IP', '0.0.0.0'), int(os.getenv('PORT', 8080)))

form.html:

#!html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>test</title>
    </head>
    <body>
        <p>{% if form.url.errors %}
            {{ form.url.errors|join(' ') }}
           {% endif %}
        </p>
        <p>
            your input url
            <a href="{{ url }}" target="_blank">{{ url }}</a>
        </p>
        <form method="post">
            <input type="text" name="url" style="width:300px;" />
            <input type="submit" value="Submit"/>
        </form>
    </body>
</html>

demo頁面: https://flask-form-phith0n.c9users.io/ 可供測試。
那麼,這段程式碼存在漏洞嗎?回顧URL的正則:

#!py
regex = r'^[a-z]+://(?P<host>[^/:]+)(?P<port>:[0-9]+)?(?P<path>\/.*)?$'
super(URL, self).__init__(regex, re.IGNORECASE, message)

有個//,實在討厭,將後面的內容全部註釋掉了,導致我不能直接執行JavaScript。繞過方法也簡單,因為//是單行註釋,所以只需換行即可。
但這裡正則修飾符是re.IGNORECASE,並沒有re.S,這就導致一旦出現換行這個正則將不再匹配。
不過這個問題很快也有了答案,在JavaScript中,可以代表換行的字元有\n \r \u2028和\u2029,而在正則裡換行僅僅是\n \r,所以我只要透過\u2028或\u2029這兩個字元代替換行即可。(\u2028的url編碼為%E2%80%A8)
所以,傳入url如下即可:

javascript://www.baidu.com/
alert(1)

輸入以上url,提交後點選連結即可觸發:

p1

這個漏洞很典型,任何開發者都不會想到如此平凡的一段程式碼竟然隱藏著深層次的威脅。
有些人可能會覺得我這個demo並不能說明實際問題,我簡單翻了一下github,不到5分鐘就找到了一個存在同樣問題的專案: https://github.com/1jingdian/1jingdian 。(雖然其站點已經關閉,但程式碼可以瀏覽)

https://github.com/1jingdian/1jingdian/blob/master/application/forms/user.py

#!py
class SettingsForm(Form):
    motto = StringField('座右銘')
    blog = StringField('部落格', validators=[Optional(), URL(message='連結格式不正確')])
    weibo = StringField('微博', validators=[Optional(), URL(message='連結格式不正確')])
    douban = StringField('豆瓣', validators=[Optional(), URL(message='連結格式不正確')])
    zhihu = StringField('知乎', validators=[Optional(), URL(message='連結格式不正確')])

這裡4個連結,全是用URL()來進行驗證。validate()透過後存入資料庫。
之後在個人頁面,提取出使用者資訊傳入模板user/profile.html
https://github.com/1jingdian/1jingdian/blob/master/application/controllers/user.py#L14

#!py
def profile(uid, page):
    user = User.query.get_or_404(uid)
    votes = user.voted_pieces.paginate(page, 20)
    return render_template('user/profile.html', user=user, votes=votes)

跟進一下profile.html
https://github.com/1jingdian/1jingdian/blob/master/application/templates/user/profile.html

#!html
{% from "macros/_user.html" import render_user_profile_header %}
...
{{ render_user_profile_header(user, active="votes") }}

呼叫了marco,傳入render_user_profile_header函式,繼續跟進:
https://github.com/1jingdian/1jingdian/blob/master/application/templates/macros/_user.html#L37

#!html
{% macro render_user_profile_header(user, active="creates") %}
   ...
         <div class="media-icons">
            {% if user.blog %}
               <a href="{{ user.blog }}" target="_blank" title="部落格">
                  <img src="{{ static('image/media/blog.png') }}" alt=""/>
               </a>
            {% endif %}
            {% if user.weibo %}
               <a href="{{ user.weibo }}" target="_blank" title="微博">
                  <img src="{{ static('image/media/weibo.jpg') }}" alt=""/>
               </a>
            {% endif %}
            {% if user.douban %}
               <a href="{{ user.douban }}" target="_blank" title="豆瓣">
                  <img src="{{ static('image/media/douban.png') }}" alt=""/>
               </a>
            {% endif %}
         </div>
      </div>

這裡將user.blog、user.weibo、user.douban都放入了a標籤的href屬性。這一系列操作實際上就是我之前那個demo的縮影,最終導致傳入的url過濾不嚴產生XSS。

0x03 開源元件漏洞到底是誰的鍋?


這是屢次受到爭議的話題之一,很多人認為開源元件之所以造成了漏洞,都是因為開發者不規範使用元件導致的。
我覺得認定一個問題是開源元件的鍋,那麼必須滿足以下條件:

  • 開發者按照文件常規的方法進行開發
  • 文件並沒有說明如此開發會存在什麼安全問題
  • 同樣的開發方式在其他同類元件中沒有漏洞,而在該元件中產生漏洞

舉幾個例子,這個漏洞: WooYun: ThinkPHP某處設計缺陷可導致getshell 。首先滿足第一個條件,正常使用S函式。當然文件中也對安全進行了說明:

p2

但這個說明,我覺得是不夠的。你『可以』設定..引數,避免快取檔名『被猜測到』。文件並沒有說明快取檔名被猜測到有什麼危害,也沒有強制要求設定這個引數。所以這個鍋,官方至少背一半。
再舉個例子: WooYun: 國際php框架slim架構上存在XXE漏洞(XXE的典型存在形式) ,很明顯的一個框架鍋,開發者在正常接收POST引數的時候就可以造成XXE漏洞,這個漏洞和開發者是沒有任何關係的。
另一個例子: WooYun: ThinkPHP架構設計不合理極易導致SQL隱碼攻擊 ,我們透過修改邏輯運算子改變開發者正常的判斷流程,造成安全問題。我們對比一下ThinkPHP和Codeigniter,CI中對於邏輯運算子的位置就和TP不相同,它在『key』的位置:

p3

正常情況下key位置是不會被使用者控制的。所以,同樣的開發方式在CI裡不存在問題,而在TP裡就存在問題,這樣的地方我認為也是ThinkPHP的鍋。
我們看本文提出的WTForm的問題,這個鍋其實WTForm可以不用獨自背。我們在文件中,可以看到它有模模糊糊地提到過validater不嚴謹的問題:

p4

當然,這個模糊的提示對於很多沒有安全基礎的人來說,很難起到作用。

0x04 開發者如何應對潛在的元件『安全特性』


那麼,沒有安全基礎的開發者,如何去應對潛在的元件安全特性。
首先,我覺得經常做code review是很有必要的,我會經常把自己寫的程式碼也當做一個開源應用進行閱讀與審計,此時會經常發現一些之前沒注意到過的安全問題。
code review的過程中,要深入地跟進一下第三方庫的原始碼,而不能僅僅是看自己寫的程式碼,這樣才能發現一些潛在的特性。這些特性往往是造成漏洞的罪魁禍首。
另外,文件的閱讀能力也是極其重要的一點。其實大量的『框架特性』,框架文件中都有一定的說明。很多開發者更喜歡去看example,覺得看程式碼比看文字(也許與英文閱讀能力也有關係)更直觀,而不願詳細閱讀說明。這種做法實際上在安全上是非常危險的,因為示例程式碼通常都是官方給出的最簡陋的程式碼,可能會忽略很多必要的安全措施。
另外,具備一定的安全基礎是每個開發必要的素質,原因不必贅述。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章