Investigating the Puppeteer mode of Open Bullet 2 (credential stuffing tool)
This is the third article of the series about Open Bullet
2. In the previous
article, we studied the Http request
mode of Open Bullet. It offers a set of features
that enable attackers to make GET/POST HTTP requests, including POST login requests automatically.
In this article, we focus on the Puppeteer mode of Open Bullet 2. We create a simple bot based on it to study its fingerprint and see how it can be detected. In particular, we show that Open Bullet 2 relies on PuppeteerExtraSharp, but it can still be detected because of inconsistencies, such as device signals that are different in a web worker compared to the main JS execution context
Creating a Puppeteer-based bot in Open Bullet 2
First, we need to set the Chrome path for Puppeteer in
RuriLib Settings
panel, cf the screenshot below.
Then, we create a new configuration and edit it using the
Stacker
panel. The configuration looks as follows:
Our Puppeteer-based bot does the following:
- Create a browser instance using the
Puppeteer
function block;
- Navigate to https://deviceandbrowserinfo.com/are_you_a_bot;
- We wait 2s;
- Take a screenshot of the page;
- Get the value of
window.fingerprint
using theExecute JS
block;
- Close the browser instance.
In the screenshot from the bot detection test page, we see that by default, Open Bullet 2 used with Puppeteer is properly detected as a bot.
The 2 tests that indicate the presence of a bot are:
hasInconsistentGPUFeatures
: this signal is equal totrue
whenever a recent browser that should support webGPU features doesn’t support it. Note that there might still be legitimate use cases where this could happen, even though it’s suspicious.
hasInconsistentWorkerValues
: this detection test verifies the values of JS signals, such asnavigator.platform
,navigator.userAgent
, collected in a web worker, and compares them with the values collected in the main JS execution context. If the values are different, this may indicate that a bot tried to override/forge JS values.
The signals collected in the web worker look as follows:
"workerData": {
"webGLVendor": "Google Inc. (Google)",
"webGLRenderer": "ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver)",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.99 Safari/537.36",
"languages": [
"en-US"
],
"platform": "MacIntel",
"hardwareConcurrency": 4,
}
The same signals collected in the main JS execution context look as follows:
{
"webGLVendor": "Intel Inc.",
"webGLRenderer": "Intel Iris OpenGL Engine",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.99 Safari/537.36",
"languages": [
"en-US",
"en"
]
"platform": "Mac OS X",
"hardwareConcurrency": 4,
}
We notice differences in the WebGL attributes, the
languages, and the platform. In particular, we notice that the WebGL renderer is equal to
"ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver)"
in the web worker, which is often a sign of bot traffic.
Why are the values different in the web worker?
If we use a normal Chrome instrumented with Puppeteer, we won’t notice any differences between the values collected in the main JS execution context and the values collected in the web worker.
The reason there are differences in the web worker is that under the hood, Open Bullet 2 doesn’t rely on Puppeteer, but on PuppeteerExtraSharp instead.
PuppeteerExtraSharp is a .NET port of the popular Puppeteer extra stealth library. It is a modified version of the Puppeteer library that aims to make bots more difficult to detect by forging their fingerprints. Moreover, it also provides features, such as automatic CAPTCHA solving through CAPTCHA farm integrations.
By default, PuppeteerExtraSharp applies a set of evasion techniques that are supposed to make bots less detectable. Note that I wrote “suppose” since it’s probably more counterproductive since the release of the latest/new headless Chrome.
One of these evasion techniques focuses on the WebGL
renderer and vendor attributes. By default, it sets the value of the WebGL vendor to
"Intel Inc."
and the value of the WebGL renderer to
"Intel Iris OpenGL Engine"
, which are the values we observed in the main JS execution
context.
However, Puppeteer extra stealth (and PuppeteerExtraSharp) only overrides these values in the main JS execution context, not in web workers, which makes it easy to detect a discrepancy. Note that the library provides an option to specify the values of the WebGL vendor and renderer, but this is not exposed in Open Bullet 2.
We observe the same issue with the
navigator.languages
attribute, where the attribute is set to
["en-US", "en"]
even though it’s equal to ["en-US"]
in the main JS execution context.
What about CDP detection?
If you read my recent article about Puppeteer detection, you may have noticed a CDP (Chrome DevTools Protocol) detection test. The test looks as follows and can be used to detect Chrome/Chromium-based browsers instrumented using CDP, which is the technology used by frameworks like Puppeteer, Playwright, and Selenium.
var detected = false;
var e = new Error();
Object.defineProperty(e, 'stack', {
get() {
detected = true;
}
});
console.log(e);
However, here, we notice that the CDP detection test is
equal to false
, which means that it wasn’t able to detect any form of CDP automation.
Interestingly, if we run the default configuration of Puppeteer extra stealth in NodeJS, the CDP automation
is working as expected:
As explained in my
DataDome blog post, the detection test aims to detect side effects introduced by the CDP
Runtime.enable
command. Under the hood, Puppeteer extra sharp uses Puppeteer
Sharp, a .NET port of Puppeteer. If we search for the Runtime.enable
command in the
Puppeteer Sharp codebase, we notice 3 occurrences:
In particular, we notice that in the
CdpWebWorker.cs
file, Puppeteer Sharps sends a Runtime.enable
command. Based on
this new information, I updated the fingerprinting script and the bot detection test code to execute the CDP
detection check in a web worker. After this improvement, we detect that it properly detects CDP automation
in the worker when Open Bullet 2 is used with Puppeteer, cf screenshot below.
Conclusion
In this third blog post of our series about Open Bullet 2, we analyzed the Puppeteer mode of Open Bullet 2. We created a simple bot based on it to study its fingerprint and see how it can be detected. We showed that Open Bullet 2 relies on PuppeteerExtraSharp, but it can still be detected because of inconsistencies, such as device signals that are different in a web worker compared to the main JS execution context.
In the next blog posts of this series, we will continue to investigate Open Bullet 2, in particular the features that enable attackers to make login attempts or to avoid bot detection techniques.