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
+
+
+
+
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.