作者:phith0n@長亭科技
Django 發布了新版本1.11.5,修復了500頁面中可能存在的一個 XSS 漏洞,這篇文章說明一下該漏洞的原理和復現,和我的一點點評。
0x01 補丁分析
因為官方說明是500頁面中出現的 BUG,所以我們重點關注的就是django/views/debug.py。
Github 上有 Django 的倉庫,下載下來,用1.11.4和1.11.5進行比較:
git clone https://github.com/django/django.git
cd django
git diff 1.11.4 1.11.5 django/views/debug.py

可見,外部關閉了全局轉義,然后在這兩個地方增加了強制轉義。那么,漏洞肯定是在這個位置觸發的。
0x02 功能點探索
如果要觸發這兩個輸出點,就必須進入這個 if 語句:{% ifchanged frame.exc_cause %}{% if frame.exc_cause %}。
首先我們來想一下,正常情況下,這個位置是干嘛用的,也就是說,功能點是什么。
作為一個老年 Django 開發,看到上圖畫框的這個關鍵句子The above exception was the direct cause of the following exception:,我是有印象的:一般是在出現數據庫異常的時候,會拋出這樣的錯誤語句。
我們可以做個簡單的測試,在 Django 命令行下,我們創建一個 username 為 phith0n 的用戶,然后再次創建一個 username 為phith0n的用戶,則會拋出一個IntegrityError異常:

見上圖,原因是觸發了數據庫的 Unique 異常。
為什么 Django 會引入這樣一個異常機制?這是為了方便開發者進行 SQL 錯誤的調試,因為 Django 的模型最終是操作數據庫,數據庫中具體出現什么錯誤,是 Django 無法100%預測的。那么,為了方便開發者快速找到是哪個操作觸發了數據庫異常,就需要將這兩個異常回溯棧關聯到一塊。
我們可以看看代碼,django/db/utils.py的__exit__函數:
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is None:
return
for dj_exc_type in (
DataError,
OperationalError,
IntegrityError,
InternalError,
ProgrammingError,
NotSupportedError,
DatabaseError,
InterfaceError,
Error,
):
db_exc_type = getattr(self.wrapper.Database, dj_exc_type.__name__)
if issubclass(exc_type, db_exc_type):
dj_exc_value = dj_exc_type(*exc_value.args)
dj_exc_value.__cause__ = exc_value
if not hasattr(exc_value, '__traceback__'):
exc_value.__traceback__ = traceback
# Only set the 'errors_occurred' flag for errors that may make
# the connection unusable.
if dj_exc_type not in (DataError, IntegrityError):
self.wrapper.errors_occurred = True
six.reraise(dj_exc_type, dj_exc_value, traceback)
其中exc_type是異常,如果其類型是DataError,OperationalError,IntegrityError,InternalError,ProgrammingError,NotSupportedError,DatabaseError,InterfaceError,Error之一,則拋出一個同類型的新異常,并設置其__cause__和__traceback__為此時上下文的exc_value和traceback。
exc_value是上一個異常的說明,traceback是上一個異常的回溯棧。這個函數其實就是關聯了上一個異常和當前的新異常。
最后,在500頁面中,__cause__被輸出。
0x03 漏洞復現
經過我的測試,我發現在使用 Postgres 數據庫并觸發異常的時候,psycopg2 會將字段名和字段值全部拋出。那么,如果字段值中包含我們可控的字符串,又由于0x02中說到的,這個字符串其實就會被設置成__cause__,最后被顯示在頁面中。
所以我們假設有如下場景:
- 用戶注冊頁面,未檢查用戶名
- 注冊一個用戶名為
<script>alert(1)</script>的用戶 - 再次注冊一個用戶名為
<script>alert(1)</script>的用戶 - 觸發duplicate key異常,導致XSS漏洞
我將上述流程整理成 vulhub 的一個環境:https://github.com/phith0n/vulhub/tree/master/django/CVE-2017-12794
編譯及啟動環境:
docker-compose build
docker-compose up -d
訪問http://your-ip:8000/create_user/?username=<script>alert(1)</script>創建一個用戶,成功;再次訪問http://your-ip:8000/create_user/?username=<script>alert(1)</script>,觸發異常:

可見,Postgres 拋出的異常為
duplicate key value violates unique constraint "xss_user_username_key"
DETAIL: Key (username)=(<script>alert(1)</script>) already exists.
這個異常被拼接進The above exception ({{ frame.exc_cause }}) was the direct cause of the following exception,最后觸發 XSS。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/402/