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.
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';
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__;
});
};
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
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.");
})();
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
43.
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