開源組件是我們大家平時開發的時候必不可少的工具,所謂『不要重復造輪子』的原因也是因為,大量封裝好的組件我們在開發中可以直接調用,減少了重復開發的工作量。
開源組件和開源程序也有一些區別,開源組件面向的使用者是開發者,而開源程序就可以直接面向用戶。開源組件,如JavaScript里的uploadify,php里的PHPExcel等;開源程序,如php寫的wordpress、joomla,node.js寫的ghost等。
就安全而言,毋庸置疑,開源組件的漏洞影響面遠比開源軟件要大。但大量開源組件的漏洞卻很少出現在我們眼中,我總結了幾條原因:
特別是現在國內浮躁的安全氛圍,可以明顯感受到第一條原因。就前段時間出現的幾個影響較大的漏洞: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,與如何正確使用第三方庫。
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。
這個漏洞實際上是出現在我寫的某個網站中。這個網站允許訪客輸入其博客地址,而后臺使用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,提交后點擊鏈接即可觸發:
這個漏洞很典型,任何開發者都不會想到如此平凡的一段代碼竟然隱藏著深層次的威脅。
有些人可能會覺得我這個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。
這是屢次受到爭議的話題之一,很多人認為開源組件之所以造成了漏洞,都是因為開發者不規范使用組件導致的。
我覺得認定一個問題是開源組件的鍋,那么必須滿足以下條件:
舉幾個例子,這個漏洞: WooYun: ThinkPHP某處設計缺陷可導致getshell 。首先滿足第一個條件,正常使用S函數。當然文檔中也對安全進行了說明:
但這個說明,我覺得是不夠的。你『可以』設置..參數,避免緩存文件名『被猜測到』。文檔并沒有說明緩存文件名被猜測到有什么危害,也沒有強制要求設置這個參數。所以這個鍋,官方至少背一半。
再舉個例子: WooYun: 國際php框架slim架構上存在XXE漏洞(XXE的典型存在形式) ,很明顯的一個框架鍋,開發者在正常接收POST參數的時候就可以造成XXE漏洞,這個漏洞和開發者是沒有任何關系的。
另一個例子: WooYun: ThinkPHP架構設計不合理極易導致SQL注入 ,我們通過修改邏輯運算符改變開發者正常的判斷流程,造成安全問題。我們對比一下ThinkPHP和Codeigniter,CI中對于邏輯運算符的位置就和TP不相同,它在『key』的位置:
正常情況下key位置是不會被用戶控制的。所以,同樣的開發方式在CI里不存在問題,而在TP里就存在問題,這樣的地方我認為也是ThinkPHP的鍋。
我們看本文提出的WTForm的問題,這個鍋其實WTForm可以不用獨自背。我們在文檔中,可以看到它有模模糊糊地提到過validater不嚴謹的問題:
當然,這個模糊的提示對于很多沒有安全基礎的人來說,很難起到作用。
那么,沒有安全基礎的開發者,如何去應對潛在的組件安全特性。
首先,我覺得經常做code review是很有必要的,我會經常把自己寫的代碼也當做一個開源應用進行閱讀與審計,此時會經常發現一些之前沒注意到過的安全問題。
code review的過程中,要深入地跟進一下第三方庫的源代碼,而不能僅僅是看自己寫的代碼,這樣才能發現一些潛在的特性。這些特性往往是造成漏洞的罪魁禍首。
另外,文檔的閱讀能力也是極其重要的一點。其實大量的『框架特性』,框架文檔中都有一定的說明。很多開發者更喜歡去看example,覺得看代碼比看文字(也許與英文閱讀能力也有關系)更直觀,而不愿詳細閱讀說明。這種做法實際上在安全上是非常危險的,因為示例代碼通常都是官方給出的最簡陋的代碼,可能會忽略很多必要的安全措施。
另外,具備一定的安全基礎是每個開發必要的素質,原因不必贅述。