Uploaded image for project: 'Core Server'
  1. Core Server
  2. SERVER-93645

SCRAM-SHA-1 Authentication broken

    • Type: Icon: Bug Bug
    • Resolution: Works as Designed
    • Priority: Icon: Major - P3 Major - P3
    • None
    • Affects Version/s: 7.0.11
    • Component/s: None
    • Server Security
    • ALL
    • Hide

      Create two users on the "test" db

      test> db.createUser({user: "testSha1", pwd: "passwordSha1", roles: [

      Unknown macro: { role}

      , { role: 'dbAdmin', db: 'test' }], mechanisms:['SCRAM-SHA-1']})

      Unknown macro: { ok}

      test> db.createUser({user: "testSha256", pwd: "passwordSha256", roles: [

      Unknown macro: { role}

      , { role: 'dbAdmin', db: 'test' }], mechanisms:['SCRAM-SHA-256']})

      Unknown macro: { ok}

      Run the application below:
      It simply commects to the Mongo server and attempts to authenticate using SCRAM-SHA algorithm.

      Notice that SCRAM-SHA-256 works as expected, but that SCRAM-SHA-1 generates an authentication failure.

      > python3 -m venv venv
      > source venv/bin/activate
      (venv) > pip install scramp
      (venv) >
      (venv) >
      (venv) > python3 mongo.py testSha256 passwordSha256 test SCRAM-SHA-256
      AuthInit Reply:  r=a2f9b2678b0e410c8dfdb410459486dctCTAnXqb2b5XeJfgYQBahZpkvfwhhf8r,s=PblZy1gpLj6Jo4xn8jhqfrTlDL5vTvlgM4O1Hg==,i=15000
      AuthCont Reply:  v=FK4U7vwRViyR/PuXzEE043ymUuojJdtz86NudnFCp1s=
      AuthFinal Reply:
      Looks Good
      (venv) >
      (venv) >
      (venv) >python3 mongo.py testSha1 passwordSha1 test SCRAM-SHA-1
      AuthInit Reply:  r=b3b5d58b9f214115a9c2f5f26dabde30ZC8ZIDgk3Lr8YetZD+M9b9/hQJGjAIwX,s=5qobARF148RY4puhsQOkSg==,i=10000
      b'a\x00\x00\x00\x01ok\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02errmsg\x00\x17\x00\x00\x00Authentication failed.\x00\x10code\x00\x12\x00\x00\x00\x02codeName\x00\x15\x00\x00\x00AuthenticationFailed\x00\x00'
      AuthCont: Reply not OK

      The following code is written in python to demonstrate the issue:
      Save this file as "mongo.py"

      This scripts sends an "AuthInit" followed by "AuthCont" followed by "AuthCont" objects to the mongo server. Reading the reply after each message to extract the "payload" to use in the SCRAM-SHA algorithm. It uses scamp a standard python module to generate the SCRAM-SHA messages.

      Authenticating with SCRAM-SHA-256 works fine. 
      Authenticating with SCRAM-SHA-1 fails.

      import socket
      import time
      import sys
      from scramp import ScramClient

      #

      1. Steps to run
      2.   > python3 -m venv venv
      3.   > source venv/bin/activate
      4.   > pip install scramp
        #
      5.   > python3 mongo.py

      def sendMessageGetPayload(mongo, data, message):

          # mongo:        Socket connected to mongo server
          # data:         bytearray of data to be sent to mongo
          # message:      error message prefix.

          #
          # Send message to mongo
          # Validate that it was sent.
          size = len(data)
          sent = mongo.send(data)
          if size != sent:
              print(bson)
              sys.exit(message + ' Send failed: Sent: ' + str(sent) + ' of ' + str(size) + ' bytes')

          #
          # Retrieve the message from Mongo.
          # The first 21 bytes is the header block.
          # For simplicity just extracted the size.
          reply = mongo.recv(21)
          messageSize     = int.from_bytes(reply[0:4], byteorder='little')

          #
          # Retrive the bson part of the essage.
          # Do a simple check to make sure that the size is correct.
          bson = mongo.recv(messageSize - 21)
          bsonSize        = int.from_bytes(bson[0:4], byteorder='little')
          if messageSize != (bsonSize + 21):
              print(bson)
              sys.exit('AuthInit: Format Error:')

          #
          # Check the bson has an OK field
          # And that field is a double with value 1.
          findOk = bson.find(b'ok\x00')
          if findOk == -1:
              sys.exit(message + ' Failed to find OK')
          if bson[findOk + 3: findOk + 3+ 8] != b'\x00\x00\x00\x00\x00\x00\xf0\x3f':
              print(bson)
              sys.exit(message + ' Reply not OK')

          #
          # If there is an OK field there should be a payload.
          # So extract the payload from the bson
          # The payload size is 4 bytes of the string payload.
          # We ignore the 1 byte binary type.
          findPayload = bson.find(b'payload\x00')
          if findPayload == -1:
              sys.exit(message + ' Could not find payload')
          payloadSize     = int.from_bytes(bson[findPayload + 8:findPayload + 8 + 4], byteorder='little')
          payload         = bson[findPayload + 13:findPayload + 13 + payloadSize].decode('ascii')

          #
          # Extract the conversionID as we need that for the reply.
          findConvId = bson.find(b'conversationId\x00')
          if findConvId == -1:
              print(bson)
              sys.exit(message + ' Could not find conversationId')
          convId = int.from_bytes(bson[findConvId + 15: findConvId + 15 + 4], byteorder='little')

          return payload, convId

      def sendAuthInit(mongo, messageId, DBName, mech, firstMessage):

          #
          # Build the AuthInit Message in BSON by hand;
          # Should look like this:
          #  

      Unknown macro: {     #       saslStart}

          authInit = bytearray()

          bsonSize = 58 + len(mech) + 1 + len(firstMessage) + len(DBName) + 1

          #
          # Put the header in first.
          authInit.extend(int(bsonSize +21).to_bytes(4, byteorder='little'))      #   MessageSize         193 => (21 Body + 172 BSON)
          authInit.extend(int(messageId).to_bytes(4, byteorder='little'))         #   MessageId           1
          authInit.extend(0x00.to_bytes(4, byteorder='little'))                   #   messageResponseId   0
          authInit.extend(0x7DD.to_bytes(4, byteorder='little'))                  #   OPCode              OP_MSG
          authInit.extend(0x00.to_bytes(4, byteorder='little'))                   #   Flags               0
          authInit.extend(0x00.to_bytes(1, byteorder='little'))                   #   Kind                0

          #
          # BSON Object
          authInit.extend(int(bsonSize).to_bytes(4, byteorder='little'))          #   Bson Size           172

          authInit.extend(0x10.to_bytes(1, byteorder='little'));authInit.extend(b'saslStart\x00');authInit.extend(0x01.to_bytes(4, byteorder='little'))
          authInit.extend(0x2.to_bytes(1, byteorder='little'));authInit.extend(b'mechanism\x00');authInit.extend(int(len(mech) + 1).to_bytes(4, byteorder='little'));authInit.extend(mech.encode('ascii'));authInit.extend(b'\x00')
          authInit.extend(0x5.to_bytes(1, byteorder='little'));authInit.extend(b'payload\x00');authInit.extend(len(firstMessage).to_bytes(4, byteorder='little'));authInit.extend(0x0.to_bytes(1, byteorder='little'));authInit.extend(firstMessage.encode('ascii'))
          authInit.extend(0x2.to_bytes(1, byteorder='little'));authInit.extend(b'$db\x00');authInit.extend(int(len(DBName) + 1).to_bytes(4, byteorder='little'));authInit.extend(DBName.encode('ascii'));authInit.extend(b'\x00')
          authInit.extend(0x0.to_bytes(1, byteorder='little'))

          #
          # Send to mongo and get the import part of the reply
          return sendMessageGetPayload(mongo, authInit, 'AuthInit:')

      def sendAuthContOne(mongo, messageId, DBName, conversationId, finalMessage):

          #
          # Build the AuthCont Message in BSON by hand;
          # Should look like this:
          #  

      Unknown macro: {     #       saslContinue}

          authCont = bytearray()

          bsonSize = 66 + len(DBName) + 1 + len(finalMessage)

          #
          # Put the header in first.
          authCont.extend(int(bsonSize + 21).to_bytes(4, byteorder='little'))     #   MessageSize         196 => (21 Body + 175 BSON)
          authCont.extend(int(messageId).to_bytes(4, byteorder='little'))         #   MessageId           2 or 3
          authCont.extend(0x00.to_bytes(4, byteorder='little'))                   #   messageResponseId   0
          authCont.extend(0x7DD.to_bytes(4, byteorder='little'))                  #   OPCode              OP_MSG
          authCont.extend(0x00.to_bytes(4, byteorder='little'))                   #   Flags               0
          authCont.extend(0x00.to_bytes(1, byteorder='little'))                   #   Kind                0

          #
          # BSON Object
          authCont.extend(int(bsonSize).to_bytes(4, byteorder='little'))          #   Bson Size           175

          authCont.extend(0x10.to_bytes(1, byteorder='little'));authCont.extend(b'saslContinue\x00');authCont.extend(0x01.to_bytes(4, byteorder='little'))
          authCont.extend(0x10.to_bytes(1, byteorder='little'));authCont.extend(b'conversationId\x00');authCont.extend(conversationId.to_bytes(4, byteorder='little'))
          authCont.extend(0x5.to_bytes(1, byteorder='little'));authCont.extend(b'payload\x00');authCont.extend(len(finalMessage).to_bytes(4, byteorder='little'));authCont.extend(0x0.to_bytes(1, byteorder='little'));authCont.extend(finalMessage.encode('ascii'))
          authCont.extend(0x2.to_bytes(1, byteorder='little'));authCont.extend(b'$db\x00');authCont.extend(int(len(DBName) + 1).to_bytes(4, byteorder='little'));authCont.extend(DBName.encode('ascii'));authCont.extend(b'\x00')
          authCont.extend(0x0.to_bytes(1, byteorder='little'))

          #
          # Send to mongo and get the import part of the reply
          return sendMessageGetPayload(mongo, authCont, 'AuthCont:')

      def getUserInput():
          if len(sys.argv) > 1:
              username= sys.argv[1]
          if len(sys.argv) > 2:
              password= sys.argv[2]
          if len(sys.argv) > 3:
              DBName  = sys.argv[3]
          if len(sys.argv) > 4:
              mech    = sys.argv[4]

          if username == '':
              usernmae = input('Input username:  ')
          if password == '':
              password = input('Input password:  ')
          if DBName == '':
              DBName   = input('Input DB Name:   ')
          if mech == '':
              mech     = input('Input Mechanism: ')

          if mech != 'SCRAM-SHA-1' and mech != 'SCRAM-SHA-256':
              sys.exit('Invalid Mechanism: Use => "SCRAM-SHA-1" or "SCRAM-SHA-256"')

          return username, password, DBName, mech

      username, password, DBName, mech = getUserInput()

      color: Color value is invalid

      #

      # Create socket
      HOST    = 'localhost'
      PORT    = 27017

      mongo = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      mongo.connect((HOST,PORT))

      #␣

      1. Use library to generate SCRAM-SHA-X messages
        client = ScramClient([mech], username, password)

      #

      1. Send the first message to the server.
        authInitReply, conversationId1 = sendAuthInit(mongo, 1, DBName, mech, client.get_client_first())
        print('AuthInit Reply: ', authInitReply)
        client.set_server_first(authInitReply)

      #

      1. Send the proof to the server.
        authContReply, conversationId2 = sendAuthContOne(mongo, 2, DBName, conversationId1, client.get_client_final())
        client.set_server_final(authContReply)
        print('AuthCont Reply: ', authContReply)
        if conversationId1 != conversationId2:
            sys.exit('AuthInit ConvId does not match AuthCont ConvId')

      #

      1. Send the OK we accept you are the server message
        authFinalReply, conversationId3 = sendAuthContOne(mongo, 3, DBName, conversationId2, '')
        print('AuthFinal Reply:', authFinalReply)
        if conversationId1 != conversationId3:
            sys.exit('AuthInit ConvId does not match AuthFinal ConvId')

      #

      1. If no exceptions and no exit then
      2. Print Looks Good
        if authFinalReply != '':
            sys.exit('AuthFinal Reply not empty!!!')

      print('Looks Good')

       

      Helper script to print out the messages used in SCRAM-SHA-? communication.

      from scramp import ScramClient
      import base64

      USERNAME = input('UserName (user)? ').strip() or 'user'
      PASSWORD = input('Password (pencil)? ').strip() or 'pencil'
      mech     = input('MECHANISMS (SCRAM-SHA-1)? ').strip() or 'SCRAM-SHA-1'
      Nonce    = input('Nonse Base64 encoded (fyko+d2lbbFgONRv9qkxdawL)? ').strip() or 'fyko+d2lbbFgONRv9qkxdawL'

      MECHANISMS = [mech]

      print()
      print()
      print('Username >' + USERNAME + '<')
      print('Password >' + PASSWORD + '<')
      print('Meachani >' + mech     + '<')
      print('Nonce    >' + Nonce    + '<')
      client = ScramClient(MECHANISMS, USERNAME, PASSWORD, channel_binding=None, c_nonce=Nonce)

      1. Get the client first message and send it to the server
        cfirst = client.get_client_first()
        print('Get Client First: >' + cfirst + '<')

      serverResponse = input('What is the server response?').strip()
      print()
      print('SR       >' + serverResponse + '<')
      client.set_server_first(serverResponse)

      cfinal = client.get_client_final()
      print('Get Client Final: result:          >' + cfinal + '<')

      serverResponse = input('What was the server response?')
      client.set_server_final(serverResponse)

      #

      1. If the server response is not correct it will throw an exception
      2. and thus never get here.
        print('All Good');
      Show
      Create two users on the "test" db test> db.createUser({user: "testSha1", pwd: "passwordSha1", roles: [ Unknown macro: { role} , { role: 'dbAdmin', db: 'test' }], mechanisms: ['SCRAM-SHA-1'] }) Unknown macro: { ok} test> db.createUser({user: "testSha256", pwd: "passwordSha256", roles: [ Unknown macro: { role} , { role: 'dbAdmin', db: 'test' }], mechanisms: ['SCRAM-SHA-256'] }) Unknown macro: { ok} Run the application below: It simply commects to the Mongo server and attempts to authenticate using SCRAM-SHA algorithm. Notice that SCRAM-SHA-256 works as expected, but that SCRAM-SHA-1 generates an authentication failure. > python3 -m venv venv > source venv/bin/activate (venv) > pip install scramp (venv) > (venv) > (venv) > python3 mongo.py testSha256 passwordSha256 test SCRAM-SHA-256 AuthInit Reply:  r=a2f9b2678b0e410c8dfdb410459486dctCTAnXqb2b5XeJfgYQBahZpkvfwhhf8r,s=PblZy1gpLj6Jo4xn8jhqfrTlDL5vTvlgM4O1Hg==,i=15000 AuthCont Reply:  v=FK4U7vwRViyR/PuXzEE043ymUuojJdtz86NudnFCp1s= AuthFinal Reply: Looks Good (venv) > (venv) > (venv) >python3 mongo.py testSha1 passwordSha1 test SCRAM-SHA-1 AuthInit Reply:  r=b3b5d58b9f214115a9c2f5f26dabde30ZC8ZIDgk3Lr8YetZD+M9b9/hQJGjAIwX,s=5qobARF148RY4puhsQOkSg==,i=10000 b'a\x00\x00\x00\x01ok\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02errmsg\x00\x17\x00\x00\x00Authentication failed.\x00\x10code\x00\x12\x00\x00\x00\x02codeName\x00\x15\x00\x00\x00AuthenticationFailed\x00\x00' AuthCont: Reply not OK The following code is written in python to demonstrate the issue: Save this file as "mongo.py" This scripts sends an "AuthInit" followed by "AuthCont" followed by "AuthCont" objects to the mongo server. Reading the reply after each message to extract the "payload" to use in the SCRAM-SHA algorithm. It uses scamp a standard python module to generate the SCRAM-SHA messages. Authenticating with SCRAM-SHA-256 works fine.  Authenticating with SCRAM-SHA-1 fails. import socket import time import sys from scramp import ScramClient # Steps to run   > python3 -m venv venv   > source venv/bin/activate   > pip install scramp #   > python3 mongo.py def sendMessageGetPayload(mongo, data, message):     # mongo:        Socket connected to mongo server     # data:         bytearray of data to be sent to mongo     # message:      error message prefix.     #     # Send message to mongo     # Validate that it was sent.     size = len(data)     sent = mongo.send(data)     if size != sent:         print(bson)         sys.exit(message + ' Send failed: Sent: ' + str(sent) + ' of ' + str(size) + ' bytes')     #     # Retrieve the message from Mongo.     # The first 21 bytes is the header block.     # For simplicity just extracted the size.     reply = mongo.recv(21)     messageSize     = int.from_bytes(reply [0:4] , byteorder='little')     #     # Retrive the bson part of the essage.     # Do a simple check to make sure that the size is correct.     bson = mongo.recv(messageSize - 21)     bsonSize        = int.from_bytes(bson [0:4] , byteorder='little')     if messageSize != (bsonSize + 21):         print(bson)         sys.exit('AuthInit: Format Error:')     #     # Check the bson has an OK field     # And that field is a double with value 1.     findOk = bson.find(b'ok\x00')     if findOk == -1:         sys.exit(message + ' Failed to find OK')     if bson [findOk + 3: findOk + 3+ 8] != b'\x00\x00\x00\x00\x00\x00\xf0\x3f':         print(bson)         sys.exit(message + ' Reply not OK')     #     # If there is an OK field there should be a payload.     # So extract the payload from the bson     # The payload size is 4 bytes of the string payload.     # We ignore the 1 byte binary type.     findPayload = bson.find(b'payload\x00')     if findPayload == -1:         sys.exit(message + ' Could not find payload')     payloadSize     = int.from_bytes(bson [findPayload + 8:findPayload + 8 + 4] , byteorder='little')     payload         = bson [findPayload + 13:findPayload + 13 + payloadSize] .decode('ascii')     #     # Extract the conversionID as we need that for the reply.     findConvId = bson.find(b'conversationId\x00')     if findConvId == -1:         print(bson)         sys.exit(message + ' Could not find conversationId')     convId = int.from_bytes(bson [findConvId + 15: findConvId + 15 + 4] , byteorder='little')     return payload, convId def sendAuthInit(mongo, messageId, DBName, mech, firstMessage):     #     # Build the AuthInit Message in BSON by hand;     # Should look like this:     #   Unknown macro: {     #       saslStart}     authInit = bytearray()     bsonSize = 58 + len(mech) + 1 + len(firstMessage) + len(DBName) + 1     #     # Put the header in first.     authInit.extend(int(bsonSize +21).to_bytes(4, byteorder='little'))      #   MessageSize         193 => (21 Body + 172 BSON)     authInit.extend(int(messageId).to_bytes(4, byteorder='little'))         #   MessageId           1     authInit.extend(0x00.to_bytes(4, byteorder='little'))                   #   messageResponseId   0     authInit.extend(0x7DD.to_bytes(4, byteorder='little'))                  #   OPCode              OP_MSG     authInit.extend(0x00.to_bytes(4, byteorder='little'))                   #   Flags               0     authInit.extend(0x00.to_bytes(1, byteorder='little'))                   #   Kind                0     #     # BSON Object     authInit.extend(int(bsonSize).to_bytes(4, byteorder='little'))          #   Bson Size           172     authInit.extend(0x10.to_bytes(1, byteorder='little'));authInit.extend(b'saslStart\x00');authInit.extend(0x01.to_bytes(4, byteorder='little'))     authInit.extend(0x2.to_bytes(1, byteorder='little'));authInit.extend(b'mechanism\x00');authInit.extend(int(len(mech) + 1).to_bytes(4, byteorder='little'));authInit.extend(mech.encode('ascii'));authInit.extend(b'\x00')     authInit.extend(0x5.to_bytes(1, byteorder='little'));authInit.extend(b'payload\x00');authInit.extend(len(firstMessage).to_bytes(4, byteorder='little'));authInit.extend(0x0.to_bytes(1, byteorder='little'));authInit.extend(firstMessage.encode('ascii'))     authInit.extend(0x2.to_bytes(1, byteorder='little'));authInit.extend(b'$db\x00');authInit.extend(int(len(DBName) + 1).to_bytes(4, byteorder='little'));authInit.extend(DBName.encode('ascii'));authInit.extend(b'\x00')     authInit.extend(0x0.to_bytes(1, byteorder='little'))     #     # Send to mongo and get the import part of the reply     return sendMessageGetPayload(mongo, authInit, 'AuthInit:') def sendAuthContOne(mongo, messageId, DBName, conversationId, finalMessage):     #     # Build the AuthCont Message in BSON by hand;     # Should look like this:     #   Unknown macro: {     #       saslContinue}     authCont = bytearray()     bsonSize = 66 + len(DBName) + 1 + len(finalMessage)     #     # Put the header in first.     authCont.extend(int(bsonSize + 21).to_bytes(4, byteorder='little'))     #   MessageSize         196 => (21 Body + 175 BSON)     authCont.extend(int(messageId).to_bytes(4, byteorder='little'))         #   MessageId           2 or 3     authCont.extend(0x00.to_bytes(4, byteorder='little'))                   #   messageResponseId   0     authCont.extend(0x7DD.to_bytes(4, byteorder='little'))                  #   OPCode              OP_MSG     authCont.extend(0x00.to_bytes(4, byteorder='little'))                   #   Flags               0     authCont.extend(0x00.to_bytes(1, byteorder='little'))                   #   Kind                0     #     # BSON Object     authCont.extend(int(bsonSize).to_bytes(4, byteorder='little'))          #   Bson Size           175     authCont.extend(0x10.to_bytes(1, byteorder='little'));authCont.extend(b'saslContinue\x00');authCont.extend(0x01.to_bytes(4, byteorder='little'))     authCont.extend(0x10.to_bytes(1, byteorder='little'));authCont.extend(b'conversationId\x00');authCont.extend(conversationId.to_bytes(4, byteorder='little'))     authCont.extend(0x5.to_bytes(1, byteorder='little'));authCont.extend(b'payload\x00');authCont.extend(len(finalMessage).to_bytes(4, byteorder='little'));authCont.extend(0x0.to_bytes(1, byteorder='little'));authCont.extend(finalMessage.encode('ascii'))     authCont.extend(0x2.to_bytes(1, byteorder='little'));authCont.extend(b'$db\x00');authCont.extend(int(len(DBName) + 1).to_bytes(4, byteorder='little'));authCont.extend(DBName.encode('ascii'));authCont.extend(b'\x00')     authCont.extend(0x0.to_bytes(1, byteorder='little'))     #     # Send to mongo and get the import part of the reply     return sendMessageGetPayload(mongo, authCont, 'AuthCont:') def getUserInput():     if len(sys.argv) > 1:         username= sys.argv [1]     if len(sys.argv) > 2:         password= sys.argv [2]     if len(sys.argv) > 3:         DBName  = sys.argv [3]     if len(sys.argv) > 4:         mech    = sys.argv [4]     if username == '':         usernmae = input('Input username:  ')     if password == '':         password = input('Input password:  ')     if DBName == '':         DBName   = input('Input DB Name:   ')     if mech == '':         mech     = input('Input Mechanism: ')     if mech != 'SCRAM-SHA-1' and mech != 'SCRAM-SHA-256':         sys.exit('Invalid Mechanism: Use => "SCRAM-SHA-1" or "SCRAM-SHA-256"')     return username, password, DBName, mech username, password, DBName, mech = getUserInput() color: Color value is invalid # # Create socket HOST    = 'localhost' PORT    = 27017 mongo = socket.socket(socket.AF_INET, socket.SOCK_STREAM) mongo.connect((HOST,PORT)) #␣ Use library to generate SCRAM-SHA-X messages client = ScramClient( [mech] , username, password) # Send the first message to the server. authInitReply, conversationId1 = sendAuthInit(mongo, 1, DBName, mech, client.get_client_first()) print('AuthInit Reply: ', authInitReply) client.set_server_first(authInitReply) # Send the proof to the server. authContReply, conversationId2 = sendAuthContOne(mongo, 2, DBName, conversationId1, client.get_client_final()) client.set_server_final(authContReply) print('AuthCont Reply: ', authContReply) if conversationId1 != conversationId2:     sys.exit('AuthInit ConvId does not match AuthCont ConvId') # Send the OK we accept you are the server message authFinalReply, conversationId3 = sendAuthContOne(mongo, 3, DBName, conversationId2, '') print('AuthFinal Reply:', authFinalReply) if conversationId1 != conversationId3:     sys.exit('AuthInit ConvId does not match AuthFinal ConvId') # If no exceptions and no exit then Print Looks Good if authFinalReply != '':     sys.exit('AuthFinal Reply not empty!!!') print('Looks Good')   Helper script to print out the messages used in SCRAM-SHA-? communication. from scramp import ScramClient import base64 USERNAME = input('UserName (user)? ').strip() or 'user' PASSWORD = input('Password (pencil)? ').strip() or 'pencil' mech     = input('MECHANISMS (SCRAM-SHA-1)? ').strip() or 'SCRAM-SHA-1' Nonce    = input('Nonse Base64 encoded (fyko+d2lbbFgONRv9qkxdawL)? ').strip() or 'fyko+d2lbbFgONRv9qkxdawL' MECHANISMS = [mech] print() print() print('Username >' + USERNAME + '<') print('Password >' + PASSWORD + '<') print('Meachani >' + mech     + '<') print('Nonce    >' + Nonce    + '<') client = ScramClient(MECHANISMS, USERNAME, PASSWORD, channel_binding=None, c_nonce=Nonce) Get the client first message and send it to the server cfirst = client.get_client_first() print('Get Client First: >' + cfirst + '<') serverResponse = input('What is the server response?').strip() print() print('SR       >' + serverResponse + '<') client.set_server_first(serverResponse) cfinal = client.get_client_final() print('Get Client Final: result:          >' + cfinal + '<') serverResponse = input('What was the server response?') client.set_server_final(serverResponse) # If the server response is not correct it will throw an exception and thus never get here. print('All Good');
    • Security 2024-09-02

      There is a bug in the implementation of SCRAM-SHA-1 authentication.

      This bug is present in both the server and and driver libraries, thus tools appear to work as expected.

       

      BUT one should note that a bug in SHA-1 hashing algorithm is a security vulnerability.

      NOTE: SCRAM-SHA-256 works as expected.

      I have used Wire shark to extract the messages sent between `mongosh` and `mongodb` and have verified that the "proof" part of the message is incorrectly calculated for `SHA-1` authentication.

      NOTE: Because the Mongo Server and the Client libraries both contain the bug authentication appears to work. 

      I have compares the generated proofs from mongosh against C++ and python libraries. The C++ and python libraries (not using the Mongo drivers) generate the same proof messages that are different to the proof message generated by mongosh.

      Example:

      Binary dump of message sent from mongosh to Mongo

      # Message 1:  Mongosh => Mongo
      #     This is an AuthInit Message with
      #     saslStart = 1
      0000   02 00 00 00 45 00 00 f9 00 00 40 00 40 06 00 00   ....E.....@.@...
      0010   7f 00 00 01 7f 00 00 01 c5 26 69 89 6f be c5 22   .........&i.o.."
      0020   05 d0 da 1c 80 18 18 e5 fe ed 00 00 01 01 08 0a   ................
      0030   3f fc 80 f0 56 de bd 85 c5 00 00 00 03 00 00 00   ?...V...........
      0040   00 00 00 00 dd 07 00 00 00 00 00 00 00 b0 00 00   ................
      0050   00 10 73 61 73 6c 53 74 61 72 74 00 01 00 00 00   ..saslStart.....
      0060   02 6d 65 63 68 61 6e 69 73 6d 00 0c 00 00 00 53   .mechanism.....S
      0070   43 52 41 4d 2d 53 48 41 2d 31 00 05 70 61 79 6c   CRAM-SHA-1..payl
      0080   6f 61 64 00 30 00 00 00 00 6e 2c 2c 6e 3d 74 65   oad.0....n,,n=te
      0090   73 74 53 68 61 31 2c 72 3d 5a 31 4d 4d 64 75 66   stSha1,r=Z1MMduf
      00a0   57 51 2b 7a 38 4f 30 73 65 6b 67 6e 70 71 4a 66   WQ+z8O0sekgnpqJf
      00b0   32 48 30 45 4e 33 73 6c 47 10 61 75 74 6f 41 75   2H0EN3slG.autoAu
      00c0   74 68 6f 72 69 7a 65 00 01 00 00 00 03 6f 70 74   thorize......opt
      00d0   69 6f 6e 73 00 19 00 00 00 08 73 6b 69 70 45 6d   ions......skipEm
      00e0   70 74 79 45 78 63 68 61 6e 67 65 00 01 00 02 24   ptyExchange....$
      00f0   64 62 00 05 00 00 00 74 65 73 74 00 00            db.....test..

      ---------

      # Message 2:  Mongo => Mongosh
      #     This is the reply to the AuthInit message.
      #     It contains the SCRAM-SHA-1 server response.
      0000   02 00 00 00 45 00 00 e8 00 00 40 00 40 06 00 00   ....E.....@.@...
      0010   7f 00 00 01 7f 00 00 01 69 89 c5 26 05 d0 da 1c   ........i..&....
      0020   6f be c5 e7 80 18 18 df fe dc 00 00 01 01 08 0a   o...............
      0030   56 de bd 87 3f fc 80 f0 b4 00 00 00 fc 00 00 00   V...?...........
      0040   03 00 00 00 dd 07 00 00 00 00 00 00 00 9f 00 00   ................
      0050   00 10 63 6f 6e 76 65 72 73 61 74 69 6f 6e 49 64   ..conversationId
      0060   00 01 00 00 00 08 64 6f 6e 65 00 00 05 70 61 79   ......done...pay
      0070   6c 6f 61 64 00 65 00 00 00 00 72 3d 5a 31 4d 4d   load.e....r=Z1MM
      0080   64 75 66 57 51 2b 7a 38 4f 30 73 65 6b 67 6e 70   dufWQ+z8O0sekgnp
      0090   71 4a 66 32 48 30 45 4e 33 73 6c 47 43 6c 35 41   qJf2H0EN3slGCl5A
      00a0   4a 55 54 78 32 7a 67 44 50 62 31 5a 41 6c 45 44   JUTx2zgDPb1ZAlED
      00b0   6d 39 58 37 67 2b 42 4d 6a 77 58 74 2c 73 3d 35   m9X7g+BMjwXt,s=5
      00c0   71 6f 62 41 52 46 31 34 38 52 59 34 70 75 68 73   qobARF148RY4puhs
      00d0   51 4f 6b 53 67 3d 3d 2c 69 3d 31 30 30 30 30 01   QOkSg==,i=10000.
      00e0   6f 6b 00 00 00 00 00 00 00 f0 3f 00               ok........?.

      ---------

      1. Message 3:   Mongosh => Mongo
        #          The is the AuthCont message.
        #          This contains the computer proof from the client
        #           to show that it knows the users password.
        0000   02 00 00 00 45 00 00 f8 00 00 40 00 40 06 00 00   ....E.....@.@...
        0010   7f 00 00 01 7f 00 00 01 c5 26 69 89 6f be c5 e7   .........&i.o...
        0020   05 d0 da d0 80 18 18 e3 fe ec 00 00 01 01 08 0a   ................
        0030   3f fc 80 f5 56 de bd 87 c4 00 00 00 04 00 00 00   ?...V...........
        0040   00 00 00 00 dd 07 00 00 00 00 00 00 00 af 00 00   ................
        0050   00 10 73 61 73 6c 43 6f 6e 74 69 6e 75 65 00 01   ..saslContinue..
        0060   00 00 00 10 63 6f 6e 76 65 72 73 61 74 69 6f 6e   ....conversation
        0070   49 64 00 01 00 00 00 05 70 61 79 6c 6f 61 64 00   Id......payload.
        0080   68 00 00 00 00 63 3d 62 69 77 73 2c 72 3d 5a 31   h....c=biws,r=Z1
        0090   4d 4d 64 75 66 57 51 2b 7a 38 4f 30 73 65 6b 67   MMdufWQ+z8O0sekg
        00a0   6e 70 71 4a 66 32 48 30 45 4e 33 73 6c 47 43 6c   npqJf2H0EN3slGCl
        00b0   35 41 4a 55 54 78 32 7a 67 44 50 62 31 5a 41 6c   5AJUTx2zgDPb1ZAl
        00c0   45 44 6d 39 58 37 67 2b 42 4d 6a 77 58 74 2c 70   EDm9X7g+BMjwXt,p
        00d0   3d 6a 4c 6d 4f 55 48 6d 58 51 63 67 31 46 65 30   =jLmOUHmXQcg1Fe0
        00e0   6a 6d 55 63 31 48 69 6a 44 4d 78 77 3d 02 24 64   jmUc1HijDMxw=.$d
        00f0   62 00 05 00 00 00 74 65 73 74 00 00               b.....test..

      ---------

      # Message 4:  Mongo -> Mongosh
      #     Reply with the validation code.
      #     So the client knows that the server also knows the
      #     password.
      0000   02 00 00 00 45 00 00 a1 00 00 40 00 40 06 00 00   ....E.....@.@...
      0010   7f 00 00 01 7f 00 00 01 69 89 c5 26 05 d0 da d0   ........i..&....
      0020   6f be c6 ab 80 18 18 dc fe 95 00 00 01 01 08 0a   o...............
      0030   56 de bd 8a 3f fc 80 f5 6d 00 00 00 fd 00 00 00   V...?...m.......
      0040   04 00 00 00 dd 07 00 00 00 00 00 00 00 58 00 00   .............X..
      0050   00 10 63 6f 6e 76 65 72 73 61 74 69 6f 6e 49 64   ..conversationId
      0060   00 01 00 00 00 08 64 6f 6e 65 00 01 05 70 61 79   ......done...pay
      0070   6c 6f 61 64 00 1e 00 00 00 00 76 3d 78 2b 5a 77   load......v=x+Zw
      0080   77 48 41 43 61 44 4f 36 4b 6b 38 47 2f 4b 37 4a   wHACaDO6Kk8G/K7J
      0090   76 67 58 34 47 4d 73 3d 01 6f 6b 00 00 00 00 00   vgX4GMs=.ok.....
      00a0   00 00 f0 3f 00                                    ...?.

      From message 1: we can find the payload as:

      n,,n=testSha1,r=Z1MMdufWQ+z8O0sekgnpqJf2H0EN3slG

      From message 2: we can find the payload as:

      r=Z1MMdufWQ+z8O0sekgnpqJf2H0EN3slGCl5AJUTx2zgDPb1ZAlEDm9X7g+BMjwXt,s=5qobARF148RY4puhsQOkSg==,i=10000

      Using the standard SCRAM-SHA-1 algorithm using the following values:

      • Username: testSha1
      • Password: passwordSha1
      • Nonce:  Z1MMdufWQ+z8O0sekgnpqJf2H0EN3slG
      • Server Reply: r=Z1MMdufWQ+z8O0sekgnpqJf2H0EN3slGCl5AJUTx2zgDPb1ZAlEDm9X7g+BMjwXt,s=5qobARF148RY4puhsQOkSg==,i=10000

      We can compute that the expected response should be (When using non Mongo based tools. Application provided below to help).

      c=biws,r=Z1MMdufWQ+z8O0sekgnpqJf2H0EN3slGCl5AJUTx2zgDPb1ZAlEDm9X7g+BMjwXt,p=bCrqdhlgFjdEhR0HIifUPK0RQV0=

      But the message extracted from the third message is:

      c=biws,r=Z1MMdufWQ+z8O0sekgnpqJf2H0EN3slGCl5AJUTx2zgDPb1ZAlEDm9X7g+BMjwXt,p=jLmOUHmXQcg1Fe0jmUc1HijDMxw=

      Notice that the proof section is different:

      • Expected: p=bCrqdhlgFjdEhR0HIifUPK0RQV0=
      • Actual: p=jLmOUHmXQcg1Fe0jmUc1HijDMxw=

        1. valClient.py
          1 kB
        2. mongo.py
          9 kB

            Assignee:
            sara.golemon@mongodb.com Sara Golemon
            Reporter:
            loki.astari@gmail.com Loki Astari
            Votes:
            0 Vote for this issue
            Watchers:
            6 Start watching this issue

              Created:
              Updated:
              Resolved: