

HPE Aruba OS‑CX (AOS‑CX) switches received a critical Remote Code Execution (RCE) fix (CVSS 9.8). This guide shows how to query the switch, verify the installed firmware, and – if needed – push the patched image using the official REST API.
⚠️ Disclaimer – The code below works against a real AOS‑CX switch only when you have valid admin credentials and network reachability. Replace placeholder values (IP, username/password, firmware URL) with your own. Always test in a lab before touching production gear.
| Item | Why you need it | Minimum version |
|---|---|---|
| HPE Aruba AOS‑CX switch | Target device exposing the REST/HTTPS API | Any version that supports the REST API (10.04+ recommended) |
| API access enabled | The switch must have rest-api service turned on (rest-api enable) | – |
| Admin (or operator) credentials | Needed for authentication token | – |
| Python 3.8+ | For the Python sample | 3.8 |
| Node.js 14+ (or Deno) | For the JS/TS sample | 14 LTS |
| HTTPS reachability | API runs over TLS; you must trust the switch’s cert or disable verification (not recommended for prod) | – |
Optional: jq or httpie | Handy for quick CLI checks | – |
# Create a virtual environment (recommended)
python3 -m venv aosxcx-env
source aosxcx-env/bin/activate # Windows: aosxcx-env\Scripts\activate
# Install required packages
pip install --upgrade pip
pip install requests python-dotenv tqdm
# Initialise a new npm project (skip if you already have one)
mkdir aosxcx-js && cd aosxcx-js
npm init -y
# Install dependencies
npm install axios dotenv
# If you prefer TypeScript:
npm install --save-dev typescript @types/node ts-node
npx tsc --init # creates tsconfig.json
All samples follow the same logical flow:
/rest/v1/login)./rest/v1/system/firmware/upload then trigger a reload.The exact endpoints may vary slightly between AOS‑CX releases; the ones below are taken from the 10.04 API reference and work on most recent trains.
aosxcx_patch.py)#!/usr/bin/env python3
"""
HPE AOS‑CX RCE patch helper – Python version.
- Logs in to the switch via REST API.
- Retrieves current firmware version.
- (Optional) Uploads and activates a patched firmware image.
"""
import os
import sys
import json
import base64
import getpass
from pathlib import Path
from typing import Optional
import requests
from dotenv import load_dotenv
from tqdm import tqdm
# ----------------------------------------------------------------------
# Load configuration from .env (see Step 4)
# ----------------------------------------------------------------------
load_dotenv() # pulls SWITCH_IP, USERNAME, PASSWORD, FIRMWARE_URL, etc.
SWITCH_IP = os.getenv("SWITCH_IP")
USERNAME = os.getenv("USERNAME")
PASSWORD = os.getenv("PASSWORD")
# If you prefer to be prompted for password at runtime, comment the line above.
FIRMWARE_URL = os.getenv("FIRMWARE_URL") # HTTP(S) location of the .swi file
VERIFY_SSL = os.getenv("VERIFY_SSL", "true").lower() == "true"
BASE_URL = f"https://{SWITCH_IP}/rest/v1"
HEADERS = {"Content-Type": "application/json"}
# ----------------------------------------------------------------------
# Helper: pretty‑print errors and exit
# ----------------------------------------------------------------------
def fail(msg: str):
print(f"[ERROR] {msg}", file=sys.stderr)
sys.exit(1)
# ----------------------------------------------------------------------
# 1️⃣ Authentication – obtain a token
# ----------------------------------------------------------------------
def get_auth_token() -> str:
login_url = f"https://icarax.com/login"
payload = {"username": USERNAME, "password": PASSWORD}
try:
resp = requests.post(login_url, json=payload, headers=HEADERS,
verify=VERIFY_SSL, timeout=10)
resp.raise_for_status()
except requests.RequestException as e:
fail(f"Unable to reach {login_url}: {e}")
data = resp.json()
token = data.get("token")
if not token:
fail("Login succeeded but no token returned.")
return token
# ----------------------------------------------------------------------
# 2️⃣ GET system information (sanity check)
# ----------------------------------------------------------------------
def get_system_info(token: str) -> dict:
url = f"{BASE_URL}/system"
headers = {**HEADERS, "Authorization": f"Bearer {token}"}
try:
resp = requests.get(url, headers=headers, verify=VERIFY_SSL, timeout=10)
resp.raise_for_status()
except requests.RequestException as e:
fail(f"Failed to fetch system info: {e}")
return resp.json()
# ----------------------------------------------------------------------
# 3️⃣ GET current firmware version
# ----------------------------------------------------------------------
def get_firmware_version(token: str) -> str:
url = f"{BASE_URL}/system/firmware"
headers = {**HEADERS, "Authorization": f"Bearer {token}"}
try:
resp = requests.get(url, headers=headers, verify=VERIFY_SSL, timeout=10)
resp.raise_for_status()
except requests.RequestException as e:
fail(f"Failed to fetch firmware version: {e}")
data = resp.json()
# Expected format: {"primary": "10.04.0010", "secondary": "10.04.0009", ...}
return data.get("primary", "unknown")
# ----------------------------------------------------------------------
# 4️⃣ OPTIONAL: Upload firmware image (streaming with progress bar)
# ----------------------------------------------------------------------
def upload_firmware(token: str, image_url: str) -> bool:
"""
Downloads the .swi file from `image_url` and POSTs it to the switch.
Returns True on success.
"""
# 4a – fetch the image (stream to avoid loading huge file into RAM)
try:
img_resp = requests.get(image_url, stream=True, timeout=30)
img_resp.raise_for_status()
except requests.RequestException as e:
fail(f"Unable to download firmware image from {image_url}: {e}")
total = int(img_resp.headers.get("content-length", 0))
upload_url = f"{BASE_URL}/system/firmware/upload"
headers = {**HEADERS, "Authorization": f"Bearer {token}"}
# Use requests' toolkit to stream the file directly
try:
with tqdm(total=total, unit='B', unit_scale=True,
desc="Uploading firmware") as pbar:
def read_chunk():
for chunk in img_resp.iter_content(chunk_size=8192):
if chunk:
pbar.update(len(chunk))
yield chunk
resp = requests.post(
upload_url,
headers=headers,
data=read_chunk(),
verify=VERIFY_SSL,
timeout=(10, None) # connect timeout, no read timeout
)
resp.raise_for_status()
except requests.RequestException as e:
fail(f"Firmware upload failed: {e}")
# The API returns JSON with a status field
result = resp.json()
if result.get("status") == "success":
print("[INFO] Firmware image uploaded successfully.")
return True
else:
fail(f"Upload rejected by switch: {result}")
return False
# ----------------------------------------------------------------------
# 5️⃣ OPTIONAL: Activate uploaded firmware and reload
# ----------------------------------------------------------------------
def activate_and_reload(token: str, firmware_slot: str = "primary") -> None:
"""
Sets the chosen slot as the next boot image and triggers a reload.
"""
url = f"{BASE_URL}/system/firmware/activate"
headers = {**HEADERS, "Authorization": f"Bearer {token}"}
payload = {"slot": firmware_slot}
try:
resp = requests.post(url, json=payload, headers=headers,
verify=VERIFY_SSL, timeout=10)
resp.raise_for_status()
except requests.RequestException as e:
fail(f"Failed to activate firmware: {e}")
print(f"[INFO] Firmware slot '{firmware_slot}' marked for next boot.")
# Initiate reload (optional – you may want to schedule this manually)
reload_url = f"{BASE_URL}/system/reload"
try:
reload_resp = requests.post(reload_url, headers=headers,
verify=VERIFY_SSL, timeout=10)
reload_resp.raise_for_status()
print("[INFO] Reload request accepted. Switch will reboot shortly.")
except requests.RequestException as e:
fail(f"Reload request failed: {e}")
# ----------------------------------------------------------------------
# Main driver
# ----------------------------------------------------------------------
def main():
if not all([SWITCH_IP, USERNAME, PASSWORD]):
fail("Missing required environment variables: SWITCH_IP, USERNAME, PASSWORD")
print("[STEP] Authenticating...")
token = get_auth_token()
print("[OK] Token acquired.")
print("[STEP] Fetching system info...")
sys_info = get_system_info(token)
print(f"[OK] Hostname: {sys_info.get('hostname')}, MAC: {sys_info.get('baseMacAddress')}")
print("[STEP] Checking current firmware version...")
current_ver = get_firmware_version(token)
print(f"[INFO] Current primary firmware: {current_ver}")
# ------------------------------------------------------------------
# Compare against the known‑good patched version (example)
# ------------------------------------------------------------------
PATCHED_VERSION = "10.04.0015" # <-- replace with the version HPE released for the RCE fix
if current_ver == PATCHED_VERSION:
print("[SUCCESS] Switch already runs the patched firmware. No action needed.")
return
print(f"[WARN] Firmware is outdated ({current_ver} < {PATCHED_VERSION}).")
if not FIRMWARE_URL:
fail("FIRMWARE_URL not set – cannot proceed with automatic patch.")
print("[STEP] Uploading patched firmware image...")
if not upload_firmware(token, FIRMWARE_URL):
return # upload_firmware already printed error and exited
print("[STEP] Activating new image and scheduling reload...")
activate_and_reload(token, firmware_slot="primary")
print("[DONE] Patch process completed. Monitor the switch for reboot.")
if __name__ == "__main__":
main()
# 1️⃣ Copy the script to a file, e.g. aosxcx_patch.py
# 2️⃣ Create a .env file (see Step 4)
# 3️⃣ Execute
python aosxcx_patch.py
aosxcx_patch.ts)/**
* HPE AOS‑CX RCE patch helper – TypeScript version.
* Demonstrates login, firmware version check, optional upload & activation.
*/
import * as dotenv from "dotenv";
import axios, { AxiosInstance, AxiosResponse } from "axios";
import { createWriteStream, promises as fs } from "fs";
import https from "https";
import { pipeline } from "stream";
import { promisify } from "util";
dotenv.config(); // loads .env into process.env
const {
SWITCH_IP,
USERNAME,
PASSWORD,
FIRMWARE_URL,
VERIFY_SSL = "true",
} = process.env;
if (!SWITCH_IP || !USERNAME || !PASSWORD) {
console.error(
"[ERROR] Missing required env vars: SWITCH_IP, USERNAME, PASSWORD"
);
process.exit(1);
}
const VERIFY = VERIFY_SSL.toLowerCase() === "true";
const BASE_URL = `https://${SWITCH_IP}/rest/v1`;
const httpsAgent = new https.Agent({
rejectUnauthorized: VERIFY, // false => ignore self‑signed certs (lab only)
});
const api: AxiosInstance = axios.create({
baseURL: BASE_URL,
httpsAgent,
timeout: 10_000,
headers: { "Content-Type": "application/json"},
});
/**
* Simple wrapper that throws a readable error on non‑2xx responses.
*/
async function safeRequest<T>(
config: Parameters<typeof api>[0]
): Promise<AxiosResponse<T>> {
try {
return await api.request<T>(config);
} catch (err: any) {
if (err.response) {
throw new Error(
`${err.response.status} ${err.response.statusText}: ${JSON.stringify(
err.response.data
)}`
);
} else if (err.request) {
throw new Error(`No response received: ${err.message}`);
} else {
throw new Error(`Request setup failed: ${err.message}`);
}
}
}
/**
* 1️⃣ Login – returns a Bearer token.
*/
async function login(): Promise<string> {
const { data } = await safeRequest<{ token: string }>({
method: "POST",
url: "/login",
data: { username: USERNAME, password: PASSWORD },
});
if (!data.token) throw new Error("Login succeeded but no token returned.");
return data.token;
}
/**
* 2️⃣ Get basic system info (hostname, MAC, etc.)
*/
async function getSystemInfo(token: string) {
const { data } = await safeRequest<any>({
method: "GET",
url: "/system",
headers: { Authorization: `Bearer ${token}` },
});
return data;
}
/**
* 3️⃣ Get current firmware version (primary slot).
*/
async function getFirmwareVersion(token: string): Promise<string> {
const { data } = await safeRequest<any>({
method: "GET",
url: "/system/firmware",
headers: { Authorization: `Bearer ${token}` },
});
return data.primary ?? "unknown";
}
/**
* 4️⃣ Download firmware image from a remote URL and stream it to the switch.
* Returns true if the switch reports success.
*/
async function uploadFirmware(token: string, imageUrl: string): Promise<boolean> {
// Fetch the image (stream to avoid buffering huge file)
const imageResp = await axios.get(imageUrl, {
responseType: "stream",
timeout: 30_000,
});
const total = parseInt(imageResp.headers["content-length"] ?? "0", 10);
let uploaded = 0;
// Progress reporting
const progress = (chunk: any) => {
uploaded += chunk.length;
const pct = total ? ((uploaded / total) * 100).toFixed(1) : "?";
process.stderr.write(`\rUploading… ${uploaded} B / ${total} B (${pct}%)`);
};
imageResp.data.on("data", progress);
imageResp.data.on("end", () => {
process.stderr.write("\n");
});
const uploadResp = await safeRequest<any>({
method: "POST",
url: "/system/firmware/upload",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/octet-stream",
},
data: imageResp.data,
// axios will pipe the stream; we need to turn off automatic JSON parsing
transformRequest: [(data) => data],
});
// Clean up listener
imageResp.data.off("data", progress);
if (uploadResp.data.status === "success") {
console.log("[INFO] Firmware image uploaded successfully.");
return true;
} else {
console.error(
`[ERROR] Upload rejected by switch: ${JSON.stringify(uploadResp.data)}`
);
return false;
}
}
/**
* 5️⃣ Activate the newly uploaded image and trigger a reload.
*/
async function activateAndReload(token: string, slot: string = "primary") {
// Activate slot
await safeRequest<any>({
method: "POST",
url: "/system/firmware/activate",
headers: { Authorization: `Bearer ${token}` },
data: { slot },
});
console.log(`[INFO] Firmware slot '${slot}' marked for next boot.`);
// Issue reload
await safeRequest<any>({
method: "POST",
url: "/system/reload",
headers: { Authorization: `Bearer ${token}` },
});
console.log("[INFO] Reload request accepted. Switch will reboot shortly.");
}
/**
* Main orchestration
*/
async function main() {
console.log("[STEP] Authenticating...");
const token = await login();
console.log("[OK] Token acquired.");
console.log("[STEP] Fetching system info...");
const sysInfo = await getSystemInfo(token);
console.log(
`[OK] Hostname: ${sysInfo.hostname}, MAC: ${sysInfo.baseMacAddress}`
);
console.log("[STEP] Checking current firmware version...");
const currentVer = await getFirmwareVersion(token);
console.log(`[INFO] Current primary firmware: ${currentVer}`);
const PATCHED_VERSION = "10.04.0015"; // <-- update to the actual fixed version
if (currentVer === PATCHED_VERSION) {
console.log("[SUCCESS] Switch already runs the patched firmware.");
return;
}
console.warn(
`[WARN] Firmware is outdated (${currentVer} < ${PATCHED_VERSION}).`
);
if (!FIRMWARE_URL) {
console.error(
"[ERROR] FIRMWARE_URL environment variable not set – cannot auto‑patch."
);
process.exit(1);
}
console.log("[STEP] Uploading patched firmware image...");
const uploadOk = await uploadFirmware(token, FIRMWARE_URL);
if (!uploadOk) {
process.exit(1);
}
console.log("[STEP] Activating new image and scheduling reload...");
await activateAndReload(token, "primary");
console.log("[DONE] Patch process completed.");
}
// Run
main().catch((err) => {
console.error("[FATAL]", err);
process.exit(1);
});
# 1️⃣ Save as aosxcx_patch.ts
# 2️⃣ Ensure .env exists (see Step 4)
# 3️⃣ Compile & execute
npx ts-node aosxcx_patch.ts
# or, after compiling:
# npx tsc aosxcx_patch.ts && node aosxcx_patch.js
Create a file named .env in the project root (never commit this to a public repo!).
| Variable | Example | Description |
|---|---|---|
SWITCH_IP | 10.1.2.3 | Management IP of the AOS‑CX switch |
USERNAME | admin | Admin or operator username |
PASSWORD | s3cureP@ss! | Password (or use secret‑manager / vault in prod) |
FIRMWARE_URL | https://internal-repo.example.com/firmware/AOS-CX_10.04.0015.swi | HTTP(S) location of the patched .swi image |
VERIFY_SSL | true | Set to false only in a lab where you use a self‑signed cert (not recommended for prod) |
Example .env
SWITCH_IP=10.1.2.3
USERNAME=admin
PASSWORD=s3cureP@ss!
FIRMWARE_URL=https://repo.example.com/firmware/AOS-CX_10.04.0015.swi
VERIFY_SSL=true
Tip: In production you should retrieve
PASSWORD(and possiblyFIRMWARE_URL) from a secret store (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault, etc.) and inject them as environment variables at runtime.
| Pattern | Why it’s useful | Code snippet (Python) |
|---|---|---|
| Retry with exponential back‑off | Handles transient network glitches | python\nimport time, random\nfor attempt in range(5):\n try:\n resp = requests.get(url, headers=hdr, verify=VERIFY_SSL, timeout=10)\n resp.raise_for_status()\n break\n except requests.RequestException as e:\n if attempt == 4: raise\n wait = 2 ** attempt + random.random()\n time.sleep(wait)\n |
| Token refresh | Tokens usually expire after ~30 min; re‑login automatically | Wrap every API call in a helper that catches 401 and calls login() again. |
| Streaming large uploads | Avoids loading multi‑hundred‑MB firmware into RAM | See upload_firmware (Python) or Axios responseType: "stream" (JS). |
| Structured logging | Makes log aggregation easier | Use logging (Python) or pino/winston (Node) with JSON output. |
| Context manager for API session | Guarantees cleanup (e.g., closing connections) | python\nwith requests.Session() as s:\n s.verify = VERIFY_SSL\n s.headers.update({"Authorization": f"Bearer {token}"})\n resp = s.get(...)\n |
| Idempotent patch check | Running the script twice does nothing if already patched | Compare current version to the desired version before any upload. |
| Symptom | Likely cause | Fix |
|---|---|---|
401 Unauthorized after login | Wrong credentials or account locked | Verify username/password; ensure the account has rest-api privilege (show aaa authentication). |
403 Forbidden on firmware endpoints | Token lacks required role | Use an admin account or grant the system role to the user (aaa authentication login restrict etc.). |
SSL: CERTIFICATE_VERIFY_FAILED | Switch uses a self‑signed cert and VERIFY_SSL=true | Either add the switch’s CA to trusted store, or set VERIFY_SSL=false only for testing. |
Connection timeout (ReadTimeoutError) | Switch unreachable, wrong IP, or firewall blocking TCP/443 | Ping the IP, confirm HTTPS is enabled (`show running-config |
| Upload hangs at 0 % | The switch expects Content-Type: application/octet-stream but you sent JSON | Ensure the upload request uses the correct content type (see code). |
| After upload, firmware version unchanged | Image not activated or reload not issued | Call the /system/firmware/activate endpoint and then /system/reload. |
| Script exits with “No response received” | The switch closed the connection prematurely (often due to overload) | Add retries, reduce concurrent requests, or increase timeout. |
| Firmware version reports “unknown” | API path changed in newer/older train | Consult the specific AOS‑CX API reference for your version; adjust the URL accordingly. |
Debug tip – enable verbose logging:
Python: logging.basicConfig(level=logging.DEBUG)
Node: set DEBUG=axios:* before running, or pass { validateStatus: () => true } to inspect raw responses.
| ✅ Item | Reason / Action |
|---|---|
| Use least‑privilege account | Create a dedicated service account with only rest-api and system roles. |
| Store secrets securely | Inject PASSWORD and FIRMWARE_URL via a secret manager; never hard‑code. |
| Validate switch certificate | Keep VERIFY_SSL=true in production; add the switch’s CA to the trust store if needed. |
| Test in a lab first | Verify the exact firmware version that resolves CVE‑XXXX (check HPE advisory). |
| Backup current config & flash | Before any firmware change, run show running-config and copy startup-config tftp: (or similar). |
| Schedule a maintenance window | Firmware reload causes traffic disruption; inform stakeholders. |
| Enable audit logging | On the switch, log all REST API accesses (aaa accounting exec default start-stop group tacacs+). |
| Monitor post‑reload | After reboot, verify show version and show logging for anomalies. |
| Rollback plan | Know how to boot the secondary image (boot system secondary) if the new image misbehaves. |
| Document the runbook | Include the exact commands, expected version numbers, and who to call if something fails. |
| Automate with CI/CD (optional) | For large fleets, wrap the script in a Jenkins/GitLab job that runs only after a manual approval gate. |
| Validate patch effectiveness | Run a vulnerability scanner (e.g., Nessus, OpenVAS) against the switch management interface to confirm the RCE vector is closed. |
With the snippets above you can:
Remember to treat the switch like any other production asset: test, backup, approve, and monitor. Happy patching! 🚀
Source: Security Week AI
Follow ICARAX for more AI insights and tutorials.
