Exploiting CVE-2002-0082

In this post we explore CVE-2002-0082 and try to exploit it. To do so we use the vulnerable virtual machine created for the Kioptrix 1 challenge.

Here is the summary for CVE-2002-0082 :

The dbm and shm session cache code in mod_ssl before 2.8.7-1.3.23, and Apache-SSL before 1.3.22+1.46, does not properly initialize memory using the i2d_SSL_SESSION function, which allows remote attackers to use a buffer overflow to execute arbitrary code via a large client certificate that is signed by a trusted Certificate Authority (CA), which produces a large serialized session.

Finding the error

By looking for CVE-2002-0082 we find an apacheweek security issue indicating that :

A buffer overflow has been found in mod_ssl in all versions prior to 2.8.7-1.3.23 (February 23rd 2002)

So we download versions 2.8.6-1.3.23 and 2.8.7-1.3.23 from the modssl website. In the CHANGES file in version 2.8.7-1.3.23 we find the following indication related to the security fix :

Fixed potential buffer overflow in DBM and SHMHT session cache if very very large certificate chains are used.

If we do a diff between the two versions we find that the files pkg.sslmod/ssl_scache_dbm.c and pkg.sslmod/ssl_scache_shmht.c have been modified.

diff --git a/pkg.sslmod/ssl_scache_dbm.c b/pkg.sslmod/ssl_scache_dbm.
c
index 323c612..669658f 100644
--- a/pkg.sslmod/ssl_scache_dbm.c
+++ b/pkg.sslmod/ssl_scache_dbm.c
@@ -143,8 +143,10 @@ BOOL ssl_scache_dbm_store(server_rec *s, UCHAR *
id, int idlen, time_t expiry, SS
     UCHAR *ucp;

     /* streamline session data */
+    if ((nData = i2d_SSL_SESSION(sess, NULL)) > sizeof(ucaData))
+        return FALSE;
     ucp = ucaData;
-    nData = i2d_SSL_SESSION(sess, &ucp);
+    i2d_SSL_SESSION(sess, &ucp);

     /* be careful: do not try to store too much bytes in a DBM file! */
diff --git a/pkg.sslmod/ssl_scache_shmht.c b/pkg.sslmod/ssl_scache_shmht.c
index 18e688a..fad41e0 100644
--- a/pkg.sslmod/ssl_scache_shmht.c
+++ b/pkg.sslmod/ssl_scache_shmht.c
@@ -175,8 +175,10 @@ BOOL ssl_scache_shmht_store(server_rec *s, UCHAR *id, int idlen, time_t expiry,
     UCHAR *ucp;

     /* streamline session data */
+    if ((nData = i2d_SSL_SESSION(sess, NULL)) > sizeof(ucaData))
+        return FALSE;
     ucp = ucaData;
-    nData = i2d_SSL_SESSION(sess, &ucp);
+    i2d_SSL_SESSION(sess, &ucp);

i2d_SSL_SESSION description

We download the OpenSSL code. If we look at the CHANGES file in version 2.8.7-1.3.23 we find that the changes were made between the 01-Feb-2002 and 23-Feb-2002. We revert the state of the code to a date prior to 01-Feb-2002 (let’s say 23-Jan-2002). We picked the commit 9b2f486c9e95c79ab9a0f10a9f61a8792ed2a18d:

git checkout 9b2f486c9e95c79ab9a0f10a9f61a8792ed2a18d

The code of the i2d_SSL_SESSION function can be found in ssl_asn1.c. Click on the file name below if you want to see the whole function.

openssl/ssl/ssl_asn1.c
int i2d_SSL_SESSION(SSL_SESSION *in, unsigned char **pp)
	{
#define LSIZE2 (sizeof(long)*2)
	int v1=0,v2=0,v3=0,v4=0,v5=0;
	unsigned char buf[4],ibuf1[LSIZE2],ibuf2[LSIZE2];
	unsigned char ibuf3[LSIZE2],ibuf4[LSIZE2],ibuf5[LSIZE2];
	long l;
	SSL_SESSION_ASN1 a;
	M_ASN1_I2D_vars(in);

	if ((in == NULL) || ((in->cipher == NULL) && (in->cipher_id == 0)))
		return(0);

	/* Note that I cheat in the following 2 assignments.  I know
	 * that if the ASN1_INTEGER passed to ASN1_INTEGER_set
	 * is > sizeof(long)+1, the buffer will not be re-OPENSSL_malloc()ed.
	 * This is a bit evil but makes things simple, no dynamic allocation
	 * to clean up :-) */
	a.version.length=LSIZE2;
	a.version.type=V_ASN1_INTEGER;
	a.version.data=ibuf1;
	ASN1_INTEGER_set(&(a.version),SSL_SESSION_ASN1_VERSION);

	a.ssl_version.length=LSIZE2;
	a.ssl_version.type=V_ASN1_INTEGER;
	a.ssl_version.data=ibuf2;
	ASN1_INTEGER_set(&(a.ssl_version),in->ssl_version);

	a.cipher.type=V_ASN1_OCTET_STRING;
	a.cipher.data=buf;

	if (in->cipher == NULL)
		l=in->cipher_id;
	else
		l=in->cipher->id;
	if (in->ssl_version == SSL2_VERSION)
		{
		a.cipher.length=3;
		buf[0]=((unsigned char)(l>>16L))&0xff;
		buf[1]=((unsigned char)(l>> 8L))&0xff;
		buf[2]=((unsigned char)(l     ))&0xff;
		}
	else
		{
		a.cipher.length=2;
		buf[0]=((unsigned char)(l>>8L))&0xff;
		buf[1]=((unsigned char)(l    ))&0xff;
		}

	a.master_key.length=in->master_key_length;
	a.master_key.type=V_ASN1_OCTET_STRING;
	a.master_key.data=in->master_key;

	a.session_id.length=in->session_id_length;
	a.session_id.type=V_ASN1_OCTET_STRING;
	a.session_id.data=in->session_id;

	a.session_id_context.length=in->sid_ctx_length;
	a.session_id_context.type=V_ASN1_OCTET_STRING;
	a.session_id_context.data=in->sid_ctx;

	a.key_arg.length=in->key_arg_length;
	a.key_arg.type=V_ASN1_OCTET_STRING;
	a.key_arg.data=in->key_arg;

#ifndef OPENSSL_NO_KRB5
	a.krb5_princ.length=in->krb5_client_princ_len;
	a.krb5_princ.type=V_ASN1_OCTET_STRING;
	a.krb5_princ.data=in->krb5_client_princ;
#endif /* OPENSSL_NO_KRB5 */

	if (in->time != 0L)
		{
		a.time.length=LSIZE2;
		a.time.type=V_ASN1_INTEGER;
		a.time.data=ibuf3;
		ASN1_INTEGER_set(&(a.time),in->time);
		}

	if (in->timeout != 0L)
		{
		a.timeout.length=LSIZE2;
		a.timeout.type=V_ASN1_INTEGER;
		a.timeout.data=ibuf4;
		ASN1_INTEGER_set(&(a.timeout),in->timeout);
		}

	if (in->verify_result != X509_V_OK)
		{
		a.verify_result.length=LSIZE2;
		a.verify_result.type=V_ASN1_INTEGER;
		a.verify_result.data=ibuf5;
		ASN1_INTEGER_set(&a.verify_result,in->verify_result);
		}


	M_ASN1_I2D_len(&(a.version),		i2d_ASN1_INTEGER);
	M_ASN1_I2D_len(&(a.ssl_version),	i2d_ASN1_INTEGER);
	M_ASN1_I2D_len(&(a.cipher),		i2d_ASN1_OCTET_STRING);
	M_ASN1_I2D_len(&(a.session_id),		i2d_ASN1_OCTET_STRING);
	M_ASN1_I2D_len(&(a.master_key),		i2d_ASN1_OCTET_STRING);
#ifndef OPENSSL_NO_KRB5
        M_ASN1_I2D_len(&(a.krb5_princ),         i2d_ASN1_OCTET_STRING);
#endif /* OPENSSL_NO_KRB5 */
	if (in->key_arg_length > 0)
		M_ASN1_I2D_len_IMP_opt(&(a.key_arg),i2d_ASN1_OCTET_STRING);
	if (in->time != 0L)
		M_ASN1_I2D_len_EXP_opt(&(a.time),i2d_ASN1_INTEGER,1,v1);
	if (in->timeout != 0L)
		M_ASN1_I2D_len_EXP_opt(&(a.timeout),i2d_ASN1_INTEGER,2,v2);
	if (in->peer != NULL)
		M_ASN1_I2D_len_EXP_opt(in->peer,i2d_X509,3,v3);
	M_ASN1_I2D_len_EXP_opt(&a.session_id_context,i2d_ASN1_OCTET_STRING,4,v4);
	if (in->verify_result != X509_V_OK)
		M_ASN1_I2D_len_EXP_opt(&(a.verify_result),i2d_ASN1_INTEGER,5,v5);

	M_ASN1_I2D_seq_total();

	M_ASN1_I2D_put(&(a.version),		i2d_ASN1_INTEGER);
	M_ASN1_I2D_put(&(a.ssl_version),	i2d_ASN1_INTEGER);
	M_ASN1_I2D_put(&(a.cipher),		i2d_ASN1_OCTET_STRING);
	M_ASN1_I2D_put(&(a.session_id),		i2d_ASN1_OCTET_STRING);
	M_ASN1_I2D_put(&(a.master_key),		i2d_ASN1_OCTET_STRING);
#ifndef OPENSSL_NO_KRB5
        M_ASN1_I2D_put(&(a.krb5_princ),         i2d_ASN1_OCTET_STRING);
#endif /* OPENSSL_NO_KRB5 \*/
	if (in->key_arg_length > 0)
		M_ASN1_I2D_put_IMP_opt(&(a.key_arg),i2d_ASN1_OCTET_STRING,0);
	if (in->time != 0L)
		M_ASN1_I2D_put_EXP_opt(&(a.time),i2d_ASN1_INTEGER,1,v1);
	if (in->timeout != 0L)
		M_ASN1_I2D_put_EXP_opt(&(a.timeout),i2d_ASN1_INTEGER,2,v2);
	if (in->peer != NULL)
		M_ASN1_I2D_put_EXP_opt(in->peer,i2d_X509,3,v3);
	M_ASN1_I2D_put_EXP_opt(&a.session_id_context,i2d_ASN1_OCTET_STRING,4,
			       v4);
	if (in->verify_result != X509_V_OK)
		M_ASN1_I2D_put_EXP_opt(&a.verify_result,i2d_ASN1_INTEGER,5,v5);
	M_ASN1_I2D_finish();
	}

Here is what the documentation has to say about this function :

Description

i2d_SSL_SESSION() transforms the SSL_SESSION object in into the ASN1 representation and stores it into the memory location pointed to by pp. The length of the resulting ASN1 representation is returned. If pp is the NULL pointer, only the length is calculated and returned.

Notes

When using i2d_SSL_SESSION(), the memory location pointed to by pp must be large enough to hold the binary representation of the session. There is no known limit on the size of the created ASN1 representation, so the necessary amount of space should be obtained by first calling i2d_SSL_SESSION() with pp=NULL , and obtain the size needed, then allocate the memory and call i2d_SSL_SESSION() again.

Return values

i2d_SSL_SESSION() returns the size of the ASN1 representation in bytes. When the session is not valid, 0 is returned and no operation is performed.

Understanding the vulnerability in mod_ssl

What i2d_SSL_SESSION does is convert an object into another and store it in pp.

The information was in the i2d_SSL_SESSION notes. The memory location pointed to by pp must be large enough to hold the binary representation of the session. The fact that the mod_ssl code did not verify the amount of space needed first was the problem :

BOOL ssl_scache_dbm_store(server_rec *s, UCHAR *id, int idlen, time_t expiry, SSL_SESSION *sess)
{
    SSLModConfigRec *mc = myModConfig();
    DBM *dbm;
    datum dbmkey;
    datum dbmval;
    UCHAR ucaData[SSL_SESSION_MAX_DER];
    int nData;
    UCHAR *ucp;

    /* streamline session data */
    ucp = ucaData;
    nData = i2d_SSL_SESSION(sess, &ucp);

    /* be careful: do not try to store too much bytes in a DBM file! \*/
    ...
 }

In the code above we can see that the sess object is converted and stored in ucp but no verification is done to verify that the converted session does not overflow the bounds of ucp.