How can I get `team` and/or `user` in the `read` function of a challenge plugin?
Manouchehri opened this issue · comments
CTFd/CTFd/plugins/challenges/__init__.py
Lines 43 to 69 in 56c5942
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.)
CTFd/CTFd/plugins/challenges/__init__.py
Line 132 in 56c5942
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.
CTFd/CTFd/api/v1/challenges.py
Line 393 in 56c5942
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?
@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.
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
orchallenge.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
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
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"