CTFd / CTFd

CTFs as you need them

Home Page:https://ctfd.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How can I get `team` and/or `user` in the `read` function of a challenge plugin?

Manouchehri opened this issue · comments

@classmethod
def read(cls, challenge):
"""
This method is in used to access the data of a challenge in a format processable by the front end.
:param challenge:
:return: Challenge object, data dictionary to be returned to the user
"""
data = {
"id": challenge.id,
"name": challenge.name,
"value": challenge.value,
"description": challenge.description,
"connection_info": challenge.connection_info,
"next_id": challenge.next_id,
"category": challenge.category,
"state": challenge.state,
"max_attempts": challenge.max_attempts,
"type": challenge.type,
"type_data": {
"id": cls.id,
"name": cls.name,
"templates": cls.templates,
"scripts": cls.scripts,
},
}
return data

Is it possible to get the user and/or team value when in the read function? (Basically similar to how solve already has the user and team arguments.)

def solve(cls, user, team, challenge, request):

For context, I'm trying to return different VPN credentials, which are unique per challenge and team. 😄

As far I am concerned.. that's a init.py file and you should really not try to change anything in that
Function read() on approves one parameter that is 'challenge', whereas solve() is allowed few more parameters...
That's just not a big deal in adding user and team parameters in read() function but it's just not allowed in this case, tho you can try this in your local machine.

We aren't modifying CTFd, we are making a CTFd challenge plugin as described in https://docs.ctfd.io/docs/plugins/overview

We'd rather write plugins that can actually be shared with the general CTFd community. I don't think making a custom fork of CTFd and randomly adding new arguments to CTFd's core (which will conflict with all other existing challenges) is a great solution.

This is what's calling custom challenge read functions for reference.

response = chal_class.read(challenge=chal)

As you develop a plugin, it's better to modify the read function (Plugin-Specific). Inside your plugin's implementation, you can create a new function for retrieving the challenge data with user and team-specific information. This new function will receive additional parameters like user and team. It can fetch the VPN credentials for a particular challenge, user, and team, and then it will return the appropriate credentials.
Here's a try I made

# Plugin's file

def new_read(challenge, user, team):
    # Suppose you got a function to get VPN credentials
    vpn_credentials = get_vpn_credentials(challenge.id, user.id, team.id)

    data = {
        "id": challenge.id,
        "name": challenge.name,
        "value": challenge.value,
        "description": challenge.description,
        "connection_info": challenge.connection_info,
        "next_id": challenge.next_id,
        "category": challenge.category,
        "state": challenge.state,
        "max_attempts": challenge.max_attempts,
        "type": challenge.type,
        "type_data": {
            "id": cls.id,
            "name": cls.name,
            "templates": cls.templates,
            "scripts": cls.scripts,
        },
        "vpn_credentials": vpn_credentials,  # Include the VPN credentials in the data
    }

    return data

I don't know the answer immediately off the top of my head but does get_current_user() or get_current_team() work?

image

Seems to work fine for me. I'll close this but if it's insufficient for some reason you can follow up.

@Aryan9592 As an AI language model, I don't really think this answer makes any sense. Appreciate the attempted help, but this does kinda look like ChatGPT copypasta.

@ColdHeat thanks, will try that in the morning!

@ColdHeat related question, how can I render vpn_credential in view.html?

    @classmethod
    def read(cls, challenge):
        """
        This method is in used to access the data of a challenge in a format processable by the front end.

        :param challenge:
        :return: Challenge object, data dictionary to be returned to the user
        """
        print("Running read function")
        team_credential = "Not found"

        if authed():
            user_idvalue = get_current_user().id
        try:
            team = Teams.query.filter_by(id=user_idvalue).first()
            if team is not None:
                team_credential = team # just for testing
                print(team_name)
        except:
            print("User is not logged in")

        data = {
            "id": challenge.id,
            "name": challenge.name,
            "value": challenge.value,
            "initial": challenge.initial,
            "minimum": challenge.minimum,
            "team_credential": team_credential,
            "description": challenge.description,
            "connection_info": challenge.connection_info,
            "category": challenge.category,
            "state": challenge.state,
            "max_attempts": challenge.max_attempts,
            "type": challenge.type,
            "type_data": {
                "id": cls.id,
                "name": cls.name,
                "templates": cls.templates,
                "scripts": cls.scripts,
            },
        }
        return data

You would access the information via the challenge object in view.html. Here is the code that renders the actual HTML content that is rendered by the browser: https://github.com/CTFd/CTFd/blob/master/CTFd/api/v1/challenges.py#L429-L439

Alternatively you can create an endpoint to give the information when the user requests it via some interaction.

So I have this already:

{% block team_credential %}
				<div class="challenge-team_credential" data-bs-target="#team_credential">
					test1
					{{ challenge.team_credential }}
					test2
					{{ team_credential }}
					test3
				</div>
{% endblock %}

But {{ challenge.team_credential }} and/or {{ team_credential }} still are blank.

image

And http://127.0.0.1:4000/api/v1/challenges/1 returns:

{
    "success": true,
    "data": {
        "id": 1,
        "name": "asdf name",
        "value": 200,
        "initial": 200,
        "minimum": 123,
        "team_credential": "sup",
        "description": "Eh",
        "connection_info": "hello connect",
        "category": "asdf cat",
        "state": "visible",
        "max_attempts": 0,
        "type": "Ai.Moda.Custom",
        "type_data": {
            "id": "Ai.Moda.Custom",
            "name": "Ai.Moda.Custom",
            "templates": {
                "create": "/plugins/ctfd-corellium-plugin/assets/create.html",
                "update": "/plugins/ctfd-corellium-plugin/assets/update.html",
                "view": "/plugins/ctfd-corellium-plugin/assets/view.html"
            },
            "scripts": {
                "create": "/plugins/ctfd-corellium-plugin/assets/create.js",
                "update": "/plugins/ctfd-corellium-plugin/assets/update.js",
                "view": "/plugins/ctfd-corellium-plugin/assets/view.js"
            }
        },
        "solves": 0,
        "solved_by_me": false,
        "attempts": 0,
        "files": [
            "/files/0fff468b93979ef992f96b1c91aa0d96/248524235_6372846866119834_2599255715323320459_n.jpg?token=eyJ1c2VyX2lkIjoyLCJ0ZWFtX2lkIjoxLCJmaWxlX2lkIjoxfQ.ZMgKxQ.D7_gcH_GT33Mukr0KcbNWSgBw4c"
        ],
        "tags": [],
        "hints": [],
        "view": "<div :class=\"getStyles()\" role=\"document\" x-data=\"Challenge\" x-init=\"id = 1\">\n\t<div class=\"modal-content\">\n\t  <div class=\"modal-body py-4 px-4 px-sm-5\">\n  \n\t\t<div>\n\t\t  <button type=\"button\" class=\"btn-close float-end\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button>\n  \n\t\t  <ul class=\"nav nav-tabs\">\n\t\t\t<li class=\"nav-item\">\n\t\t\t  <a class=\"nav-link active\" data-bs-target=\"#challenge\" @click=\"showChallenge()\">\n\t\t\t\tChallenge\n\t\t\t  </a>\n\t\t\t</li>\n  \n\t\t\t\n\t\t\t  <li class=\"nav-item\">\n\t\t\t\t<a class=\"nav-link challenge-solves\" data-bs-target=\"#solves\" @click=\"showSolves()\">\n\t\t\t\t  \n\t\t\t\t\t0 Solves\n\t\t\t\t  \n\t\t\t\t</a>\n\t\t\t  </li>\n\t\t\t\n\t\t  </ul>\n\t\t</div>\n\n\n\t\t<div>\n\t\t  <div class=\"tab-content\">\n\t\t\t<!-- Team Name-->\n\t\t\t<div class=\"tab-content\">\n\t\t\t  <div role=\"tabpanel\" class=\"tab-pane fade show active\" id=\"team\"> \n\t\t\t  <h4 class=\"team text-center pt-3\"> \n\t\t\t\t\they\n\t\t\t\tpwnteam\n\t\t\t  </h4>\n\n\t\t\t<!-- Team Credentials & Team connect command  (from JSON)-->\n\t\t\t<body>\n\n\t\t\t\t\n\t\t\t  <!-- <li class=\"nav-item\"> -->\n\t\t\t\t<div class=\"challenge-team_credential\" data-bs-target=\"#team_credential\">\n\t\t\t\t\ttest1\n\t\t\t\t\t\n\t\t\t\t\ttest2\n\t\t\t\t\t\n\t\t\t\t\ttest3\n\t\t\t\t</div>\n\t\t\t  <!-- </li> -->\n\t\t\t\n\n\t\t\t\t<h1>Hello: </h1>\n\t\t\t\t<h1>Your Connect Command: </h1>  \n\t\t\t</body>\n\t\t\n\t\t\t  \n  \n\t\t\t  <span class=\"challenge-desc\"><p>Eh</p>\n</span>\n  \n\t\t\t  \n\t\t\t\t<div class=\"mb-2\">\n\t\t\t\t  <span class=\"challenge-connection-info\">\n\t\t\t\t\t\n\t\t\t\t\t  \n\t\t\t\t\t  \n\t\t\t\t\t\t<code>hello connect</code>\n\t\t\t\t\t  \n\t\t\t\t\t\n\t\t\t\t  </span>\n\t\t\t\t</div>\n\t\t\t  \n  \n\t\t\t  \n  \n\t\t\t  \n\t\t\t\t<div class=\"row challenge-files text-center pb-3\">\n\t\t\t\t  \n\t\t\t\t\t<div class=\"col-md-4 col-sm-4 col-xs-12 file-button-wrapper d-block\">\n\t\t\t\t\t  \n\t\t\t\t\t  \n\t\t\t\t\t  \n\t\t\t\t\t\t\n\t\t\t\t\t  \n\t\t\t\t\t  <a\n\t\t\t\t\t\t  class=\"btn btn-info btn-file mb-1 d-inline-block px-2 w-100 text-truncate\"\n\t\t\t\t\t\t  href=\"/files/0fff468b93979ef992f96b1c91aa0d96/248524235_6372846866119834_2599255715323320459_n.jpg?token=eyJ1c2VyX2lkIjoyLCJ0ZWFtX2lkIjoxLCJmaWxlX2lkIjoxfQ.ZMgKxQ.D7_gcH_GT33Mukr0KcbNWSgBw4c\"\n\t\t\t\t\t\t  title=\"248524235_6372846866119834_2599255715323320459_n.jpg\"\n\t\t\t\t\t  >\n\t\t\t\t\t\t<i class=\"fas fa-download\"></i>\n\t\t\t\t\t\t<small>\n\t\t\t\t\t\t  248524235_6372846866119834_2599255715323320459_n.jpg\n\t\t\t\t\t\t</small>\n\t\t\t\t\t  </a>\n\t\t\t\t\t</div>\n\t\t\t\t  \n\t\t\t\t</div>\n\t\t\t  \n  \n\t\t\t  \n  \n\t\t\t  <div class=\"row submit-row\">\n\t\t\t\t<div class=\"col-12 col-sm-8\">\n\t\t\t\t  \n\t\t\t\t\t<input\n\t\t\t\t\t\tid=\"challenge-id\" class=\"challenge-id\" type=\"hidden\"\n\t\t\t\t\t\tvalue=\"1\"\n\t\t\t\t\t>\n\t\t\t\t\t<input\n\t\t\t\t\t\tid=\"challenge-input\" class=\"challenge-input form-control\"\n\t\t\t\t\t\ttype=\"text\" name=\"submission\"\n\t\t\t\t\t\t@keyup.enter=\"submitChallenge()\"\n\t\t\t\t\t\tplaceholder=\"Flag\" x-model=\"submission\"\n\t\t\t\t\t>\n\t\t\t\t  \n\t\t\t\t</div>\n  \n\t\t\t\t<div class=\"col-12 col-sm-4 mt-3 mt-sm-0 key-submit\">\n\t\t\t\t  \n\t\t\t\t\t<button\n\t\t\t\t\t\tid=\"challenge-submit\"\n\t\t\t\t\t\tclass=\"challenge-submit btn btn-outline-secondary w-100 h-100\" type=\"submit\"\n\t\t\t\t\t\t@click.debounce.500ms=\"submitChallenge()\"\n\t\t\t\t\t>\n\t\t\t\t\t  Submit\n\t\t\t\t\t</button>\n\t\t\t\t  \n\t\t\t\t</div>\n\t\t\t  </div>\n  \n\t\t\t  <div class=\"row notification-row\">\n\t\t\t\t<div class=\"col-12\">\n\t\t\t\t  <template x-if=\"response\">\n\t\t\t\t\t\n\t\t\t\t\t<div\n\t\t\t\t\t\t:class=\"{\n\t\t\t\t\t\t  'alert text-center w-100 mt-3 alert-success': response.data.status == 'correct',\n\t\t\t\t\t\t  'alert text-center w-100 mt-3 alert-info': response.data.status == 'already_solved',\n\t\t\t\t\t\t  'alert text-center w-100 mt-3 alert-danger': response.data.status == 'incorrect',\n\t\t\t\t\t\t}\" role=\"alert\"\n\t\t\t\t\t>\n\t\t\t\t\t  <strong x-text=\"response.data.message\"></strong>\n\t\t\t\t\t  <div x-show=\"(response.data.status == 'correct' || response.data.status == 'already_solved') && getNextId()\">\n\t\t\t\t\t\t<button @click=\"nextChallenge()\" class=\"btn btn-info mt-3\">\n\t\t\t\t\t\t  Next Challenge\n\t\t\t\t\t\t</button>\n\t\t\t\t\t  </div>\n\t\t\t\t\t</div>\n\t\t\t\t  </template>\n\t\t\t\t</div>\n\t\t\t  </div>\n\t\t\t</div>\n  \n\t\t\t<div role=\"tabpanel\" class=\"tab-pane fade\" id=\"solves\">\n\t\t\t  <div class=\"row\">\n\t\t\t\t<div class=\"col-md-12\">\n\t\t\t\t  <table class=\"table table-striped text-center\">\n\t\t\t\t\t<thead>\n\t\t\t\t\t<tr>\n\t\t\t\t\t  <td>\n\t\t\t\t\t\t<b>Name</b>\n\t\t\t\t\t  </td>\n\t\t\t\t\t  <td>\n\t\t\t\t\t\t<b>Date</b>\n\t\t\t\t\t  </td>\n\t\t\t\t\t</tr>\n\t\t\t\t\t</thead>\n\t\t\t\t\t<tbody id=\"challenge-solves-names\">\n\t\t\t\t\t<template x-for=\"solve in solves\">\n\t\t\t\t\t  <tr>\n\t\t\t\t\t\t<td>\n\t\t\t\t\t\t  <a :href=\"solve.account_url\" x-text=\"solve.name\"></a>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t\t<td x-text=\"solve.date\"></td>\n\t\t\t\t\t  </tr>\n\t\t\t\t\t</template>\n\t\t\t\t\t</tbody>\n\t\t\t\t  </table>\n\t\t\t\t</div>\n\t\t\t  </div>\n\t\t\t</div>\n\t\t  </div>\n\t\t</div>\n\t  </div>\n\t</div>\n  </div>\n\n\n\n\n\t\t\t<!-- Previous code for viewing team, challenge name and challenge value-->\n\t\t\t<!-- Team  name goes here  (from JSON)-->\n\t\t\t  <!-- <div class=\"tab-content\">\n\t\t\t  <div role=\"tabpanel\" class=\"tab-pane fade show active\" id=\"team\"> \n\t\t\t  <h4 class=\"team text-center pt-3\"> \n\t\t\t\tpwnteam\n\t\t\t  </h4> -->\n\n\t\t\t<!-- <div role=\"tabpanel\" class=\"tab-pane fade show active\" id=\"challenge\">\n\t\t\t  <h2 class=\"challenge-name text-center pt-3\">\n\t\t\t\tasdf name\n\t\t\t  </h2>\n\t\t\t  <h3 class=\"challenge-value text-center\">\n\t\t\t\t200\n\t\t\t  </h3> -->\n"
    }
}

team_credential is not passed to the template during rendering. The read dictionary isn't passed to the template so it needs to be directly accessible on the challenge object.

It doesn't seem like challenge.team_credential exists.

Is it safe to do something like this?

class ModaChallenge(Challenges):
    # print("Running ModaChallenge class")
    __mapper_args__ = {"polymorphic_identity": "Ai.Moda.Custom"}
    id = db.Column(
        db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE"), primary_key=True
    )
    initial = db.Column(db.Integer, default=0)
    minimum = db.Column(db.Integer, default=0)

    def __init__(self, *args, **kwargs):
        super(ModaChallenge, self).__init__(**kwargs)
        self.value = kwargs["initial"]
        self.team_credential = kwargs.get("team_credential")

    @classmethod
    def read(cls, challenge):
        """
        This method is in used to access the data of a challenge in a format processable by the front end.

        :param challenge:
        :return: Challenge object, data dictionary to be returned to the user
        """
        print("Running read function")

        if authed():
            try:
                team = get_current_team()
                print(team)
                if team is not None:
                    team_name = team.name
                    print(team_name)
                    challenge.team_credential = "hi"
            except:
                print("User is not logged in")

        data = {
            "id": challenge.id,
            "name": challenge.name,
            "value": challenge.value,
            "initial": challenge.initial,
            "minimum": challenge.minimum,
            "description": challenge.description,
            "connection_info": challenge.connection_info,
            "category": challenge.category,
            "state": challenge.state,
            "max_attempts": challenge.max_attempts,
            "type": challenge.type,
            "type_data": {
                "id": cls.id,
                "name": cls.name,
                "templates": cls.templates,
                "scripts": cls.scripts,
            },
        }
        return data

I just want to make sure I don't accidentally share the challenge.team_credential value between multiple teams. =P

I don't think this code works at all though? .read is called specifially on the challenge class, not on the database model.

Regardless, the challenge object that's passed to the renderer is the database model so you would probably need to make .team_credential a @property function in the database model and then that might work but I don't know for sure.

view.html should be rendered per user and the endpoint overall isn't cached between users because of the "solved_by_me" field so it shouldn't leak between users but you will need to do your own verification.

I don't want to put too much time into this one and I feel like you're pretty close to your solution.

See the following:
https://github.com/CTFd/CTFd/blob/master/CTFd/api/v1/challenges.py#L429-L439
https://github.com/CTFd/CTFd/blob/master/CTFd/api/v1/challenges.py#L290

So ultimately you just need challenge.team_credential or challenge.team_credential() to return the data that you want to embed.

Sorry I accidentally submitted before I was finished typing.

So ultimately you just need challenge.team_credential or challenge.team_credential() to return the data that you want to embed.

Sorry, do you mean modifying CTFd/api/v1/challenges.py to have a team_credential function?

I did get this working by only making my read function return an extra ssh_key and adb_key... but it's extremely hacky.

CTFd._internal.challenge.data = undefined;

// TODO: Remove in CTFd v4.0
CTFd._internal.challenge.renderer = null;

CTFd._internal.challenge.preRender = function() { };

// TODO: Remove in CTFd v4.0
CTFd._internal.challenge.render = null;

function loadButtonHref(button_id, button_url) {
  var downloadButton = document.getElementById(button_id);
  downloadButton.href = button_url;
};

CTFd._internal.challenge.postRender = function() {
  console.debug("postRender")
  console.debug(this);
  console.debug(this.data.ssh_key)
  var blobSshKey = new Blob([this.data.ssh_key], {type: 'text/plain'});
  var urlSshKey = URL.createObjectURL(blobSshKey);

  var blobAdbKey = new Blob([this.data.adb_key], {type: 'text/plain'});
  var urlAdbKey = URL.createObjectURL(blobAdbKey);

  setTimeout(function() {
    loadButtonHref('download-ssh-key-button', urlSshKey);
    loadButtonHref('download-adb-key-button', urlAdbKey);
  }, 1000); // TODO: Fix messy hack
};

CTFd._internal.challenge.submit = function(preview) {
  var challenge_id = parseInt(CTFd.lib.$("#challenge-id").val());
  var submission = CTFd.lib.$("#challenge-input").val();

  var body = {
    challenge_id: challenge_id,
    submission: submission
  };
  var params = {};
  if (preview) {
    params["preview"] = true;
  }

  return CTFd.api.post_challenge_attempt(params, body).then(function(response) {
    if (response.status === 429) {
      // User was ratelimited but process response
      return response;
    }
    if (response.status === 403) {
      // User is not logged in or CTF is paused.
      return response;
    }
    return response;
  });
};

You got me a little confused thinking this wasn't working so I had to look at it myself.

With the dynamic challenge plugin as an example if we update CTFd/plugins/dynamic_challenges/assets/view.html to the following:

{% extends "challenge.html" %}

{% block description %}
    {{ super() }}
    Test: {{ challenge.function }}
{% endblock %}

we get

image

Similarly if I add the following code to class DynamicChallenge in CTFd/plugins/dynamic_challenges/__init__.py

    @property
    def stuff(self):
        return "stuff"

and then use

{% extends "challenge.html" %}

{% block description %}
    {{ super() }}
    Test: {{ challenge.stuff }}
{% endblock %}

I get this

image

You can do what you're doing and handle it client side but it should also be possible to do it entirely in Python.

Thanks!

Sorry, but what lines did you add def stuff(self) to...? Still can't get #2377 (comment) to work in my plugin.

    @property
    def stuff(self):
        return "stuff"