From 3e1f3d5eec73e10b403cad6fe927219cba3302dd Mon Sep 17 00:00:00 2001 From: dadav <33197631+dadav@users.noreply.github.com> Date: Sun, 3 Nov 2019 12:13:02 +0100 Subject: [PATCH 1/5] Add deps --- builder/pwnagotchi.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/builder/pwnagotchi.yml b/builder/pwnagotchi.yml index 0164502e..cdb5868e 100644 --- a/builder/pwnagotchi.yml +++ b/builder/pwnagotchi.yml @@ -99,6 +99,8 @@ - bc - fonts-freefont-ttf - fbi + - python3-flask + - python3-flask-cors tasks: - name: change hostname From 0aaeeb8011b2784505b62c03551933f06f12398d Mon Sep 17 00:00:00 2001 From: dadav <33197631+dadav@users.noreply.github.com> Date: Sun, 3 Nov 2019 17:08:45 +0100 Subject: [PATCH 2/5] First step --- pwnagotchi/plugins/default/example.py | 5 + pwnagotchi/ui/web.py | 157 ++++++++------------------ 2 files changed, 52 insertions(+), 110 deletions(-) diff --git a/pwnagotchi/plugins/default/example.py b/pwnagotchi/plugins/default/example.py index 3aa5c7d8..5a136519 100644 --- a/pwnagotchi/plugins/default/example.py +++ b/pwnagotchi/plugins/default/example.py @@ -15,6 +15,11 @@ class Example(plugins.Plugin): def __init__(self): logging.debug("example plugin created") + # called when http://:/plugins// is called + # must return a response + def on_webhook(self, path, args, req_method): + pass + # called when the plugin is loaded def on_loaded(self): logging.warning("WARNING: this plugin should be disabled! options = " % self.options) diff --git a/pwnagotchi/ui/web.py b/pwnagotchi/ui/web.py index 3f1dc488..91edaffd 100644 --- a/pwnagotchi/ui/web.py +++ b/pwnagotchi/ui/web.py @@ -8,6 +8,11 @@ import logging import pwnagotchi from pwnagotchi.agent import Agent from pwnagotchi import plugins +from flask import Flask +from flask import send_file +from flask import request +from flask import abort +from flask_cors import CORS frame_path = '/root/pwnagotchi.png' frame_format = 'PNG' @@ -88,141 +93,73 @@ STATUS_PAGE = """ """ -class Handler(BaseHTTPRequestHandler): - AllowedOrigin = None # CORS headers are not sent +class Handler: + def __init__(self, app): + self._app = app + self._app.add_url_rule('/', 'index', self.index) + self._app.add_url_rule('/ui', 'ui', self.ui) + self._app.add_url_rule('/shutdown', 'shutdown', self.shutdown, methods=['POST']) + # plugins + self._app.add_url_rule('/plugins', 'plugins', self.plugins, strict_slashes=False, defaults={'name': None, 'subpath': None}) + self._app.add_url_rule('/plugins/', 'plugins', self.plugins, strict_slashes=False, methods=['GET','POST'], defaults={'subpath': None}) + self._app.add_url_rule('/plugins//', 'plugins', self.plugins, methods=['GET','POST']) - # suppress internal logging - def log_message(self, format, *args): - return - def _send_cors_headers(self): - # misc security - self.send_header("X-Frame-Options", "DENY") - self.send_header("X-Content-Type-Options", "nosniff") - self.send_header("X-XSS-Protection", "1; mode=block") - self.send_header("Referrer-Policy", "same-origin") - # cors - if Handler.AllowedOrigin: - self.send_header("Access-Control-Allow-Origin", Handler.AllowedOrigin) - self.send_header('Access-Control-Allow-Credentials', 'true') - self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - self.send_header("Access-Control-Allow-Headers", - "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") - self.send_header("Vary", "Origin") + def index(self): + return INDEX % (pwnagotchi.name(), 1000) - # just render some html in a 200 response - def _html(self, html): - self.send_response(200) - self._send_cors_headers() - self.send_header('Content-type', 'text/html') - self.end_headers() - try: - self.wfile.write(bytes(html, "utf8")) - except: + def plugins(self, name, subpath): + if name is None: + # show plugins overview pass + else: + + # call plugin on_webhook + arguments = request.args + req_method = request.method + + # need to return something here + if name in plugins.loaded and hasattr(plugins.loaded[name], 'on_webhook'): + return plugins.loaded[name].on_webhook(subpath, args=arguments, req_method=req_method) + + abort(500) - # serve the main html page - def _index(self): - other_mode = 'AUTO' if Agent.INSTANCE.mode == 'manual' else 'MANU' - self._html(INDEX % ( - pwnagotchi.name(), - other_mode, - other_mode, - 1000)) # serve a message and shuts down the unit - def _shutdown(self): - self._html(STATUS_PAGE % (pwnagotchi.name(), 'Shutting down ...')) + def shutdown(self): pwnagotchi.shutdown() - - # serve a message and restart the unit in the other mode - def _restart(self): - other_mode = 'AUTO' if Agent.INSTANCE.mode == 'manual' else 'MANU' - self._html(STATUS_PAGE % (pwnagotchi.name(), 'Restart in %s mode ...' % other_mode)) - pwnagotchi.restart(other_mode) + return SHUTDOWN % pwnagotchi.name() # serve the PNG file with the display image - def _image(self): - global frame_lock, frame_path, frame_ctype + def ui(self): + global frame_lock, frame_path with frame_lock: - self.send_response(200) - self._send_cors_headers() - self.send_header('Content-type', frame_ctype) - self.end_headers() - try: - with open(frame_path, 'rb') as fp: - shutil.copyfileobj(fp, self.wfile) - except: - pass - - # check the Origin header vs CORS - def _is_allowed(self): - if not Handler.AllowedOrigin or Handler.AllowedOrigin == '*': - return True - - # TODO: FIX doesn't work with GET requests same-origin - origin = self.headers.get('origin') - if not origin: - logging.warning("request with no Origin header from %s" % self.address_string()) - return False - - if origin != Handler.AllowedOrigin: - logging.warning("request with blocked Origin from %s: %s" % (self.address_string(), origin)) - return False - - return True - - def do_OPTIONS(self): - self.send_response(200) - self._send_cors_headers() - self.end_headers() - - def do_POST(self): - if not self._is_allowed(): - return - elif self.path.startswith('/shutdown'): - self._shutdown() - elif self.path.startswith('/restart'): - self._restart() - else: - self.send_response(404) - - def do_GET(self): - if not self._is_allowed(): - return - elif self.path == '/': - self._index() - elif self.path.startswith('/ui'): - self._image() - elif self.path.startswith('/plugins'): - matches = re.match(r'\/plugins\/([^\/]+)(\/.*)?', self.path) - if matches: - groups = matches.groups() - plugin_name = groups[0] - right_path = groups[1] if len(groups) == 2 else None - plugins.one(plugin_name, 'webhook', self, right_path) - else: - self.send_response(404) + return send_file(frame_path, mimetype='image/png') -class Server(object): +class Server: def __init__(self, config): self._enabled = config['video']['enabled'] self._port = config['video']['port'] self._address = config['video']['address'] - self._httpd = None + self._origin = None if 'origin' in config['video']: - Handler.AllowedOrigin = config['video']['origin'] + self._origin = config['video']['origin'] if self._enabled: _thread.start_new_thread(self._http_serve, ()) def _http_serve(self): if self._address is not None: - self._httpd = HTTPServer((self._address, self._port), Handler) - logging.info("web ui available at http://%s:%d/" % (self._address, self._port)) - self._httpd.serve_forever() + app = Flask(__name__) + + if self._origin: + CORS(app, resources={r"*": {"origins": self._origin}}) + + Handler(app) + + app.run(host=self._address, port=self._port, debug=False) else: logging.info("could not get ip of usb0, video server not starting") From 11fb95d29950f92ed5394d5f6c5fa1636e9e5f5e Mon Sep 17 00:00:00 2001 From: dadav <33197631+dadav@users.noreply.github.com> Date: Mon, 4 Nov 2019 18:26:10 +0100 Subject: [PATCH 3/5] Add CSRF support --- builder/pwnagotchi.yml | 1 + pwnagotchi/ui/web.py | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/builder/pwnagotchi.yml b/builder/pwnagotchi.yml index cdb5868e..f369014d 100644 --- a/builder/pwnagotchi.yml +++ b/builder/pwnagotchi.yml @@ -101,6 +101,7 @@ - fbi - python3-flask - python3-flask-cors + - python3-flaskext.wtf tasks: - name: change hostname diff --git a/pwnagotchi/ui/web.py b/pwnagotchi/ui/web.py index 91edaffd..758ea61a 100644 --- a/pwnagotchi/ui/web.py +++ b/pwnagotchi/ui/web.py @@ -1,6 +1,6 @@ import re import _thread -from http.server import BaseHTTPRequestHandler, HTTPServer +import secrets from threading import Lock import shutil import logging @@ -12,7 +12,9 @@ from flask import Flask from flask import send_file from flask import request from flask import abort +from flask import render_template_string from flask_cors import CORS +from flask_wtf.csrf import CSRFProtect frame_path = '/root/pwnagotchi.png' frame_format = 'PNG' @@ -70,8 +72,10 @@ INDEX = """
+
+
@@ -93,7 +97,7 @@ STATUS_PAGE = """ """ -class Handler: +class RequestHandler: def __init__(self, app): self._app = app self._app.add_url_rule('/', 'index', self.index) @@ -106,12 +110,12 @@ class Handler: def index(self): - return INDEX % (pwnagotchi.name(), 1000) + return render_template_string(INDEX % (pwnagotchi.name(), 1000)) def plugins(self, name, subpath): if name is None: # show plugins overview - pass + abort(404) else: # call plugin on_webhook @@ -120,7 +124,7 @@ class Handler: # need to return something here if name in plugins.loaded and hasattr(plugins.loaded[name], 'on_webhook'): - return plugins.loaded[name].on_webhook(subpath, args=arguments, req_method=req_method) + return render_template_string(plugins.loaded[name].on_webhook(subpath, args=arguments, req_method=req_method)) abort(500) @@ -128,7 +132,7 @@ class Handler: # serve a message and shuts down the unit def shutdown(self): pwnagotchi.shutdown() - return SHUTDOWN % pwnagotchi.name() + return render_template_string(STATUS_PAGE % pwnagotchi.name()) # serve the PNG file with the display image def ui(self): @@ -154,11 +158,13 @@ class Server: def _http_serve(self): if self._address is not None: app = Flask(__name__) + app.secret_key = secrets.token_urlsafe(256) if self._origin: CORS(app, resources={r"*": {"origins": self._origin}}) - Handler(app) + CSRFProtect(app) + RequestHandler(app) app.run(host=self._address, port=self._port, debug=False) else: From 4503e71bfbd693a53ac574a262a5ea507eb3ab3e Mon Sep 17 00:00:00 2001 From: dadav <33197631+dadav@users.noreply.github.com> Date: Mon, 4 Nov 2019 18:38:12 +0100 Subject: [PATCH 4/5] Rebased --- pwnagotchi/ui/web.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/pwnagotchi/ui/web.py b/pwnagotchi/ui/web.py index 758ea61a..e0d1e31a 100644 --- a/pwnagotchi/ui/web.py +++ b/pwnagotchi/ui/web.py @@ -75,8 +75,8 @@ INDEX = """
- +
@@ -110,7 +110,12 @@ class RequestHandler: def index(self): - return render_template_string(INDEX % (pwnagotchi.name(), 1000)) + other_mode = 'AUTO' if Agent.INSTANCE.mode == 'manual' else 'MANU' + return render_template_string(INDEX % ( + pwnagotchi.name(), + other_mode, + other_mode, + 1000)) def plugins(self, name, subpath): if name is None: @@ -132,7 +137,13 @@ class RequestHandler: # serve a message and shuts down the unit def shutdown(self): pwnagotchi.shutdown() - return render_template_string(STATUS_PAGE % pwnagotchi.name()) + return render_template_string(STATUS_PAGE % (pwnagotchi.name(), 'Shutting down ...')) + + # serve a message and restart the unit in the other mode + def restart(self): + other_mode = 'AUTO' if Agent.INSTANCE.mode == 'manual' else 'MANU' + pwnagotchi.restart(other_mode) + return render_template_string(STATUS_PAGE % (pwnagotchi.name(), 'Restart in %s mode ...' % other_mode)) # serve the PNG file with the display image def ui(self): From 7fc46ddcf606c640a3108b79ee85746b3e26d2c5 Mon Sep 17 00:00:00 2001 From: dadav <33197631+dadav@users.noreply.github.com> Date: Mon, 4 Nov 2019 18:43:33 +0100 Subject: [PATCH 5/5] Add restart route --- pwnagotchi/ui/web.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pwnagotchi/ui/web.py b/pwnagotchi/ui/web.py index e0d1e31a..ee56f9d2 100644 --- a/pwnagotchi/ui/web.py +++ b/pwnagotchi/ui/web.py @@ -103,6 +103,7 @@ class RequestHandler: self._app.add_url_rule('/', 'index', self.index) self._app.add_url_rule('/ui', 'ui', self.ui) self._app.add_url_rule('/shutdown', 'shutdown', self.shutdown, methods=['POST']) + self._app.add_url_rule('/restart', 'restart', self.restart, methods=['POST']) # plugins self._app.add_url_rule('/plugins', 'plugins', self.plugins, strict_slashes=False, defaults={'name': None, 'subpath': None}) self._app.add_url_rule('/plugins/', 'plugins', self.plugins, strict_slashes=False, methods=['GET','POST'], defaults={'subpath': None})