SlideShare a Scribd company logo
Headless Browser
Hide & Seek
Sergey Shekyan, Bei Zhang
Shape Security
Who We Are
• Bei Zhang
Senior Software Engineer at Shape Security, focused on analysis
and countermeasures of automated web attacks. Previously, he
worked at the Chrome team at Google with a focus on the Chrome
Apps API. His interests include web security, source code analysis,
and algorithms.
• Sergey Shekyan
Principal Engineer at Shape Security, focused on the development of
the new generation web security product. Prior to Shape Security, he
spent 4 years at Qualys developing their on demand web
application vulnerability scanning service. Sergey presented
research at security conferences around the world, covering various
information security topics.
Is There A Problem?
What Is a Headless Browser and How it Works
Scriptable browser environment that doesn’t require GUI
• Existing browser layout engine with bells and whistles
(PhantomJS - WebKit, SlimerJS - Gecko, TrifleJS - Trident)
• Custom software that models a browser (ZombieJS,
HtmlUnit)
• Selenium (WebDriver API)
What Is a Headless Browser and How it Works
Discussion will focus on PhantomJS:
• Backed by WebKit engine
• Cross-platform
• Popular
• True headless
PhantomJS World
PhantomJS
JavaScript Context
QWebFrame
QtWebKit
Web Page
JavaScript
Context
Control
Callback
Injection
PageEvent
Callbacks are
serialized
var page = require('webpage').create();
page.open(url, function(status) {
var title = page.evaluate(function() {
return document.title;
});
console.log('Page title is ' + title);
});
Legitimate uses and how you can benefit
• Web Application functional and performance
testing
• Crawler that can provide certain amount of
interaction to reveal web application topology,
Automated DOM XSS, CSRF detection
• SEO (render dynamic web page into static
HTML to feed to search engines)
• Reporting, image generation
Malicious Use of Headless Browser
• Fuzzing
• Botnet
• Content scraping
• Login brute force attacks
• Click fraud
• Bidding wars
Web admins tend to block PhantomJS in production,
so pretending to be a real browser is healthy choice
How It Is Different From a Real Browser
• Outdated WebKit engine (close to Safari 5 engine, 4 y.o.)
• Uses Qt Framework’s QtWebKit wrapper around WebKit
• Qt rendering engine
• Qt network stack, SSL implementation
• Qt Cookie Jar, that doesn’t support RFC 2965
• No Media Hardware support (no video and audio)
• Exposes window.callPhantom and window._phantom
• No sandboxing
Good vs. Bad
Headless Browser Seek
• Look at user agent string
if (/PhantomJS/.test(window.navigator.userAgent)) {
console.log(‘PhantomJS environment detected.’);
}
Headless Browser Hide
• Making user-agent (and navigator.userAgent) a
“legitimate” one:
var page = require(‘webpage').create();
page.settings.userAgent = ‘Mozilla/5.0 (Macintosh; Intel Mac
OS X 10.9; rv:30.0) Gecko/20100101 Firefox/30.0';
Score board
Phantom Web site
User Agent String Win Lose
Headless Browser Seek
• Sniff for PluginArray content
if (!(navigator.plugins instanceof PluginArray) ||
navigator.plugins.length == 0) {
    console.log("PhantomJS environment detected.");
  } else {
    console.log("PhantomJS environment not detected.");
  }
Headless Browser Hide
• Fake navigator object, populate PluginArray with whatever values you need.
• Spoofing Plugin objects inside the PluginArray is tedious and hard.
• Websites can actually create a plugin to test it.
• CONCLUSION: Not a good idea to spoof plugins.
page.onInitialized = function () {
    page.evaluate(function () {
        var oldNavigator = navigator;
        var oldPlugins = oldNavigator.plugins;
        var plugins = {};
        plugins.length = 1;
        plugins.__proto__ = oldPlugins.__proto__;
        window.navigator = {plugins: plugins};
        window.navigator.__proto__ = oldNavigator.__proto__;
    });
};
Score board
Phantom Web site
User Agent String Win Lose
Inspect PluginArray Lose Win
Headless Browser Seek
• Alert/prompt/confirm popup suppression timing
detection
var start = Date.now();
  prompt('I`m just kidding');
  var elapse = Date.now() - start;
  if (elapse < 15) {
    console.log("PhantomJS environment detected. #1");
  } else {
    console.log("PhantomJS environment not detected.");
  }
Headless Browser Hide
• Can’t use setTimeout, but blocking the callback by
all means would work
page.onAlert = page.onConfirm = page.onPrompt = function ()
{
    for (var i = 0; i < 1e8; i++) {
    }
    return "a";
};
Score board
Phantom Web site
User Agent String Win Lose
Inspect PluginArray Lose Win
Timed alert() Lose Win
Headless Browser Seek
• Default order of headers is consistently different
in PhantomJS. Camel case in some header
values is also a good point to look at.
PhantomJS 1.9.7
GET / HTTP/1.1
User-Agent:
Accept:
Connection: Keep-Alive
Accept-Encoding:
Accept-Language:
Host:
Chrome 37
GET / HTTP/1.1
Host:
Connection: keep-alive
Accept:
User-Agent:
Accept-Encoding:
Accept-Language:
Headless Browser Hide
• A custom proxy server in front of PhantomJS
instance that makes headers look consistent with
user agent string
Score board
Phantom Web site
User Agent String Win Lose
Inspect PluginArray Lose Win
Timed alert() Lose Win
HTTP Header order Win Lose
Headless Browser Seek
• PhantomJS exposes APIs:
• window.callPhantom
• window._phantom // not documented.
if (window.callPhantom || window._phantom) {
  console.log("PhantomJS environment detected.");
} else {
 console.log("PhantomJS environment not detected.");
}
Headless Browser Hide
• store references to original callPhantom, _phantom
• delete window.callPhantom, window._phantom
page.onInitialized = function () {
    page.evaluate(function () {
        var p = window.callPhantom;
        delete window._phantom;
        delete window.callPhantom;
        Object.defineProperty(window, "myCallPhantom", {
            get: function () { return p;},
            set: function () {}, enumerable: false});
        setTimeout(function () { window.myCallPhantom();}, 1000);
    });
};
page.onCallback = function (obj) { console.log(‘profit!'); };
Unguessable name
Score board
Phantom Web site
User Agent String Win Lose
Inspect PluginArray Lose Win
Timed alert() Lose Win
HTTP Header order Win Lose
window.callPhantom Win Lose
Headless Browser Seek
• Spoofing DOM API properties of real browsers:
• WebAudio
• WebRTC
• WebSocket
• Device APIs
• FileAPI
• WebGL
• CSS3 - not observable. Defeats printing.
• Our research on WebSockets: http://goo.gl/degwTr
Score board
Phantom Web site
User Agent String Win Lose
Inspect PluginArray Lose Win
Timed alert() Lose Win
HTTP Header order Win Lose
window.callPhantom Win Lose
HTML5 features Lose Win
Headless Browser Seek
• Significant difference in JavaScript Engine: bind() is
not defined in PhantomJS prior to version 2
(function () {
    if (!Function.prototype.bind) {
      console.log("PhantomJS environment detected.");
      return;
    }
    console.log("PhantomJS environment not detected.");
  })();
Function.prototype.bind = function () {
var func = this;
var self = arguments[0];
var rest = [].slice.call(arguments, 1);
return function () {
var args = [].slice.call(arguments, 0);
return func.apply(self, rest.concat(args));
};
};
Headless Browser Seek
• Detecting spoofed Function.prototype.bind for
PhantomJS prior to version 2
(function () {
    if (!Function.prototype.bind) {
      console.log("PhantomJS environment detected. #1");
      return;
    }
    if (Function.prototype.bind.toString().replace(/bind/g, 'Error') !=
Error.toString()) {
      console.log("PhantomJS environment detected. #2");
      return;
    }
    console.log("PhantomJS environment not detected.");
  })();
Headless Browser Hide
• Spoofing Function.prototype.toString
function functionToString() {
    if (this === bind) {
        return nativeFunctionString;
    }
    return oldCall.call(oldToString, this);
}
Headless Browser Seek
• Detecting spoofed Function.prototype.bind for
PhantomJS prior to version 2
(function () {
    if (!Function.prototype.bind) {
      console.log("PhantomJS environment detected. #1");
      return;
    }
    if (Function.prototype.bind.toString().replace(/bind/g, 'Error') !=
Error.toString()) {
      console.log("PhantomJS environment detected. #2");
      return;
    }
    if (Function.prototype.toString.toString().replace(/toString/g, 'Error') != Error.toString()) {
      console.log("PhantomJS environment detected. #3");
      return;
    }
    console.log("PhantomJS environment not detected.");
  })();
Headless Browser Hide
• Spoofing Function.prototype.toString.toString
function functionToString() {
    if (this === bind) {
        return nativeFunctionString;
    }
    if (this === functionToString) {
        return nativeToStringFunctionString;
    }
    if (this === call) {
        return nativeCallFunctionString;
    }
    if (this === apply) {
        return nativeApplyFunctionString;
    }
    var idx = indexOfArray(bound, this);
    if (idx >= 0) {
        return nativeBoundFunctionString;
    }
    return oldCall.call(oldToString, this);
}
Score board
Phantom Web site
User Agent String Win Lose
Inspect PluginArray Lose Win
Timed alert() Lose Win
HTTP Header order Win Lose
window.callPhantom Win Lose
HTML5 features Lose Win
Function.prototype.bind Win Lose
Headless Browser Seek
• PhantomJS2 is very EcmaScript5 spec compliant,
so checking for outstanding JavaScript features is
not going to work
Headless Browser Seek
• Stack trace generated by PhantomJs
• Stack trace generated by SlimerJS
• Hmm, there is something common…
at querySelectorAll (phantomjs://webpage.evaluate():9:10)
at phantomjs://webpage.evaluate():19:30
at phantomjs://webpage.evaluate():20:7
at global code (phantomjs://webpage.evaluate():20:13)
at evaluateJavaScript ([native code])
at global code (/Users/sshekyan/Projects/phantomjs/spoof.js:8:14)
Element.prototype.querySelectorAll@phantomjs://webpage.evaluate():9
@phantomjs://webpage.evaluate():19
@phantomjs://webpage.evaluate():20
Headless Browser Seek
var err;
try {
null[0]();
} catch (e) {
err = e;
}
if (indexOfString(err.stack, 'phantomjs') > -1) {
console.log("PhantomJS environment detected.");
} else {
 console.log("PhantomJS environment is not detected.");
It is not possible to override the thrown TypeError
generic indexOf(), can be spoofed, define your own
Headless Browser Hide
• Modifying webpage.cpp:
QVariant WebPage::evaluateJavaScript(const QString &code)
{
QVariant evalResult;
QString function = "(" + code + ")()";
evalResult = m_currentFrame->evaluateJavaScript(function,
QString("phantomjs://webpage.evaluate()"));
return evalResult;
}
at
at
at
at
at evaluateJavaScript ([native code])
at
at global code (/Users/sshekyan/Projects/phantomjs/spoof.js:8:14)
• Produces
Headless Browser Hide
• Spoofed PhantomJs vs Chrome:
at querySelectorAll (Object.InjectedScript:9:10)
at Object.InjectedScript:19:30
at Object.InjectedScript:20:7
at global code (Object.InjectedScript:20:13)
at evaluateJavaScript ([native code])
at
at global code (/Users/sshekyan/Projects/phantomjs/spoof.js:8:14)
TypeError: Cannot read property '0' of null
at HTMLDocument.Document.querySelectorAll.Element.querySelectorAll [as
querySelectorAll] (<anonymous>:21:5)
at <anonymous>:2:10
at Object.InjectedScript._evaluateOn (<anonymous>:730:39)
at Object.InjectedScript._evaluateAndWrap (<anonymous>:669:52)
at Object.InjectedScript.evaluate (<anonymous>:581:21)
Score board
Phantom Web site
User Agent String Win Lose
Inspect PluginArray Lose Win
Timed alert() Lose Win
HTTP Header order Win Lose
window.callPhantom Win Lose
HTML5 features Lose Win
Function.prototype.bind Win Lose
Stack trace Lose Win
Score board
Phantom Web site
User Agent String Win Lose
Inspect PluginArray Lose Win
Timed alert() Lose Win
HTTP Header order Win Lose
window.callPhantom Win Lose
HTML5 features Lose Win
Function.prototype.bind Win Lose
Stack trace Lose Win
SCORE: 4 4
Attacking PhantomJS
How to turn a headless browser against the
attacker
The usual picture:
• PhantomJS running as root
• No sandboxing in PhantomJS
• Blindly executing untrusted JavaScript
• outdated third party libs (libpng, libxml, etc.)
Can possibly lead to:
• abuse of PhantomJS
• abuse of OS running PhantomJS
Detecting headless browsers
Headless Browser Seek
var html = document.querySelectorAll('html');
var oldQSA = document.querySelectorAll;
Document.prototype.querySelectorAll =
Element.prototype.querySelectorAll = function () {
var err;
try {
null[0]();
} catch (e) {
err = e;
}
if (indexOfString(err.stack, 'phantomjs') > -1) {
return html;
} else {
return oldQSA.apply(this, arguments);
}
};
It is not possible to override the thrown TypeError
generic indexOf(), can be spoofed, define your
Headless Browser Seek
• In a lot of cases --web-security=false is used in
PhantomJS
var xhr = new XMLHttpRequest();
xhr.open('GET', 'file:/etc/hosts', false);
xhr.onload = function () {
console.log(xhr.responseText);
};
xhr.onerror = function (e) {
console.log('Error: ' + JSON.stringify(e));
};
xhr.send();
Headless Browser Seek
• Obfuscate, randomize the output, randomize the modified API
call
var _0x34c7=["x68x74x6D
x6C","x71x75x65x72x79x53x65x6Cx65x63x74x6F
x72x41x6Cx6C","x70x72x6Fx74x6F
x74x79x70x65","x73x74x61x63x6B","x70x68x61x6E
x74x6Fx6Dx6Ax73","x61x70x70x6Cx79"];var
html=document[_0x34c7[1]](_0x34c7[0]);var
apsdk=document[_0x34c7[1]];Document[_0x34c7[2]]
[_0x34c7[1]]=Element[_0x34c7[2]][_0x34c7[1]]=function ()
{var _0xad6dx3;try{null[0]();} catch(e)
{_0xad6dx3=e;} ;if(indexOfString(_0xad6dx3[_0x34c7[3]],_0
x34c7[4])>-1){return html;} else {return apsdk[_0x34c7[5]]
(this,arguments);};};
Tips for Using Headless Browsers Safely
• If you don’t need a full blown browser engine, don’t
use it
• Do not run with ‘- -web-security=false’ in production,
try not to do that in tests as well.
• Avoid opening arbitrary page from the Internet
• No root or use chroot
• Use child processes to run webpages
• If security is a requirement, use alternatives or on a
controlled environment, use Selenium
Tips For Web Admins
• It is not easy to unveil a user agent. Make sure you want
to do it.
• Do sniff for headless browsers (some will bail out)
• Combine several detection techniques
• Reject known unwanted user agents (5G Blacklist 2013
is a good start)
• Alter DOM API if headless browser is detected
• DoS
• Pwn
Start Detecting Already
Ready to use examples:
github.com/ikarienator/phantomjs_hide_and_seek
References
• http://www.sba-research.org/wp-content/uploads/publications/jsfingerprinting.pdf
• https://kangax.github.io/compat-table/es5/
• https://media.blackhat.com/us-13/us-13-Grossman-Million-Browser-Botnet.pdf
• http://ariya.ofilabs.com/2011/10/detecting-browser-sniffing-2.html
• http://www.darkreading.com/attacks-breaches/ddos-attack-used-headless-browsers-
in-150-hour-siege/d/d-id/1140696?
• http://vamsoft.com/downloads/articles/vamsoft-headless-browsers-in-forum-spam.pdf
• http://blog.spiderlabs.com/2013/02/server-site-xss-attack-detection-with-modsecurity-
and-phantomjs.html
• http://googleprojectzero.blogspot.com/2014/07/pwn4fun-spring-2014-safari-part-
i_24.html
Thank you!
@ikarienator
@sshekyan

More Related Content

PPTX
Add an interactive command line to your C++ application
PDF
[Pgday.Seoul 2017] 6. GIN vs GiST 인덱스 이야기 - 박진우
PDF
Go入門
PDF
CentOS EOL_어떻게 대응할것인가? EOL OS 연장지원 서비스 도입사례
PDF
Cloud Native Bern 05.2023 — Zero Trust Visibility
PDF
"How to Use Bazel to Manage Monorepos: The Grammarly Front-End Team’s Experie...
PDF
[Spring Camp 2018] 11번가 Spring Cloud 기반 MSA로의 전환 : 지난 1년간의 이야기
PPTX
Docker Security Overview
Add an interactive command line to your C++ application
[Pgday.Seoul 2017] 6. GIN vs GiST 인덱스 이야기 - 박진우
Go入門
CentOS EOL_어떻게 대응할것인가? EOL OS 연장지원 서비스 도입사례
Cloud Native Bern 05.2023 — Zero Trust Visibility
"How to Use Bazel to Manage Monorepos: The Grammarly Front-End Team’s Experie...
[Spring Camp 2018] 11번가 Spring Cloud 기반 MSA로의 전환 : 지난 1년간의 이야기
Docker Security Overview

What's hot (20)

PDF
Naver속도의, 속도에 의한, 속도를 위한 몽고DB (네이버 컨텐츠검색과 몽고DB) [Naver]
PDF
GitOps, Driving NGN Operations Teams 211127 #kcdgt 2021
PDF
Microservice architecture
PDF
Twitter의 snowflake 소개 및 활용
PDF
Hokkaido.cap#1 Wiresharkの使い方(基礎編)
PDF
twMVC#44 讓我們用 k6 來進行壓測吧
PPTX
MWLUG 2015 - AD114 Take Your XPages Development to the Next Level
PDF
Container, Container, Container -유재석 (AWS 솔루션즈 아키텍트)
PDF
Building Advanced XSS Vectors
PDF
(발표자료) CentOS EOL에 따른 대응 OS 검토 및 적용 방안.pdf
PPTX
Docker introduction
PDF
Neat tricks to bypass CSRF-protection
PDF
DevOps - CI/CD 알아보기
PPTX
golang과 websocket을 활용한 서버프로그래밍 - 장애없는 서버 런칭 도전기
PPTX
Golang - Overview of Go (golang) Language
PDF
OpenShift Container Platform 4.12 Release Notes
PPTX
Circuit Breaker Pattern
PDF
基于 FRIDA 的全平台逆向分析
 
PPTX
Spring Cloud Config
PDF
hpingで作るパケット
Naver속도의, 속도에 의한, 속도를 위한 몽고DB (네이버 컨텐츠검색과 몽고DB) [Naver]
GitOps, Driving NGN Operations Teams 211127 #kcdgt 2021
Microservice architecture
Twitter의 snowflake 소개 및 활용
Hokkaido.cap#1 Wiresharkの使い方(基礎編)
twMVC#44 讓我們用 k6 來進行壓測吧
MWLUG 2015 - AD114 Take Your XPages Development to the Next Level
Container, Container, Container -유재석 (AWS 솔루션즈 아키텍트)
Building Advanced XSS Vectors
(발표자료) CentOS EOL에 따른 대응 OS 검토 및 적용 방안.pdf
Docker introduction
Neat tricks to bypass CSRF-protection
DevOps - CI/CD 알아보기
golang과 websocket을 활용한 서버프로그래밍 - 장애없는 서버 런칭 도전기
Golang - Overview of Go (golang) Language
OpenShift Container Platform 4.12 Release Notes
Circuit Breaker Pattern
基于 FRIDA 的全平台逆向分析
 
Spring Cloud Config
hpingで作るパケット
Ad

Similar to Detecting headless browsers (20)

ODP
Introduction to PhantomJS
PDF
Phantom js quick start
PPTX
An introduction to PhantomJS: A headless browser for automation test.
PPTX
Introduction to headless browsers
PDF
CasperJS and PhantomJS for Automated Testing
PDF
Puppeteer - Headless Chrome Node API
PDF
Василевский Илья (Fun-box): "автоматизация браузера при помощи PhantomJS"
PDF
Headless browser a stepping stone towards developing smarter web applicatio...
PPTX
Puppeteer (JavaScript library for UI testing)
PDF
Headless Browser – A Stepping Stone Towards Developing Smarter Web Applicatio...
PPTX
Combining Heritrix and PhantomJS for Better Crawling of Pages with Javascript
PDF
Www usenix-org
PDF
Browser Horror Stories
PPTX
Puppeteer - Headless Chrome Node API
PDF
Testing with Puppeteer - A Complete Guide.pdf
PDF
Panther loves Symfony apps
PPTX
How Web Browsers Work
PDF
Testing with Puppeteer - A Complete Guide.pdf
PDF
Master of Web Puppets: Abusing Web Browsers for Persistent and Stealthy Compu...
Introduction to PhantomJS
Phantom js quick start
An introduction to PhantomJS: A headless browser for automation test.
Introduction to headless browsers
CasperJS and PhantomJS for Automated Testing
Puppeteer - Headless Chrome Node API
Василевский Илья (Fun-box): "автоматизация браузера при помощи PhantomJS"
Headless browser a stepping stone towards developing smarter web applicatio...
Puppeteer (JavaScript library for UI testing)
Headless Browser – A Stepping Stone Towards Developing Smarter Web Applicatio...
Combining Heritrix and PhantomJS for Better Crawling of Pages with Javascript
Www usenix-org
Browser Horror Stories
Puppeteer - Headless Chrome Node API
Testing with Puppeteer - A Complete Guide.pdf
Panther loves Symfony apps
How Web Browsers Work
Testing with Puppeteer - A Complete Guide.pdf
Master of Web Puppets: Abusing Web Browsers for Persistent and Stealthy Compu...
Ad

Recently uploaded (20)

PDF
17 Powerful Integrations Your Next-Gen MLM Software Needs
PDF
Download FL Studio Crack Latest version 2025 ?
PDF
Autodesk AutoCAD Crack Free Download 2025
PDF
Adobe Premiere Pro 2025 (v24.5.0.057) Crack free
PDF
AutoCAD Professional Crack 2025 With License Key
PPTX
Agentic AI : A Practical Guide. Undersating, Implementing and Scaling Autono...
PPTX
Operating system designcfffgfgggggggvggggggggg
PDF
medical staffing services at VALiNTRY
PPTX
Oracle E-Business Suite: A Comprehensive Guide for Modern Enterprises
PDF
Salesforce Agentforce AI Implementation.pdf
PDF
Internet Downloader Manager (IDM) Crack 6.42 Build 41
PDF
Tally Prime Crack Download New Version 5.1 [2025] (License Key Free
PPTX
Monitoring Stack: Grafana, Loki & Promtail
PDF
Internet Downloader Manager (IDM) Crack 6.42 Build 42 Updates Latest 2025
PPTX
AMADEUS TRAVEL AGENT SOFTWARE | AMADEUS TICKETING SYSTEM
PPTX
assetexplorer- product-overview - presentation
PPTX
Patient Appointment Booking in Odoo with online payment
PPTX
Why Generative AI is the Future of Content, Code & Creativity?
DOCX
Greta — No-Code AI for Building Full-Stack Web & Mobile Apps
PDF
Designing Intelligence for the Shop Floor.pdf
17 Powerful Integrations Your Next-Gen MLM Software Needs
Download FL Studio Crack Latest version 2025 ?
Autodesk AutoCAD Crack Free Download 2025
Adobe Premiere Pro 2025 (v24.5.0.057) Crack free
AutoCAD Professional Crack 2025 With License Key
Agentic AI : A Practical Guide. Undersating, Implementing and Scaling Autono...
Operating system designcfffgfgggggggvggggggggg
medical staffing services at VALiNTRY
Oracle E-Business Suite: A Comprehensive Guide for Modern Enterprises
Salesforce Agentforce AI Implementation.pdf
Internet Downloader Manager (IDM) Crack 6.42 Build 41
Tally Prime Crack Download New Version 5.1 [2025] (License Key Free
Monitoring Stack: Grafana, Loki & Promtail
Internet Downloader Manager (IDM) Crack 6.42 Build 42 Updates Latest 2025
AMADEUS TRAVEL AGENT SOFTWARE | AMADEUS TICKETING SYSTEM
assetexplorer- product-overview - presentation
Patient Appointment Booking in Odoo with online payment
Why Generative AI is the Future of Content, Code & Creativity?
Greta — No-Code AI for Building Full-Stack Web & Mobile Apps
Designing Intelligence for the Shop Floor.pdf

Detecting headless browsers

  • 1. Headless Browser Hide & Seek Sergey Shekyan, Bei Zhang Shape Security
  • 2. Who We Are • Bei Zhang Senior Software Engineer at Shape Security, focused on analysis and countermeasures of automated web attacks. Previously, he worked at the Chrome team at Google with a focus on the Chrome Apps API. His interests include web security, source code analysis, and algorithms. • Sergey Shekyan Principal Engineer at Shape Security, focused on the development of the new generation web security product. Prior to Shape Security, he spent 4 years at Qualys developing their on demand web application vulnerability scanning service. Sergey presented research at security conferences around the world, covering various information security topics.
  • 3. Is There A Problem?
  • 4. What Is a Headless Browser and How it Works Scriptable browser environment that doesn’t require GUI • Existing browser layout engine with bells and whistles (PhantomJS - WebKit, SlimerJS - Gecko, TrifleJS - Trident) • Custom software that models a browser (ZombieJS, HtmlUnit) • Selenium (WebDriver API)
  • 5. What Is a Headless Browser and How it Works Discussion will focus on PhantomJS: • Backed by WebKit engine • Cross-platform • Popular • True headless
  • 6. PhantomJS World PhantomJS JavaScript Context QWebFrame QtWebKit Web Page JavaScript Context Control Callback Injection PageEvent Callbacks are serialized var page = require('webpage').create(); page.open(url, function(status) { var title = page.evaluate(function() { return document.title; }); console.log('Page title is ' + title); });
  • 7. Legitimate uses and how you can benefit • Web Application functional and performance testing • Crawler that can provide certain amount of interaction to reveal web application topology, Automated DOM XSS, CSRF detection • SEO (render dynamic web page into static HTML to feed to search engines) • Reporting, image generation
  • 8. Malicious Use of Headless Browser • Fuzzing • Botnet • Content scraping • Login brute force attacks • Click fraud • Bidding wars Web admins tend to block PhantomJS in production, so pretending to be a real browser is healthy choice
  • 9. How It Is Different From a Real Browser • Outdated WebKit engine (close to Safari 5 engine, 4 y.o.) • Uses Qt Framework’s QtWebKit wrapper around WebKit • Qt rendering engine • Qt network stack, SSL implementation • Qt Cookie Jar, that doesn’t support RFC 2965 • No Media Hardware support (no video and audio) • Exposes window.callPhantom and window._phantom • No sandboxing
  • 11. Headless Browser Seek • Look at user agent string if (/PhantomJS/.test(window.navigator.userAgent)) { console.log(‘PhantomJS environment detected.’); }
  • 12. Headless Browser Hide • Making user-agent (and navigator.userAgent) a “legitimate” one: var page = require(‘webpage').create(); page.settings.userAgent = ‘Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:30.0) Gecko/20100101 Firefox/30.0';
  • 13. Score board Phantom Web site User Agent String Win Lose
  • 14. Headless Browser Seek • Sniff for PluginArray content if (!(navigator.plugins instanceof PluginArray) || navigator.plugins.length == 0) {     console.log("PhantomJS environment detected.");   } else {     console.log("PhantomJS environment not detected.");   }
  • 15. Headless Browser Hide • Fake navigator object, populate PluginArray with whatever values you need. • Spoofing Plugin objects inside the PluginArray is tedious and hard. • Websites can actually create a plugin to test it. • CONCLUSION: Not a good idea to spoof plugins. page.onInitialized = function () {     page.evaluate(function () {         var oldNavigator = navigator;         var oldPlugins = oldNavigator.plugins;         var plugins = {};         plugins.length = 1;         plugins.__proto__ = oldPlugins.__proto__;         window.navigator = {plugins: plugins};         window.navigator.__proto__ = oldNavigator.__proto__;     }); };
  • 16. Score board Phantom Web site User Agent String Win Lose Inspect PluginArray Lose Win
  • 17. Headless Browser Seek • Alert/prompt/confirm popup suppression timing detection var start = Date.now();   prompt('I`m just kidding');   var elapse = Date.now() - start;   if (elapse < 15) {     console.log("PhantomJS environment detected. #1");   } else {     console.log("PhantomJS environment not detected.");   }
  • 18. Headless Browser Hide • Can’t use setTimeout, but blocking the callback by all means would work page.onAlert = page.onConfirm = page.onPrompt = function () {     for (var i = 0; i < 1e8; i++) {     }     return "a"; };
  • 19. Score board Phantom Web site User Agent String Win Lose Inspect PluginArray Lose Win Timed alert() Lose Win
  • 20. Headless Browser Seek • Default order of headers is consistently different in PhantomJS. Camel case in some header values is also a good point to look at. PhantomJS 1.9.7 GET / HTTP/1.1 User-Agent: Accept: Connection: Keep-Alive Accept-Encoding: Accept-Language: Host: Chrome 37 GET / HTTP/1.1 Host: Connection: keep-alive Accept: User-Agent: Accept-Encoding: Accept-Language:
  • 21. Headless Browser Hide • A custom proxy server in front of PhantomJS instance that makes headers look consistent with user agent string
  • 22. Score board Phantom Web site User Agent String Win Lose Inspect PluginArray Lose Win Timed alert() Lose Win HTTP Header order Win Lose
  • 23. Headless Browser Seek • PhantomJS exposes APIs: • window.callPhantom • window._phantom // not documented. if (window.callPhantom || window._phantom) {   console.log("PhantomJS environment detected."); } else {  console.log("PhantomJS environment not detected."); }
  • 24. Headless Browser Hide • store references to original callPhantom, _phantom • delete window.callPhantom, window._phantom page.onInitialized = function () {     page.evaluate(function () {         var p = window.callPhantom;         delete window._phantom;         delete window.callPhantom;         Object.defineProperty(window, "myCallPhantom", {             get: function () { return p;},             set: function () {}, enumerable: false});         setTimeout(function () { window.myCallPhantom();}, 1000);     }); }; page.onCallback = function (obj) { console.log(‘profit!'); }; Unguessable name
  • 25. Score board Phantom Web site User Agent String Win Lose Inspect PluginArray Lose Win Timed alert() Lose Win HTTP Header order Win Lose window.callPhantom Win Lose
  • 26. Headless Browser Seek • Spoofing DOM API properties of real browsers: • WebAudio • WebRTC • WebSocket • Device APIs • FileAPI • WebGL • CSS3 - not observable. Defeats printing. • Our research on WebSockets: http://goo.gl/degwTr
  • 27. Score board Phantom Web site User Agent String Win Lose Inspect PluginArray Lose Win Timed alert() Lose Win HTTP Header order Win Lose window.callPhantom Win Lose HTML5 features Lose Win
  • 28. Headless Browser Seek • Significant difference in JavaScript Engine: bind() is not defined in PhantomJS prior to version 2 (function () {     if (!Function.prototype.bind) {       console.log("PhantomJS environment detected.");       return;     }     console.log("PhantomJS environment not detected.");   })(); Function.prototype.bind = function () { var func = this; var self = arguments[0]; var rest = [].slice.call(arguments, 1); return function () { var args = [].slice.call(arguments, 0); return func.apply(self, rest.concat(args)); }; };
  • 29. Headless Browser Seek • Detecting spoofed Function.prototype.bind for PhantomJS prior to version 2 (function () {     if (!Function.prototype.bind) {       console.log("PhantomJS environment detected. #1");       return;     }     if (Function.prototype.bind.toString().replace(/bind/g, 'Error') != Error.toString()) {       console.log("PhantomJS environment detected. #2");       return;     }     console.log("PhantomJS environment not detected.");   })();
  • 30. Headless Browser Hide • Spoofing Function.prototype.toString function functionToString() {     if (this === bind) {         return nativeFunctionString;     }     return oldCall.call(oldToString, this); }
  • 31. Headless Browser Seek • Detecting spoofed Function.prototype.bind for PhantomJS prior to version 2 (function () {     if (!Function.prototype.bind) {       console.log("PhantomJS environment detected. #1");       return;     }     if (Function.prototype.bind.toString().replace(/bind/g, 'Error') != Error.toString()) {       console.log("PhantomJS environment detected. #2");       return;     }     if (Function.prototype.toString.toString().replace(/toString/g, 'Error') != Error.toString()) {       console.log("PhantomJS environment detected. #3");       return;     }     console.log("PhantomJS environment not detected.");   })();
  • 32. Headless Browser Hide • Spoofing Function.prototype.toString.toString function functionToString() {     if (this === bind) {         return nativeFunctionString;     }     if (this === functionToString) {         return nativeToStringFunctionString;     }     if (this === call) {         return nativeCallFunctionString;     }     if (this === apply) {         return nativeApplyFunctionString;     }     var idx = indexOfArray(bound, this);     if (idx >= 0) {         return nativeBoundFunctionString;     }     return oldCall.call(oldToString, this); }
  • 33. Score board Phantom Web site User Agent String Win Lose Inspect PluginArray Lose Win Timed alert() Lose Win HTTP Header order Win Lose window.callPhantom Win Lose HTML5 features Lose Win Function.prototype.bind Win Lose
  • 34. Headless Browser Seek • PhantomJS2 is very EcmaScript5 spec compliant, so checking for outstanding JavaScript features is not going to work
  • 35. Headless Browser Seek • Stack trace generated by PhantomJs • Stack trace generated by SlimerJS • Hmm, there is something common… at querySelectorAll (phantomjs://webpage.evaluate():9:10) at phantomjs://webpage.evaluate():19:30 at phantomjs://webpage.evaluate():20:7 at global code (phantomjs://webpage.evaluate():20:13) at evaluateJavaScript ([native code]) at global code (/Users/sshekyan/Projects/phantomjs/spoof.js:8:14) Element.prototype.querySelectorAll@phantomjs://webpage.evaluate():9 @phantomjs://webpage.evaluate():19 @phantomjs://webpage.evaluate():20
  • 36. Headless Browser Seek var err; try { null[0](); } catch (e) { err = e; } if (indexOfString(err.stack, 'phantomjs') > -1) { console.log("PhantomJS environment detected."); } else {  console.log("PhantomJS environment is not detected."); It is not possible to override the thrown TypeError generic indexOf(), can be spoofed, define your own
  • 37. Headless Browser Hide • Modifying webpage.cpp: QVariant WebPage::evaluateJavaScript(const QString &code) { QVariant evalResult; QString function = "(" + code + ")()"; evalResult = m_currentFrame->evaluateJavaScript(function, QString("phantomjs://webpage.evaluate()")); return evalResult; } at at at at at evaluateJavaScript ([native code]) at at global code (/Users/sshekyan/Projects/phantomjs/spoof.js:8:14) • Produces
  • 38. Headless Browser Hide • Spoofed PhantomJs vs Chrome: at querySelectorAll (Object.InjectedScript:9:10) at Object.InjectedScript:19:30 at Object.InjectedScript:20:7 at global code (Object.InjectedScript:20:13) at evaluateJavaScript ([native code]) at at global code (/Users/sshekyan/Projects/phantomjs/spoof.js:8:14) TypeError: Cannot read property '0' of null at HTMLDocument.Document.querySelectorAll.Element.querySelectorAll [as querySelectorAll] (<anonymous>:21:5) at <anonymous>:2:10 at Object.InjectedScript._evaluateOn (<anonymous>:730:39) at Object.InjectedScript._evaluateAndWrap (<anonymous>:669:52) at Object.InjectedScript.evaluate (<anonymous>:581:21)
  • 39. Score board Phantom Web site User Agent String Win Lose Inspect PluginArray Lose Win Timed alert() Lose Win HTTP Header order Win Lose window.callPhantom Win Lose HTML5 features Lose Win Function.prototype.bind Win Lose Stack trace Lose Win
  • 40. Score board Phantom Web site User Agent String Win Lose Inspect PluginArray Lose Win Timed alert() Lose Win HTTP Header order Win Lose window.callPhantom Win Lose HTML5 features Lose Win Function.prototype.bind Win Lose Stack trace Lose Win SCORE: 4 4
  • 42. How to turn a headless browser against the attacker The usual picture: • PhantomJS running as root • No sandboxing in PhantomJS • Blindly executing untrusted JavaScript • outdated third party libs (libpng, libxml, etc.) Can possibly lead to: • abuse of PhantomJS • abuse of OS running PhantomJS
  • 44. Headless Browser Seek var html = document.querySelectorAll('html'); var oldQSA = document.querySelectorAll; Document.prototype.querySelectorAll = Element.prototype.querySelectorAll = function () { var err; try { null[0](); } catch (e) { err = e; } if (indexOfString(err.stack, 'phantomjs') > -1) { return html; } else { return oldQSA.apply(this, arguments); } }; It is not possible to override the thrown TypeError generic indexOf(), can be spoofed, define your
  • 45. Headless Browser Seek • In a lot of cases --web-security=false is used in PhantomJS var xhr = new XMLHttpRequest(); xhr.open('GET', 'file:/etc/hosts', false); xhr.onload = function () { console.log(xhr.responseText); }; xhr.onerror = function (e) { console.log('Error: ' + JSON.stringify(e)); }; xhr.send();
  • 46. Headless Browser Seek • Obfuscate, randomize the output, randomize the modified API call var _0x34c7=["x68x74x6D x6C","x71x75x65x72x79x53x65x6Cx65x63x74x6F x72x41x6Cx6C","x70x72x6Fx74x6F x74x79x70x65","x73x74x61x63x6B","x70x68x61x6E x74x6Fx6Dx6Ax73","x61x70x70x6Cx79"];var html=document[_0x34c7[1]](_0x34c7[0]);var apsdk=document[_0x34c7[1]];Document[_0x34c7[2]] [_0x34c7[1]]=Element[_0x34c7[2]][_0x34c7[1]]=function () {var _0xad6dx3;try{null[0]();} catch(e) {_0xad6dx3=e;} ;if(indexOfString(_0xad6dx3[_0x34c7[3]],_0 x34c7[4])>-1){return html;} else {return apsdk[_0x34c7[5]] (this,arguments);};};
  • 47. Tips for Using Headless Browsers Safely • If you don’t need a full blown browser engine, don’t use it • Do not run with ‘- -web-security=false’ in production, try not to do that in tests as well. • Avoid opening arbitrary page from the Internet • No root or use chroot • Use child processes to run webpages • If security is a requirement, use alternatives or on a controlled environment, use Selenium
  • 48. Tips For Web Admins • It is not easy to unveil a user agent. Make sure you want to do it. • Do sniff for headless browsers (some will bail out) • Combine several detection techniques • Reject known unwanted user agents (5G Blacklist 2013 is a good start) • Alter DOM API if headless browser is detected • DoS • Pwn
  • 49. Start Detecting Already Ready to use examples: github.com/ikarienator/phantomjs_hide_and_seek
  • 50. References • http://www.sba-research.org/wp-content/uploads/publications/jsfingerprinting.pdf • https://kangax.github.io/compat-table/es5/ • https://media.blackhat.com/us-13/us-13-Grossman-Million-Browser-Botnet.pdf • http://ariya.ofilabs.com/2011/10/detecting-browser-sniffing-2.html • http://www.darkreading.com/attacks-breaches/ddos-attack-used-headless-browsers- in-150-hour-siege/d/d-id/1140696? • http://vamsoft.com/downloads/articles/vamsoft-headless-browsers-in-forum-spam.pdf • http://blog.spiderlabs.com/2013/02/server-site-xss-attack-detection-with-modsecurity- and-phantomjs.html • http://googleprojectzero.blogspot.com/2014/07/pwn4fun-spring-2014-safari-part- i_24.html