├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── main.yml │ ├── packer.yml │ └── rebuild-latest-release.yml ├── .gitignore ├── .vscode └── settings.json ├── Dockerfile.m4 ├── LICENSE.md ├── Makefile ├── README.md ├── config ├── bettercap │ └── caplets │ │ ├── pwnagotchi-auto.cap │ │ └── pwnagotchi-manual.cap ├── pwnagotchi │ └── config.toml └── systemd │ └── journald.conf ├── packer ├── Makefile ├── build.pkr.hcl ├── config.pkr.hcl ├── qemu │ └── start-vm.sh ├── rootfs │ ├── boot │ │ ├── cmdline.txt │ │ └── config.txt │ ├── etc │ │ ├── default │ │ │ ├── crda │ │ │ ├── dnsmasq │ │ │ ├── keyboard │ │ │ └── locale │ │ ├── dnsmasq.d │ │ │ └── 01-usb0.conf │ │ ├── dphys-swapfile │ │ ├── hostname │ │ ├── hosts │ │ ├── locale.gen │ │ ├── modules │ │ ├── network │ │ │ └── interfaces.d │ │ │ │ ├── eth0-cfg │ │ │ │ ├── lo-cfg │ │ │ │ ├── usb0-cfg │ │ │ │ └── wlan0-cfg │ │ ├── pwnagotchi.env │ │ ├── rc.local │ │ ├── sysctl.d │ │ │ └── 60-swappiness.conf │ │ ├── systemd │ │ │ └── system │ │ │ │ └── pwnagotchi.service │ │ └── timezone │ └── usr │ │ └── local │ │ └── bin │ │ ├── download-frozen-image │ │ ├── reload-brcmfmac-driver │ │ └── rpi-nexmon-update └── sources.pkr.hcl ├── patches └── bettercap-nexutil-support.patch ├── requirements.txt ├── resources ├── pwnagotchi.jpg └── scripts │ └── linux_connection_share.sh ├── run.sh └── scripts ├── bin ├── container-healthcheck ├── container-init ├── envsubst2 ├── monstart ├── monstop ├── pwnlib ├── pwnlog ├── pwnver ├── service-bettercap-start ├── service-pwnagotchi-start └── service-pwngrid-start └── service ├── bettercap.service ├── container.target ├── pwnagotchi.service └── pwngrid.service /.dockerignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /packer/ 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/dependabot-2.0.json 2 | version: 2 3 | 4 | updates: 5 | - package-ecosystem: "docker" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | groups: 10 | docker-all: 11 | patterns: ["*"] 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "monthly" 17 | groups: 18 | github-actions-all: 19 | patterns: ["*"] 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | name: "Main" 3 | 4 | on: 5 | push: 6 | tags: ["*"] 7 | branches: ["*"] 8 | pull_request: 9 | branches: ["*"] 10 | workflow_dispatch: 11 | 12 | permissions: {} 13 | 14 | jobs: 15 | build: 16 | name: "Build ${{ matrix.arch }} image" 17 | runs-on: "ubuntu-latest" 18 | permissions: 19 | contents: "read" 20 | strategy: 21 | matrix: 22 | arch: 23 | [ 24 | "native", 25 | "generic-amd64", 26 | "raspios-arm64v8", 27 | "raspios-arm32v7", 28 | "raspios-arm32v6", 29 | ] 30 | steps: 31 | - name: "Checkout project" 32 | uses: "actions/checkout@v5" 33 | - name: "Register binfmt entries" 34 | if: "matrix.arch != 'native'" 35 | run: | 36 | make binfmt-register 37 | - name: "Build and save image" 38 | run: | 39 | make IMAGE_BUILD_OPTS="--pull" "build-${{ matrix.arch }}-image" "save-${{ matrix.arch }}-image" 40 | - name: "Upload artifacts" 41 | if: "startsWith(github.ref, 'refs/tags/v') && matrix.arch != 'native'" 42 | uses: "actions/upload-artifact@v4" 43 | with: 44 | name: "dist-${{ matrix.arch }}" 45 | path: "./dist/" 46 | retention-days: 1 47 | 48 | push: 49 | name: "Push ${{ matrix.arch }} image" 50 | if: "startsWith(github.ref, 'refs/tags/v')" 51 | needs: ["build"] 52 | runs-on: "ubuntu-latest" 53 | permissions: 54 | contents: "read" 55 | strategy: 56 | matrix: 57 | arch: 58 | [ 59 | "generic-amd64", 60 | "raspios-arm64v8", 61 | "raspios-arm32v7", 62 | "raspios-arm32v6", 63 | ] 64 | steps: 65 | - name: "Checkout project" 66 | uses: "actions/checkout@v5" 67 | - name: "Download artifacts" 68 | uses: "actions/download-artifact@v5" 69 | with: 70 | name: "dist-${{ matrix.arch }}" 71 | path: "./dist/" 72 | - name: "Login to Docker Hub" 73 | uses: "docker/login-action@v3" 74 | with: 75 | registry: "docker.io" 76 | username: "${{ secrets.DOCKERHUB_USERNAME }}" 77 | password: "${{ secrets.DOCKERHUB_TOKEN }}" 78 | - name: "Load and push image" 79 | run: | 80 | make "load-${{ matrix.arch }}-image" "push-${{ matrix.arch }}-image" 81 | 82 | push-manifest: 83 | name: "Push manifest" 84 | if: "startsWith(github.ref, 'refs/tags/v')" 85 | needs: ["push"] 86 | runs-on: "ubuntu-latest" 87 | permissions: 88 | contents: "read" 89 | steps: 90 | - name: "Checkout project" 91 | uses: "actions/checkout@v5" 92 | - name: "Login to Docker Hub" 93 | uses: "docker/login-action@v3" 94 | with: 95 | registry: "docker.io" 96 | username: "${{ secrets.DOCKERHUB_USERNAME }}" 97 | password: "${{ secrets.DOCKERHUB_TOKEN }}" 98 | - name: "Push manifest" 99 | run: | 100 | make push-cross-manifest 101 | 102 | publish-github-release: 103 | name: "Publish GitHub release" 104 | if: "startsWith(github.ref, 'refs/tags/v')" 105 | needs: ["push-manifest"] 106 | runs-on: "ubuntu-latest" 107 | permissions: 108 | contents: "write" 109 | steps: 110 | - name: "Publish" 111 | uses: "hectorm/ghaction-release@d0426a7a369ce2c1ed615e1a583788b22745ccfe" 112 | -------------------------------------------------------------------------------- /.github/workflows/packer.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | name: "Packer" 3 | 4 | on: 5 | push: 6 | tags: ["*"] 7 | branches: ["*"] 8 | pull_request: 9 | branches: ["*"] 10 | schedule: 11 | - cron: "20 04 1,15 * *" 12 | workflow_dispatch: 13 | 14 | permissions: {} 15 | 16 | jobs: 17 | validate-packer: 18 | name: "Validate Packer configuration" 19 | runs-on: "ubuntu-24.04" 20 | permissions: 21 | contents: "read" 22 | defaults: 23 | run: 24 | working-directory: "./packer/" 25 | steps: 26 | - name: "Checkout project" 27 | uses: "actions/checkout@v5" 28 | - name: "Install dependencies" 29 | run: | 30 | curl --proto '=https' --tlsv1.3 -sSf 'https://apt.releases.hashicorp.com/gpg' | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/hashicorp.gpg 31 | printf '%s\n' "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/trusted.gpg.d/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list >/dev/null 32 | sudo apt-get update && sudo apt-get install -y --no-install-recommends packer 33 | - name: "Init Packer" 34 | run: | 35 | packer init ./ 36 | - name: "Validate configuration" 37 | run: | 38 | packer validate ./ 39 | - name: "Check configuration format" 40 | run: | 41 | packer fmt -check -diff ./ 42 | 43 | build-packer: 44 | name: "Build Packer image" 45 | needs: ["validate-packer"] 46 | runs-on: "ubuntu-24.04" 47 | permissions: 48 | contents: "read" 49 | defaults: 50 | run: 51 | working-directory: "./packer/" 52 | steps: 53 | - name: "Checkout project" 54 | uses: "actions/checkout@v5" 55 | - name: "Install dependencies" 56 | run: | 57 | curl --proto '=https' --tlsv1.3 -sSf 'https://apt.releases.hashicorp.com/gpg' | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/hashicorp.gpg 58 | printf '%s\n' "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/trusted.gpg.d/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list >/dev/null 59 | sudo apt-get update && sudo apt-get install -y --no-install-recommends packer qemu-utils qemu-system-x86 60 | - name: "Init Packer" 61 | run: | 62 | packer init ./ 63 | - name: "Build image" 64 | run: | 65 | sudo make build-armhf PACKER_LOG=1 66 | -------------------------------------------------------------------------------- /.github/workflows/rebuild-latest-release.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | name: "Rebuild latest release" 3 | 4 | on: 5 | schedule: 6 | - cron: "20 04 * * 1" 7 | workflow_dispatch: 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | trigger-rebuild: 13 | name: "Trigger rebuild" 14 | runs-on: "ubuntu-latest" 15 | permissions: 16 | actions: "write" 17 | contents: "read" 18 | steps: 19 | - name: "Trigger rebuild" 20 | uses: "hectorm/ghaction-trigger-workflow@04c79e7a4e0c0b94bbcff3829f38359e34f1ea9e" 21 | with: 22 | workflow-id: "main.yml" 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /packer/dist/ 3 | /packer/packer_cache/ 4 | /packer/crash.log 5 | .env 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.hcl": "terraform", 4 | "*.hcl.sample": "terraform", 5 | "meta-data": "yaml", 6 | "user-data": "yaml" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Dockerfile.m4: -------------------------------------------------------------------------------- 1 | m4_changequote([[, ]]) 2 | 3 | m4_ifelse(m4_index(DEBIAN_IMAGE_NAME, [[rpi]]), [[-1]], 4 | [[m4_define([[IS_RASPIOS]], 0)]], 5 | [[m4_define([[IS_RASPIOS]], 1)]] 6 | ) 7 | 8 | ################################################## 9 | ## "projects" stage 10 | ################################################## 11 | 12 | FROM --platform=${BUILDPLATFORM} docker.io/alpine:3 AS projects 13 | 14 | RUN apk add --no-cache ca-certificates curl git unzip 15 | 16 | # Download Nexmon 17 | ARG NEXMON_TREEISH=cea7c4b952b3e67110dc1032b8996dae0db9a857 18 | ARG NEXMON_REMOTE=https://github.com/hectorm/nexmon.git 19 | RUN mkdir /tmp/nexmon/ 20 | WORKDIR /tmp/nexmon/ 21 | RUN git clone "${NEXMON_REMOTE:?}" ./ 22 | RUN git checkout "${NEXMON_TREEISH:?}" 23 | RUN git submodule update --init --recursive 24 | 25 | # Download Bettercap 26 | ARG BETTERCAP_TREEISH=v2.32.0 27 | ARG BETTERCAP_REMOTE=https://github.com/bettercap/bettercap.git 28 | RUN mkdir /tmp/bettercap/ 29 | WORKDIR /tmp/bettercap/ 30 | RUN git clone "${BETTERCAP_REMOTE:?}" ./ 31 | RUN git checkout "${BETTERCAP_TREEISH:?}" 32 | RUN git submodule update --init --recursive 33 | 34 | # Download Bettercap UI 35 | ARG BETTERCAP_UI_VERSION=v1.3.0 36 | ARG BETTERCAP_UI_PKG_URL=https://github.com/bettercap/ui/releases/download/${BETTERCAP_UI_VERSION}/ui.zip 37 | RUN mkdir /tmp/bettercap/dist/ 38 | WORKDIR /tmp/bettercap/dist/ 39 | RUN curl -Lo ./ui.zip "${BETTERCAP_UI_PKG_URL:?}" 40 | RUN unzip -q ./ui.zip 41 | 42 | # Download PwnGRID 43 | ARG PWNGRID_TREEISH=v1.10.3 44 | ARG PWNGRID_REMOTE=https://github.com/evilsocket/pwngrid.git 45 | RUN mkdir /tmp/pwngrid/ 46 | WORKDIR /tmp/pwngrid/ 47 | RUN git clone "${PWNGRID_REMOTE:?}" ./ 48 | RUN git checkout "${PWNGRID_TREEISH:?}" 49 | RUN git submodule update --init --recursive 50 | 51 | # Download Pwnagotchi 52 | ARG PWNAGOTCHI_TREEISH=ef0f35da0a4c7708a0e99dc0f75a4182efe328a5 53 | ARG PWNAGOTCHI_REMOTE=https://github.com/evilsocket/pwnagotchi.git 54 | RUN mkdir /tmp/pwnagotchi/ 55 | WORKDIR /tmp/pwnagotchi/ 56 | RUN git clone "${PWNAGOTCHI_REMOTE:?}" ./ 57 | RUN git checkout "${PWNAGOTCHI_TREEISH:?}" 58 | RUN git submodule update --init --recursive 59 | 60 | ################################################## 61 | ## "base" stage 62 | ################################################## 63 | 64 | FROM DEBIAN_IMAGE_NAME:DEBIAN_IMAGE_TAG AS base 65 | 66 | # Install base packages 67 | RUN export DEBIAN_FRONTEND=noninteractive \ 68 | && apt-get update \ 69 | && apt-get install -y --no-install-recommends \ 70 | ca-certificates \ 71 | locales \ 72 | tzdata \ 73 | && rm -rf /var/lib/apt/lists/* 74 | 75 | # Setup locale 76 | ENV LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 77 | RUN printf '%s\n' "${LANG:?} UTF-8" > /etc/locale.gen \ 78 | && localedef -c -i "${LANG%%.*}" -f UTF-8 "${LANG:?}" ||: 79 | 80 | # Setup timezone 81 | ENV TZ=UTC 82 | RUN printf '%s\n' "${TZ:?}" > /etc/timezone \ 83 | && ln -snf "/usr/share/zoneinfo/${TZ:?}" /etc/localtime 84 | 85 | ################################################## 86 | ## "build-base" stage 87 | ################################################## 88 | 89 | FROM base AS build-base 90 | 91 | # Install build packages 92 | RUN export DEBIAN_FRONTEND=noninteractive \ 93 | && apt-get update \ 94 | && apt-get install -y --no-install-recommends \ 95 | build-essential \ 96 | cmake \ 97 | file \ 98 | git \ 99 | jq \ 100 | pkgconf \ 101 | unzip \ 102 | wget \ 103 | && rm -rf /var/lib/apt/lists/* 104 | 105 | ################################################## 106 | ## "build-python" stage 107 | ################################################## 108 | 109 | FROM build-base AS build-python 110 | 111 | # Install Python 112 | RUN export DEBIAN_FRONTEND=noninteractive \ 113 | && apt-get update \ 114 | && apt-get install -y --no-install-recommends \ 115 | cython3 \ 116 | python3 \ 117 | python3-dev \ 118 | python3-distutils \ 119 | python3-pip \ 120 | python3-setuptools \ 121 | python3-venv \ 122 | python3-wheel \ 123 | && rm -rf /var/lib/apt/lists/* 124 | 125 | ENV PIP_NO_CACHE_DIR=0 126 | m4_ifelse(IS_RASPIOS, 1, [[ENV PIP_EXTRA_INDEX_URL=https://www.piwheels.org/simple]]) 127 | 128 | RUN python3 --version 129 | 130 | ################################################## 131 | ## "build-golang" stage 132 | ################################################## 133 | 134 | FROM build-base AS build-golang 135 | 136 | # Install Go 137 | ENV GOROOT=/usr/local/go/ GOPATH=/go/ 138 | RUN mkdir -p "${GOROOT:?}" "${GOPATH:?}/bin" "${GOPATH:?}/src" 139 | RUN GOLANG_VERSION=$(wget -qO- 'https://golang.org/dl/?mode=json' | jq -r 'map(select(.version | startswith("go1."))) | first | .version') \ 140 | && case "$(uname -m)" in x86_64) GOLANG_ARCH=amd64 ;; aarch64) GOLANG_ARCH=arm64 ;; armv6l|armv7l) GOLANG_ARCH=armv6l ;; esac \ 141 | && GOLANG_PKG_URL=https://dl.google.com/go/${GOLANG_VERSION:?}.linux-${GOLANG_ARCH:?}.tar.gz \ 142 | && wget -qO- "${GOLANG_PKG_URL:?}" | tar -xz --strip-components=1 -C "${GOROOT:?}" 143 | 144 | ENV GO111MODULE=on 145 | ENV CGO_ENABLED=1 146 | ENV PATH=${GOROOT}/bin:${GOPATH}/bin:${PATH} 147 | 148 | RUN go version 149 | 150 | ################################################## 151 | ## "build-nexutil" stage 152 | ################################################## 153 | 154 | m4_ifelse(IS_RASPIOS, 1, [[ 155 | 156 | FROM build-base AS build-nexutil 157 | 158 | # Build Nexutil 159 | COPY --from=projects --chown=root:root /tmp/nexmon/ /tmp/nexmon/ 160 | WORKDIR /tmp/nexmon/utilities/nexutil/ 161 | RUN make nexutil 162 | RUN mv ./nexutil /usr/local/bin/nexutil 163 | RUN file /usr/local/bin/nexutil 164 | RUN /usr/local/bin/nexutil --version 165 | 166 | ]]) 167 | 168 | ################################################## 169 | ## "build-bettercap" stage 170 | ################################################## 171 | 172 | FROM build-golang AS build-bettercap 173 | 174 | # Install dependencies 175 | RUN export DEBIAN_FRONTEND=noninteractive \ 176 | && apt-get update \ 177 | && apt-get install -y --no-install-recommends \ 178 | libnetfilter-queue-dev \ 179 | libpcap-dev \ 180 | libusb-1.0-0-dev \ 181 | && rm -rf /var/lib/apt/lists/* 182 | 183 | # Build Bettercap 184 | COPY --from=projects --chown=root:root /tmp/bettercap/ /tmp/bettercap/ 185 | COPY ./patches/bettercap-*.patch /tmp/bettercap/ 186 | WORKDIR /tmp/bettercap/ 187 | RUN git apply -v ./bettercap-*.patch 188 | RUN go mod download -x 189 | RUN go build -v -o ./dist/bettercap ./ 190 | RUN mv ./dist/bettercap /usr/local/bin/bettercap 191 | RUN mkdir -p /usr/local/share/bettercap/ 192 | RUN mv ./dist/ui/ /usr/local/share/bettercap/ui/ 193 | RUN file /usr/local/bin/bettercap 194 | RUN /usr/local/bin/bettercap --version 195 | 196 | ################################################## 197 | ## "build-pwngrid" stage 198 | ################################################## 199 | 200 | FROM build-golang AS build-pwngrid 201 | 202 | # Install dependencies 203 | RUN export DEBIAN_FRONTEND=noninteractive \ 204 | && apt-get update \ 205 | && apt-get install -y --no-install-recommends \ 206 | libpcap-dev \ 207 | && rm -rf /var/lib/apt/lists/* 208 | 209 | # Build PwnGRID 210 | COPY --from=projects --chown=root:root /tmp/pwngrid/ /tmp/pwngrid/ 211 | WORKDIR /tmp/pwngrid/ 212 | RUN go mod download -x 213 | RUN go build -v -o ./dist/pwngrid ./cmd/pwngrid/*.go 214 | RUN mv ./dist/pwngrid /usr/local/bin/pwngrid 215 | RUN file /usr/local/bin/pwngrid 216 | RUN /usr/local/bin/pwngrid --version 217 | 218 | ################################################## 219 | ## "build-pwnagotchi" stage 220 | ################################################## 221 | 222 | FROM build-python AS build-pwnagotchi 223 | 224 | # Install dependencies 225 | RUN export DEBIAN_FRONTEND=noninteractive \ 226 | && apt-get update \ 227 | && apt-get install -y --no-install-recommends \ 228 | fonts-dejavu \ 229 | gfortran \ 230 | libaec-dev \ 231 | libarchive-dev \ 232 | libatlas-base-dev \ 233 | libavcodec-dev \ 234 | libavformat-dev \ 235 | libavresample-dev \ 236 | libavutil-dev \ 237 | libbz2-dev \ 238 | libevent-dev \ 239 | libfreetype6-dev \ 240 | libfuse-dev \ 241 | libgstreamer-plugins-base1.0-dev \ 242 | libgstreamer1.0-dev \ 243 | libgtk-3-dev \ 244 | libhdf5-dev \ 245 | libhwloc-dev \ 246 | libice-dev \ 247 | libjbig-dev \ 248 | libjpeg62-turbo-dev \ 249 | liblcms2-dev \ 250 | libltdl-dev \ 251 | libopenexr-dev \ 252 | libopenjp2-7-dev \ 253 | libopenmpi-dev \ 254 | libpng-dev \ 255 | libqt4-dev \ 256 | libqt4-opengl-dev \ 257 | libsm-dev \ 258 | libssl-dev \ 259 | libswresample-dev \ 260 | libswscale-dev \ 261 | libtiff-dev \ 262 | libvpx-dev \ 263 | libwebp-dev \ 264 | libzstd-dev \ 265 | m4_ifelse(IS_RASPIOS, 1, [[libjasper-dev]]) \ 266 | && rm -rf /var/lib/apt/lists/* 267 | 268 | # Build Pwnagotchi 269 | COPY --from=projects --chown=root:root /tmp/pwnagotchi/ /tmp/pwnagotchi/ 270 | WORKDIR /tmp/pwnagotchi/ 271 | # Modify some hardcoded paths 272 | RUN sed -ri 's|^\s*(DefaultPath)\s*=.+$|\1 = "/root/"|' ./pwnagotchi/identity.py 273 | # Create virtual environment and install requirements 274 | ENV PWNAGOTCHI_VENV=/usr/local/lib/pwnagotchi/ 275 | ENV PWNAGOTCHI_ENABLE_INSTALLER=false 276 | COPY ./requirements.txt ./requirements.txt 277 | RUN python3 -m venv --symlinks "${PWNAGOTCHI_VENV:?}" 278 | RUN "${PWNAGOTCHI_VENV:?}"/bin/python -m pip install --upgrade pip==23.0.1 279 | RUN "${PWNAGOTCHI_VENV:?}"/bin/python -m pip install -r ./requirements.txt 280 | RUN "${PWNAGOTCHI_VENV:?}"/bin/python -m pip install ./ 281 | RUN "${PWNAGOTCHI_VENV:?}"/bin/pwnagotchi --version 282 | 283 | ################################################## 284 | ## "main" stage 285 | ################################################## 286 | 287 | FROM base AS main 288 | 289 | # Install system packages 290 | RUN export DEBIAN_FRONTEND=noninteractive \ 291 | && apt-get update \ 292 | && apt-get install -y --no-install-recommends \ 293 | fbi \ 294 | fonts-dejavu \ 295 | gawk \ 296 | gettext-base \ 297 | iproute2 \ 298 | iw \ 299 | libaec0 \ 300 | libarchive13 \ 301 | libatlas3-base \ 302 | libavcodec58 \ 303 | libavformat58 \ 304 | libavresample4 \ 305 | libavutil56 \ 306 | libbz2-1.0 \ 307 | libevent-2.1-6 \ 308 | libevent-pthreads-2.1-6 \ 309 | libfreetype6 \ 310 | libfuse2 \ 311 | libgfortran5 \ 312 | libgstreamer-plugins-base1.0-0 \ 313 | libgstreamer1.0-0 \ 314 | libgtk-3-0 \ 315 | libhdf5-103 \ 316 | libhwloc5 \ 317 | libice6 \ 318 | libjbig0 \ 319 | libjpeg62-turbo \ 320 | liblcms2-2 \ 321 | libltdl7 \ 322 | libnetfilter-queue1 \ 323 | libopenexr23 \ 324 | libopenjp2-7 \ 325 | libopenmpi3 \ 326 | libpcap0.8 \ 327 | libpng16-16 \ 328 | libqt4-opengl \ 329 | libqt4-test \ 330 | libqtcore4 \ 331 | libsm6 \ 332 | libssl1.1 \ 333 | libswresample3 \ 334 | libswscale5 \ 335 | libsz2 \ 336 | libtiff5 \ 337 | libusb-1.0-0 \ 338 | libvpx5 \ 339 | libwebp6 \ 340 | libwebpdemux2 \ 341 | libwebpmux3 \ 342 | libzstd1 \ 343 | nano \ 344 | net-tools \ 345 | netcat-openbsd \ 346 | openmpi-bin \ 347 | python3 \ 348 | python3-distutils \ 349 | systemd \ 350 | tcpdump \ 351 | wireless-tools \ 352 | m4_ifelse(IS_RASPIOS, 1, [[libjasper1]]) \ 353 | && rm -rf /var/lib/apt/lists/* 354 | 355 | # Remove default systemd unit dependencies 356 | RUN find \ 357 | /lib/systemd/system/*.target.wants/ \ 358 | /etc/systemd/system/*.target.wants/ \ 359 | -not -name 'systemd-tmpfiles-setup.service' \ 360 | -not -name 'systemd-journal*' \ 361 | -mindepth 1 -print -delete 362 | 363 | # Copy systemd config 364 | COPY --chown=root:root ./config/systemd/ /etc/systemd/ 365 | RUN find /etc/systemd/ -type f -name '*.conf' -not -perm 0644 -exec chmod 0644 '{}' ';' 366 | 367 | # Copy Nexutil build 368 | m4_ifelse(IS_RASPIOS, 1, [[COPY --from=build-nexutil --chown=root:root /usr/local/bin/nexutil /usr/local/bin/nexutil]]) 369 | 370 | # Copy Bettercap build 371 | COPY --from=build-bettercap --chown=root:root /usr/local/bin/bettercap /usr/local/bin/bettercap 372 | COPY --from=build-bettercap --chown=root:root /usr/local/share/bettercap/ /usr/local/share/bettercap/ 373 | 374 | # Copy Bettercap caplets 375 | COPY --chown=root:root ./config/bettercap/caplets/ /usr/local/share/bettercap/caplets/ 376 | RUN find /usr/local/share/bettercap/caplets/ -type d -not -perm 0755 -exec chmod 0755 '{}' ';' 377 | RUN find /usr/local/share/bettercap/caplets/ -type f -not -perm 0644 -exec chmod 0644 '{}' ';' 378 | 379 | # Copy PwnGRID build 380 | COPY --from=build-pwngrid --chown=root:root /usr/local/bin/pwngrid /usr/local/bin/pwngrid 381 | 382 | # Copy Pwnagotchi build 383 | COPY --from=build-pwnagotchi --chown=root:root /usr/local/lib/pwnagotchi/ /usr/local/lib/pwnagotchi/ 384 | RUN ln -s /usr/local/lib/pwnagotchi/bin/pwnagotchi /usr/local/bin/pwnagotchi 385 | 386 | # Copy Pwnagotchi config 387 | COPY --chown=root:root ./config/pwnagotchi/ /etc/pwnagotchi/ 388 | RUN find /etc/pwnagotchi/ -type d -not -perm 0755 -exec chmod 0755 '{}' ';' 389 | RUN find /etc/pwnagotchi/ -type f -not -perm 0644 -exec chmod 0644 '{}' ';' 390 | 391 | # Copy scripts 392 | COPY --chown=root:root ./scripts/bin/ /usr/local/bin/ 393 | RUN find /usr/local/bin/ -type d -not -perm 0755 -exec chmod 0755 '{}' ';' 394 | RUN find /usr/local/bin/ -type f -not -perm 0755 -exec chmod 0755 '{}' ';' 395 | 396 | # Copy and enable services 397 | COPY --chown=root:root ./scripts/service/ /etc/systemd/system/ 398 | RUN find /etc/systemd/system/ -type f -regex '.+\.\(target\|service\)' -not -perm 0644 -exec chmod 0644 '{}' ';' 399 | RUN systemctl set-default container.target 400 | RUN systemctl enable bettercap.service pwnagotchi.service pwngrid.service 401 | 402 | # Environment 403 | ENV PWNAGOTCHI_NAME=pwnagotchi 404 | ENV PWNAGOTCHI_LANG=en 405 | ENV PWNAGOTCHI_USERNAME=pwnagotchi 406 | ENV PWNAGOTCHI_PASSWORD=pwnagotchi 407 | ENV PWNAGOTCHI_IFACE_NET=phy0 408 | ENV PWNAGOTCHI_IFACE_MON=mon0 409 | ENV PWNAGOTCHI_IFACE_USB=usb0 410 | ENV PWNAGOTCHI_MAX_BLIND_EPOCHS=10 411 | ENV PWNAGOTCHI_WHITELIST=[] 412 | ENV PWNAGOTCHI_FILTER= 413 | ENV PWNAGOTCHI_WEB_ENABLED=true 414 | ENV PWNAGOTCHI_WEB_ADDRESS=0.0.0.0 415 | ENV PWNAGOTCHI_DISPLAY_ENABLED=true 416 | ENV PWNAGOTCHI_DISPLAY_ROTATION=180 417 | ENV PWNAGOTCHI_DISPLAY_TYPE=waveshare_2 418 | ENV PWNAGOTCHI_PLUGIN_GRID_ENABLED=true 419 | ENV PWNAGOTCHI_PLUGIN_GRID_REPORT=true 420 | ENV PWNAGOTCHI_PLUGIN_GRID_EXCLUDE=[] 421 | ENV PWNAGOTCHI_PLUGIN_LED_ENABLED=true 422 | ENV PWNAGOTCHI_PLUGIN_MEMTEMP_ENABLED=true 423 | ENV PWNAGOTCHI_PLUGIN_SESSION_STATS_ENABLED=true 424 | ENV PWNAGOTCHI_PERSONALITY_ADVERTISE=true 425 | ENV PWNAGOTCHI_PERSONALITY_DEAUTH=true 426 | ENV PWNAGOTCHI_PERSONALITY_ASSOCIATE=true 427 | ENV PWNAGOTCHI_PERSONALITY_CHANNELS=[] 428 | 429 | STOPSIGNAL SIGRTMIN+3 430 | HEALTHCHECK --start-period=30s --interval=10s --timeout=5s --retries=1 CMD ["/usr/local/bin/container-healthcheck"] 431 | ENTRYPOINT ["/usr/local/bin/container-init"] 432 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © Héctor Molinero Fernández 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | SHELL := /bin/sh 4 | .SHELLFLAGS := -euc 5 | 6 | DOCKER := $(shell command -v docker 2>/dev/null) 7 | GIT := $(shell command -v git 2>/dev/null) 8 | M4 := $(shell command -v m4 2>/dev/null) 9 | 10 | DISTDIR := ./dist 11 | DOCKERFILE_TEMPLATE := ./Dockerfile.m4 12 | 13 | IMAGE_REGISTRY := docker.io 14 | IMAGE_NAMESPACE := hectorm 15 | IMAGE_PROJECT := pwnagotchi 16 | IMAGE_NAME := $(IMAGE_REGISTRY)/$(IMAGE_NAMESPACE)/$(IMAGE_PROJECT) 17 | ifeq ($(shell '$(GIT)' status --porcelain 2>/dev/null),) 18 | IMAGE_GIT_TAG := $(shell '$(GIT)' tag --list --contains HEAD 2>/dev/null) 19 | IMAGE_GIT_SHA := $(shell '$(GIT)' rev-parse --verify --short HEAD 2>/dev/null) 20 | IMAGE_VERSION := $(if $(IMAGE_GIT_TAG),$(IMAGE_GIT_TAG),$(if $(IMAGE_GIT_SHA),$(IMAGE_GIT_SHA),nil)) 21 | else 22 | IMAGE_GIT_BRANCH := $(shell '$(GIT)' symbolic-ref --short HEAD 2>/dev/null) 23 | IMAGE_VERSION := $(if $(IMAGE_GIT_BRANCH),$(IMAGE_GIT_BRANCH)-dirty,nil) 24 | endif 25 | 26 | IMAGE_BUILD_OPTS := 27 | 28 | IMAGE_NATIVE_DOCKERFILE := $(DISTDIR)/Dockerfile 29 | IMAGE_NATIVE_TARBALL := $(DISTDIR)/$(IMAGE_PROJECT).tzst 30 | IMAGE_GENERIC_AMD64_DOCKERFILE := $(DISTDIR)/Dockerfile.generic-amd64 31 | IMAGE_GENERIC_AMD64_TARBALL := $(DISTDIR)/$(IMAGE_PROJECT).generic-amd64.tzst 32 | IMAGE_RASPIOS_ARM64V8_DOCKERFILE := $(DISTDIR)/Dockerfile.raspios-arm64v8 33 | IMAGE_RASPIOS_ARM64V8_TARBALL := $(DISTDIR)/$(IMAGE_PROJECT).raspios-arm64v8.tzst 34 | IMAGE_RASPIOS_ARM32V7_DOCKERFILE := $(DISTDIR)/Dockerfile.raspios-arm32v7 35 | IMAGE_RASPIOS_ARM32V7_TARBALL := $(DISTDIR)/$(IMAGE_PROJECT).raspios-arm32v7.tzst 36 | IMAGE_RASPIOS_ARM32V6_DOCKERFILE := $(DISTDIR)/Dockerfile.raspios-arm32v6 37 | IMAGE_RASPIOS_ARM32V6_TARBALL := $(DISTDIR)/$(IMAGE_PROJECT).raspios-arm32v6.tzst 38 | 39 | export DOCKER_BUILDKIT := 1 40 | export BUILDKIT_PROGRESS := plain 41 | 42 | ################################################## 43 | ## "all" target 44 | ################################################## 45 | 46 | .PHONY: all 47 | all: save-native-image 48 | 49 | ################################################## 50 | ## "build-*" targets 51 | ################################################## 52 | 53 | .PHONY: build-native-image 54 | build-native-image: $(IMAGE_NATIVE_DOCKERFILE) 55 | 56 | $(IMAGE_NATIVE_DOCKERFILE): $(DOCKERFILE_TEMPLATE) 57 | mkdir -p '$(DISTDIR)' 58 | '$(M4)' \ 59 | --prefix-builtins \ 60 | --define=DEBIAN_IMAGE_NAME=docker.io/debian \ 61 | --define=DEBIAN_IMAGE_TAG=buster \ 62 | '$(DOCKERFILE_TEMPLATE)' > '$@' 63 | '$(DOCKER)' build $(IMAGE_BUILD_OPTS) \ 64 | --tag '$(IMAGE_NAME):$(IMAGE_VERSION)' \ 65 | --tag '$(IMAGE_NAME):latest' \ 66 | --file '$@' ./ 67 | 68 | .PHONY: build-cross-images 69 | build-cross-images: build-generic-amd64-image build-raspios-arm64v8-image build-raspios-arm32v7-image build-raspios-arm32v6-image 70 | 71 | .PHONY: build-generic-amd64-image 72 | build-generic-amd64-image: $(IMAGE_GENERIC_AMD64_DOCKERFILE) 73 | 74 | $(IMAGE_GENERIC_AMD64_DOCKERFILE): $(DOCKERFILE_TEMPLATE) 75 | mkdir -p '$(DISTDIR)' 76 | '$(M4)' \ 77 | --prefix-builtins \ 78 | --define=DEBIAN_IMAGE_NAME=docker.io/amd64/debian \ 79 | --define=DEBIAN_IMAGE_TAG=buster \ 80 | '$(DOCKERFILE_TEMPLATE)' > '$@' 81 | '$(DOCKER)' build $(IMAGE_BUILD_OPTS) \ 82 | --tag '$(IMAGE_NAME):$(IMAGE_VERSION)-generic-amd64' \ 83 | --tag '$(IMAGE_NAME):latest-generic-amd64' \ 84 | --platform linux/amd64 \ 85 | --file '$@' ./ 86 | 87 | .PHONY: build-raspios-arm64v8-image 88 | build-raspios-arm64v8-image: $(IMAGE_RASPIOS_ARM64V8_DOCKERFILE) 89 | 90 | $(IMAGE_RASPIOS_ARM64V8_DOCKERFILE): $(DOCKERFILE_TEMPLATE) 91 | mkdir -p '$(DISTDIR)' 92 | '$(M4)' \ 93 | --prefix-builtins \ 94 | --define=DEBIAN_IMAGE_NAME=docker.io/balenalib/rpi-raspbian \ 95 | --define=DEBIAN_IMAGE_TAG=buster \ 96 | '$(DOCKERFILE_TEMPLATE)' > '$@' 97 | '$(DOCKER)' build $(IMAGE_BUILD_OPTS) \ 98 | --tag '$(IMAGE_NAME):$(IMAGE_VERSION)-raspios-arm64v8' \ 99 | --tag '$(IMAGE_NAME):latest-raspios-arm64v8' \ 100 | --platform linux/arm64/v8 \ 101 | --file '$@' ./ 102 | 103 | .PHONY: build-raspios-arm32v7-image 104 | build-raspios-arm32v7-image: $(IMAGE_RASPIOS_ARM32V7_DOCKERFILE) 105 | 106 | $(IMAGE_RASPIOS_ARM32V7_DOCKERFILE): $(DOCKERFILE_TEMPLATE) 107 | mkdir -p '$(DISTDIR)' 108 | '$(M4)' \ 109 | --prefix-builtins \ 110 | --define=DEBIAN_IMAGE_NAME=docker.io/balenalib/rpi-raspbian \ 111 | --define=DEBIAN_IMAGE_TAG=buster \ 112 | '$(DOCKERFILE_TEMPLATE)' > '$@' 113 | '$(DOCKER)' build $(IMAGE_BUILD_OPTS) \ 114 | --tag '$(IMAGE_NAME):$(IMAGE_VERSION)-raspios-arm32v7' \ 115 | --tag '$(IMAGE_NAME):latest-raspios-arm32v7' \ 116 | --platform linux/arm/v7 \ 117 | --file '$@' ./ 118 | 119 | .PHONY: build-raspios-arm32v6-image 120 | build-raspios-arm32v6-image: $(IMAGE_RASPIOS_ARM32V6_DOCKERFILE) 121 | 122 | $(IMAGE_RASPIOS_ARM32V6_DOCKERFILE): $(DOCKERFILE_TEMPLATE) 123 | mkdir -p '$(DISTDIR)' 124 | '$(M4)' \ 125 | --prefix-builtins \ 126 | --define=DEBIAN_IMAGE_NAME=docker.io/balenalib/rpi-raspbian \ 127 | --define=DEBIAN_IMAGE_TAG=buster \ 128 | '$(DOCKERFILE_TEMPLATE)' > '$@' 129 | '$(DOCKER)' build $(IMAGE_BUILD_OPTS) \ 130 | --tag '$(IMAGE_NAME):$(IMAGE_VERSION)-raspios-arm32v6' \ 131 | --tag '$(IMAGE_NAME):latest-raspios-arm32v6' \ 132 | --platform linux/arm/v6 \ 133 | --file '$@' ./ 134 | 135 | ################################################## 136 | ## "save-*" targets 137 | ################################################## 138 | 139 | define save_image 140 | '$(DOCKER)' save '$(1)' | zstd -T0 > '$(2)' 141 | endef 142 | 143 | .PHONY: save-native-image 144 | save-native-image: $(IMAGE_NATIVE_TARBALL) 145 | 146 | $(IMAGE_NATIVE_TARBALL): $(IMAGE_NATIVE_DOCKERFILE) 147 | $(call save_image,$(IMAGE_NAME):$(IMAGE_VERSION),$@) 148 | 149 | .PHONY: save-cross-images 150 | save-cross-images: save-generic-amd64-image save-raspios-arm64v8-image save-raspios-arm32v7-image save-raspios-arm32v6-image 151 | 152 | .PHONY: save-generic-amd64-image 153 | save-generic-amd64-image: $(IMAGE_GENERIC_AMD64_TARBALL) 154 | 155 | $(IMAGE_GENERIC_AMD64_TARBALL): $(IMAGE_GENERIC_AMD64_DOCKERFILE) 156 | $(call save_image,$(IMAGE_NAME):$(IMAGE_VERSION)-generic-amd64,$@) 157 | 158 | .PHONY: save-raspios-arm64v8-image 159 | save-raspios-arm64v8-image: $(IMAGE_RASPIOS_ARM64V8_TARBALL) 160 | 161 | $(IMAGE_RASPIOS_ARM64V8_TARBALL): $(IMAGE_RASPIOS_ARM64V8_DOCKERFILE) 162 | $(call save_image,$(IMAGE_NAME):$(IMAGE_VERSION)-raspios-arm64v8,$@) 163 | 164 | .PHONY: save-raspios-arm32v7-image 165 | save-raspios-arm32v7-image: $(IMAGE_RASPIOS_ARM32V7_TARBALL) 166 | 167 | $(IMAGE_RASPIOS_ARM32V7_TARBALL): $(IMAGE_RASPIOS_ARM32V7_DOCKERFILE) 168 | $(call save_image,$(IMAGE_NAME):$(IMAGE_VERSION)-raspios-arm32v7,$@) 169 | 170 | .PHONY: save-raspios-arm32v6-image 171 | save-raspios-arm32v6-image: $(IMAGE_RASPIOS_ARM32V6_TARBALL) 172 | 173 | $(IMAGE_RASPIOS_ARM32V6_TARBALL): $(IMAGE_RASPIOS_ARM32V6_DOCKERFILE) 174 | $(call save_image,$(IMAGE_NAME):$(IMAGE_VERSION)-raspios-arm32v6,$@) 175 | 176 | ################################################## 177 | ## "load-*" targets 178 | ################################################## 179 | 180 | define load_image 181 | zstd -dc '$(1)' | '$(DOCKER)' load 182 | endef 183 | 184 | define tag_image 185 | '$(DOCKER)' tag '$(1)' '$(2)' 186 | endef 187 | 188 | .PHONY: load-native-image 189 | load-native-image: 190 | $(call load_image,$(IMAGE_NATIVE_TARBALL)) 191 | $(call tag_image,$(IMAGE_NAME):$(IMAGE_VERSION),$(IMAGE_NAME):latest) 192 | 193 | .PHONY: load-cross-images 194 | load-cross-images: load-generic-amd64-image load-raspios-arm64v8-image load-raspios-arm32v7-image load-raspios-arm32v6-image 195 | 196 | .PHONY: load-generic-amd64-image 197 | load-generic-amd64-image: 198 | $(call load_image,$(IMAGE_GENERIC_AMD64_TARBALL)) 199 | $(call tag_image,$(IMAGE_NAME):$(IMAGE_VERSION)-generic-amd64,$(IMAGE_NAME):latest-generic-amd64) 200 | 201 | .PHONY: load-raspios-arm64v8-image 202 | load-raspios-arm64v8-image: 203 | $(call load_image,$(IMAGE_RASPIOS_ARM64V8_TARBALL)) 204 | $(call tag_image,$(IMAGE_NAME):$(IMAGE_VERSION)-raspios-arm64v8,$(IMAGE_NAME):latest-raspios-arm64v8) 205 | 206 | .PHONY: load-raspios-arm32v7-image 207 | load-raspios-arm32v7-image: 208 | $(call load_image,$(IMAGE_RASPIOS_ARM32V7_TARBALL)) 209 | $(call tag_image,$(IMAGE_NAME):$(IMAGE_VERSION)-raspios-arm32v7,$(IMAGE_NAME):latest-raspios-arm32v7) 210 | 211 | .PHONY: load-raspios-arm32v6-image 212 | load-raspios-arm32v6-image: 213 | $(call load_image,$(IMAGE_RASPIOS_ARM32V6_TARBALL)) 214 | $(call tag_image,$(IMAGE_NAME):$(IMAGE_VERSION)-raspios-arm32v6,$(IMAGE_NAME):latest-raspios-arm32v6) 215 | 216 | ################################################## 217 | ## "push-*" targets 218 | ################################################## 219 | 220 | define push_image 221 | '$(DOCKER)' push '$(1)' 222 | endef 223 | 224 | define push_cross_manifest 225 | '$(DOCKER)' manifest create --amend '$(1)' '$(2)-generic-amd64' '$(2)-raspios-arm64v8' '$(2)-raspios-arm32v7' '$(2)-raspios-arm32v6' 226 | '$(DOCKER)' manifest annotate '$(1)' '$(2)-generic-amd64' --os linux --arch amd64 227 | '$(DOCKER)' manifest annotate '$(1)' '$(2)-raspios-arm64v8' --os linux --arch arm64 --variant v8 228 | '$(DOCKER)' manifest annotate '$(1)' '$(2)-raspios-arm32v7' --os linux --arch arm --variant v7 229 | '$(DOCKER)' manifest annotate '$(1)' '$(2)-raspios-arm32v6' --os linux --arch arm --variant v6 230 | '$(DOCKER)' manifest push --purge '$(1)' 231 | endef 232 | 233 | .PHONY: push-native-image 234 | push-native-image: 235 | @printf '%s\n' 'Unimplemented' 236 | 237 | .PHONY: push-cross-images 238 | push-cross-images: push-generic-amd64-image push-raspios-arm64v8-image push-raspios-arm32v7-image push-raspios-arm32v6-image 239 | 240 | .PHONY: push-generic-amd64-image 241 | push-generic-amd64-image: 242 | $(call push_image,$(IMAGE_NAME):$(IMAGE_VERSION)-generic-amd64) 243 | $(call push_image,$(IMAGE_NAME):latest-generic-amd64) 244 | 245 | .PHONY: push-raspios-arm64v8-image 246 | push-raspios-arm64v8-image: 247 | $(call push_image,$(IMAGE_NAME):$(IMAGE_VERSION)-raspios-arm64v8) 248 | $(call push_image,$(IMAGE_NAME):latest-raspios-arm64v8) 249 | 250 | .PHONY: push-raspios-arm32v7-image 251 | push-raspios-arm32v7-image: 252 | $(call push_image,$(IMAGE_NAME):$(IMAGE_VERSION)-raspios-arm32v7) 253 | $(call push_image,$(IMAGE_NAME):latest-raspios-arm32v7) 254 | 255 | .PHONY: push-raspios-arm32v6-image 256 | push-raspios-arm32v6-image: 257 | $(call push_image,$(IMAGE_NAME):$(IMAGE_VERSION)-raspios-arm32v6) 258 | $(call push_image,$(IMAGE_NAME):latest-raspios-arm32v6) 259 | 260 | push-cross-manifest: 261 | $(call push_cross_manifest,$(IMAGE_NAME):$(IMAGE_VERSION),$(IMAGE_NAME):$(IMAGE_VERSION)) 262 | $(call push_cross_manifest,$(IMAGE_NAME):latest,$(IMAGE_NAME):latest) 263 | 264 | ################################################## 265 | ## "binfmt-*" targets 266 | ################################################## 267 | 268 | .PHONY: binfmt-register 269 | binfmt-register: 270 | '$(DOCKER)' run --rm --privileged docker.io/hectorm/qemu-user-static:latest --reset --persistent yes --credential yes 271 | 272 | ################################################## 273 | ## "version" target 274 | ################################################## 275 | 276 | .PHONY: version 277 | version: 278 | @LATEST_IMAGE_VERSION=$$('$(GIT)' describe --abbrev=0 2>/dev/null || printf 'v0'); \ 279 | if printf '%s' "$${LATEST_IMAGE_VERSION:?}" | grep -q '^v[0-9]\{1,\}$$'; then \ 280 | NEW_IMAGE_VERSION=$$(awk -v v="$${LATEST_IMAGE_VERSION:?}" 'BEGIN {printf("v%.0f", substr(v,2)+1)}'); \ 281 | '$(GIT)' commit --allow-empty -m "$${NEW_IMAGE_VERSION:?}"; \ 282 | '$(GIT)' tag -a "$${NEW_IMAGE_VERSION:?}" -m "$${NEW_IMAGE_VERSION:?}"; \ 283 | else \ 284 | >&2 printf 'Malformed version string: %s\n' "$${LATEST_IMAGE_VERSION:?}"; \ 285 | exit 1; \ 286 | fi 287 | 288 | ################################################## 289 | ## "clean" target 290 | ################################################## 291 | 292 | .PHONY: clean 293 | clean: 294 | rm -f '$(IMAGE_NATIVE_DOCKERFILE)' '$(IMAGE_GENERIC_AMD64_DOCKERFILE)' '$(IMAGE_RASPIOS_ARM64V8_DOCKERFILE)' '$(IMAGE_RASPIOS_ARM32V7_DOCKERFILE)' '$(IMAGE_RASPIOS_ARM32V6_DOCKERFILE)' 295 | rm -f '$(IMAGE_NATIVE_TARBALL)' '$(IMAGE_GENERIC_AMD64_TARBALL)' '$(IMAGE_RASPIOS_ARM64V8_TARBALL)' '$(IMAGE_RASPIOS_ARM32V7_TARBALL)' '$(IMAGE_RASPIOS_ARM32V6_TARBALL)' 296 | if [ -d '$(DISTDIR)' ] && [ -z "$$(ls -A '$(DISTDIR)')" ]; then rmdir '$(DISTDIR)'; fi 297 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pwnagotchi on Docker 2 | 3 | I leave here, without any support, the Docker image that I use for my [Pwnagotchi](https://pwnagotchi.ai). 4 | 5 | ![My Pwnagotchi](./resources/pwnagotchi.jpg) 6 | -------------------------------------------------------------------------------- /config/bettercap/caplets/pwnagotchi-auto.cap: -------------------------------------------------------------------------------- 1 | # enable interface monitor mode and define wifi interface to be ${PWNAGOTCHI_IFACE_MON} 2 | set wifi.interface ${PWNAGOTCHI_IFACE_MON} 3 | 4 | # api listening on http://127.0.0.1:8081/ and ui to http://127.0.0.1 5 | set api.rest.address 127.0.0.1 6 | set api.rest.port 8081 7 | set api.rest.username ${PWNAGOTCHI_USERNAME} 8 | set api.rest.password ${PWNAGOTCHI_PASSWORD} 9 | 10 | # go! 11 | api.rest on 12 | -------------------------------------------------------------------------------- /config/bettercap/caplets/pwnagotchi-manual.cap: -------------------------------------------------------------------------------- 1 | # enable interface monitor mode and define wifi interface to be ${PWNAGOTCHI_IFACE_MON} 2 | set wifi.interface ${PWNAGOTCHI_IFACE_MON} 3 | 4 | # api listening on http://0.0.0.0:8081/ and ui to http://0.0.0.0 5 | set api.rest.address 0.0.0.0 6 | set api.rest.port 8081 7 | set http.server.address 0.0.0.0 8 | set http.server.port 80 9 | set http.server.path /usr/local/share/bettercap/ui 10 | set api.rest.username ${PWNAGOTCHI_USERNAME} 11 | set api.rest.password ${PWNAGOTCHI_PASSWORD} 12 | 13 | # go! 14 | api.rest on 15 | http.server on 16 | -------------------------------------------------------------------------------- /config/pwnagotchi/config.toml: -------------------------------------------------------------------------------- 1 | # Main 2 | main.name = "${PWNAGOTCHI_NAME}" 3 | main.lang = "${PWNAGOTCHI_LANG}" 4 | main.iface = "${PWNAGOTCHI_IFACE_MON}" 5 | main.mon_max_blind_epochs = ${PWNAGOTCHI_MAX_BLIND_EPOCHS} 6 | main.whitelist = ${PWNAGOTCHI_WHITELIST} 7 | main.filter = "${PWNAGOTCHI_FILTER}" 8 | main.custom_plugins = "/usr/local/lib/pwnagotchi/plugins/" 9 | 10 | # Logs 11 | main.log.path = "" # Print only to console 12 | main.log.rotation.enabled = false 13 | 14 | # Grid plugin 15 | main.plugins.grid.enabled = ${PWNAGOTCHI_PLUGIN_GRID_ENABLED} 16 | main.plugins.grid.report = ${PWNAGOTCHI_PLUGIN_GRID_REPORT} 17 | main.plugins.grid.exclude = ${PWNAGOTCHI_PLUGIN_GRID_EXCLUDE} 18 | 19 | # Led plugin 20 | main.plugins.led.enabled = ${PWNAGOTCHI_PLUGIN_LED_ENABLED} 21 | 22 | # Memtemp plugin 23 | main.plugins.memtemp.enabled = ${PWNAGOTCHI_PLUGIN_MEMTEMP_ENABLED} 24 | 25 | # Session stats plugin 26 | main.plugins.session-stats.enabled = ${PWNAGOTCHI_PLUGIN_SESSION_STATS_ENABLED} 27 | 28 | # Auto update plugin 29 | main.plugins.auto-update.enabled = false 30 | 31 | # Webcfg plugin 32 | main.plugins.webcfg.enabled = false 33 | 34 | # Web UI 35 | ui.web.enabled = ${PWNAGOTCHI_WEB_ENABLED} 36 | ui.web.address = "${PWNAGOTCHI_WEB_ADDRESS}" 37 | ui.web.username = "${PWNAGOTCHI_USERNAME}" 38 | ui.web.password = """${PWNAGOTCHI_PASSWORD}""" 39 | 40 | # Display UI 41 | ui.display.enabled = ${PWNAGOTCHI_DISPLAY_ENABLED} 42 | ui.display.rotation = ${PWNAGOTCHI_DISPLAY_ROTATION} 43 | ui.display.type = "${PWNAGOTCHI_DISPLAY_TYPE}" 44 | 45 | # Bettercap 46 | bettercap.username = "${PWNAGOTCHI_USERNAME}" 47 | bettercap.password = """${PWNAGOTCHI_PASSWORD}""" 48 | 49 | # Personality 50 | personality.advertise = ${PWNAGOTCHI_PERSONALITY_ADVERTISE} 51 | personality.deauth = ${PWNAGOTCHI_PERSONALITY_DEAUTH} 52 | personality.associate = ${PWNAGOTCHI_PERSONALITY_ASSOCIATE} 53 | personality.channels = ${PWNAGOTCHI_PERSONALITY_CHANNELS} 54 | -------------------------------------------------------------------------------- /config/systemd/journald.conf: -------------------------------------------------------------------------------- 1 | [Journal] 2 | Storage=auto 3 | RuntimeMaxUse=1M 4 | SystemMaxUse=10M 5 | -------------------------------------------------------------------------------- /packer/Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | SHELL := /bin/sh 4 | .SHELLFLAGS := -euc 5 | 6 | PACKER := $(shell command -v packer 2>/dev/null) 7 | QEMU_IMG := $(shell command -v qemu-img 2>/dev/null) 8 | 9 | PACKER_WORK_DIR := ./ 10 | PACKER_CACHE_DIR := ./packer_cache/ 11 | PACKER_ARMHF_OUT := ./dist/armhf/pwnagotchi.img 12 | 13 | ifneq ($(SUDO_USER),) 14 | export HOME := $(shell getent passwd "$(SUDO_USER)" | cut -d: -f6) 15 | endif 16 | 17 | ################################################## 18 | ## "all" target 19 | ################################################## 20 | 21 | .PHONY: all 22 | all: build 23 | 24 | ################################################## 25 | ## "build" target 26 | ################################################## 27 | 28 | .PHONY: build 29 | build: build-armhf 30 | 31 | .PHONY: build-armhf 32 | build-armhf: $(PACKER_ARMHF_OUT) 33 | 34 | $(PACKER_ARMHF_OUT): 35 | mkdir -p '$(dir $(PACKER_ARMHF_OUT))' 36 | '$(PACKER)' build -force -only=arm-image.armhf '$(PACKER_WORK_DIR)' 37 | '$(QEMU_IMG)' resize -f raw '$(PACKER_ARMHF_OUT)' 8G 38 | 39 | ################################################## 40 | ## "clean" target 41 | ################################################## 42 | 43 | .PHONY: clean 44 | clean: 45 | rm -rf '$(PACKER_ARMHF_OUT)' '$(PACKER_CACHE_DIR)' 46 | -------------------------------------------------------------------------------- /packer/build.pkr.hcl: -------------------------------------------------------------------------------- 1 | build { 2 | sources = [ 3 | "source.arm-image.armhf" 4 | ] 5 | 6 | provisioner "file" { 7 | direction = "upload" 8 | source = "./rootfs" 9 | destination = "/tmp" 10 | } 11 | 12 | provisioner "shell" { 13 | environment_vars = [ 14 | "TZ=UTC", 15 | "LANG=en_US.UTF-8", 16 | "LC_ALL=en_US.UTF-8", 17 | "DPKG_FORCE=confold", 18 | "DEBIAN_FRONTEND=noninteractive", 19 | "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" 20 | ] 21 | skip_clean = true 22 | execute_command = "/usr/bin/env -i {{ .Vars }} /bin/sh -eux {{ .Path }}" 23 | inline = [ 24 | < /etc/apt/sources.list.d/docker.list 77 | apt-get update && apt-get install -y docker-ce 78 | EOF 79 | , 80 | </dev/null; do sleep 1; done 29 | 30 | # Create a snapshot image to preserve the original image 31 | qemu-img create -f qcow2 -b "${ORIGINAL_DISK:?}" -F raw "${SNAPSHOT_DISK:?}" 32 | qemu-img resize "${SNAPSHOT_DISK:?}" 8G 33 | 34 | # Remove keys from the known_hosts file 35 | ssh-keygen -R '[127.0.0.1]:1122' 2>/dev/null 36 | ssh-keygen -R '[localhost]:1122' 2>/dev/null 37 | 38 | # hostfwd helper 39 | hostfwd() { printf ',hostfwd=%s::%s-:%s' "$@"; } 40 | 41 | # Launch VM 42 | qemu-system-aarch64 \ 43 | -machine raspi3b \ 44 | -kernel "${TMP_DIR:?}"/kernel.img -dtb "${TMP_DIR:?}"/rpi.dtb \ 45 | -append "$(printf '%s ' \ 46 | 'console=ttyAMA0 root=/dev/mmcblk0p2 rootfstype=ext4 fsck.repair=no rootwait' \ 47 | 'systemd.mask=rpi-eeprom-update.service systemd.mask=hciuart.service' 48 | )" \ 49 | -nographic -serial mon:stdio \ 50 | -device usb-net,netdev=n0 \ 51 | -netdev user,id=n0,ipv4=on,ipv6=off,net=10.3.14.0/24,host=10.3.14.1"$(hostfwd \ 52 | tcp 1122 22 \ 53 | )" \ 54 | -drive file="${SNAPSHOT_DISK:?}",if=sd,format=qcow2 55 | -------------------------------------------------------------------------------- /packer/rootfs/boot/cmdline.txt: -------------------------------------------------------------------------------- 1 | console=serial0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait quiet init=/usr/lib/raspi-config/init_resize.sh modules-load=dwc2,g_ether g_ether.host_addr=ab:c3:98:f9:76:d0 g_ether.dev_addr=a3:35:e2:f2:58:b7 2 | -------------------------------------------------------------------------------- /packer/rootfs/boot/config.txt: -------------------------------------------------------------------------------- 1 | gpu_mem=16 2 | enable_uart=1 3 | dtoverlay=dwc2 4 | dtoverlay=spi1-3cs 5 | dtoverlay=i2c-rtc,mcp7941x 6 | dtparam=spi=on 7 | dtparam=i2c_arm=on 8 | dtparam=i2c1=on 9 | -------------------------------------------------------------------------------- /packer/rootfs/etc/default/crda: -------------------------------------------------------------------------------- 1 | REGDOMAIN=CH 2 | -------------------------------------------------------------------------------- /packer/rootfs/etc/default/dnsmasq: -------------------------------------------------------------------------------- 1 | ENABLED=1 2 | DNSMASQ_EXCEPT=lo 3 | CONFIG_DIR=/etc/dnsmasq.d,.dpkg-dist,.dpkg-old,.dpkg-new 4 | -------------------------------------------------------------------------------- /packer/rootfs/etc/default/keyboard: -------------------------------------------------------------------------------- 1 | XKBMODEL="pc105" 2 | XKBLAYOUT="us" 3 | XKBVARIANT="" 4 | XKBOPTIONS="" 5 | BACKSPACE="guess" 6 | -------------------------------------------------------------------------------- /packer/rootfs/etc/default/locale: -------------------------------------------------------------------------------- 1 | LANG=en_US.UTF-8 2 | -------------------------------------------------------------------------------- /packer/rootfs/etc/dnsmasq.d/01-usb0.conf: -------------------------------------------------------------------------------- 1 | # Disable local DNS as we only need DHCP 2 | port=0 3 | 4 | # Ensure we're authoritative, so any requests will get answered without timeout 5 | dhcp-authoritative 6 | 7 | # Do not check if the address is in use, since there is only one address to lease 8 | no-ping 9 | 10 | # Store leasefile in "/run/" so it gets cleared on reboot 11 | dhcp-leasefile=/run/dnsmasq.leases 12 | 13 | # Listen on usb0, i.e. ethernet gadget 14 | listen-address=10.3.14.15 15 | 16 | # Ensure IP leased is default route so we can access internet if shared 17 | dhcp-range=10.3.14.1,10.3.14.1,255.255.255.0,24h 18 | 19 | # Offer empty default route 20 | dhcp-option=3 21 | 22 | # Offer empty DNS 23 | dhcp-option=6 24 | 25 | # BOOTP is not recommended and gives leases FOREVER so 26 | # do not enable unless you fix the MAC address of usb0 from changing 27 | #bootp-dynamic 28 | -------------------------------------------------------------------------------- /packer/rootfs/etc/dphys-swapfile: -------------------------------------------------------------------------------- 1 | CONF_SWAPFILE=/var/swap 2 | CONF_SWAPSIZE=512 3 | -------------------------------------------------------------------------------- /packer/rootfs/etc/hostname: -------------------------------------------------------------------------------- 1 | pwnagotchi 2 | -------------------------------------------------------------------------------- /packer/rootfs/etc/hosts: -------------------------------------------------------------------------------- 1 | 127.0.0.1 localhost pwnagotchi 2 | 255.255.255.255 broadcasthost 3 | ::1 localhost ip6-localhost ip6-loopback 4 | fe00::0 ip6-localnet 5 | ff00::0 ip6-mcastprefix 6 | ff02::1 ip6-allnodes 7 | ff02::2 ip6-allrouters 8 | ff02::3 ip6-allhosts 9 | -------------------------------------------------------------------------------- /packer/rootfs/etc/locale.gen: -------------------------------------------------------------------------------- 1 | en_US.UTF-8 UTF-8 2 | -------------------------------------------------------------------------------- /packer/rootfs/etc/modules: -------------------------------------------------------------------------------- 1 | i2c-dev 2 | -------------------------------------------------------------------------------- /packer/rootfs/etc/network/interfaces.d/eth0-cfg: -------------------------------------------------------------------------------- 1 | allow-hotplug eth0 2 | iface eth0 inet dhcp 3 | -------------------------------------------------------------------------------- /packer/rootfs/etc/network/interfaces.d/lo-cfg: -------------------------------------------------------------------------------- 1 | auto lo 2 | iface lo inet loopback 3 | -------------------------------------------------------------------------------- /packer/rootfs/etc/network/interfaces.d/usb0-cfg: -------------------------------------------------------------------------------- 1 | allow-hotplug usb0 2 | iface usb0 inet static 3 | address 10.3.14.15/24 4 | gateway 10.3.14.1 5 | -------------------------------------------------------------------------------- /packer/rootfs/etc/network/interfaces.d/wlan0-cfg: -------------------------------------------------------------------------------- 1 | allow-hotplug wlan0 2 | iface wlan0 inet dhcp 3 | -------------------------------------------------------------------------------- /packer/rootfs/etc/pwnagotchi.env: -------------------------------------------------------------------------------- 1 | PWNAGOTCHI_USERNAME=pwnagotchi 2 | PWNAGOTCHI_PASSWORD=pwnagotchi 3 | PWNAGOTCHI_IFACE_NET=wlan0 4 | -------------------------------------------------------------------------------- /packer/rootfs/etc/rc.local: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | IP_ADDRESS=$(hostname -I ||:) 6 | if [ -n "${IP_ADDRESS?}" ]; then 7 | printf '%s\n' "My IP address is: ${IP_ADDRESS:?}" 8 | fi 9 | 10 | if [ -c /dev/vchiq ] && ! tvservice --status | grep -Eq 'HDMI|DVI'; then 11 | tvservice --off 12 | fi 13 | 14 | exit 0 15 | -------------------------------------------------------------------------------- /packer/rootfs/etc/sysctl.d/60-swappiness.conf: -------------------------------------------------------------------------------- 1 | vm.swappiness=1 2 | -------------------------------------------------------------------------------- /packer/rootfs/etc/systemd/system/pwnagotchi.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Pwnagotchi container 3 | Requires=docker.service 4 | After=docker.service 5 | 6 | [Service] 7 | Type=simple 8 | User=root 9 | Group=root 10 | Environment=CONTAINER=pwnagotchi 11 | Environment=IMAGE=docker.io/hectorm/pwnagotchi 12 | Environment=TAG=latest 13 | ExecStartPre=/usr/bin/install -d -m 0755 -o 0 -g 0 /var/lib/pwnagotchi/ 14 | ExecStartPre=/usr/bin/install -d -m 0755 -o 0 -g 0 /var/log/pwnagotchi/ 15 | ExecStartPre=/bin/sh -euc ' \ 16 | if docker ps -af name="${CONTAINER}" --format '{{.Names}}' | grep -Fxq "${CONTAINER}"; then \ 17 | docker rm -f "${CONTAINER}"; \ 18 | fi \ 19 | ' 20 | ExecStartPre=/bin/sh -euc ' \ 21 | if [ -e /var/lib/pwnagotchi-image.tar ]; then \ 22 | docker load -i /var/lib/pwnagotchi-image.tar; \ 23 | rm -f /var/lib/pwnagotchi-image.tar; \ 24 | docker system prune -f; \ 25 | fi \ 26 | ' 27 | ExecStart=/usr/bin/docker run \ 28 | --rm --name ${CONTAINER} --hostname %H \ 29 | --tty --attach STDOUT --attach STDERR \ 30 | --log-opt max-size=10m --log-opt max-file=1 \ 31 | --privileged --network host \ 32 | --mount type=tmpfs,dst=/run/,tmpfs-mode=0755 \ 33 | --mount type=tmpfs,dst=/tmp/,tmpfs-mode=1777 \ 34 | --mount type=bind,src=/sys/fs/cgroup/,dst=/sys/fs/cgroup/,ro \ 35 | --mount type=bind,src=/var/lib/pwnagotchi/,dst=/root/ \ 36 | --mount type=bind,src=/var/log/pwnagotchi/,dst=/var/log/journal/ \ 37 | --env PWNAGOTCHI_NAME=%H \ 38 | --env-file /etc/pwnagotchi.env \ 39 | ${IMAGE}:${TAG} 40 | ExecStop=/usr/bin/docker stop -t 60 ${CONTAINER} 41 | ExecStopPost=/usr/local/bin/reload-brcmfmac-driver 42 | TimeoutStartSec=1800s 43 | TimeoutStopSec=60s 44 | Restart=always 45 | 46 | [Install] 47 | WantedBy=multi-user.target 48 | -------------------------------------------------------------------------------- /packer/rootfs/etc/timezone: -------------------------------------------------------------------------------- 1 | UTC 2 | -------------------------------------------------------------------------------- /packer/rootfs/usr/local/bin/download-frozen-image: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Downloaded from: 4 | # https://github.com/moby/moby/blob/master/contrib/download-frozen-image-v2.sh 5 | 6 | set -eo pipefail 7 | 8 | # hello-world latest ef872312fe1b 3 months ago 910 B 9 | # hello-world latest ef872312fe1bbc5e05aae626791a47ee9b032efa8f3bda39cc0be7b56bfe59b9 3 months ago 910 B 10 | 11 | # debian latest f6fab3b798be 10 weeks ago 85.1 MB 12 | # debian latest f6fab3b798be3174f45aa1eb731f8182705555f89c9026d8c1ef230cbf8301dd 10 weeks ago 85.1 MB 13 | 14 | # check if essential commands are in our PATH 15 | for cmd in curl jq; do 16 | if ! command -v $cmd &> /dev/null; then 17 | echo >&2 "error: \"$cmd\" not found!" 18 | exit 1 19 | fi 20 | done 21 | 22 | usage() { 23 | echo "usage: $0 dir image[:tag][@digest] ..." 24 | echo " $0 /tmp/old-hello-world hello-world:latest@sha256:8be990ef2aeb16dbcb9271ddfe2610fa6658d13f6dfb8bc72074cc1ca36966a7" 25 | [ -z "$1" ] || exit "$1" 26 | } 27 | 28 | dir="$1" # dir for building tar in 29 | shift || usage 1 >&2 30 | 31 | if ! [ $# -gt 0 ] && [ "$dir" ]; then 32 | usage 2 >&2 33 | fi 34 | mkdir -p "$dir" 35 | 36 | # hacky workarounds for Bash 3 support (no associative arrays) 37 | images=() 38 | rm -f "$dir"/tags-*.tmp 39 | manifestJsonEntries=() 40 | doNotGenerateManifestJson= 41 | # repositories[busybox]='"latest": "...", "ubuntu-14.04": "..."' 42 | 43 | # bash v4 on Windows CI requires CRLF separator... and linux doesn't seem to care either way 44 | newlineIFS=$'\n' 45 | major=$(echo "${BASH_VERSION%%[^0.9]}" | cut -d. -f1) 46 | if [ "$major" -ge 4 ]; then 47 | newlineIFS=$'\r\n' 48 | fi 49 | 50 | registryBase='https://registry-1.docker.io' 51 | authBase='https://auth.docker.io' 52 | authService='registry.docker.io' 53 | 54 | # https://github.com/moby/moby/issues/33700 55 | fetch_blob() { 56 | local token="$1" 57 | shift 58 | local image="$1" 59 | shift 60 | local digest="$1" 61 | shift 62 | local targetFile="$1" 63 | shift 64 | local curlArgs=("$@") 65 | 66 | local curlHeaders 67 | curlHeaders="$( 68 | curl -S "${curlArgs[@]}" \ 69 | -H "Authorization: Bearer $token" \ 70 | "$registryBase/v2/$image/blobs/$digest" \ 71 | -o "$targetFile" \ 72 | -D- 73 | )" 74 | curlHeaders="$(echo "$curlHeaders" | tr -d '\r')" 75 | if grep -qE "^HTTP/[0-9].[0-9] 3" <<< "$curlHeaders"; then 76 | rm -f "$targetFile" 77 | 78 | local blobRedirect 79 | blobRedirect="$(echo "$curlHeaders" | awk -F ': ' 'tolower($1) == "location" { print $2; exit }')" 80 | if [ -z "$blobRedirect" ]; then 81 | echo >&2 "error: failed fetching '$image' blob '$digest'" 82 | echo "$curlHeaders" | head -1 >&2 83 | return 1 84 | fi 85 | 86 | curl -fSL "${curlArgs[@]}" \ 87 | "$blobRedirect" \ 88 | -o "$targetFile" 89 | fi 90 | } 91 | 92 | # handle 'application/vnd.docker.distribution.manifest.v2+json' manifest 93 | handle_single_manifest_v2() { 94 | local manifestJson="$1" 95 | shift 96 | 97 | local configDigest 98 | configDigest="$(echo "$manifestJson" | jq --raw-output '.config.digest')" 99 | local imageId="${configDigest#*:}" # strip off "sha256:" 100 | 101 | local configFile="$imageId.json" 102 | fetch_blob "$token" "$image" "$configDigest" "$dir/$configFile" -s 103 | 104 | local layersFs 105 | layersFs="$(echo "$manifestJson" | jq --raw-output --compact-output '.layers[]')" 106 | local IFS="$newlineIFS" 107 | local layers 108 | mapfile -t layers <<< "$layersFs" 109 | unset IFS 110 | 111 | echo "Downloading '$imageIdentifier' (${#layers[@]} layers)..." 112 | local layerId= 113 | local layerFiles=() 114 | for i in "${!layers[@]}"; do 115 | local layerMeta="${layers[$i]}" 116 | 117 | local layerMediaType 118 | layerMediaType="$(echo "$layerMeta" | jq --raw-output '.mediaType')" 119 | local layerDigest 120 | layerDigest="$(echo "$layerMeta" | jq --raw-output '.digest')" 121 | 122 | # save the previous layer's ID 123 | local parentId="$layerId" 124 | # create a new fake layer ID based on this layer's digest and the previous layer's fake ID 125 | layerId="$(echo "$parentId"$'\n'"$layerDigest" | sha256sum | cut -d' ' -f1)" 126 | # this accounts for the possibility that an image contains the same layer twice (and thus has a duplicate digest value) 127 | 128 | mkdir -p "$dir/$layerId" 129 | echo '1.0' > "$dir/$layerId/VERSION" 130 | 131 | if [ ! -s "$dir/$layerId/json" ]; then 132 | local parentJson 133 | parentJson="$(printf ', parent: "%s"' "$parentId")" 134 | local addJson 135 | addJson="$(printf '{ id: "%s"%s }' "$layerId" "${parentId:+$parentJson}")" 136 | # this starter JSON is taken directly from Docker's own "docker save" output for unimportant layers 137 | jq "$addJson + ." > "$dir/$layerId/json" <<- 'EOJSON' 138 | { 139 | "created": "0001-01-01T00:00:00Z", 140 | "container_config": { 141 | "Hostname": "", 142 | "Domainname": "", 143 | "User": "", 144 | "AttachStdin": false, 145 | "AttachStdout": false, 146 | "AttachStderr": false, 147 | "Tty": false, 148 | "OpenStdin": false, 149 | "StdinOnce": false, 150 | "Env": null, 151 | "Cmd": null, 152 | "Image": "", 153 | "Volumes": null, 154 | "WorkingDir": "", 155 | "Entrypoint": null, 156 | "OnBuild": null, 157 | "Labels": null 158 | } 159 | } 160 | EOJSON 161 | fi 162 | 163 | case "$layerMediaType" in 164 | application/vnd.docker.image.rootfs.diff.tar.gzip) 165 | local layerTar="$layerId/layer.tar" 166 | layerFiles=("${layerFiles[@]}" "$layerTar") 167 | # TODO figure out why "-C -" doesn't work here 168 | # "curl: (33) HTTP server doesn't seem to support byte ranges. Cannot resume." 169 | # "HTTP/1.1 416 Requested Range Not Satisfiable" 170 | if [ -f "$dir/$layerTar" ]; then 171 | # TODO hackpatch for no -C support :'( 172 | echo "skipping existing ${layerId:0:12}" 173 | continue 174 | fi 175 | local token 176 | token="$(curl -fsSL "$authBase/token?service=$authService&scope=repository:$image:pull" | jq --raw-output '.token')" 177 | fetch_blob "$token" "$image" "$layerDigest" "$dir/$layerTar" --progress-bar 178 | ;; 179 | 180 | *) 181 | echo >&2 "error: unknown layer mediaType ($imageIdentifier, $layerDigest): '$layerMediaType'" 182 | exit 1 183 | ;; 184 | esac 185 | done 186 | 187 | # change "$imageId" to be the ID of the last layer we added (needed for old-style "repositories" file which is created later -- specifically for older Docker daemons) 188 | imageId="$layerId" 189 | 190 | # munge the top layer image manifest to have the appropriate image configuration for older daemons 191 | local imageOldConfig 192 | imageOldConfig="$(jq --raw-output --compact-output '{ id: .id } + if .parent then { parent: .parent } else {} end' "$dir/$imageId/json")" 193 | jq --raw-output "$imageOldConfig + del(.history, .rootfs)" "$dir/$configFile" > "$dir/$imageId/json" 194 | 195 | local manifestJsonEntry 196 | manifestJsonEntry="$( 197 | echo '{}' | jq --raw-output '. + { 198 | Config: "'"$configFile"'", 199 | RepoTags: ["'"${image#library\/}:$tag"'"], 200 | Layers: '"$(echo '[]' | jq --raw-output ".$(for layerFile in "${layerFiles[@]}"; do echo " + [ \"$layerFile\" ]"; done)")"' 201 | }' 202 | )" 203 | manifestJsonEntries=("${manifestJsonEntries[@]}" "$manifestJsonEntry") 204 | } 205 | 206 | get_target_arch() { 207 | if [ -n "${TARGETARCH:-}" ]; then 208 | echo "${TARGETARCH}" 209 | return 0 210 | fi 211 | 212 | if type go > /dev/null; then 213 | go env GOARCH 214 | return 0 215 | fi 216 | 217 | if type dpkg > /dev/null; then 218 | debArch="$(dpkg --print-architecture)" 219 | case "${debArch}" in 220 | armel | armhf) 221 | echo "arm" 222 | return 0 223 | ;; 224 | *64el) 225 | echo "${debArch%el}le" 226 | return 0 227 | ;; 228 | *) 229 | echo "${debArch}" 230 | return 0 231 | ;; 232 | esac 233 | fi 234 | 235 | if type uname > /dev/null; then 236 | uArch="$(uname -m)" 237 | case "${uArch}" in 238 | x86_64) 239 | echo amd64 240 | return 0 241 | ;; 242 | arm | armv[0-9]*) 243 | echo arm 244 | return 0 245 | ;; 246 | aarch64) 247 | echo arm64 248 | return 0 249 | ;; 250 | mips*) 251 | echo >&2 "I see you are running on mips but I don't know how to determine endianness yet, so I cannot select a correct arch to fetch." 252 | echo >&2 "Consider installing \"go\" on the system which I can use to determine the correct arch or specify it explicitly by setting TARGETARCH" 253 | exit 1 254 | ;; 255 | *) 256 | echo "${uArch}" 257 | return 0 258 | ;; 259 | esac 260 | 261 | fi 262 | 263 | # default value 264 | echo >&2 "Unable to determine CPU arch, falling back to amd64. You can specify a target arch by setting TARGETARCH" 265 | echo amd64 266 | } 267 | 268 | while [ $# -gt 0 ]; do 269 | imageTag="$1" 270 | shift 271 | image="${imageTag%%[:@]*}" 272 | imageTag="${imageTag#*:}" 273 | digest="${imageTag##*@}" 274 | tag="${imageTag%%@*}" 275 | 276 | # add prefix library if passed official image 277 | if [[ "$image" != *"/"* ]]; then 278 | image="library/$image" 279 | fi 280 | 281 | imageFile="${image//\//_}" # "/" can't be in filenames :) 282 | 283 | token="$(curl -fsSL "$authBase/token?service=$authService&scope=repository:$image:pull" | jq --raw-output '.token')" 284 | 285 | manifestJson="$( 286 | curl -fsSL \ 287 | -H "Authorization: Bearer $token" \ 288 | -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \ 289 | -H 'Accept: application/vnd.docker.distribution.manifest.list.v2+json' \ 290 | -H 'Accept: application/vnd.docker.distribution.manifest.v1+json' \ 291 | "$registryBase/v2/$image/manifests/$digest" 292 | )" 293 | if [ "${manifestJson:0:1}" != '{' ]; then 294 | echo >&2 "error: /v2/$image/manifests/$digest returned something unexpected:" 295 | echo >&2 " $manifestJson" 296 | exit 1 297 | fi 298 | 299 | imageIdentifier="$image:$tag@$digest" 300 | 301 | schemaVersion="$(echo "$manifestJson" | jq --raw-output '.schemaVersion')" 302 | case "$schemaVersion" in 303 | 2) 304 | mediaType="$(echo "$manifestJson" | jq --raw-output '.mediaType')" 305 | 306 | case "$mediaType" in 307 | application/vnd.docker.distribution.manifest.v2+json) 308 | handle_single_manifest_v2 "$manifestJson" 309 | ;; 310 | application/vnd.docker.distribution.manifest.list.v2+json) 311 | layersFs="$(echo "$manifestJson" | jq --raw-output --compact-output '.manifests[]')" 312 | IFS="$newlineIFS" 313 | mapfile -t layers <<< "$layersFs" 314 | unset IFS 315 | 316 | found="" 317 | targetArch="$(get_target_arch)" 318 | # parse first level multi-arch manifest 319 | for i in "${!layers[@]}"; do 320 | layerMeta="${layers[$i]}" 321 | maniArch="$(echo "$layerMeta" | jq --raw-output '.platform.architecture')" 322 | if [ "$maniArch" = "${targetArch}" ]; then 323 | digest="$(echo "$layerMeta" | jq --raw-output '.digest')" 324 | # get second level single manifest 325 | submanifestJson="$( 326 | curl -fsSL \ 327 | -H "Authorization: Bearer $token" \ 328 | -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \ 329 | -H 'Accept: application/vnd.docker.distribution.manifest.list.v2+json' \ 330 | -H 'Accept: application/vnd.docker.distribution.manifest.v1+json' \ 331 | "$registryBase/v2/$image/manifests/$digest" 332 | )" 333 | handle_single_manifest_v2 "$submanifestJson" 334 | found="found" 335 | break 336 | fi 337 | done 338 | if [ -z "$found" ]; then 339 | echo >&2 "error: manifest for $maniArch is not found" 340 | exit 1 341 | fi 342 | ;; 343 | *) 344 | echo >&2 "error: unknown manifest mediaType ($imageIdentifier): '$mediaType'" 345 | exit 1 346 | ;; 347 | esac 348 | ;; 349 | 350 | 1) 351 | if [ -z "$doNotGenerateManifestJson" ]; then 352 | echo >&2 "warning: '$imageIdentifier' uses schemaVersion '$schemaVersion'" 353 | echo >&2 " this script cannot (currently) recreate the 'image config' to put in a 'manifest.json' (thus any schemaVersion 2+ images will be imported in the old way, and their 'docker history' will suffer)" 354 | echo >&2 355 | doNotGenerateManifestJson=1 356 | fi 357 | 358 | layersFs="$(echo "$manifestJson" | jq --raw-output '.fsLayers | .[] | .blobSum')" 359 | IFS="$newlineIFS" 360 | mapfile -t layers <<< "$layersFs" 361 | unset IFS 362 | 363 | history="$(echo "$manifestJson" | jq '.history | [.[] | .v1Compatibility]')" 364 | imageId="$(echo "$history" | jq --raw-output '.[0]' | jq --raw-output '.id')" 365 | 366 | echo "Downloading '$imageIdentifier' (${#layers[@]} layers)..." 367 | for i in "${!layers[@]}"; do 368 | imageJson="$(echo "$history" | jq --raw-output ".[${i}]")" 369 | layerId="$(echo "$imageJson" | jq --raw-output '.id')" 370 | imageLayer="${layers[$i]}" 371 | 372 | mkdir -p "$dir/$layerId" 373 | echo '1.0' > "$dir/$layerId/VERSION" 374 | 375 | echo "$imageJson" > "$dir/$layerId/json" 376 | 377 | # TODO figure out why "-C -" doesn't work here 378 | # "curl: (33) HTTP server doesn't seem to support byte ranges. Cannot resume." 379 | # "HTTP/1.1 416 Requested Range Not Satisfiable" 380 | if [ -f "$dir/$layerId/layer.tar" ]; then 381 | # TODO hackpatch for no -C support :'( 382 | echo "skipping existing ${layerId:0:12}" 383 | continue 384 | fi 385 | token="$(curl -fsSL "$authBase/token?service=$authService&scope=repository:$image:pull" | jq --raw-output '.token')" 386 | fetch_blob "$token" "$image" "$imageLayer" "$dir/$layerId/layer.tar" --progress-bar 387 | done 388 | ;; 389 | 390 | *) 391 | echo >&2 "error: unknown manifest schemaVersion ($imageIdentifier): '$schemaVersion'" 392 | exit 1 393 | ;; 394 | esac 395 | 396 | echo 397 | 398 | if [ -s "$dir/tags-$imageFile.tmp" ]; then 399 | echo -n ', ' >> "$dir/tags-$imageFile.tmp" 400 | else 401 | images=("${images[@]}" "$image") 402 | fi 403 | echo -n '"'"$tag"'": "'"$imageId"'"' >> "$dir/tags-$imageFile.tmp" 404 | done 405 | 406 | echo -n '{' > "$dir/repositories" 407 | firstImage=1 408 | for image in "${images[@]}"; do 409 | imageFile="${image//\//_}" # "/" can't be in filenames :) 410 | image="${image#library\/}" 411 | 412 | [ "$firstImage" ] || echo -n ',' >> "$dir/repositories" 413 | firstImage= 414 | echo -n $'\n\t' >> "$dir/repositories" 415 | echo -n '"'"$image"'": { '"$(cat "$dir/tags-$imageFile.tmp")"' }' >> "$dir/repositories" 416 | done 417 | echo -n $'\n}\n' >> "$dir/repositories" 418 | 419 | rm -f "$dir"/tags-*.tmp 420 | 421 | if [ -z "$doNotGenerateManifestJson" ] && [ "${#manifestJsonEntries[@]}" -gt 0 ]; then 422 | echo '[]' | jq --raw-output ".$(for entry in "${manifestJsonEntries[@]}"; do echo " + [ $entry ]"; done)" > "$dir/manifest.json" 423 | else 424 | rm -f "$dir/manifest.json" 425 | fi 426 | 427 | echo "Download of images into '$dir' complete." 428 | echo "Use something like the following to load the result into a Docker daemon:" 429 | echo " tar -cC '$dir' . | docker load" 430 | -------------------------------------------------------------------------------- /packer/rootfs/usr/local/bin/reload-brcmfmac-driver: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Workaround for: 4 | # https://github.com/evilsocket/pwnagotchi/issues/267 5 | 6 | set -eu 7 | 8 | reload_driver() { 9 | printf '%s\n' 'Reloading brcmfmac driver...' 10 | 11 | modprobe -rv brcmfmac || return 1; sleep 3 12 | modprobe -v brcmfmac || return 1; sleep 3 13 | 14 | printf 'mmc1:0001:1' > /sys/bus/sdio/drivers/brcmfmac/unbind || return 1; sleep 3 15 | printf 'mmc1:0001:2' > /sys/bus/sdio/drivers/brcmfmac/unbind || return 1; sleep 3 16 | printf 'mmc1:0001:1' > /sys/bus/sdio/drivers/brcmfmac/bind || return 1; sleep 3 17 | printf 'mmc1:0001:2' > /sys/bus/sdio/drivers/brcmfmac/bind || return 1; sleep 3 18 | } 19 | 20 | if [ -e /sys/bus/sdio/devices/mmc1:0001:1/device ]; then 21 | t=0; tmax=3 22 | until [ "${t:?}" -ge "${tmax:?}" ] || reload_driver; do 23 | t=$((t+1)); sleep 10 24 | done 25 | fi 26 | -------------------------------------------------------------------------------- /packer/rootfs/usr/local/bin/rpi-nexmon-update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | NEXMON_TREEISH='cea7c4b952b3e67110dc1032b8996dae0db9a857' 6 | NEXMON_REMOTE='https://github.com/hectorm/nexmon.git' 7 | NEXMON_SRCDIR='/usr/local/src/nexmon/' 8 | BRCMFMAC_NVRAM_URL='https://raw.githubusercontent.com/openwrt/cypress-nvram/de707b6d535e566479c2ed3e1d09ae5393aa7db3/brcmfmac43430-sdio.raspberrypi%2Cmodel-zero-w.txt' 9 | 10 | # Install dependencies 11 | apt-get update 12 | apt-get install -y \ 13 | autoconf automake automake-1.15 bison build-essential flex gawk git libtool-bin pkgconf \ 14 | debianutils libfl-dev libgmp-dev libgmp3-dev qpdf texinfo \ 15 | raspberrypi-kernel raspberrypi-kernel-headers 16 | 17 | # Freeze Broadcom/Cypress 802.11 firmware package 18 | apt-mark hold firmware-brcm80211 19 | 20 | # Get kernel release 21 | KERNEL_VERSION="$(dpkg -L raspberrypi-kernel | grep -Pom1 '(?<=/lib/modules/)[0-9]+(\.[0-9]+)+\+(?=/)')" 22 | KERNEL_VERSION_MINOR="$(printf '%s' "${KERNEL_VERSION:?}" | grep -Pom1 '^[0-9]+\.[0-9]+')" 23 | 24 | # Create source directory 25 | if [ ! -e "${NEXMON_SRCDIR:?}" ]; then 26 | rm -rf "${NEXMON_SRCDIR:?}" 27 | mkdir -p "${NEXMON_SRCDIR:?}" 28 | fi 29 | 30 | # Clone project 31 | cd "${NEXMON_SRCDIR:?}" 32 | if [ ! -e "${NEXMON_SRCDIR:?}"/.git/ ]; then 33 | git clone "${NEXMON_REMOTE:?}" ./ 34 | else 35 | git reset --hard HEAD 36 | git clean -d --force 37 | fi 38 | git checkout "${NEXMON_TREEISH:?}" 39 | git submodule update --init --recursive 40 | 41 | # Disable statistics 42 | touch "${NEXMON_SRCDIR:?}"/DISABLE_STATISTICS 43 | 44 | # Build libisl if not installed 45 | if [ ! -e /usr/lib/arm-linux-gnueabihf/libisl.so.10 ]; then 46 | cd "${NEXMON_SRCDIR:?}"/buildtools/isl-0.10/ 47 | ./configure && make -j"$(nproc)" && make install 48 | ln -sf /usr/local/lib/libisl.so /usr/lib/arm-linux-gnueabihf/libisl.so.10 49 | fi 50 | 51 | # Build libmpfr if not installed 52 | if [ ! -e /usr/lib/arm-linux-gnueabihf/libmpfr.so.4 ]; then 53 | cd "${NEXMON_SRCDIR:?}"/buildtools/mpfr-3.1.4/ 54 | ./configure && make -j"$(nproc)" && make install 55 | ln -sf /usr/local/lib/libmpfr.so /usr/lib/arm-linux-gnueabihf/libmpfr.so.4 56 | fi 57 | 58 | # Setup build environment 59 | cd "${NEXMON_SRCDIR:?}" 60 | # shellcheck disable=SC1091 61 | . ./setup_env.sh && make 62 | 63 | # Build and install firmware 64 | cd "${NEXMON_SRCDIR:?}"/patches/bcm43430a1/7_45_41_46/nexmon/ 65 | make ./brcmfmac43430-sdio.bin 66 | install -Dm 644 ./brcmfmac43430-sdio.bin /lib/firmware/brcm/brcmfmac43430-sdio.bin 67 | curl -Lo /lib/firmware/brcm/brcmfmac43430-sdio.raspberrypi,model-zero-w.txt "${BRCMFMAC_NVRAM_URL:?}" 68 | cd "${NEXMON_SRCDIR:?}"/patches/bcm43455c0/7_45_206/nexmon/ 69 | make -C /lib/modules/"${KERNEL_VERSION:?}"/build M="${PWD:?}"/brcmfmac_"${KERNEL_VERSION_MINOR:?}".y-nexmon -j2 70 | install -Dm 644 ./brcmfmac_"${KERNEL_VERSION_MINOR:?}".y-nexmon/brcmfmac.ko /lib/modules/"${KERNEL_VERSION:?}"/kernel/drivers/net/wireless/broadcom/brcm80211/brcmfmac/brcmfmac.ko 71 | 72 | # Build and install nexutil 73 | cd "${NEXMON_SRCDIR:?}"/utilities/nexutil/ 74 | make && make install 75 | -------------------------------------------------------------------------------- /packer/sources.pkr.hcl: -------------------------------------------------------------------------------- 1 | source "arm-image" "armhf" { 2 | image_type = "raspberrypi" 3 | 4 | iso_url = "https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2021-05-28/2021-05-07-raspios-buster-armhf-lite.zip" 5 | iso_checksum = "file:https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2021-05-28/2021-05-07-raspios-buster-armhf-lite.zip.sha256" 6 | 7 | qemu_binary = "qemu-arm-static" 8 | qemu_args = ["-cpu", "arm1176"] 9 | 10 | output_filename = "./dist/armhf/pwnagotchi.img" 11 | target_image_size = 6 * 1024 * 1024 * 1024 12 | } 13 | -------------------------------------------------------------------------------- /patches/bettercap-nexutil-support.patch: -------------------------------------------------------------------------------- 1 | diff --git a/network/net_linux.go b/network/net_linux.go 2 | index dd0977a..ef8568c 100644 3 | --- a/network/net_linux.go 4 | +++ b/network/net_linux.go 5 | @@ -23,7 +23,15 @@ func SetInterfaceChannel(iface string, channel int) error { 6 | return nil 7 | } 8 | 9 | - if core.HasBinary("iw") { 10 | + if core.HasBinary("nexutil") { 11 | + // Debug("SetInterfaceChannel(%s, %d) nexutil based", iface, channel) 12 | + out, err := core.Exec("nexutil", []string{"-I", iface, "-i", "-s", "30", "-v", fmt.Sprintf("%d", channel)}) 13 | + if err != nil { 14 | + return fmt.Errorf("nexutil: out=%s err=%s", out, err) 15 | + } else if out != "" { 16 | + return fmt.Errorf("Unexpected output while setting interface %s to channel %d: %s", iface, channel, out) 17 | + } 18 | + } else if core.HasBinary("iw") { 19 | // Debug("SetInterfaceChannel(%s, %d) iw based", iface, channel) 20 | out, err := core.Exec("iw", []string{"dev", iface, "set", "channel", fmt.Sprintf("%d", channel)}) 21 | if err != nil { 22 | @@ -40,7 +48,7 @@ func SetInterfaceChannel(iface string, channel int) error { 23 | return fmt.Errorf("Unexpected output while setting interface %s to channel %d: %s", iface, channel, out) 24 | } 25 | } else { 26 | - return fmt.Errorf("no iw or iwconfig binaries found in $PATH") 27 | + return fmt.Errorf("no nexutil, iw or iwconfig binaries found in $PATH") 28 | } 29 | 30 | SetInterfaceCurrentChannel(iface, channel) 31 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | absl-py==1.0.0 2 | astor==0.8.1 3 | atari-py==0.2.9 4 | certifi==2022.5.18.1 5 | charset-normalizer==2.0.12 6 | click==8.1.3 7 | cloudpickle==1.6.0 8 | cycler==0.11.0 9 | dbus-python==1.2.18 10 | file-read-backwards==2.0.0 11 | Flask==2.1.2 12 | Flask-Cors==3.0.10 13 | Flask-WTF==1.0.1 14 | fonttools==4.33.3 15 | gast==0.5.3 16 | google-pasta==0.2.0 17 | grpcio==1.44.0 18 | gym==0.18.3 19 | h5py==3.7.0 20 | idna==3.3 21 | importlib-metadata==4.11.4 22 | inky==1.3.2 23 | itsdangerous==2.1.2 24 | Jinja2==3.1.2 25 | joblib==1.1.0 26 | Keras-Applications==1.0.8 27 | Keras-Preprocessing==1.1.2 28 | kiwisolver==1.4.1 29 | Markdown==3.3.7 30 | MarkupSafe==2.1.1 31 | matplotlib==3.5.2 32 | numpy==1.21.4 33 | oauthlib==3.2.0 34 | opencv-python==4.5.5.64 35 | packaging==21.3 36 | pandas==1.3.5 37 | Pillow==8.2.0 38 | protobuf==3.20.1 39 | pycryptodome==3.14.1 40 | pyglet==1.5.15 41 | pyparsing==3.0.9 42 | python-dateutil==2.8.2 43 | pytz==2022.1 44 | PyYAML==6.0 45 | requests==2.27.1 46 | requests-oauthlib==1.3.1 47 | RPi.GPIO==0.7.1 48 | scapy==2.4.5 49 | scipy==1.7.3 50 | six==1.16.0 51 | smbus2==0.4.1 52 | spidev==3.5 53 | stable-baselines==2.10.2 54 | tensorboard==1.13.1 55 | tensorflow==1.13.1 56 | tensorflow-estimator==1.14.0; platform_machine == 'armv6l' 57 | tensorflow-estimator==1.13.0; platform_machine != 'armv6l' 58 | termcolor==1.1.0 59 | toml==0.10.2 60 | tweepy==4.10.0 61 | typing_extensions==4.2.0 62 | urllib3==1.26.9 63 | websockets==10.3 64 | Werkzeug==2.0.3 65 | wrapt==1.14.1 66 | WTForms==3.0.1 67 | zipp==3.8.0 68 | -------------------------------------------------------------------------------- /resources/pwnagotchi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorm/docker-pwnagotchi/ff26d97a57d898018e1b53e22f4242be71c46461/resources/pwnagotchi.jpg -------------------------------------------------------------------------------- /resources/scripts/linux_connection_share.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | UPSTREAM_IFACE=${2:-$(awk '$2 == "00000000" {print($1); exit}' /proc/net/route)} 6 | USB_IFACE=${1:-$(awk '$2 == "000E030A" {print($1); exit}' /proc/net/route)} 7 | 8 | iptables -A FORWARD -o "${UPSTREAM_IFACE:?}" -i "${USB_IFACE:?}" -s '10.3.14.0/24' -m conntrack --ctstate NEW -j ACCEPT 9 | iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 10 | iptables -t nat -F POSTROUTING 11 | iptables -t nat -A POSTROUTING -o "${UPSTREAM_IFACE:?}" -j MASQUERADE 12 | 13 | printf 1 > /proc/sys/net/ipv4/ip_forward 14 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | export LC_ALL=C 5 | 6 | DOCKER=$(command -v docker 2>/dev/null) 7 | 8 | IMAGE_REGISTRY=docker.io 9 | IMAGE_NAMESPACE=hectorm 10 | IMAGE_PROJECT=pwnagotchi 11 | IMAGE_TAG=latest 12 | IMAGE_NAME=${IMAGE_REGISTRY:?}/${IMAGE_NAMESPACE:?}/${IMAGE_PROJECT:?}:${IMAGE_TAG:?} 13 | CONTAINER_NAME=${IMAGE_PROJECT:?} 14 | 15 | imageExists() { [ -n "$("${DOCKER:?}" images -q "${1:?}")" ]; } 16 | containerExists() { "${DOCKER:?}" ps -af name="${1:?}" --format '{{.Names}}' | grep -Fxq "${1:?}"; } 17 | containerIsRunning() { "${DOCKER:?}" ps -f name="${1:?}" --format '{{.Names}}' | grep -Fxq "${1:?}"; } 18 | 19 | if ! imageExists "${IMAGE_NAME:?}" && ! imageExists "${IMAGE_NAME#docker.io/}"; then 20 | >&2 printf '%s\n' "\"${IMAGE_NAME:?}\" image doesn't exist!" 21 | exit 1 22 | fi 23 | 24 | if containerIsRunning "${CONTAINER_NAME:?}"; then 25 | printf '%s\n' "Stopping \"${CONTAINER_NAME:?}\" container..." 26 | "${DOCKER:?}" stop "${CONTAINER_NAME:?}" >/dev/null 27 | fi 28 | 29 | if containerExists "${CONTAINER_NAME:?}"; then 30 | printf '%s\n' "Removing \"${CONTAINER_NAME:?}\" container..." 31 | "${DOCKER:?}" rm "${CONTAINER_NAME:?}" >/dev/null 32 | fi 33 | 34 | printf '%s\n' "Creating \"${CONTAINER_NAME:?}\" container..." 35 | "${DOCKER:?}" run \ 36 | --tty --detach \ 37 | --name "${CONTAINER_NAME:?}" \ 38 | --hostname "${CONTAINER_NAME:?}" \ 39 | --restart on-failure:3 \ 40 | --privileged --net host \ 41 | --env PWNAGOTCHI_IFACE_MON=wlp3s0 \ 42 | --env PWNAGOTCHI_DISPLAY_ENABLED=false \ 43 | --env PWNAGOTCHI_PLUGIN_GRID_ENABLED=false \ 44 | --env PWNAGOTCHI_PLUGIN_LED_ENABLED=false \ 45 | --mount type=tmpfs,dst=/run/,tmpfs-mode=0755 \ 46 | --mount type=tmpfs,dst=/tmp/,tmpfs-mode=1777 \ 47 | "${IMAGE_NAME:?}" "$@" >/dev/null 48 | 49 | printf '%s\n\n' 'Done!' 50 | exec "${DOCKER:?}" logs -f "${CONTAINER_NAME:?}" 51 | -------------------------------------------------------------------------------- /scripts/bin/container-healthcheck: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | # Check Bettercap port 6 | if ! nc -zv 127.0.0.1 8081 >/dev/null 2>&1; then 7 | >&2 printf '%s\n' 'Bettercap is not running' 8 | exit 1 9 | fi 10 | 11 | # Check PwnGRID port 12 | if ! nc -zv 127.0.0.1 8666 >/dev/null 2>&1; then 13 | >&2 printf '%s\n' 'PwnGRID is not running' 14 | exit 1 15 | fi 16 | 17 | # Check Pwnagotchi port 18 | if ! nc -zv 127.0.0.1 8080 >/dev/null 2>&1; then 19 | >&2 printf '%s\n' 'Pwnagotchi is not running' 20 | exit 1 21 | fi 22 | -------------------------------------------------------------------------------- /scripts/bin/container-init: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | # Dump environment variables 6 | awk -f- > /etc/environment <<-'EOF' 7 | BEGIN { 8 | for (v in ENVIRON) { 9 | if (v !~ /^(AWKPATH|AWKLIBPATH|PWD|HOME|USER|USERNAME|SHELL|TERM|SHLVL)$/) { 10 | gsub(/'/, "'\\''", ENVIRON[v]); 11 | print(v"='"ENVIRON[v]"'"); 12 | } 13 | } 14 | } 15 | EOF 16 | 17 | # Start init system 18 | exec /sbin/init 19 | -------------------------------------------------------------------------------- /scripts/bin/envsubst2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | 3 | {while(match($0, "\\${[A-Z_]+[A-Z0-9_]*}")) { 4 | LSTART=substr($0, 0, RSTART - 1); 5 | LVAR=substr($0, RSTART + 2, RLENGTH - 3); 6 | LEND=substr($0, RSTART + RLENGTH); 7 | $0=LSTART""ENVIRON[LVAR]""LEND; 8 | }} 9 | 10 | {print($0)} 11 | -------------------------------------------------------------------------------- /scripts/bin/monstart: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | # shellcheck source=./pwnlib 6 | . /usr/local/bin/pwnlib 7 | 8 | start_monitor_interface 9 | -------------------------------------------------------------------------------- /scripts/bin/monstop: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | # shellcheck source=./pwnlib 6 | . /usr/local/bin/pwnlib 7 | 8 | stop_monitor_interface 9 | -------------------------------------------------------------------------------- /scripts/bin/pwnlib: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Based on: 4 | # https://github.com/evilsocket/pwnagotchi/blob/master/builder/data/usr/bin/pwnlib 5 | 6 | set -eu 7 | 8 | if [ -f /etc/profile.d/env.sh ]; then 9 | # shellcheck disable=SC1091 10 | . /etc/profile.d/env.sh 11 | fi 12 | 13 | # Starts monitor interface 14 | start_monitor_interface() { 15 | if ! exists_dev_interface "${PWNAGOTCHI_IFACE_MON:?}"; then 16 | if exists_phy_interface "${PWNAGOTCHI_IFACE_NET:?}"; then 17 | iw phy "${PWNAGOTCHI_IFACE_NET:?}" interface add "${PWNAGOTCHI_IFACE_MON:?}" type monitor 18 | elif exists_dev_interface "${PWNAGOTCHI_IFACE_NET:?}"; then 19 | iw dev "${PWNAGOTCHI_IFACE_NET:?}" interface add "${PWNAGOTCHI_IFACE_MON:?}" type monitor 20 | else 21 | >&2 printf '%s\n' "'${PWNAGOTCHI_IFACE_NET:?}' interface could not be found" 22 | exit 1 23 | fi 24 | fi 25 | if ! is_dev_interface_up "${PWNAGOTCHI_IFACE_MON:?}"; then 26 | ip link set "${PWNAGOTCHI_IFACE_MON:?}" up 27 | fi 28 | } 29 | 30 | # Stops monitor interface 31 | stop_monitor_interface() { 32 | if exists_dev_interface "${PWNAGOTCHI_IFACE_MON:?}"; then 33 | if is_dev_interface_up "${PWNAGOTCHI_IFACE_MON:?}"; then 34 | ip link set "${PWNAGOTCHI_IFACE_MON:?}" down 35 | fi 36 | iw dev "${PWNAGOTCHI_IFACE_MON:?}" del 37 | fi 38 | } 39 | 40 | # Returns 0 if the specificed interface exists 41 | exists_phy_interface() { test -e "/sys/class/ieee80211/${1:?}/"; } 42 | exists_dev_interface() { test -e "/sys/class/net/${1:?}/"; } 43 | 44 | # Returns 0 if the specificed interface is up 45 | is_dev_interface_up() { grep -Fxq 'up' "/sys/class/net/${1:?}/operstate"; } 46 | 47 | # Returns 0 if conditions for AUTO mode are met 48 | is_auto_mode() { 49 | action=${1-} 50 | 51 | # Check override file first 52 | if [ -f /root/.pwnagotchi-manual ]; then 53 | if [ "${action?}" = 'delete' ]; then 54 | # Remove the override file if found 55 | rm -f /root/.pwnagotchi-manual 56 | fi 57 | return 1 58 | fi 59 | 60 | # Check override file first 61 | if [ -f /root/.pwnagotchi-auto ]; then 62 | if [ "${action?}" = 'delete' ]; then 63 | # Remove the override file if found 64 | rm -f /root/.pwnagotchi-auto 65 | fi 66 | return 0 67 | fi 68 | 69 | if exists_dev_interface "${PWNAGOTCHI_IFACE_USB:?}"; then 70 | # If USB interface is up, we're in MANU 71 | if is_dev_interface_up "${PWNAGOTCHI_IFACE_USB:?}"; then 72 | return 1 73 | fi 74 | fi 75 | 76 | # No override, but none of the interfaces is up -> AUTO 77 | return 0 78 | } 79 | -------------------------------------------------------------------------------- /scripts/bin/pwnlog: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | exec journalctl \ 6 | --unit=pwnagotchi \ 7 | --unit=pwngrid \ 8 | --unit=bettercap \ 9 | --lines 1000 \ 10 | --follow 11 | -------------------------------------------------------------------------------- /scripts/bin/pwnver: -------------------------------------------------------------------------------- 1 | #!/usr/lib/pwnagotchi/bin/python 2 | 3 | import pwnagotchi 4 | 5 | print(pwnagotchi.version) 6 | -------------------------------------------------------------------------------- /scripts/bin/service-bettercap-start: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | # shellcheck source=./pwnlib 6 | . /usr/local/bin/pwnlib 7 | 8 | # Replace environment variables in the Bettercap caplets 9 | if [ -w /usr/local/share/bettercap/caplets/pwnagotchi-auto.cap ]; then 10 | envsubst2 -i inplace /usr/local/share/bettercap/caplets/pwnagotchi-auto.cap 11 | fi 12 | if [ -w /usr/local/share/bettercap/caplets/pwnagotchi-manual.cap ]; then 13 | envsubst2 -i inplace /usr/local/share/bettercap/caplets/pwnagotchi-manual.cap 14 | fi 15 | 16 | # Start monitor interface 17 | start_monitor_interface 18 | 19 | if is_auto_mode 'no_delete'; then 20 | exec bettercap -no-colors -caplet pwnagotchi-auto -iface "${PWNAGOTCHI_IFACE_MON:?}" 21 | else 22 | exec bettercap -no-colors -caplet pwnagotchi-manual -iface "${PWNAGOTCHI_IFACE_MON:?}" 23 | fi 24 | -------------------------------------------------------------------------------- /scripts/bin/service-pwnagotchi-start: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | # shellcheck source=./pwnlib 6 | . /usr/local/bin/pwnlib 7 | 8 | # Wait for Bettercap 9 | until nc -zv 127.0.0.1 8081 >/dev/null 2>&1; do sleep 1; done 10 | 11 | # Wait for PwnGRID 12 | until nc -zv 127.0.0.1 8666 >/dev/null 2>&1; do sleep 1; done 13 | 14 | # Replace environment variables in the Pwnagotchi configuration 15 | if [ -w /etc/pwnagotchi/config.toml ]; then 16 | envsubst2 -i inplace /etc/pwnagotchi/config.toml 17 | fi 18 | 19 | # Fixes: https://github.com/piwheels/packages/issues/59 20 | if [ -f /usr/lib/arm-linux-gnueabihf/libatomic.so.1 ]; then 21 | export LD_PRELOAD=/usr/lib/arm-linux-gnueabihf/libatomic.so.1 22 | fi 23 | 24 | if is_auto_mode 'delete'; then 25 | exec pwnagotchi 26 | else 27 | exec pwnagotchi --manual 28 | fi 29 | -------------------------------------------------------------------------------- /scripts/bin/service-pwngrid-start: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | # shellcheck source=./pwnlib 6 | . /usr/local/bin/pwnlib 7 | 8 | # Generate Pwnagotchi keys if they do not exist 9 | if [ ! -f /root/id_rsa ]; then 10 | pwngrid -generate -keys /root/ 11 | fi 12 | 13 | exec pwngrid \ 14 | -address 127.0.0.1:8666 \ 15 | -keys /root/ \ 16 | -iface "${PWNAGOTCHI_IFACE_MON:?}" \ 17 | -client-token /root/.api-enrollment.json 18 | -------------------------------------------------------------------------------- /scripts/service/bettercap.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Bettercap service 3 | Wants=network.target 4 | 5 | [Service] 6 | Type=simple 7 | EnvironmentFile=/etc/environment 8 | ExecStart=/usr/local/bin/service-bettercap-start 9 | Restart=always 10 | RestartSec=30 11 | 12 | [Install] 13 | WantedBy=default.target 14 | -------------------------------------------------------------------------------- /scripts/service/container.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Container target 3 | AllowIsolate=yes 4 | -------------------------------------------------------------------------------- /scripts/service/pwnagotchi.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Pwnagotchi service 3 | Wants=network.target 4 | After=bettercap.service pwngrid.service 5 | 6 | [Service] 7 | Type=simple 8 | EnvironmentFile=/etc/environment 9 | ExecStart=/usr/local/bin/service-pwnagotchi-start 10 | Restart=always 11 | RestartSec=30 12 | 13 | [Install] 14 | WantedBy=default.target 15 | -------------------------------------------------------------------------------- /scripts/service/pwngrid.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=PwnGRID service 3 | Wants=network.target 4 | 5 | [Service] 6 | Type=simple 7 | EnvironmentFile=/etc/environment 8 | ExecStart=/usr/local/bin/service-pwngrid-start 9 | Restart=always 10 | RestartSec=30 11 | 12 | [Install] 13 | WantedBy=default.target 14 | --------------------------------------------------------------------------------