ipa tools: remove sensitive material from the commandline

When command line tools accept passwords, remove them from the command
line so that they don't get visible in '/proc/pid/commandline'.

There is no common method to access the original ARGV vector and modify
it from Python. Since this mostly affects Linux systems where IPA
services run, we expect use of GNU libc and thus can rely on internal
glibc symbols. If they aren't available, the code will skip removing
passwords.

Fixes: CVE-2024-11029

Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com>
This commit is contained in:
Alexander Bokovoy authored and Antonio Torres committed 2025-01-15 11:15:39 +01:00
1 parent cf84a22228
commit 3b38efe758
12 files changed
+90 -21

No files matched your search

@@ -15,6 +15,7 @@ import six
import gssapi
from ipalib.install.kinit import kinit_keytab, kinit_password
from ipapython.admintool import admin_cleanup_global_argv
if six.PY3:
unicode = str
@@ -52,11 +53,13 @@ def parse_options():
"--password",
action="store",
dest="password",
help="Display debugging information",
help="Password for Active Directory administrator",
sensitive=True
)
options, args = parser.parse_args()
safe_options = parser.get_safe_opts(options)
admin_cleanup_global_argv(parser, options, sys.argv)
# We only use first argument of the passed args but as D-BUS interface
# in oddjobd cannot expose optional, we fill in empty slots from IPA side
+2 -1
View File
@@ -35,7 +35,7 @@ from ipaserver.install.installutils import (
read_password,
check_server_configuration,
run_script)
from ipapython.admintool import ScriptError
from ipapython.admintool import ScriptError, admin_cleanup_global_argv
from ipapython import version
from ipapython import ipautil
from ipalib import api, errors, krb_utils
@@ -93,6 +93,7 @@ def parse_options():
options, _args = parser.parse_args()
safe_options = parser.get_safe_opts(options)
admin_cleanup_global_argv(parser, options, sys.argv)
return safe_options, options
+2
View File
@@ -42,6 +42,7 @@ from ipalib.constants import DOMAIN_LEVEL_1
from ipapython.config import IPAOptionParser
from ipapython.ipa_log_manager import standard_logging_setup
from ipaplatform.paths import paths
from ipapython.admintool import admin_cleanup_global_argv
logger = logging.getLogger(os.path.basename(__file__))
@@ -132,6 +133,7 @@ def parse_options():
options, args = parser.parse_args()
safe_options = parser.get_safe_opts(options)
admin_cleanup_global_argv(parser, options, sys.argv)
if args:
parser.error("Too many arguments provided")
+4 -2
View File
@@ -24,13 +24,13 @@ from __future__ import print_function
import sys
from ipaplatform.paths import paths
try:
from optparse import OptionParser # pylint: disable=deprecated-module
from ipapython import ipautil, config
from ipaserver.install import installutils
from ipaserver.install.ldapupdate import LDAPUpdate
from ipalib import api, errors
from ipapython.ipa_log_manager import standard_logging_setup
from ipapython.dn import DN
from ipapython.admintool import admin_cleanup_global_argv
except ImportError as e:
print("""\
There was a problem importing one of the required Python modules. The
@@ -46,7 +46,8 @@ nis_config_dn = DN(('cn', 'NIS Server'), ('cn', 'plugins'), ('cn', 'config'))
def parse_options():
usage = "%prog [options] <enable|disable|status>\n"
usage += "%prog [options]\n"
parser = OptionParser(usage=usage, formatter=config.IPAFormatter())
parser = config.IPAOptionParser(usage=usage,
formatter=config.IPAFormatter())
parser.add_option("-d", "--debug", action="store_true", dest="debug",
help="Display debugging information about the update(s)")
@@ -55,6 +56,7 @@ def parse_options():
config.add_standard_options(parser)
options, args = parser.parse_args()
admin_cleanup_global_argv(parser, options, sys.argv)
return options, args
+6 -6
View File
@@ -32,8 +32,8 @@ from ipaserver.install import (replication, installutils, bindinstance,
from ipalib import api, errors
from ipalib.constants import FQDN
from ipalib.util import has_managed_topology, print_replication_status
from ipapython import ipautil, ipaldap, version
from ipapython.admintool import ScriptError
from ipapython import ipautil, ipaldap, version, config
from ipapython.admintool import admin_cleanup_global_argv, ScriptError
from ipapython.dn import DN
logger = logging.getLogger(os.path.basename(__file__))
@@ -54,11 +54,10 @@ commands = {
def parse_options():
from optparse import OptionParser # pylint: disable=deprecated-module
parser = OptionParser(version=version.VERSION)
parser = config.IPAOptionParser(version=version.VERSION)
parser.add_option("-H", "--host", dest="host", help="starting host")
parser.add_option("-p", "--password", dest="dirman_passwd", help="Directory Manager password")
parser.add_option("-p", "--password", dest="dirman_passwd", sensitive=True,
help="Directory Manager password")
parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False,
help="provide additional information")
parser.add_option("-f", "--force", dest="force", action="store_true", default=False,
@@ -66,6 +65,7 @@ def parse_options():
parser.add_option("--from", dest="fromhost", help="Host to get data from")
options, args = parser.parse_args()
admin_cleanup_global_argv(parser, options, sys.argv)
valid_syntax = False
+3 -2
View File
@@ -24,7 +24,6 @@ import logging
import os
import re
import sys
from optparse import OptionParser # pylint: disable=deprecated-module
from ipaplatform.paths import paths
from ipapython import config
@@ -32,6 +31,7 @@ from ipaserver.install import installutils
from ipalib import api, errors
from ipapython.ipa_log_manager import standard_logging_setup
from ipapython.dn import DN
from ipapython.admintool import admin_cleanup_global_argv
logger = logging.getLogger(os.path.basename(__file__))
@@ -51,9 +51,10 @@ def parse_options():
action="store_true",
help="List available Managed Entries")
parser.add_option("-p", "--password", dest="dirman_password",
help="Directory Manager password")
sensitive=True, help="Directory Manager password")
options, args = parser.parse_args()
admin_cleanup_global_argv(parser, options, sys.argv)
return options, args
+4 -3
View File
@@ -23,15 +23,15 @@ from __future__ import print_function
import logging
from ipapython import ipachangeconf
from ipapython.config import IPAOptionParser
from ipapython.config import (IPAOptionParser, OptionGroup,
OptionValueError)
from ipapython.admintool import admin_cleanup_global_argv
from ipapython.dn import DN
from ipapython import version
from ipapython import ipautil, certdb
from ipalib import api, errors, x509
from ipalib.constants import FQDN
from ipaserver.install import installutils
# pylint: disable=deprecated-module
from optparse import OptionGroup, OptionValueError
# pylint: enable=deprecated-module
from ipapython.ipa_log_manager import standard_logging_setup
import copy
@@ -189,6 +189,7 @@ def parse_options():
options, _args = parser.parse_args()
safe_options = parser.get_safe_opts(options)
admin_cleanup_global_argv(parser, options, sys.argv)
if options.master and options.replica:
parser.error("on-master and on-replica options are mutually exclusive!")
+7 -3
View File
@@ -43,6 +43,7 @@ from ipalib.util import (
print_replication_status,
verify_host_resolvable,
)
from ipapython.admintool import admin_cleanup_global_argv
from ipapython.ipa_log_manager import standard_logging_setup
from ipapython.dn import DN
from ipapython.config import IPAOptionParser
@@ -84,7 +85,8 @@ class NoRUVsFound(Exception):
def parse_options():
parser = IPAOptionParser(version=version.VERSION)
parser.add_option("-H", "--host", dest="host", help="starting host")
parser.add_option("-p", "--password", dest="dirman_passwd", help="Directory Manager password")
parser.add_option("-p", "--password", dest="dirman_passwd", sensitive=True,
help="Directory Manager password")
parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False,
help="provide additional information")
parser.add_option("-d", "--debug", dest="debug", action="store_true", default=False,
@@ -95,7 +97,7 @@ def parse_options():
help="DANGER: clean up references to a ghost master")
parser.add_option("--binddn", dest="binddn", default=None, type="dn",
help="Bind DN to use with remote server")
parser.add_option("--bindpw", dest="bindpw", default=None,
parser.add_option("--bindpw", dest="bindpw", default=None, sensitive=True,
help="Password for Bind DN to use with remote server")
parser.add_option("--winsync", dest="winsync", action="store_true", default=False,
help="This is a Windows Sync Agreement")
@@ -103,13 +105,15 @@ def parse_options():
help="Full path and filename of CA certificate to use with TLS/SSL to the remote server")
parser.add_option("--win-subtree", dest="win_subtree", default=None,
help="DN of Windows subtree containing the users you want to sync (default cn=Users,<domain suffix)")
parser.add_option("--passsync", dest="passsync", default=None,
parser.add_option("--passsync", dest="passsync",
default=None, sensitive=True,
help="Password for the IPA system user used by the Windows PassSync plugin to synchronize passwords")
parser.add_option("--from", dest="fromhost", help="Host to get data from")
parser.add_option("--no-lookup", dest="nolookup", action="store_true", default=False,
help="do not perform DNS lookup checks")
options, args = parser.parse_args()
admin_cleanup_global_argv(parser, options, sys.argv)
valid_syntax = False
+40
View File
@@ -39,6 +39,45 @@ SERVER_NOT_CONFIGURED = 2
logger = logging.getLogger(__name__)
def admin_cleanup_global_argv(option_parser, options, argv):
"""Takes option parser and generated options and scrubs sensitive arguments
from the global program arguments. Note that this only works for GNU GLIBC
as Python has no generic way to get access to the original argv values to
modify them in place.
The code assumes Python behavior, e.g. there are two additional args in the
list (/path/to/python -I ...) than what's passed as 'argv' here.
"""
import ctypes
import ctypes.util
try:
_c = ctypes.CDLL(ctypes.util.find_library("c"))
if _c._name is None:
return
_argv = ctypes.POINTER(ctypes.c_voidp).in_dll(_c, "_dl_argv")
# since we run as 'python -I <executable> ...', add two args
_argc = len(argv) + 2
all_options = []
if '_get_all_options' in dir(option_parser):
# OptParse parser
all_options = option_parser._get_all_options()
elif '_actions' in dir(option_parser):
# ArgParse parser
all_options = option_parser._actions
for opt in all_options:
if getattr(opt, 'sensitive', False):
v = getattr(options, opt.dest)
for i in range(0, _argc):
vi = ctypes.cast(_argv[i],
ctypes.c_char_p
).value.decode('utf-8')
if vi == v:
ctypes.memset(_argv[i], ord('X'), len(v))
except Exception:
pass
class ScriptError(Exception):
"""An exception that records an error message and a return value
"""
@@ -148,6 +187,7 @@ class AdminTool:
cls._option_parsers[cls] = cls.option_parser
options, args = cls.option_parser.parse_args(argv[1:])
admin_cleanup_global_argv(cls.option_parser, options, argv)
command_class = cls.get_command_class(options, args)
command = command_class(options, args)
+16 -1
View File
@@ -28,6 +28,7 @@ from ipaplatform.paths import paths
from ipapython.dn import DN
from ipapython.ipaldap import LDAPClient, LDAPEntry, realm_to_ldapi_uri
from ipapython.ipa_log_manager import standard_logging_setup
from ipapython.admintool import admin_cleanup_global_argv
from ipaserver.install.ipa_migrate_constants import (
DS_CONFIG, DB_OBJECTS, DS_INDEXES, BIND_DN, LOG_FILE_NAME,
STRIP_OP_ATTRS, STRIP_ATTRS, STRIP_OC, PROD_ATTRS,
@@ -284,6 +285,18 @@ class LDIFParser(ldif.LDIFParser):
self.mc.process_db_entry(entry_dn=dn, entry_attrs=entry_attrs)
class SensitiveStoreAction(argparse._StoreAction):
def __init__(self, *, sensitive, **options):
super(SensitiveStoreAction, self).__init__(**options)
self.sensitive = sensitive
def _get_kwargs(self):
names = super(SensitiveStoreAction, self)._get_kwargs()
sensitive_name = 'sensitive'
names.extend((sensitive_name, getattr(self, sensitive_name)))
return names
#
# Migrate IPA to IPA Class
#
@@ -344,7 +357,8 @@ class IPAMigrate():
help='Password for the Bind DN. If a password '
'is not provided then the user will be '
'prompted to enter it',
default=None)
default=None, sensitive=True,
action=SensitiveStoreAction)
parser.add_argument('-j', '--bind-pw-file',
help='A text file containing the clear text '
'password for the Bind DN', default=None)
@@ -2023,6 +2037,7 @@ class IPAMigrate():
parser = argparse.ArgumentParser(description=desc, allow_abbrev=True)
self.add_options(parser)
self.validate_options()
admin_cleanup_global_argv(parser, self.args, sys.argv)
# Check for dryrun mode
if self.args.dryrun or self.args.dryrun_record is not None:
+1 -1
View File
@@ -185,7 +185,7 @@ class Restore(admintool.AdminTool):
super(Restore, cls).add_options(parser, debug_option=True)
parser.add_option(
"-p", "--password", dest="password",
"-p", "--password", dest="password", sensitive=True,
help="Directory Manager password")
parser.add_option(
"--gpg-keyring", dest="gpg_keyring",
+1 -1
View File
@@ -72,7 +72,7 @@ class ServerCertInstall(admintool.AdminTool):
help="Name of the certificate to install")
parser.add_option(
"-p", "--dirman-password",
dest="dirman_password",
dest="dirman_password", sensitive=True,
help="Directory Manager password")
def validate_options(self):