url: https://staging.scantist.io/login api doc: https://api-staging.scantist.io/docs (I thought that I need to fuzz the whole API. Luckily I found the doc after the first few minutes of fuzzing through wfuzz). For the attacks, you may directly go to section 3.
1. Recon
ip lookup: Amazon CloudFront Singapore Server, ip 54.192.151.100 nmap result:
PORT STATE SERVICE VERSION
80/tcp open http Amazon CloudFront httpd
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: CloudFront
|_http-title: Did not follow redirect to https://staging.scantist.io/
443/tcp open ssl
|_http-server-header: CloudFront
|_http-title: ERROR: The request could not be satisfied
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: WAP|general purpose
Running: Actiontec embedded, Linux 2.4.X|3.X, Microsoft Windows XP|7|2012
OS CPE: cpe:/h:actiontec:mi424wr-gen3i cpe:/o:linux:linux_kernel cpe:/o:linux:linux_kernel:2.4.37 cpe:/o:linux:linux_kernel:3.2 cpe:/o:microsoft:windows_xp::sp3 cpe:/o:microsoft:windows_7 cpe:/o:microsoft:windows_server_2012
OS details: Actiontec MI424WR-GEN3I WAP, DD-WRT v24-sp2 (Linux 2.4.37), Linux 3.2, Microsoft Windows XP SP3, Microsoft Windows XP SP3 or Windows 7 or Windows Server 2012
Network Distance: 2 hops
TCP Sequence Prediction: Difficulty=256 (Good luck!)
IP ID Sequence Generation: Incremental
Some quick, intuitive thoughts: /robots.txt
Allow crawling of all content
User-agent: *
Disallow:
DirBuster: There are several directories well protected. The most interesting files are listed below: /setting/env.js
// prettier-ignore
var env = {
“NODE_ENV”: “staging”,
“API_BASE”: “https://api-staging.scantist.io/v1",
“PUSHER_KEY”: “765ec374ae0a69f4ce44”,
“PUSHER_HOST”: “api-staging.scantist.io”,
“PUSHER_PORT”: 8082,
“GITHUB_CLIENT_ID”: “8ebd199ca3dd2846ba99”,
“GITHUB_OAUTH_SCOPE”: “user:email,repo”,
“GITLAB_CLIENT_ID”:
“9ab685da0bf18f92ca75e3e87b729f9a688eeda555b8deba9ccc06729d2d89b4”,
“GITLAB_OAUTH_SCOPE”: “read_user+read_repository+api”,
“GITHUB_URL”: “https://github.com",
“GITLAB_URL”: “https://gitlab.com",
“CLIENT_URL”: “https://staging.scantist.io/callback",
“BITBUCKET_CLIENT_ID”: “8nUrxhwtrR6Tv9rRQE”,
“BITBUCKET_OAUTH_SCOPE”: “”,
“BITBUCKET_URL”: “https://bitbucket.org/site",
“BITBUCKET_SERVER”: “http://13.250.81.217:7990",
“SENTRY_DSN”: “https://9e3bec10de7d4173afccfef99a4e7c4e@sentry.io/265321",
“STRIPE_PUBLIC_KEY”: “pk_live_ORr4CMaMUojrPTm4euEcbRJy”,
“CLIENT_ROOT”: “https://staging.scantist.io",
“IS_ON_PREM”: “false”,
“IS_CHINESE_VERSION”: “false”,
“IS_BITBUCKET_SERVER”: “false”,
“UPLOAD_SIZE”: 10000,
“COMPANY_NAME”: “Scantist”,
“COMPANY_LINK”: “https://scantist.com",
};
/setting/index.js
// prettier-ignore
var const_env = {
“NODE_ENV”: “development”,
“API_BASE”: “https://localhost:8000/v1",
“PUSHER_KEY”: “765ec374ae0a69f4ce44”,
“PUSHER_HOST”: “localhost”,
“PUSHER_PORT”: “8082”,
“GITHUB_CLIENT_ID”: “5b58ab5f27bbd41fa683”,
“GITHUB_OAUTH_SCOPE”: “user:email,repo”,
“GITLAB_CLIENT_ID”:
“fb68efcd3ca031b40531b44997c4c8b7bed3e69c104a50c69d43aac8a2add50e”,
“GITLAB_OAUTH_SCOPE”: “read_user+read_repository+api”,
“BITBUCKET_CLIENT_ID”: “6CwN8sUj8Q2GkFVDxf”,
“BITBUCKET_OAUTH_SCOPE”: “”,
“BITBUCKET_URL”: “https://bitbucket.org/site",
“BITBUCKET_SERVER”: “http://localhost:7990",
“CLIENT_URL”: “http://localhost:8080/callback",
“GITHUB_URL”: “https://github.com",
“GITLAB_URL”: “https://gitlab.com",
“STRIPE_PUBLIC_KEY”: “pk_test_3KsWuv3x5d1Sa6k2wXT1UbAJ”,
“CLIENT_ROOT”: “http://localhost:8080",
“IS_ON_PREM”: “true”,
“IS_CHINESE_VERSION”: “false”,
“IS_BITBUCKET_SERVER”: “true”,
“UPLOAD_SIZE”: 10000,
“LOGO_PATH”: “/static/img/scantist_logo.png”,
“LONG_LOGO_PATH”: “/static/img/scantistlogo_whitebg.png”,
“TITLE”: “Scantist SCA”,
“COMPANY_NAME”: “Scantist”,
“COMPANY_LINK”: “https://scantist.com",
“UNPACK_WEBPACK_NANO”: “true”,
“ENABLE_AZURE_SYNC”: “false”,
“SAML_ENABLED”: “true”,
“GITHUB_BROKER”: “false”,
“TOKEN_CREATION”: false,
}
window.scantist_env = const_env; //{ …const_env, …env };
var keys = Object.keys(env);
for (var index = 0; index < keys.length; index++) {
window.scantist_env[keys[index]] = env[keys[index]];
}
document.title = window.scantist_env.TITLE;
window.onload = function() {
$(“#html_tab_icon”).attr(“href”, window.scantist_env.LOGO_PATH);
};
/static/js/manifest.a9edd26d9ab20bf087d1.js { “indent_size”: “4”, “indent_char”: “ “, “max_preserve_newlines”: “5”, “preserve_newlines”: true, “keep_array_indentation”: false, “break_chained_methods”: false, “indent_scripts”: “normal”, “brace_style”: “collapse”, “space_before_conditional”: true, “unescape_strings”: false, “jslint_happy”: false, “end_with_newline”: false, “wrap_line_length”: “0”, “indent_inner_html”: false, “comma_first”: false, “e4x”: false, “indent_empty_lines”: false } static/js/jquery-2.0.3.min.js So interestingly, I believe the website is similar in structure as one of the sample site I saw here: http://www.xinrongji.cc/static/js/ To summarize this section, we know:
- The target site is communicating with an api-server (https://api-staging.scantist.io/v1).
- The authentication on api server is done by Pusher (https://pusher.com/)). Access control is done through JWT (JSON Web Token)
- Some gitlab/github auth are involved. This is normal, since users can use github/gitlab to sign up.
2. Login Page
Use burp to retrieve general information (login through api, sha256 hashed password). After using BurpSuite to trace the traffic redirection, I realize that the site is using JWT token for authentication, which is something that I’m not familiar with. The websites I used to learn information are listed below: https://medium.com/dev-bits/a-guide-for-adding-jwt-token-based-authentication-to-your-single-page-nodejs-applications-c403f7cf04f4 https://www.nccgroup.trust/uk/about-us/newsroom-and-events/blogs/2019/january/jwt-attack-walk-through/ https://www.sjoerdlangkemper.nl/2016/09/28/attacking-jwt-authentication/ In the last link, brute force on JWT token is introduced as a possible way to attack the server. Since I consider the primary goal is to conduct the penetration test, I skip this section.
3. File Upload Function
It is interesting how the server handle uploaded files. After some testing on Burp, I summarize the data transfer procedure as below: (1) Initiate a new project and upload a file (or import from github, gitlab, etc.) (2) Upload new files whenever there is a new version. (2.1) Sever back-end use post request to send the data to the target API on /v1/upload/
POST /v1/upload/ HTTP/1.1
Host: api-staging.scantist.io
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://staging.scantist.io/u/Gelei/org/Gelei/projects/2517
Authorization: Token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoic2xpZGluZyIsImV4cCI6MTU4NzE0MTE2OCwianRpIjoiYjc0ODM0OGViOWY1NDFjMjgzYjA0OTBiMjUzMTM1OTYiLCJyZWZyZXNoX2V4cCI6MTU4Njg4MTk2OCwidXNlcl9pZCI6MTM1M30.4V0r847Y3mBH-xEXcDkwik4pcz7RUQxm6Qyed2hVaa8
Content-Length: 3244
Content-Type: multipart/form-data; boundary=—————————2051000857823914733838784542
Origin: https://staging.scantist.io
Connection: close
—————————–2051000857823914733838784542
Content-Disposition: form-data; name=”file”; filename=”sample_python.py”
Content-Type: text/x-python
import curses
from curses import KEY_RIGHT, KEY_LEFT, KEY_UP, KEY_DOWN
from random import randint
following sections are neglected
(2.2) The api returned the location of file that it is stored on the target server.
HTTP/1.1 201 Created
Server: nginx/1.13.0
Date: Wed, 15 Apr 2020 16:33:20 GMT
Content-Type: application/json
Content-Length: 124
Connection: close
Allow: POST, OPTIONS
X-Frame-Options: DENY
Vary: Origin
Access-Control-Allow-Origin: https://staging.scantist.io
Strict-Transport-Security: max-age=60; includeSubDomains
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Strict-Transport-Security: max-age=31536000
Please note that the file is at: scantist-static-staging.s3.amazonaws.com/media/code_upload/. The interesting things start from here and I’ll highlight the attack in the next section. (2.3) Client update the corresponding information to /vi/project/project_number/uploads/
POST /v1/projects/2517/uploads/ HTTP/1.1
Host: api-staging.scantist.io
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://staging.scantist.io/u/Gelei/org/Gelei/projects/2517
Content-Type: application/json
Authorization: Token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoic2xpZGluZyIsImV4cCI6MTU4NzE0MTE2OCwianRpIjoiYjc0ODM0OGViOWY1NDFjMjgzYjA0OTBiMjUzMTM1OTYiLCJyZWZyZXNoX2V4cCI6MTU4Njg4MTk2OCwidXNlcl9pZCI6MTM1M30.4V0r847Y3mBH-xEXcDkwik4pcz7RUQxm6Qyed2hVaa8
Content-Length: 154
Origin: https://staging.scantist.io
Connection: close
{“download_link”:”sample_python.py”,”file_size”:0.0028772354125976562,”filename”:”sample_python.py”,”file_modified”:”2020-04-15 12:32:41”,”version”:”1.0”}
In return, the server gives “created 201” status code.
HTTP/1.1 201 Created
Server: nginx/1.13.0
Date: Wed, 15 Apr 2020 16:33:27 GMT
Content-Type: application/json
Content-Length: 256
Connection: close
Allow: GET, POST, HEAD, OPTIONS
X-Frame-Options: DENY
Vary: Origin
Access-Control-Allow-Origin: https://staging.scantist.io
Strict-Transport-Security: max-age=60; includeSubDomains
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Strict-Transport-Security: max-age=31536000
{“id”:2526,”project”:”2517”,”download_link”:”sample_python.py”,”file_size”:0.0028772354125976562,”created”:”2020-04-16T00:33:27.581668+08:00”,”uploader_name”:”Gelei”,”filename”:”sample_python.py”,”file_modified”:”2020-04-15T12:32:41+08:00”,”version”:”1.0”}
(2.4) After all, the client sends get request on the same api /vi/project/project_number/uploads/ to get project information. The responses are neglected since they are not directly related to the topic here. To summarize everything: three servers are involved in this process. staging-scantist.io as front-end, api-staging.scantist.io as backend api and amazon s3 bucket as the place to store all the files.
Attacks on File Upload Function: Directory enumeration on api
It is worth noticing that in the above example, the sample python file is stored on a certain aws s3 location. After testing, it can be proved that the url is open without any authentication nor authorization. Anyone who knows the link can download files from the S3. For instance: https://scantist-static-prod.s3.amazonaws.com/media/code_upload/sample_python.py Since all files are uploaded to this place, we can enumerate in the directory to uncover other’s source code. This can be easily done through dirbuster. So many files can be enumerated easily: Those files shouldn’t be available to anyone. There might be a chance that some people are uploading the server source code onto this site. In no way should the source code uploaded by other entities be available to random clients from the Internet.
Abuse File Upload Function: JSON record modification
While the following section is not testified, I assume that code execution on the remote server is possible. Firstly, I would like to explain how the user upload function can be controlled by users. As mentioned in the previous section, users upload files to target Amazon S3 bucket through a post command to API (and API may help to upload the files to Amazon S3). The upload result in JSON format will then be passed to API that manages the project information.This is realized through the following post request:
POST /v1/projects/project_id/uploads/ HTTP/1.1 HTTP/1.1
Host: api-staging.scantist.io
##############
Other headers and post info #############
{“id”:id,”project”:project_id,”download_link”:”download_link“,”file_size”:0.0028772354125976562,”created”:”2020-04-16T00:33:27.581668+08:00”,”uploader_name”:”Gelei”,”filename”:”sample_python.py”,”file_modified”:”2020-04-15T12:32:41+08:00”,”version”:”1.0”}
Yet this post request can be reforged by the user. File name, download link, file size and many other information can be manipulated. For instance: Though there are limited JS functions to sanitize filename, the post request is not properly sanitized. The filename can be crafted in certain ways that they cannot be downloaded. One example would be the one in the above figure, where ../../ in URL will be falsely interpreted by download request. For instance, some sample log files that I recorded from one of the attacks:
[2020-04-15 22:34:39.201582+08:00]id=14970, [2020-04-15 22:34:39.198196+08:00]id=14970,
{‘error’: ‘download_all_from_repo|exception=An error occurred (400) when calling the HeadObject operation: Bad Request,
params={\‘e\‘: ClientError(\‘An error occurred (400) when calling the HeadObject operation: Bad Request\‘,),
\‘remote_path\‘: “media/code_upload/../../../../../../../../../etc/passwd\‘“,
\‘local_file\‘: “./external/clone_dir/upload/2517/file_name_attack_3/../../../../../../../../../etc/passwd\‘“,
\‘proj_name\‘: “../../../../../../../../../etc/passwd\‘“,
\‘repo_path\‘: \‘./external/clone_dir/upload/2517/file_name_attack_3\‘,
\‘repo_name\‘: \‘file_name_attack_3\‘, \‘pro_id\‘: 2517,
\‘repo_url\‘: “../../../../../../../../../etc/passwd\‘“}’}
Attack File Upload Function: Possible Code Execution
It has been mentioned in the previous section that users can randomly craft file names that are uploaded to the server. Since the server is handling uploaded source code as plain text file, and binary in .zip file. The file should be decompressed first. This is probably done in a way of “unzip filename“ on the server. If the file name is properly crafted, for instance, “test.zip && ls &.zip”, then the code execution may become:
unzip test.zip && ls &.zip
where the files will be executed in sequence. Thus, command execution is possible. Of course, there are higher possibilities that the unzip/scan process is much more complicated than what I assume here. A blind test on code execution is time- consuming. If the source code can be reviewed, I can easily identify if the vulnerability of code execution exists here.