# Rouge Access Point Watcher

## Bigun

We have a potential security issue where peoples are setting up their own access points in an area that should never have one setup.

Is there a software suite that can be installed on a small portable device (like a raspberry pi or an Intel NUC) that can send an e-mail when an unauthorized AP is detected?

----------

## eccerr0r

Are you allowed to bring cell phones into that region?

Are ad hoc networks allowed?

Technically all that's needed is to get iwtools installed and scanning should find most networks as long as they beacon.

----------

## Bigun

 *eccerr0r wrote:*   

> Are you allowed to bring cell phones into that region?
> 
> Are ad hoc networks allowed?
> 
> Technically all that's needed is to get iwtools installed and scanning should find most networks as long as they beacon.

 

*I* am yes.  But this is something that I would need to leave installed, in several different places.

----------

## Ralphred

 *Bigun wrote:*   

> But this is something that I would need to leave installed, in several different places.

 

Here you go mate, wrote it on my Pi4, so know it runs on them. Need wpa_supplicant and sendmail, installed and running.

```
#!/usr/bin/env python

#AP_check.py 

#Scans for AP's every 2 minutes, and sends emails about unknown/unauthorised AP's.

import sys, time, os, signal

from subprocess import Popen, PIPE

class bad_ap_checker():

        def __init__(self):

                ### Start user variables ###

                self.debug=True                         # True: only print email to be set, don't send. False: actually send emails

                self.location='some identifying location'

                self.email_to='some_admin@example.com some_security_bod@example.com'

                self.email_from='Rogue_AP_Finder@example.com'

                self.email_subject='Rogue AP found at: %s.'%self.location

                self.email_sig='\nSent by a dodgy.py, a script running on some machine.'

                self.allowed_ssid_macs=['f8:e9:03:dc:ab:68','f8:e9:03:dc:ab:67']  ##only needs to match one of these lines to be left alone, set to =[] to disable.                                             

                self.allowed_ssid_names=['VB0-2','VB0-2']                                    ##^^

                self.purge_time=900                     #time in seconds before sending a subsequent email about the same AP

                self.wlan_interface='wlan0'

                ### end user variables ###

                signal.signal(signal.SIGINT, self.eksit)

                signal.signal(signal.SIGUSR1, self.eksit)

                signal.signal(signal.SIGTERM, self.eksit)

                self.found_aps={}

                self.email_message=''

        def eksit(self,sig,frame):

                sys.exit(0)

        def send_email(self):

                email_message='%s\n\n%s\n.\n'%(self.email_message,self.email_sig)

                if self.debug:

                        print('Would send the following email, but debug is set\n\n%s'%email_message)

                else:

                        os.system('echo "%s"| sendmail %s'%(email_message,self.email_to))

        def compose_message(self,message,clean=False):

                if clean:

                        self.email_message='From: %s\nTo: %s\nSubject: %s'%(self.email_from,self.email_to,self.email_subject)

                        return

                email_message='%s\n%s'%(self.email_message,message)

                self.email_message=email_message

        def run_scan(self):

                if self.debug:print('Running scan...')

                f=open('/dev/null','w')

                args=['wpa_cli','-i',self.wlan_interface,'scan']

                proc=Popen(args,stdout=f)

                proc.wait()

        def read_scan(self):

                if self.debug:print('Reading scan results...')

                args=['wpa_cli','-i',self.wlan_interface,'scan_results']

                proc=Popen(args,stdout=PIPE, universal_newlines=True)

                results=proc.communicate()[0].split('\n')

                results.pop(0)                  #lose the header

                results.pop(len(results)-1)     #lose the '' at the end

                bad_aps=[]

                for result in results:

                        bad_aps.append(result)

                        test=result.split('\t')

                        if test[0] in self.allowed_ssid_macs or test[4] in self.allowed_ssid_names:

                                bad_aps.pop(bad_aps.index(result))

                                continue

                        for known in self.found_aps:

                                if test[0]==self.found_aps[known]:

                                        bad_aps.pop(bad_aps.index(result))

                                        continue

                if self.debug:print('%s rogue AP\'s found.'%len(bad_aps))

                return bad_aps

        def main_loop(self):

                while True:

                        purge=time.time()-self.purge_time

                        whens=[]

                        for key in self.found_aps:whens.append(key)

                        for when in whens:

                                if when < purge:

                                        self.found_aps.pop(when)

                        self.run_scan()

                        timer=20 #Give 20 seconds for scan to complete

                        while timer >0:

                                time.sleep(1)

                                timer-=1

                        bad_aps=self.read_scan()

                        if bad_aps:

                                self.compose_message('',clean=True)

                                for bad_ap in bad_aps:

                                        test=bad_ap.split('\t')

                                        self.compose_message('A rogue AP with SSID: %s and MAC: %s has been found at %s.'%(test[4],test[0],self.location))

                                        self.found_aps[time.time()]=bad_ap.split('\t')[0]

                                self.send_email()

                        timer=100 #rest of finish the 2 minute scan interval

                        while timer >0:

                                time.sleep(1)

                                timer-=1

if __name__=='__main__':

        checker=bad_ap_checker()

        checker.main_loop()
```

That won't catch folks not broadcasting the AP's SSID, you'd need to scan for people 'looking for a specific ssid', and then check if it exists to detect those, but by then you are not dealing with your average Joe off the street.

----------

## Bigun

 *Ralphred wrote:*   

> **snip**

 

The script works brilliantly!  

I'm having issues trying to start it automatically via crontab:

```
@reboot python /home/pi/AP_checker.py > /home/pi/error.log 2>&1
```

and it keeps bombing out:

```
Traceback (most recent call last):

  File "/home/pi/AP_checker.py", line 102, in <module>

    checker.main_loop()

  File "/home/pi/AP_checker.py", line 81, in main_loop

    self.run_scan()

  File "/home/pi/AP_checker.py", line 49, in run_scan

    proc=Popen(args,stdout=f)

  File "/usr/lib/python2.7/subprocess.py", line 394, in __init__

    errread, errwrite)

  File "/usr/lib/python2.7/subprocess.py", line 1047, in _execute_child

    raise child_exception

OSError: [Errno 2] No such file or directory
```

----------

## Bigun

Way overkill, put I build a system.d daemon to handle starting it up and it worked.

----------

## szatox

I suppose it bombed because cron launches jobs in very limited environment.

Like in there are very few variables set and those that are set may differ from your (user's) environmental variables.

Try running "set" as a user and as a cron job (dump output to a file to inspect it), the difference is massive.

----------

## Ralphred

 *Bigun wrote:*   

>  *Ralphred wrote:*   **snip** The script works brilliantly!

 

Glad it works for you: About 5 years ago I suffered a brain injury and lost ~20 IQ points, doing things like this successfully are a win on two points, my attempts to claw back lost abilities are working, and your problem is solved  :Smile: 

----------

## Hu

The cited line tries to run an external program, and does not specify an absolute path.  For this to work, the program must be on $PATH.  Consider:

```
$ python -c 'import subprocess;subprocess.Popen(args=("ls", "--version"));' | head -n1

ls (GNU coreutils) 8.32

$ PATH=/ /usr/bin/python -c 'import subprocess;subprocess.Popen(args=("ls", "--version"));'

Traceback (most recent call last):
```

In the latter case, since I cleared $PATH, ls cannot be found.  (Nor could a bare python, which is why it is shown qualified.)  As szatox says, cron has likely done a similar thing.  It offered a severely reduced $PATH, and the external program cannot be found.  You could patch the script to reset $PATH to a more expansive value, or patch the script to call wpa_cli with an absolute path.

----------

## Bigun

 *Ralphred wrote:*   

>  *Bigun wrote:*    *Ralphred wrote:*   **snip** The script works brilliantly! 
> 
> Glad it works for you: About 5 years ago I suffered a brain injury and lost ~20 IQ points, doing things like this successfully are a win on two points, my attempts to claw back lost abilities are working, and your problem is solved 

 

That's awesome!  Not the injury, the accomplishment.  You may have had a bad day on the test, I'd maybe give some thought at re-trying the test.

I added one more function to the script, in case anyone else starts using it.  I added mac address manufacturer support (the first 3 sets from the mac address):

```
#!/usr/bin/env python

#AP_check.py

#Scans for AP's every 2 minutes, and sends emails about unknown/unauthorized AP's.

import sys, time, os, signal

from subprocess import Popen, PIPE

class bad_ap_checker():

        def __init__(self):

                ### Start user variables ###

                self.debug=True                         # True: only print email to be set, don't send. False: actually send emails

                self.location='Somewhere'

                self.email_to='someone@something.com another_someone@something.com'

                self.email_from='someoneelse@something.com'

                self.email_subject='Rogue AP found at: %s.'%self.location

                self.email_sig='\nFrom Pi'

                self.allowed_ssid_macs=['12:34:56:78:90:ab']  ##only needs to match one of these lines to be left alone, set to =[] to disable.                                             

                self.allowed_ssid_names=['some_trusted_AP']                                    ##^^

                self.allowed_mac_manufacturers=['12:34:56']  ##^^

                self.purge_time=900                     #time in seconds before sending a subsequent email about the same AP

                self.wlan_interface='wlan0'

                ### end user variables ###

                signal.signal(signal.SIGINT, self.eksit)

                signal.signal(signal.SIGUSR1, self.eksit)

                signal.signal(signal.SIGTERM, self.eksit)

                self.found_aps={}

                self.email_message=''

        def eksit(self,sig,frame):

                sys.exit(0)

        def send_email(self):

                email_message='%s\n\n%s\n.\n'%(self.email_message,self.email_sig)

                if self.debug:

                        print('Would send the following email, but debug is set\n\n%s'%email_message)

                else:

                        os.system('echo "%s"| sendmail %s'%(email_message,self.email_to))

        def compose_message(self,message,clean=False):

                if clean:

                        self.email_message='From: %s\nTo: %s\nSubject: %s'%(self.email_from,self.email_to,self.email_subject)

                        return

                email_message='%s\n%s'%(self.email_message,message)

                self.email_message=email_message

        def run_scan(self):

                if self.debug:print('Running scan...')

                f=open('/dev/null','w')

                args=['wpa_cli','-i',self.wlan_interface,'scan']

                proc=Popen(args,stdout=f)

                proc.wait()

        def read_scan(self):

                if self.debug:print('Reading scan results...')

                args=['wpa_cli','-i',self.wlan_interface,'scan_results']

                proc=Popen(args,stdout=PIPE, universal_newlines=True)

                results=proc.communicate()[0].split('\n')

                results.pop(0)                  #lose the header

                results.pop(len(results)-1)     #lose the '' at the end

                bad_aps=[]

                for result in results:

                        bad_aps.append(result)

                        test=result.split('\t')

                        mac=test[0].split(':')

                        manufacturer=mac[0]+':'+mac[1]+':'+mac[2]

                        if test[0] in self.allowed_ssid_macs or test[4] in self.allowed_ssid_names or manufacturer in self.allowed_mac_manufacturers:

                                bad_aps.pop(bad_aps.index(result))

                                continue

                        for known in self.found_aps:

                                if test[0]==self.found_aps[known]:

                                        bad_aps.pop(bad_aps.index(result))

                                        continue

                if self.debug:print('%s rogue AP\'s found.'%len(bad_aps))

                return bad_aps

        def main_loop(self):

                while True:

                        purge=time.time()-self.purge_time

                        whens=[]

                        for key in self.found_aps:whens.append(key)

                        for when in whens:

                                if when < purge:

                                        self.found_aps.pop(when)

                        self.run_scan()

                        timer=20 #Give 20 seconds for scan to complete

                        while timer >0:

                                time.sleep(1)

                                timer-=1

                        bad_aps=self.read_scan()

                        if bad_aps:

                                self.compose_message('',clean=True)

                                for bad_ap in bad_aps:

                                        test=bad_ap.split('\t')

                                        self.compose_message('A rogue AP with SSID: %s and MAC: %s has been found at %s.'%(test[4],test[0],self.location))

                                        self.found_aps[time.time()]=bad_ap.split('\t')[0]

                                self.send_email()

                        timer=100 #rest of finish the 2 minute scan interval

                        while timer >0:

                                time.sleep(1)

                                timer-=1

if __name__=='__main__':

        checker=bad_ap_checker()

        checker.main_loop()
```

----------

## Ralphred

I'm gonna go all python tutor on your

```
mac=test[0].split(':')

manufacturer=mac[0]+':'+mac[1]+':'+mac[2]
```

you can use the string class' built join function to change the last line to

```
manufacturer=':'.join([mac[0],mac[1],mac[2]])
```

because ':' is a string, but

```
[mac[0],mac[1],mac[2]]
```

is the same as

```
mac[0:3]
```

so you are only using it once, so you may as well not assign a variable to it and use

```
manufacturer=':'.join(test[0].split(':')[0:3])
```

but that's ugly, and

```
manufacturer=test[0][0:8]
```

does the job just fine  :Wink: 

----------

## Bigun

I'm having some issues getting things working using sendmail (not sure why because it was working before).  Is there a way we can use python's smtplib library to handle sending the mail?

I'm trying to get it working using the library and I'm having some issues.  It does work, but I can't get the body of the e-mail to populate.

*edit*

I got it working, but I'm sure you could make it more elegant than I can.  No pressure.

----------

