Skip to content

Latest commit

 

History

History
94 lines (65 loc) · 6.33 KB

File metadata and controls

94 lines (65 loc) · 6.33 KB

JumpServer — SQL Server change_secret Automation: Jinja2-Concatenated Unparameterized T-SQL Injection

Summary

Field Value
Product JumpServer (open-source Privileged Access Management / bastion host)
Vendor repository https://github.com/jumpserver/jumpserver
Vulnerability type CWE-89 (SQL Injection) — attacker-controlled account fields concatenated into T-SQL and executed unparameterized
Component apps/accounts/automations/change_secret/database/sqlserver/main.yml + apps/libs/ansible/modules/mssql_script.py
Affected versions v3.0.0 (introduced 2023-01-16, commit 56d533c8) through v4.10.16 (latest)
Fixed version Not fixed as of v4.10.16
Privileges required Low — account/asset management privileges to set an account's username/secret on a SQL Server asset
Verification Verified at the template/string level on jumpserver/jms_all:v4.10.16: the shipped change_secret SQL Server playbook interpolates account.username/account.secret directly into the T-SQL string and executes it unparameterized (cursor.execute(query, None)). Runtime landing requires a live SQL Server target (not stood up here).

Vulnerability Description

The SQL Server change_secret automation renders a T-SQL script by Jinja2-interpolating the account's username and secret directly into the SQL string, then passes the whole string to mssql_script, which runs cursor.execute(query, sql_params) with sql_params=None (the playbook passes no params:), i.e. no parameter binding. account.secret is only Jinja2-syntax-escaped (escape_jinja2_syntax), never SQL-escaped. A secret value containing a single quote and additional statements breaks out of the quoted literal and appends attacker T-SQL that executes in the same batch under the automation's privileges.

Impact

A user with account-management privileges can execute arbitrary T-SQL on any managed SQL Server asset when a change_secret automation runs for an account whose new secret/username they control — create logins, read/write arbitrary tables, and (if xp_cmdshell is enabled) execute OS commands. Impact is on the managed database asset, not JumpServer itself.

GitHub Address

https://github.com/jumpserver/jumpserver

Affected Version Range

v3.0.0 – v4.10.16 (latest). The unparameterized Jinja2-concatenated template is present on the current release.

CVSS 3.1

Base score: 8.0 (High) CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N

Rationale: network-reachable, low complexity, requires account/asset-management privileges (PR:L), no interaction; arbitrary SQL on the managed database asset crosses the JumpServer↔managed-asset trust boundary (Scope changed), full C/I of that database. Scope limited to the target database rather than JumpServer itself.

Exploitation Conditions

  • The attacker holds account/asset-management RBAC (e.g. accounts.change_account / automation permissions) and can set an account's secret with secret_strategy=specific.
  • A SQL Server asset + account is registered and a change_secret automation runs against it.

Exploitation Steps

  1. Create/edit a SQL Server asset account, secret_strategy=specific, secret = a T-SQL breakout, e.g.: N3wP4ss!', DEFAULT_DATABASE=master; CREATE LOGIN attacker_login WITH PASSWORD='Xx123456!'; --
  2. Trigger the SQL Server change_secret automation for that account.
  3. The injected statements execute in the same batch on the SQL Server target (e.g. an attacker login is created).

SINK

apps/libs/ansible/modules/mssql_script.py — unparameterized execution:

script = module.params['script']
sql_params = module.params['params']     # the change_secret playbook passes no params -> None
...
cursor.execute(query, sql_params)        # sql_params=None => pymssql sends the query string verbatim

SOURCE

The account secret (and username) fields, Jinja2-interpolated into the T-SQL template apps/accounts/automations/change_secret/database/sqlserver/main.yml:

- name: Change SQLServer password
  script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}',
           DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version"
- name: Add SQLServer user
  script: "CREATE LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', ...;
           CREATE USER {{ account.username }} FOR LOGIN {{ account.username }}"

account.secret is only escape_jinja2_syntax-processed (Jinja2 escaping), not SQL-escaped.

Call Stack

POST .../change-secret-automations/<id>/execute/
  └─ ChangeSecretManager.get_secret()  (reads execution.snapshot['secret'], only Jinja2-escaped)   apps/accounts/automations/base/manager.py
       └─ Ansible renders change_secret/database/sqlserver/main.yml (Jinja2 interpolates account.username/secret into T-SQL)
            └─ apps/libs/ansible/modules/mssql_script.py -> cursor.execute(query, None)   ← unparameterized T-SQL executes on SQL Server

Dynamic Verification

Official jumpserver/jms_all:v4.10.16template/string-level confirmation: the shipped SQL Server change_secret playbook (verbatim from the box) interpolates account.username/account.secret into the T-SQL string, and mssql_script.py runs cursor.execute(query, None) (no binding), so a secret containing a '+; breaks out of the literal and appends attacker T-SQL. Full runtime landing (injected statements executing) requires a live SQL Server asset, which was not stood up here; the injection is confirmed at the render/execute-path level. (Honest scope: this is a Level-1 confirmation of the injectable path, not a live SQL Server exploitation run.)

PoC

poc_12_sqlserver_tsql_injection.py — a --live mode runs the full injection against a real SQL Server (see the docker command in the file header); the default mode demonstrates the template render / string breakout without an external service.

Suggested Fix

  1. Never interpolate account.username/account.secret into the SQL string. Use parameterized queries (cursor.execute(query, params) with placeholders) in mssql_script.py, and pass credentials as bound parameters from the playbook.
  2. Where identifiers (login names, database names) must be dynamic, validate them against a strict identifier grammar and quote via the driver's identifier-quoting, not string concatenation.