“WebUI “是一个术语,用于宽泛地描述用网络技术(即HTML、CSS、JavaScript)实现的Chrome浏览器的部分UI。
Chromium中的WebUI的例子。
Settings (chrome://settings) History (chrome://history) Downloads (chrome://downloads) 关于webui具体怎么工作在这里将不展开,请参考官方文档详细阅读,本文将重点介绍webui中常见的几类漏洞模式。
https://chromium.googlesource.com/chromium/src/+/master/docs/webui_explainer.md
我们将以一个简单的漏洞模式来学习webui的数据流传递。
具体的说就是每个WebUI都会注册很多WebUIMessageHandler,而每个Handler上又会注册多个Message Callback,每个Message Callback都有一个对应的Message Name,可以通过这个Message Name来调用到对应的webui函数,并传入参数。
具体来说就是形如以下调用:
chrome.send("recordNavigation",[1337,0]);
case1: issue-1303614
由于该漏洞代码只存在于chromium dev,不存在发行版中,所以没有CVE,只有对应的issue编号。
Root Cause
-
https://bugs.chromium.org/p/chromium/issues/detail?id=1303614
让我们看一下代码,这里注册了一个名为recordNavigation的Message Callback,它将对应调用到HandleRecordNavigation函数,并处理传入的参数。
它将对传入的参数列表依次调用ConvertToNavigationView,将其强制转换为NavigationView类型的枚举值,分别得到from_view和to_view。
但由于这里并没有检查传入的参数是否小于NavigationView类型能处理的最大值,注意这里仅仅只有一个debug check,这个debug check在release发行版里是不存在的,所以可以试做没有检查。
这将导致在EmitScreenOpenDuration函数处理cast之后得到的from_view的时候, 触发一个堆溢出。
这里它将对kOpenDurationMetrics列表进行find,但是由于没有检查传入的参数是否小于NavigationView类型能处理的最大值,所以它将find不到。
我们知道在c++里,find如果找不到,迭代器iter将指向end,这其实代表的是指向容器的最后一个元素的下一个。
而这里同样也没有检查find找不到的情况,也就是没有检查iter是否指向end,就直接解引用了。它同样也是使用了一个Debug Check,但这其实是无用的。
所以对iter解引用将直接越界,造成buffer overflow。
// content::WebUIMessageHandler:
void DiagnosticsMetricsMessageHandler::RegisterMessages() {
DCHECK(web_ui());
web_ui()->RegisterMessageCallback(
kRecordNavigation, //----->"recordNavigation"
base::BindRepeating(
&DiagnosticsMetricsMessageHandler::HandleRecordNavigation,
base::Unretained(this)));
}
enum class NavigationView {
kSystem = 0,
kConnectivity = 1,
kInput = 2,
kMaxValue = kInput,
};
// Converts base::Value<int> to NavigationView based on enum values.
NavigationView ConvertToNavigationView(const base::Value& value) {
DCHECK(value.is_int());
DCHECK_LE(value.GetInt(), static_cast<int>(NavigationView::kMaxValue));
**return static_cast<NavigationView>(value.GetInt());**
}
// Message Handlers:
void DiagnosticsMetricsMessageHandler::HandleRecordNavigation(
const base::Value::List& args) {
DCHECK_EQ(2u, args.size());
DCHECK_NE(args[0], args[1]);
**const NavigationView from_view = ConvertToNavigationView(args[0]);**
const NavigationView to_view = ConvertToNavigationView(args[1]);
const base::Time updated_start_time = base::Time::Now();
// Recordable navigation event occurred.
**EmitScreenOpenDuration(from_view, updated_start_time - navigation_started_);**
// `current_view_` updated to recorded `to_view` and reset timer.
current_view_ = to_view;
navigation_started_ = updated_start_time;
}
void EmitScreenOpenDuration(const NavigationView screen,
const base::TimeDelta& time_elapsed) {
// Map of screens within Diagnostics app to matching duration metric name.
constexpr auto kOpenDurationMetrics =
base::MakeFixedFlatMap<NavigationView, base::StringPiece>({
{NavigationView::kConnectivity,
"ChromeOS.DiagnosticsUi.Connectivity.OpenDuration"},
{NavigationView::kInput, "ChromeOS.DiagnosticsUi.Input.OpenDuration"},
{NavigationView::kSystem,
"ChromeOS.DiagnosticsUi.System.OpenDuration"},
});
**auto* iter = kOpenDurationMetrics.find(screen);**
DCHECK(iter != kOpenDurationMetrics.end());
base::UmaHistogramLongTimes100(std::string(iter->second), time_elapsed);
}
poc
browsing chrome://diagnostics
and open devtools
execute chrome.send("recordNavigation",[1337,0]);
in console.
patch
补丁就是加上了我刚刚提到的没有加的检查。
auto* iter = kOpenDurationMetrics.find(screen);
- DCHECK(iter != kOpenDurationMetrics.end());
+ if (iter == kOpenDurationMetrics.end())
+ return;
other case
https://bugs.chromium.org/p/chromium/issues/detail?id=1303613
|cross-thread calback race
case1: CVE-2022-1311
Root Cause
-
https://bugs.chromium.org/p/chromium/issues/detail?id=1310717 -
https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/threading_and_tasks.md
Chrome将运行UI并管理所有网页和插件进程的主进程称为“浏览器进程”或“浏览器”,而每个网页都运行在一个单独的进程里,这个进程称为渲染进程。
鉴于渲染进程在单独的进程中运行,所以Chrome有机会通过沙箱限制其对系统资源的访问,所有渲染器对网络和文件资源的访问都通过IPC来通知浏览器进程来完成。
在一个进程中,往往有如下几种线程:
-
一个 main thread -
在 Browser 进程中 (BrowserThread::UI):用于更新 UI -
在 Render 进程中:运行Blink -
一个 io thread -
在 Browser 进程中(BrowserThread::IO): 用于处理 IPC 消息以及网络请求 -
在 Render 进程中:用于处理IPC消息 -
一些使用 base::Tread 创建的,有特殊用途的线程(可能存在) -
一些在使用线程池时产生的线程(可能存在)
void CrostiniUpgrader::Backup(const ContainerId& container_id,
bool show_file_chooser,
content::WebContents* web_contents) {
if (show_file_chooser) {
CrostiniExportImport::GetForProfile(profile_)->ExportContainer(
container_id, web_contents, MakeFactory());
return;
}
base::FilePath default_path =
CrostiniExportImport::GetForProfile(profile_)->GetDefaultBackupPath();
**base::ThreadPool::PostTaskAndReplyWithResult**(
FROM_HERE, {base::MayBlock()},
base::BindOnce(&base::PathExists, default_path),
base::BindOnce(&CrostiniUpgrader::OnBackupPathChecked,
weak_ptr_factory_.GetWeakPtr(), container_id, **web_contents**,
default_path));
}
我介绍一个我挖掘的漏洞,首先我们要知道Chrome线程内部是怎么实现任务的同步的,其实是通过派发一个回调给一个处理线程的MessageLoop,然后MessageLoop会调度该回调以执行其操作。
这个漏洞就是这么产生的,ThreadPool::PostTaskAndReplyWithResult是UI线程向线程池里的线程发送一个PathExists函数的回调,然后线程池会检查backup路径是否存在,然后当线程池执行完任务PathExists,它会向UI线程发送一个OnRestorePathChecked函数的回调,一个回调其实和一个闭包是相似的,它会包括一个函数指针和它使用的函数参数。
在这个过程中就可能产生条件竞争。因为OnRestorePathChecked的参数里包括了一个原始指针web_contents,这样的指针是没有被保护的,所以如果我们在线程池里正在执行PathExists的同时,我们在UI线程这边通过关闭网页把web_contents释放掉,从而当OnRestorePathChecked被发回到UI线程执行的时候,此时web_contents已经被释放掉了,解引用它的指针就会触发UAF。
other case
https://bugs.chromium.org/p/chromium/issues/detail?id=1320624https://bugs.chromium.org/p/chromium/issues/detail?id=1322744https://bugs.chromium.org/p/chromium/issues/detail?id=1311701https://bugs.chromium.org/p/chromium/issues/detail?id=1304145
|Listener no check destroyed
case1: issue-1315102
Root Cause
-
https://bugs.chromium.org/p/chromium/issues/detail?id=1315102
SupportToolMessageHandler::HandleStartDataExport
会创建一个 select_file_dialog_
[1] 并显示一个 SelectFileDialog对话框。
当 [1] 被调用时,this
原始指针被传递给ui::SelectFileDialog::Create
,并且传递的this
原始指针被保存在listener_
[2] 中。
当用户选择一个文件夹时,listener_->FileSelected(paths[0], index, params);
[3]被调用来处理用户的文件夹选择。
但是,SupportToolMessageHandler::~SupportToolMessageHandler
[4] 是默认析构函数,不会调 select_file_dialog_->ListenerDestroyed();
将listener_
置为nullptr。
如果用户在 SupportToolMessageHandler
被释放后选择了一个文件夹(即 listener_
被释放),UAF 将在 [3] 中触发。
因此,我们可以构建以下 UAF 链:
-
通过chrome.send调用 SupportToolMessageHandler::HandleStartDataExport
-
通过关闭webui网页来释放 SupportToolMessageHandler
-
在SelectFileDialog里选择一个文件,在[3]中触发UAF。
scoped_refptr<ui::SelectFileDialog> select_file_dialog_;
void SupportToolMessageHandler::HandleStartDataExport(
const base::Value::List& args) {
CHECK_EQ(1U, args.size());
const base::Value::List* pii_items = args[0].GetIfList();
DCHECK(pii_items);
selected_pii_to_keep_ = GetSelectedPIIToKeep(pii_items);
AllowJavascript();
content::WebContents* web_contents = web_ui()->GetWebContents();
gfx::NativeWindow owning_window =
web_contents ? web_contents->GetTopLevelNativeWindow()
: gfx::kNullNativeWindow;
select_file_dialog_ = ui::SelectFileDialog::Create(
this,
std::make_unique<ChromeSelectFilePolicy>(web_ui()->GetWebContents()));
select_file_dialog_->SelectFile(
ui::SelectFileDialog::SELECT_SAVEAS_FILE,
/*title=*/std::u16string(),
/*default_path=*/
GetDefaultFileToExport(handler_->GetCaseID(), data_collection_time_),
/*file_types=*/nullptr,
/*file_type_index=*/0,
/*default_extension=*/base::FilePath::StringType(), owning_window,
/*params=*/nullptr);
}
void SupportToolMessageHandler::FileSelected(const base::FilePath& path,
int index,
void* params) {
FireWebUIListener("support-data-export-started");
select_file_dialog_.reset();
this->handler_->ExportCollectedData(
std::move(selected_pii_to_keep_), path,
base::BindOnce(&SupportToolMessageHandler::OnDataExportDone,
weak_ptr_factory_.GetWeakPtr()));
}
void SupportToolMessageHandler::FileSelectionCanceled(void* params) {
selected_pii_to_keep_.clear();
select_file_dialog_.reset();
}
// Checks `errors` and fires WebUIListener with the error message or the
// exported path according to the returned errors.
// type DataExportResult = {
// success: boolean,
// path: string,
// error: string,
// }
void SupportToolMessageHandler::OnDataExportDone(
base::FilePath path,
std::set<SupportToolError> errors) {
data_path_ = path;
base::Value::Dict data_export_result;
const auto& export_error = std::find_if(
errors.begin(), errors.end(), [](const SupportToolError& error) {
return (error.error_code == SupportToolErrorCode::kDataExportError);
});
if (export_error == errors.end()) {
data_export_result.Set("success", true);
std::string displayed_path = data_path_.AsUTF8Unsafe();
#if BUILDFLAG(IS_CHROMEOS_ASH)
displayed_path = file_manager::util::GetPathDisplayTextForSettings(
Profile::FromWebUI(web_ui()), displayed_path);
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
data_export_result.Set("path", displayed_path);
data_export_result.Set("error", std::string());
} else {
// If a data export error is found in the returned set of errors, send the
// error message to UI with empty string as path since it means the export
// operation has failed.
data_export_result.Set("success", false);
data_export_result.Set("path", std::string());
data_export_result.Set("error", export_error->error_message);
}
FireWebUIListener("data-export-completed",
base::Value(std::move(data_export_result)));
}
void SupportToolMessageHandler::HandleShowExportedDataInFolder(
const base::Value::List& args) {
platform_util::ShowItemInFolder(Profile::FromWebUI(web_ui()), data_path_);
}
////////////////////////////////////////////////////////////////////////////////
//
// SupportToolUI
//
////////////////////////////////////////////////////////////////////////////////
SupportToolUI::SupportToolUI(content::WebUI* web_ui) : WebUIController(web_ui) {
web_ui->AddMessageHandler(std::make_unique<SupportToolMessageHandler>());
// Set up the chrome://support-tool/ source.
Profile* profile = Profile::FromWebUI(web_ui);
content::WebUIDataSource::Add(
profile, CreateSupportToolHTMLSource(web_ui->GetWebContents()->GetURL()));
}
SupportToolUI::~SupportToolUI() = default;
**select_file_dialog_ = ui::SelectFileDialog::Create(
this,** //----------> [1]
**std::make_unique<ChromeSelectFilePolicy>(web_ui()->GetWebContents()));**
select_file_dialog_->SelectFile(
ui::SelectFileDialog::SELECT_SAVEAS_FILE,
/*title=*/std::u16string(),
/*default_path=*/
GetDefaultFileToExport(handler_->GetCaseID(), data_collection_time_),
/*file_types=*/nullptr,
/*file_type_index=*/0,
/*default_extension=*/base::FilePath::StringType(), owning_window,
/*params=*/nullptr);
}
// The listener to be notified of selection completion.
raw_ptr<Listener> listener_;
SelectFileDialog::SelectFileDialog(Listener* listener,
std::unique_ptr<ui::SelectFilePolicy> policy)
: **listener_(listener)**, select_file_policy_(std::move(policy)) { // [2]
DCHECK(listener_);
}
void SelectFileDialogImpl::OnSelectFileExecuted(
Type type,
std::unique_ptr<RunState> run_state,
void* params,
const std::vector<base::FilePath>& paths,
int index) {
if (listener_) {
// The paths vector is empty when the user cancels the dialog.
if (paths.empty()) {
listener_->FileSelectionCanceled(params);
} else {
switch (type) {
case SELECT_FOLDER:
case SELECT_UPLOAD_FOLDER:
case SELECT_EXISTING_FOLDER:
case SELECT_SAVEAS_FILE:
case SELECT_OPEN_FILE:
DCHECK_EQ(paths.size(), 1u);
listener_->FileSelected(paths[0], index, params); // [3]
break;
case SELECT_OPEN_MULTI_FILE:
listener_->MultiFilesSelected(paths, params);
break;
case SELECT_NONE:
NOTREACHED();
}
}
}
EndRun(std::move(run_state));
}
~SupportToolMessageHandler() override = default; // [4]
Patch
补丁就是让select_file_dialog观察webui(也就是它的listerner)的生命周期,以及避免double show。
SupportToolMessageHandler::~SupportToolMessageHandler() {
if (select_file_dialog_) {
select_file_dialog_->ListenerDestroyed();
}
}
...
void SupportToolMessageHandler::HandleStartDataExport(
const base::Value::List& args) {
CHECK_EQ(1U, args.size());
const base::Value::List* pii_items = args[0].GetIfList();
DCHECK(pii_items);
// Early return if the select file dialog is already active.
if (select_file_dialog_)
return;
selected_pii_to_keep_ = GetSelectedPIIToKeep(pii_items);
other case
https://bugs.chromium.org/p/chromium/issues/detail?id=1305068https://bugs.chromium.org/p/chromium/issues/detail?id=1306391https://bugs.chromium.org/p/chromium/issues/detail?id=1304884
-End-
原文始发于微信公众号(360漏洞研究院):技术前瞻|WebUI: The easiest attack surface in Chromes