From 91e07c75c429a7aeef108e28328e2df5555def78 Mon Sep 17 00:00:00 2001 From: Ata Date: Sun, 5 Jul 2020 12:44:41 +0300 Subject: [PATCH 1/3] Fix repository link in package --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ad518de..3b613bc 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "puff-fuzz", - "version": "0.0.5", + "version": "0.0.6", "description": "Simple Clientside vulnerability/xss fuzzer", "main": "puff.js", - "repository":"https://github.com/FlameOfIgnis/puff/issues", + "repository":"https://github.com/FlameOfIgnis/puff", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, From 51ef04c21520924e4f5490d28a5454580f3c189b Mon Sep 17 00:00:00 2001 From: Ata Date: Sun, 5 Jul 2020 16:52:45 +0300 Subject: [PATCH 2/3] Major refactor --- SingleUrlFuzzer.js | 123 +++++++++++++++ callbacks.js | 78 ++++++++++ configwiz.js | 38 +++++ io.js | 54 +++++++ package-lock.json | 2 +- package.json | 4 +- pretty.js | 17 +++ puff.js | 367 ++++++--------------------------------------- terminator.js | 72 +++++++++ threading.js | 34 +++++ 10 files changed, 461 insertions(+), 328 deletions(-) create mode 100644 SingleUrlFuzzer.js create mode 100644 callbacks.js create mode 100644 configwiz.js create mode 100644 io.js create mode 100644 pretty.js create mode 100644 terminator.js create mode 100644 threading.js diff --git a/SingleUrlFuzzer.js b/SingleUrlFuzzer.js new file mode 100644 index 0000000..8bbcf6c --- /dev/null +++ b/SingleUrlFuzzer.js @@ -0,0 +1,123 @@ + +const {fail,succ,warn,info,gstart,bstart,ystart,rstart,colstop} = require('./pretty.js') +const fs = require('fs') +function replaceKeyword(url,pld){ + pld = pld.replace(/ /g, '%20') + var t=url; + t = t.replace(/FUZZ/g,pld.replace(/\n|\r/g,'')) + return t; + +} + + +class SingleUrlFuzzer{ + /* + * Fuzzer for when both wordlist and url parameter is supplied. + * Read url from the parameter, and read + * + */ + constructor(url,cbhandler, threadHandler, terminator, wordlist, verbose){ + this.url = url + this.wlistFpointer=0; + this.cbHandler=cbhandler + this.terminator = terminator + this.verbose = verbose + this.threadHandler = threadHandler; + this.wordlist = wordlist; + + //read wordlist + if(this.verbose) console.log(`${warn} Reading Wordlist`) + try{ + this.wlistContent = fs.readFileSync(wordlist).toString().split("\n") + }catch(e){ + console.log(`${fail} Wordlist file was not found`) + console.log(e) + process.exit(1) + } + if(this.verbose) console.log(`${succ} Wordlist loaded, ${this.wlistContent.length} lines.`) + + } + + async acquire(){ + /* + * Acquire next url from wordlist + */ + this.wlistFpointer+=1 + return this.wlistContent[this.wlistFpointer]; + } + + async checkFinished(){ + /* + * Check if wordlist finished + */ + + //if this thread is done + if(this.wlistFpointer>=this.wlistContent.length){ + this.terminator.terminatedCount+=1 + this.wlistFpointer+=1 + if(this.verbose){ + outputHandler.deleteLastLine() + console.log("Thread finished") + } + + //Only terminate program if all the threads have finished, so it doesn't lose the progress on those pending requests. + if(this.terminator.terminatedCount==threadHandler.workerCount){ + //TODO, timeout possible idle/stuck threads and terminate + if(this.verbose){ + outputHandler.deleteLastLine() + outputHandler.write('Last url checked, waiting for all threads to finish') + } + + this.terminator.terminate() + } + } + } + + async loadNextUrl(thread){ + /* + * Load next url from from the wordlist + */ + this.checkFinished() + var line = await this.acquire() + thread.url = await replaceKeyword(this.url, line) + thread.pld = line + this.processURL(thread,thread.url) + } + + + async processURL(thread, url){ + /* + * Process url, visit, try to trigger events etc. + */ + try{ + thread.goto(thread.url) + + //capture window response + const response = await thread.waitForNavigation(); + + //acquire possible redirect chain + var chain = (response.request().redirectChain()) + + //get http response + thread.status = response.status(); + + //if there was a redirect chain, output it. If not, its a normal response + if(chain.length){ + thread.wasHTTPRedirect = true; + this.cbHandler.catchRedirect(thread, chain) + }else{ + this.cbHandler.catchNormal(thread) + } + }catch(e){ + //Not properly implemented yet, dom-errors, http timeouts + this.cbHandler.catchLoadFailure(thread) + } + + //recurses + this.loadNextUrl(thread) + } +} + +module.exports = { + SingleUrlFuzzer:SingleUrlFuzzer +} \ No newline at end of file diff --git a/callbacks.js b/callbacks.js new file mode 100644 index 0000000..8a2ef83 --- /dev/null +++ b/callbacks.js @@ -0,0 +1,78 @@ + + +const {fail,succ,warn,info,gstart,bstart,ystart,rstart,colstop} = require('./pretty.js') + + +class TriggerHandler{ + /* + * This class contains and handles events triggered by the document. + * catchRedirect, catchRedirectJS, catchXSS, catchLoadFailure, catchNormal + * + */ + constructor(oHandler){ + this.outputHandler = oHandler + } + + catchRedirect(thread, chain){ + this.outputHandler.write(`${ystart}[${thread.status}] [REDIRECT-HTTP] ${thread.url}${thread.pld}`) + for(var i=0;i ${chain[i].response().url()}`) + } + this.outputHandler.write(colstop) + this.outputHandler.bLastOutputImportant=true + + } + + //not implemented yet, lost when refactoring from electron to puppeteer + catchRedirectJS(thread, target){ + return + if(thread.wasHTTPRedirect){ + thread.wasHTTPRedirect=false; + return + } + initCallback('redirect-js') + this.outputHandler.write(`${bstart}[200] [REDIRECT-JS] ${thread.url}${thread.pld}`) + this.outputHandler.write(` |--> ${target}`) + this.outputHandler.write(colstop) + + } + + + catchXSS(thread, href){ + this.outputHandler.write(`${rstart}[${thread.id}][${200}] [XSS] ${thread.url} ${colstop}`) + this.outputHandler.bLastOutputImportant=true + pendingOutput.push({ + url:thread.url, + payload:thread.pld + }) + + //xss windows tend to get load looped, but not sure if needed + //thread.evaluate(() => window.stop()); + } + + catchLoadFailure(thread){ + this.outputHandler.write(`${bstart}[${thread.status}] [FAILURE] ${thread.pld} ${colstop}`, 5000) + this.outputHandler.bLastOutputImportant=true + } + + catchNormal(thread){ + + if(thread.justRedirected){ + thread.justRedirected=false + return + } + this.outputHandler.write(`${gstart}[${thread.id}][${thread.status}] ${colstop} ${thread.url}`) + if(thread.status==200){ + this.outputHandler.bLastOutputImportant=false + }else{ + if(status)this.outputHandler.bLastOutputImportant=true + else this.outputHandler.bLastOutputImportant=false + } + return + } +} + + +module.exports ={ + TriggerHandler: TriggerHandler +} diff --git a/configwiz.js b/configwiz.js new file mode 100644 index 0000000..1c5e5d6 --- /dev/null +++ b/configwiz.js @@ -0,0 +1,38 @@ + +const path = require('path') +const fs = require('fs') +const glob = require('glob') +module.exports={ + + setChromePath: function(new_path){ + /* + * Set the chrome path in the config.json file + */ + var conf_temp = require(path.join(__dirname,'/config.json')) + console.log("Chrome path changing from '" + conf_temp.chromium_path + "' to '" + new_path + "'") + conf_temp.chromium_path = new_path + fs.writeFileSync(path.join(__dirname,'/config.json'), JSON.stringify(conf_temp), 'utf8'); + }, + + resolveChromiumPath: function(config){ + /* + * Resolve wildcards in the chromium path. + * Also resolves keyword 'default' + * For now, default maps to '/node_modules/puppeteer/.local-chromium/*\/*\/chrome(.exe?)' + * If default keyword is used, resolved path will be written to the config file after calling. + */ + var chromium_path = ''; + if(!config.chromium_path) config.chromium_path='default'; + else if(config.chromium_path.includes('*')) chromium_path = glob.sync(config.chromium_path, {})[0]; + else chromium_path = config.chromium_path; + + if(chromium_path=='default'){//resolve default path + if(process.platform=='win32') chromium_path = glob.sync(path.join(__dirname, "/node_modules/puppeteer/.local-chromium/*/*/chrome.exe"))[0] + else chromium_path = glob.sync(path.join(__dirname, "/node_modules/puppeteer/.local-chromium/*/*/chrome"))[0] + config.chromium_path=chromium_path + fs.writeFileSync(path.join(__dirname,'/config.json'), JSON.stringify(config), 'utf8'); //NEEDS FIX // CAUSING TROUBLE FOR OSX + } + return chromium_path; + } + +} diff --git a/io.js b/io.js new file mode 100644 index 0000000..4f266e4 --- /dev/null +++ b/io.js @@ -0,0 +1,54 @@ +//responsible for writing request output + + + + +class ResponseWriter{ + /* + * This class is responsible for outputting the http request responses to the terminal. + * + */ + constructor(demo,oa){ + this.bLastOutputImportant=true; + this.demo = demo; + this.oa=oa; + } + + write(message, clamp=process.stdout.columns-2){ + /* + * write the message, delete last line if it was marked not important. + * Clamp to the length of second parameter if passed. + */ + + //if message is longer than the clamp length, clamp it and append ... + if(message.length >=clamp) + message = (message.substring(0, clamp - 3) + "...") + + //if demo mode is activated, hide base url + if(this.demo){ + message = message.replace(/http(s)?:\/\/.*?\//, "https://[REDACTED]/") + } + + //output all mode, write every response, even normal ones + if(this.oa){ + process.stdout.write("\n" + message) + }else{ + //if last output wasn't registered as important, delete the last line + if(!this.bLastOutputImportant){ + this.deleteLastLine(true) + process.stdout.write(message) + }else{ + process.stdout.write("\n" + message) + } + } + } + + deleteLastLine(force=false){ + if(!force && this.bLastOutputImportant) return + process.stdout.write("\r\x1b[K") + } +} + +module.exports ={ + OutputHandler: ResponseWriter +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9fb8af2..20f3dbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "puff-fuzz", - "version": "0.0.4", + "version": "0.0.6", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3b613bc..d276da7 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "puff-fuzz", - "version": "0.0.6", + "version": "0.1.0", "description": "Simple Clientside vulnerability/xss fuzzer", "main": "puff.js", - "repository":"https://github.com/FlameOfIgnis/puff", + "repository": "https://github.com/FlameOfIgnis/puff", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/pretty.js b/pretty.js new file mode 100644 index 0000000..306473f --- /dev/null +++ b/pretty.js @@ -0,0 +1,17 @@ + + + +module.exports = { + /* + * Pretty output colors + */ + fail: "[\033[91m+\033[0m]", + succ: "[\033[92m+\033[0m]", + warn: "[\033[93m+\033[0m]", + info: "[\033[94m+\033[0m]", + gstart: "\033[92m", + bstart: "\033[94m", + ystart: "\033[93m", + rstart: "\033[91m", + colstop:"\033[0m" +} \ No newline at end of file diff --git a/puff.js b/puff.js index a3dae91..a1f59e8 100644 --- a/puff.js +++ b/puff.js @@ -1,21 +1,29 @@ #!/usr/bin/env node + +// dependecies const puppeteer = require('puppeteer'); const { program } = require('commander'); const fs = require('fs') const path = require('path') -var glob = require('glob') -//pretty colors -var fail="[\033[91m+\033[0m]" -var succ="[\033[92m+\033[0m]" -var warn="[\033[93m+\033[0m]" -var info="[\033[94m+\033[0m]" -var gstart = "\033[92m" -var bstart = "\033[94m" -var ystart = "\033[93m" -var rstart = "\033[91m" -var colstop = "\033[0m" + +//config wizard and io handlers +const {setChromePath,resolveChromiumPath} = require('./configwiz.js') +const {OutputHandler} = require('./io.js') +outputHandler = new OutputHandler(program.demo, program.outputAll) + +// page callbacks and pretty output headers +const {TriggerHandler} = require('./callbacks.js') +cbHandler = new TriggerHandler(outputHandler) +const {fail,succ,warn,info,gstart,bstart,ystart,rstart,colstop} = require('./pretty.js') + +// graceful termination +const {Terminator} = require('./terminator.js') +var terminator = new Terminator(process, program); + +// threading +const {ThreadHandler} = require('./threading.js') ///////////////////////////////////////// //SET CLI PARAMETERS @@ -32,347 +40,56 @@ program .option('-c, --chromePath ', 'Set chromium path permenantly') program.parse(process.argv); -var pendingOutput=[] - -//graceful termination, output is not lost on sigint/sigterm -function gracefulTermination(bTerminate=true){ - console.log('Exiting....') - if(program.output){ - if(!pendingOutput.length){ - console.log('No vulnerabilities found, not creating an output file.') - if(bTerminate){ - process.exit(1) - }else{ - return - } - } - - //there were results and output pending, write it in json format - var result = {} - baseUrl = program.url.replace(/^http(s)?:\/\//, '') - baseUrl = baseUrl.replace(/\/.*$/, '') - - result.host = baseUrl - result.fuzzTarget = program.url - result.host_ip = remoteAddr - result.port = remotePort - result.source = 'puff-fuzzer' - result.found = [] - for(var i=0;i=clamp) - message = (message.substring(0, clamp - 3) + "...") - - //if demo mode is activated, hide base url - if(demo){ - message = message.replace(/http(s)?:\/\/.*?\//, "https://[REDACTED]/") - } - - //output all mode, write every response, even normal ones - if(oa){ - process.stdout.write("\n" + message) - }else{ - //if last output wasn't registered as important, delete the last line - if(!bLastOutputImportant){ - process.stdout.write("\r\x1b[K") - process.stdout.write(message) - }else{ - process.stdout.write("\n" + message) - } - } - - -} - - //parse cli params to variables var verbose = program.verbose || false -var url=program.url var wordlist=program.wordlist var workerCount=program.threads -var demo = program.demo || false -var status = program.status || false -var oa = program.outputAll || false var browser = false -var outputFile = program.output|| false var sslIgnore = program.ignoreSSL|| false - var threads = [] -var wlistContent = false -var wlistFpointer=0 -var preloadFile; -var bLastOutputImportant=true -var remoteAddr = false -var remotePort = false - -if(program.chromePath){ - var conf_temp = require(path.join(__dirname,'/config.json')) - console.log("Chrome path changing from '" + conf_temp.chromium_path + "' to '" + program.chromePath + "'") - conf_temp.chromium_path = program.chromePath - fs.writeFileSync(path.join(__dirname,'/config.json'), JSON.stringify(conf_temp), 'utf8'); -} - -if(!(program.wordlist || program.url)){ - console.log('Wordlist and url are required parameters.') - process.exit() -} - - var config = require(path.join(__dirname,'/config.json')) -//create new thread, in this context, create new chromium tab -var threadIDCounter = 0 -async function makeNewThread(browser, callback){ - const page = await browser.newPage(); - page.id=threadIDCounter++; - await page.evaluateOnNewDocument(preloadFile); - await page.exposeFunction('xssCallback', (href)=>{ - catchXSS(page, href) - loadNextUrl(thread) - }) - - await page.exposeFunction('jsRedirectCallback', (href)=>{ - //page.justRedirected=true - catchRedirectJS(page, href) - }) - if(verbose) console.log("Created thread") - callback(page) - return page -} - -//load a url in thread -async function loadURL(thread, url){ - try{ - thread.goto(thread.url) - - //capture window response - const response = await thread.waitForNavigation(); - remoteAddr = response._remoteAddress.ip - remotePort = response._remoteAddress.port - - //acquire possible redirect chain - chain = (response.request().redirectChain()) - - //get http response - thread.status = response.status(); - - //if there was a redirect chain, output it. If not, its a normal response - if(chain.length){ - thread.wasHTTPRedirect = true; - catchRedirect(thread, chain) - }else{ - catchNormal(thread) - } - }catch(e){ - //Not properly implemented yet, dom-errors, http timeouts - catchLoadFailure(thread) - } - - //recurses - loadNextUrl(thread) -} - -//Called when a pool requests next url from wlist, but wlist has finished -//Attempt terminate only once, call gracefulTermination to handle file input first -terminated = false -function terminateProgram(){ - gracefulTermination(false); - if(!terminated){ - terminated=true - //deletes if last output was normal - writeRequestResponse("") - browser.close() - } -} - -//Count how many threads were terminated -var terminatedCount = 0 - -//prepare thread for loading url -async function loadNextUrl(thread){ - thread.url = "" - - //if this thread is done - if(wlistFpointer>=wlistContent.length){ - terminatedCount+=1 - wlistFpointer+=1 - - //Only terminate program if all the threads have finished, so it doesn't lose the progress on those pending requests. - if(terminatedCount==workerCount){ - //TODO, timeout possible idle/stuck threads and terminate - terminateProgram() - } - - //Return because no more paylaods left in the wordlist - return - }else{ - - //acquire next line from wordlist for pool - var line = wlistContent[wlistFpointer] - wlistFpointer+=1 - - //not sure if needed, whitespace in url seems to be a global 400 - line = line.replace(/ /g, '%20') - thread.pld = line - var t=url - t = t.replace(/FUZZ/g,line.replace(/\n|\r/g,'')) - thread.url = t - - loadURL(thread,thread.url) - } -} - - -//Deprecated and im too lazy to remove it from the code -function initCallback(page){ - return page.url - //this had something else, im too lazy to change it -} - -//Catch a redirect and output it -function catchRedirect(thread, chain){ - thread.url = initCallback(thread) - writeRequestResponse(`${ystart}[${thread.status}] [REDIRECT-HTTP] ${thread.url}${thread.pld}`) - for(var i=0;i ${chain[i].response().url()}`) - } - writeRequestResponse(colstop) - bLastOutputImportant=true - -} - -//catch a js based redirect, quite not ready (race condition with normal redirect, no good solution with 100% precision yet) -function catchRedirectJS(thread, target){ - return - if(thread.wasHTTPRedirect){ - thread.wasHTTPRedirect=false; - return - } - initCallback('redirect-js') - writeRequestResponse(`${bstart}[200] [REDIRECT-JS] ${thread.url}${thread.pld}`) - writeRequestResponse(` |--> ${target}`) - writeRequestResponse(colstop) - -} - -//catch when an xss occurs -function catchXSS(thread, href){ - thread.url = initCallback(thread) - writeRequestResponse(`${rstart}[${thread.id}][${200}] [XSS] ${thread.url} ${colstop}`) - bLastOutputImportant=true - pendingOutput.push({ - url:thread.url, - payload:thread.pld - }) - - //xss windows tend to get load looped, but not sure if needed - //thread.evaluate(() => window.stop()); +//if -c is passed, set new chrome path +if(program.chromePath){ + setChromePath(program.chromePath) } -//reserved for dom errors TODO -function catchLoadFailure(thread){ - thread.url = initCallback(thread) - writeRequestResponse(`${bstart}[${thread.status}] [FAILURE] ${thread.pld} ${colstop}`, 5000) - bLastOutputImportant=true -} +//resolve ch +var chromium_path = resolveChromiumPath(config); -//just a normal response -function catchNormal(thread){ - - thread.url = initCallback(thread) - if(thread.justRedirected){ - thread.justRedirected=false - return - } - writeRequestResponse(`${gstart}[${thread.id}][${thread.status}] ${colstop} ${thread.url}`) - if(thread.status==200){ - bLastOutputImportant=false - }else{ - if(status)bLastOutputImportant=true - else bLastOutputImportant=false - } - return +//check if required parameters were given +if(!(program.wordlist || program.url)){ + console.log('Wordlist and url are required parameters.') + process.exit() } -var chromium_path; -console.log(config) -//resolve chromium path -if(!config.chromium_path) config.chromium_path='default'; -else if(config.chromium_path.includes('*')) chromium_path = glob.sync(config.chromium_path, {})[0]; -else chromium_path = config.chromium_path; - -console.log(config) -console.log(JSON.stringify(config)) -if(chromium_path=='default'){//resolve default path - console.log(config) - console.log(JSON.stringify(config)) - if(process.platform=='win32') chromium_path = glob.sync(path.join(__dirname, "/node_modules/puppeteer/.local-chromium/*/*/chrome.exe"))[0] - else chromium_path = glob.sync(path.join(__dirname, "/node_modules/puppeteer/.local-chromium/*/*/chrome"))[0] - config.chromium_path=chromium_path - console.log(config) - console.log(JSON.stringify(config)) - fs.writeFileSync(path.join(__dirname,'/config.json'), JSON.stringify(config), 'utf8'); //NEEDS FIX // CAUSING TROUBLE FOR OSX - -} //init tool (async () => { - //if its demo mode, clear commandline, and remove the actual command (so it hides the url in cli) - if(demo){ - process.stdout.clearLine() - process.stdout.cursorTo(0,0) - process.stdout.write(' '.repeat(128)) - process.stdout.cursorTo(0,0) + try{ + browser = await puppeteer.launch({executablePath:chromium_path,args: ['--no-sandbox', '--disable-setuid-sandbox'], ignoreHTTPSErrors: sslIgnore}); + }catch(e){ + console.log(`${fail} Failed to launch chromium browser. `) + console.log(e) + process.exit(1) } + terminator.browser = browser; + threadHandler = new ThreadHandler(browser) - browser = await puppeteer.launch({executablePath:chromium_path,args: ['--no-sandbox', '--disable-setuid-sandbox'], ignoreHTTPSErrors: sslIgnore}); + + const {SingleUrlFuzzer} = require('./SingleUrlFuzzer.js') + var suFuzzer = new SingleUrlFuzzer(program.url, cbHandler, threadHandler, terminator, wordlist, verbose); - //preload our junk to browser - preloadFile = await fs.readFileSync(__dirname + '/preload.js', 'utf8'); - //read wordlist - if(verbose) console.log(`${warn} Reading Wordlist`) - wlistContent = await fs.readFileSync(wordlist).toString().split("\n") - if(verbose) console.log(`${succ} Wordlist loaded, ${wlistContent.length} lines.`) - //initialize threads for(var i=0;i{ + cbHandler.catchXSS(page, href) + callback(thread) + }) + + await page.exposeFunction('jsRedirectCallback', (href)=>{ + cbHandler.catchRedirectJS(page, href) + }) + + fuzzer.loadNextUrl(thread) + return page + } +} + + +module.exports = { + ThreadHandler: ThreadHandler +} \ No newline at end of file From 1a041166e2c60623d3e1b1f735660caf9c871225 Mon Sep 17 00:00:00 2001 From: Ata Date: Sun, 5 Jul 2020 17:05:30 +0300 Subject: [PATCH 3/3] Minor order of operations adjustment --- puff.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/puff.js b/puff.js index a1f59e8..aa8f407 100644 --- a/puff.js +++ b/puff.js @@ -50,13 +50,13 @@ var threads = [] -var config = require(path.join(__dirname,'/config.json')) //if -c is passed, set new chrome path if(program.chromePath){ setChromePath(program.chromePath) } +var config = require(path.join(__dirname,'/config.json')) //resolve ch var chromium_path = resolveChromiumPath(config);