commit 87154280a8f225803095aa501ce1a345d97d6829 Author: Hector Martin Date: Tue Oct 30 08:39:07 2018 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c843c94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.* +!.gitignore +log/* +!log/.keep +bundle/* +!bundle/.keep +*.pyc +tmp +*.old +config.py + diff --git a/README.md b/README.md new file mode 100644 index 0000000..9975f9d --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +## LetterBomb web service implementation + +This is the LetterBomb Wii System Menu 4.3 exploit implementation running on +https://please.hackmii.com/. Requires Python 2.7, Flask, and geoip2. + +In case you're wondering, `country_regions.txt` is based on reporting data +from Homebrew Channel updates. This was implemented because we found out +that about 30% of our users are stupid and won't pick the correct system +menu version (and then complain that it doesn't work), so we use GeoIP to +guess the right default for them. Similarly, the MAC address check was +implemented because people would type in garbage for the MAC address and +then complain that it doesn't work. + +This does not include the HackMii Installer bundle. Those files would go +in `bundle/`. + diff --git a/app.py b/app.py new file mode 100644 index 0000000..e3e7b6a --- /dev/null +++ b/app.py @@ -0,0 +1,164 @@ +import os, zipfile, StringIO, hashlib, hmac, struct, logging, urllib, random, json +import geoip2.database +from logging.handlers import SMTPHandler +from datetime import datetime, timedelta +from flask import Flask, request, g, render_template, make_response, redirect, url_for + +app = Flask(__name__) +app.config.from_object("config") + +TEMPLATES = { + 'U':"templateU.bin", + 'E':"templateE.bin", + 'J':"templateJ.bin", + 'K':"templateK.bin", +} + +BUNDLEBASE = os.path.join(app.root_path, 'bundle') +#BUNDLE = [(name, os.path.join(BUNDLEBASE,name)) for name in os.listdir(BUNDLEBASE)] + +#OUI_LIST = [i.decode('hex') for i in open(os.path.join(app.root_path, 'oui_list.txt')).read().split("\n") if len(i)==6] + +COUNTRY_REGIONS = dict([l.split(" ") for l in open(os.path.join(app.root_path, 'country_regions.txt')).read().split("\n") if l]) + +#gi = pygeoip.GeoIP(os.path.join(app.root_path, 'GeoIP.dat')) +gi = geoip2.database.Reader('/usr/share/GeoIP/GeoLite2-Country.mmdb') + +class RequestFormatter(logging.Formatter): + def format(self, record): + s = logging.Formatter.format(self, record) + try: + return '[%s] [%s] [%s %s] '%(self.formatTime(record), request.remote_addr, request.method, request.path) + s + except: + return '[%s] [SYS] '%self.formatTime(record) + s + +if not app.debug: + mail_handler = SMTPHandler(app.config['SMTP_SERVER'], + app.config['APP_EMAIL'], + app.config['ADMIN_EMAIL'], 'LetterBomb ERROR') + mail_handler.setLevel(logging.ERROR) + app.logger.addHandler(mail_handler) + + handler = logging.FileHandler(os.path.join(app.root_path, 'log', 'info.log')) + handler.setLevel(logging.INFO) + handler.setFormatter(RequestFormatter()) + app.logger.addHandler(handler) + + app.logger.setLevel(logging.INFO) + app.logger.warning('Starting...') + +def region(): + try: + country = gi.country(request.remote_addr).country.iso_code + app.logger.info("GI: %s -> %s", request.remote_addr, country) + return COUNTRY_REGIONS.get(country, 'E') + except: + app.logger.exception("GeoIP exception") + return 'E' + +def _index(error=None): + g.recaptcha_args = 'k=%s' % app.config['RECAPTCHA_PUBLICKEY'] + rs = make_response(render_template('index.html', region=region(), error=error)) + #rs.headers['Cache-Control'] = 'private, max-age=0, no-store, no-cache, must-revalidate' + #rs.headers['Etag'] = str(random.randrange(2**64)) + rs.headers['Expires'] = 'Thu, 01 Dec 1983 20:00:00 GMT' + return rs + + +@app.route('/') +def index(): + return _index() + + +def captcha_check(): + try: + oform = { + #"privatekey": app.config['RECAPTCHA_PRIVATEKEY'], + "secret": app.config['RECAPTCHA_PRIVATEKEY'], + "remoteip": request.remote_addr, + #"challenge": request.form.get('recaptcha_challenge_field',['']), + #"response": request.form.get('recaptcha_response_field',['']) + "response": request.form.get('g-recaptcha-response',['']) + } + + #f = urllib.urlopen("http://api-verify.recaptcha.net/verify", urllib.urlencode(oform)) + f = urllib.urlopen("https://www.google.com/recaptcha/api/siteverify", urllib.urlencode(oform)) + + #result = f.readline().replace("\n","") + #error = f.readline().replace("\n","") + d = json.load(f) + result = d["success"] + f.close() + + if not result:# != 'true': + #if error != 'incorrect-captcha-sol': + app.logger.info("ReCaptcha fail: %r, %r", oform, d) + #g.recaptcha_args += "&error=" + error + return False + + except: + #g.recaptcha_args += "&error=unknown" + return False + return True + +@app.route('/haxx', methods=["POST"]) +def haxx(): + OUI_LIST = [i.decode('hex') for i in open(os.path.join(app.root_path, 'oui_list.txt')).read().split("\n") if len(i)==6] + g.recaptcha_args = 'k=%s' % app.config['RECAPTCHA_PUBLICKEY'] + dt = datetime.utcnow() - timedelta(1) + delta = (dt - datetime(2000, 1, 1)) + timestamp = delta.days * 86400 + delta.seconds + + try: + mac = ''.join([chr(int(request.form[i],16)) for i in "abcdef"]) + template = TEMPLATES[request.form['region']] + bundle = 'bundle' in request.form + except: + return _index("Invalid input.") + if not captcha_check(): + return _index("Are you a human?") + + if mac == "\x00\x17\xab\x99\x99\x99": + app.logger.info('Derp MAC %s at %d ver %s bundle %r', mac.encode('hex'), timestamp, request.form['region'], bundle) + return _index("If you're using Dolphin, try File->Open instead ;-).") + + if not any([mac.startswith(i) for i in OUI_LIST]): + app.logger.info('Bad MAC %s at %d ver %s bundle %r', mac.encode('hex'), timestamp, request.form['region'], bundle) + return _index("The exploit will only work if you enter your Wii's MAC address.") + + + key = hashlib.sha1(mac+"\x75\x79\x79").digest() + + blob = bytearray(open(os.path.join(app.root_path, template),'rb').read()) + blob[0x08:0x10] = key[:8] + blob[0xb0:0xc4] = "\x00"*20 + blob[0x7c:0x80] = struct.pack(">I", timestamp) + blob[0x80:0x8a] = "%010d"%timestamp + blob[0xb0:0xc4] = hmac.new(key[8:], str(blob), hashlib.sha1).digest() + + path = "private/wii/title/HAEA/%s/%s/%04d/%02d/%02d/%02d/%02d/HABA_#1/txt/%08X.000" % ( + key[:4].encode('hex').upper(), key[4:8].encode('hex').upper(), + dt.year, dt.month-1, dt.day, dt.hour, dt.minute, timestamp + ) + + zipdata = StringIO.StringIO() + zip = zipfile.ZipFile(zipdata, 'w') + zip.writestr(path, str(blob)) + BUNDLE = [(name, os.path.join(BUNDLEBASE,name)) for name in os.listdir(BUNDLEBASE) if not name.startswith(".")] + if bundle: + for name, path in BUNDLE: + zip.write(path, name) + zip.close() + + app.logger.info('LetterBombed %s at %d ver %s bundle %r', mac.encode('hex'), timestamp, request.form['region'], bundle) + + rs = make_response(zipdata.getvalue()) + zipdata.close() + rs.headers.add('Content-Disposition', 'attachment', filename="LetterBomb.zip") + rs.headers['Content-Type'] = 'application/zip' + return rs + +application=app + +if __name__ == "__main__": + app.run('0.0.0.0', 10142) diff --git a/bundle/.keep b/bundle/.keep new file mode 100644 index 0000000..e69de29 diff --git a/config.py.sample b/config.py.sample new file mode 100644 index 0000000..f123859 --- /dev/null +++ b/config.py.sample @@ -0,0 +1,13 @@ +# -!- coding: utf-8 -!- + +# for cookies and stuff, nobody really cares +SECRET_KEY = 'fillme' + +RECAPTCHA_PUBLICKEY = "fillme" +RECAPTCHA_PRIVATEKEY = "fillme" + +DEBUG = False +ADMIN_EMAIL = ['foo@bar.com'] +SMTP_SERVER = '127.0.0.1' +APP_EMAIL = 'foo@bar.com' + diff --git a/country_regions.txt b/country_regions.txt new file mode 100644 index 0000000..eb7703a --- /dev/null +++ b/country_regions.txt @@ -0,0 +1,209 @@ + E +A1 U +A2 E +AW U +AO E +AI U +AX E +AL E +AD E +AN U +AP J +AE U +AR U +AM U +AS U +AG U +AU E +AT E +AZ E +BE E +BJ E +BF E +BD U +BG E +BH U +BS U +BA E +BY E +BZ U +BM U +BO U +BR U +BB U +BN U +BW E +CF E +CA U +CH E +CL U +CN U +CI E +CM E +CD E +CO U +CV E +CR U +CU U +KY U +CY E +CZ E +DE E +DJ E +DM U +DK E +DO U +DZ E +EC U +EG U +ER E +ES E +EE E +EU E +FI E +FJ E +FK E +FR E +FO E +FM U +GA E +GB E +GE U +GG E +GH E +GI E +GP E +GQ E +GR E +GD U +GL E +GT U +GU U +GY U +HK J +HN U +HR E +HT U +HU E +ID U +IM E +IN U +IO U +IE E +IR U +IQ U +IS E +IL E +IT E +JM U +JE E +JO U +JP J +KZ E +KE E +KG U +KH U +KN U +KR K +KW U +LA U +LB U +LY E +LC U +LI E +LK E +LT E +LU E +LV E +MO J +MF E +MA E +MC E +MD E +MG E +MV U +MX U +MH U +MK E +ML E +MT E +MM U +ME E +MN U +MP U +MZ E +MR E +MQ E +MU E +MW E +MY U +NA E +NC E +NE E +NF E +NG E +NI U +NL E +NO E +NP U +NZ E +OM U +PK U +PA U +PE U +PH U +PG E +PL E +PR U +PT E +PY U +PS U +PF E +QA U +RE E +RO E +RU E +SA U +SD U +SN E +SG U +SV U +SM E +PM E +RS E +SR U +SK E +SI E +SE E +SZ E +SC U +SY U +TC U +TG J +TH U +TJ U +TK U +TM E +TL E +TT U +TO E +TR E +TW J +TZ U +UG U +UA E +UY U +US U +UZ U +VA U +VC U +VE U +VG U +VI U +VN U +VU E +YE U +YT E +ZA E +ZM U +ZW E diff --git a/letterbomb.fcgi b/letterbomb.fcgi new file mode 100755 index 0000000..1138eae --- /dev/null +++ b/letterbomb.fcgi @@ -0,0 +1,6 @@ +#!/usr/bin/python2 +from flup.server.fcgi import WSGIServer +from app import app + +if __name__ == '__main__': + WSGIServer(app, debug=False).run() diff --git a/log/.keep b/log/.keep new file mode 100644 index 0000000..e69de29 diff --git a/oui_list.txt b/oui_list.txt new file mode 100644 index 0000000..8b06db7 --- /dev/null +++ b/oui_list.txt @@ -0,0 +1,59 @@ +0009BF +001656 +0017AB +00191D +0019FD +001AE9 +001B7A +001BEA +001CBE +001DBC +001E35 +001EA9 +001F32 +001FC5 +002147 +0021BD +00224C +0022AA +0022D7 +002331 +0023CC +00241E +002444 +0024F3 +0025A0 +002659 +002709 +0403D6 +182A7B +2C10C1 +34AF2C +40D28A +40F407 +582F40 +58BDA3 +5C521E +606BFF +64B5C6 +78A2A0 +7CBB8A +8C56C5 +8CCDE8 +9458CB +98B6E9 +9CE635 +A438CC +A45C27 +A4C0E1 +B87826 +B88AEC +B8AE6E +CC9E00 +CCFB65 +D86BF7 +DC68EB +E00C7F +E0E751 +E84ECE +ECC40D diff --git a/static/letterbomb_icon.png b/static/letterbomb_icon.png new file mode 100644 index 0000000..0f159b8 Binary files /dev/null and b/static/letterbomb_icon.png differ diff --git a/static/mac.css b/static/mac.css new file mode 100644 index 0000000..39a1e5c --- /dev/null +++ b/static/mac.css @@ -0,0 +1,57 @@ +body { + font: 1.0em "Trebuchet MS", Verdana, Arial, sans-serif; + text-align:center; +} + +input.button { + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + border-radius: 10px; + + -moz-box-shadow: 2px 2px 3px #666; + -webkit-box-shadow: 2px 2px 3px #666; + box-shadow: 2px 2px 3px #666; + + font-size: 20px; + padding: 4px 7px; + outline: 0; + -webkit-appearance: none; + + text-align:center; +} + +input.buttonr:enabled { + border-color:red; +} +input.buttonb:enabled { + border-color:blue; +} + +input.box { + border: 1px solid #ccc; + + text-transform:uppercase; + + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + border-radius: 10px; + + -moz-box-shadow: 2px 2px 3px #666; + -webkit-box-shadow: 2px 2px 3px #666; + box-shadow: 2px 2px 3px #666; + + font-size: 20px; + padding: 4px 7px; + outline: 0; + -webkit-appearance: none; + + text-align:center; +} + +input.box:focus { + border-color: #339933; +} + +h2 { + margin-bottom: 0; +} diff --git a/templateE.bin b/templateE.bin new file mode 100644 index 0000000..7dcef70 Binary files /dev/null and b/templateE.bin differ diff --git a/templateJ.bin b/templateJ.bin new file mode 100644 index 0000000..7b161ad Binary files /dev/null and b/templateJ.bin differ diff --git a/templateK.bin b/templateK.bin new file mode 100644 index 0000000..967dd83 Binary files /dev/null and b/templateK.bin differ diff --git a/templateU.bin b/templateU.bin new file mode 100644 index 0000000..34549a4 Binary files /dev/null and b/templateU.bin differ diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..52545d3 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,95 @@ + + + + LetterBomb + + + + + +

LetterBomb

+
+

+

System Menu Version

+ 4.3U + 4.3E + 4.3J + 4.3K +

+ +

+

MAC Address

+ + + + + + +
Necessary to create and sign the correct file +

+

+ Bundle the HackMii Installer for me! +

+
+ +
+
+{% if error %} +
{{ error }}
+{% endif %} +
You must have JavaScript enabled to use this site.
+ +

+ +

+
+ + + diff --git a/update_oui.sh b/update_oui.sh new file mode 100755 index 0000000..c033036 --- /dev/null +++ b/update_oui.sh @@ -0,0 +1,5 @@ +#!/bin/sh +wget -O - http://standards.ieee.org/develop/regauth/oui/oui.txt | grep -i nintendo | grep base\ 16 | awk '{print $1}' | sort > oui_list.txt.new || exit 1 +diff -urN oui_list.txt oui_list.txt.new +mv oui_list.txt oui_list.txt.old +mv oui_list.txt.new oui_list.txt