Initial commit

This commit is contained in:
Hector Martin 2018-10-30 08:39:07 +00:00
commit 87154280a8
17 changed files with 635 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
.*
!.gitignore
log/*
!log/.keep
bundle/*
!bundle/.keep
*.pyc
tmp
*.old
config.py

16
README.md Normal file
View File

@ -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/`.

164
app.py Normal file
View File

@ -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)

0
bundle/.keep Normal file
View File

13
config.py.sample Normal file
View File

@ -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'

209
country_regions.txt Normal file
View File

@ -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

6
letterbomb.fcgi Executable file
View File

@ -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()

0
log/.keep Normal file
View File

59
oui_list.txt Normal file
View File

@ -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

BIN
static/letterbomb_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

57
static/mac.css Normal file
View File

@ -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;
}

BIN
templateE.bin Normal file

Binary file not shown.

BIN
templateJ.bin Normal file

Binary file not shown.

BIN
templateK.bin Normal file

Binary file not shown.

BIN
templateU.bin Normal file

Binary file not shown.

95
templates/index.html Normal file
View File

@ -0,0 +1,95 @@
<!DOCTYPE HTML>
<html>
<head>
<title>LetterBomb</title>
<link rel="stylesheet" type="text/css" href="/static/mac.css" />
<script type="text/javascript">
captcha_passed = false;
function keyHandler(e, obj, next) {
if (window.event) { // IE
keynum = e.keyCode;
} else if(e.which) { // sane browsers
keynum = e.which;
}
if (keynum == 0x08 || keynum == 0x09 || (keynum >= 96 && keynum <= 105))
return true;
keychar = String.fromCharCode(keynum);
if (keychar.match(/[0-9A-F]/) == null) {
return false;
} else {
return true;
}
}
function check() {
var ids = ["a","b","c","d","e","f"];
for (var i = 0; i < 6; i++) {
var val = document.getElementById(ids[i]).value;
if (val.match(/[0-9a-fA-F][0-9a-fA-F]/) == null) {
return false;
}
}
return captcha_passed;
}
function update() {
var ok = check();
document.getElementById('submit_btn').disabled = !ok;
document.getElementById('submit_btn2').disabled = !ok;
}
function doNext(obj, next) {
if (obj.value.length == 2 && next != null) {
document.getElementById(next).value = '';
document.getElementById(next).focus();
}
update();
}
function captcha_ok() {
captcha_passed = true;
update();
}
function captcha_expired() {
captcha_passed = false;
update();
}
</script>
</head>
<body>
<img src='/static/letterbomb_icon.png' style="margin-bottom: -30px;"/>
<h1>LetterBomb</h1>
<form method="POST" action="{{ url_for('haxx') }}">
<p>
<h2>System Menu Version</h2>
<input type='radio' name='region' value='U' {% if region=='U' %}checked{% endif %}>4.3U
<input type='radio' name='region' value='E' {% if region=='E' %}checked{% endif %}>4.3E
<input type='radio' name='region' value='J' {% if region=='J' %}checked{% endif %}>4.3J
<input type='radio' name='region' value='K' {% if region=='K' %}checked{% endif %}>4.3K
</p>
<p>
<h2>MAC Address</h2>
<input type='text' maxlength='2' size='2' name='a' id='a' class='box' onkeydown="return keyHandler(event);" onkeyup="doNext(this, 'b');" placeholder='AA' />
<input type='text' maxlength='2' size='2' name='b' id='b' class='box' onkeydown="return keyHandler(event);" onkeyup="doNext(this, 'c');" placeholder='BB' />
<input type='text' maxlength='2' size='2' name='c' id='c' class='box' onkeydown="return keyHandler(event);" onkeyup="doNext(this, 'd');" placeholder='CC' />
<input type='text' maxlength='2' size='2' name='d' id='d' class='box' onkeydown="return keyHandler(event);" onkeyup="doNext(this, 'e');" placeholder='DD' />
<input type='text' maxlength='2' size='2' name='e' id='e' class='box' onkeydown="return keyHandler(event);" onkeyup="doNext(this, 'f');" placeholder='EE' />
<input type='text' maxlength='2' size='2' name='f' id='f' class='box' onkeydown="return keyHandler(event);" onkeyup="update();" placeholder='FF' />
<br />Necessary to create and sign the correct file
</p>
<p>
<input type='checkbox' name='bundle' value='1' checked /> <b>Bundle the HackMii Installer for me!</b>
</p>
<div style="float: center; display: inline-block;" class="recaptcha">
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<div class="g-recaptcha" data-sitekey="{{ config.RECAPTCHA_PUBLICKEY }}" data-callback="captcha_ok" data-expired-callback="captcha_expired"></div>
</div>
{% if error %}
<div style="color: red; font-weight: bold; font-size: 18pt;">{{ error }}</div>
{% endif %}
<div style="color: red; font-weight: bold; font-size: 18pt;" id="nojs">You must have JavaScript enabled to use this site.</div>
<script>document.getElementById("nojs").style.display = "none";</script>
<p>
<input type='submit' value='Cut the red wire' id='submit_btn' class='button buttonr' disabled /> <input type='submit' value='Cut the blue wire' id='submit_btn2' class='button buttonb' disabled />
</p>
</form>
</body>
</html>

5
update_oui.sh Executable file
View File

@ -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